+ ))
+ GeoJSON.displayName = 'GeoJSON'
+
+ return {
+ __esModule: true,
+ ...ActualReactLeaflet,
+ GeoJSON,
+ MapContainer
+ }
+})
+
+describe('Map component tests', () => {
+ test('component renders', () => {
+ render()
+ const mapNode = screen.getByTestId('map')
+
+ expect(mapNode).toBeTruthy()
+ })
+
+ test('renders map with GeoJSON when polygon prop is provided', () => {
+ render()
+ const geoJsonNode = screen.getByTestId('geojson')
+
+ expect(geoJsonNode).toBeTruthy()
+ })
+
+ test('does not render GeoJSON when polygon prop is not provided', () => {
+ render()
+ const geoJsonNode = screen.queryByTestId('geojson')
+
+ expect(geoJsonNode).toBeFalsy()
+ })
+})
diff --git a/adhocracy4/maps_react/templates/a4maps_react/map_choose_point_widget.html b/adhocracy4/maps_react/templates/a4maps_react/map_choose_point_widget.html
new file mode 100644
index 000000000..b971118ad
--- /dev/null
+++ b/adhocracy4/maps_react/templates/a4maps_react/map_choose_point_widget.html
@@ -0,0 +1,4 @@
+{% load react_maps_tags %}
+
+{% react_choose_point polygon point name %}
+
diff --git a/adhocracy4/maps_react/templatetags/__init__.py b/adhocracy4/maps_react/templatetags/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/adhocracy4/maps_react/templatetags/react_maps_tags.py b/adhocracy4/maps_react/templatetags/react_maps_tags.py
new file mode 100644
index 000000000..c8a4f84b6
--- /dev/null
+++ b/adhocracy4/maps_react/templatetags/react_maps_tags.py
@@ -0,0 +1,20 @@
+import json
+
+from django import template
+from django.utils.html import format_html
+
+from adhocracy4.maps_react.utils import get_map_settings
+
+register = template.Library()
+
+
+@register.simple_tag()
+def react_choose_point(polygon, point, name):
+ attributes = {
+ "map": get_map_settings(polygon=polygon, point=point, name=name),
+ }
+
+ return format_html(
+ '',
+ attributes=json.dumps(attributes),
+ )
diff --git a/adhocracy4/maps_react/utils.py b/adhocracy4/maps_react/utils.py
new file mode 100644
index 000000000..0c9e508b8
--- /dev/null
+++ b/adhocracy4/maps_react/utils.py
@@ -0,0 +1,27 @@
+from django.conf import settings
+
+
+def get_map_settings(**kwargs):
+ map_settings = {
+ "mapboxToken": "",
+ "omtToken": "",
+ "attribution": "",
+ "useVectorMap": 0,
+ "baseUrl": settings.A4_MAP_BASEURL,
+ **kwargs,
+ }
+
+ if hasattr(settings, "A4_MAP_ATTRIBUTION"):
+ map_settings["attribution"] = settings.A4_MAP_ATTRIBUTION
+
+ if hasattr(settings, "A4_USE_VECTORMAP") and settings.A4_USE_VECTORMAP:
+ map_settings["useVectorMap"] = 1
+
+ if hasattr(settings, "A4_MAPBOX_TOKEN"):
+ map_settings["mapboxToken"] = settings.A4_MAPBOX_TOKEN
+
+ if hasattr(settings, "A4_OPENMAPTILES_TOKEN"):
+ map_settings["omtToken"] = settings.A4_OPENMAPTILES_TOKEN
+
+ # Filter out the keys that have a value of ""
+ return {key: val for key, val in map_settings.items() if val != ""}
diff --git a/adhocracy4/maps_react/widgets.py b/adhocracy4/maps_react/widgets.py
new file mode 100644
index 000000000..4330c49c7
--- /dev/null
+++ b/adhocracy4/maps_react/widgets.py
@@ -0,0 +1,33 @@
+from django.contrib.staticfiles import finders
+from django.core.exceptions import ImproperlyConfigured
+from django.forms.widgets import Widget
+from django.template import loader
+
+
+class MapChoosePointWidget(Widget):
+ def __init__(self, polygon, attrs=None):
+ self.polygon = polygon
+ super().__init__(attrs)
+
+ class Media:
+ js = ("a4maps_choose_point.js",)
+
+ css = {"all": ["a4maps_choose_point.css"]}
+
+ def render(self, name, value, attrs, renderer=None):
+ if not finders.find("a4maps_choose_point.js"):
+ raise ImproperlyConfigured(
+ "Configure your frontend build tool to generate a4maps_choose_point.js."
+ )
+
+ context = {
+ "name": name,
+ "polygon": self.polygon,
+ }
+
+ if value != "null" and value:
+ context["point"] = value
+
+ return loader.render_to_string(
+ "a4maps_react/map_choose_point_widget.html", context
+ )
diff --git a/changelog/7704.md b/changelog/7704.md
new file mode 100644
index 000000000..01e236825
--- /dev/null
+++ b/changelog/7704.md
@@ -0,0 +1,11 @@
+### Added
+
+- react components for generating a leaflet map:
+ - A basic map component that renders a polygon, tilelayer and zoom controls
+ - MaplibreGL Tilelayer to implement `@maplibre/maplibre-gl-leaflet`
+ - MarkerClusterLayer to implement `leaflet.markercluster`
+ - MapPopup to provide basic html wrapping
+ - AddMarkerControl to allow users to set a marker on a map within a
+ constraining polygon
+ - GeoJsonMarker to fetch the coords from GeoJson and render a jsx Marker
+- added an utility in python to easily get all relevant map settings
diff --git a/docs/react_maps.md b/docs/react_maps.md
new file mode 100644
index 000000000..58e402b5a
--- /dev/null
+++ b/docs/react_maps.md
@@ -0,0 +1,237 @@
+# Map Components Documentation
+
+This document provides an annotated reference to the various custom map
+components used in the project. These components leverage the react-leaflet
+library, which provides React-friendly abstractions for the popular Leaflet.
+js, an open-source library used to create interactive maps. These components
+get further customization and extension to cater to the specific
+requirements of the project.
+They include a variety of features including custom map controls, geometric
+layers, markers, pop-up windows, and more. In the following sections, you will
+find a brief overview, key functions, and methods for each of these components
+which are designed to offer a unique interactivity and to display the data in a
+visually appealing and user-friendly manner.
+
+## Components List
+
+* **Map**:
+ Main map displaying component, using polygon and map layers defined
+ in other components. Also adds zoom functionality.
+* **MaplibreGlLayer**:
+ Map layer component which uses maplibre-gl-leaflet to
+ render maps using vector tiles from an external source.
+* **AddMarkerControl**:
+ Leaflet Control used for adding a draggable marker to the map
+ on click events within certain constraints.
+* **GeoJsonMarker**:
+ Represents a GeoJSON marker with a custom icon on the
+ map, allowing the developer to use JSX for its content.
+* **MapPopup**:
+ A custom styled Popup component wrapping react-leaflet Popup
+ which appears on the map. Might include an image and text.
+* **MarkerClusterLayer**:
+ Layer component which groups nearby markers into
+ clusters for easier visualization and understanding.
+
+For more detailed understanding, dive deeper into individual components and
+refer to official documentation
+of [react-leaflet](https://react-leaflet.js.org/)
+and [Leaflet.js](https://leafletjs.com/reference.html).
+
+## Making non-react leaflet modules into React components
+
+We are using `@react-leaflet/core` to wrap basic Leaflet elements into a react
+wrapper. This allows us to use JSX syntax instead of having to do a complicated
+mixture of JSX and JS.
+You can read more about this method and
+examples [here](https://react-leaflet.js.org/docs/core-architecture/).
+Examples of those within a4 will be the `AddMarkerControl`, `MaplibreGlLayer`,
+`MarkerClusterLayer`, `GeoJsonMarker`.
+
+## Map
+
+This is a Map component which uses the React Leaflet library to create maps. The
+map can include polygons and tiles, and supports zoom control. It uses
+`react-leaflet`, `MaplibreGlLayer` for tiles rendering, and the GeoJSON feature
+to
+parse geographical features.
+
+### Props
+
+* **attribution**: This prop is passed to MaplibreGlLayer. It allows you to
+ specify an attribution displayed.
+* **baseUrl**: This prop is passed to MaplibreGlLayer. It determines the vector
+ tiles URL.
+* **omtToken**: This prop is passed to MaplibreGlLayer. It's added to the URL
+ for tiles in case you need to pass a token.
+* **polygon**: This prop is used to generate a polygon on the map. It will be in
+ the center of the map.
+* **children**: Add any other Components you will need in here. Markers, Popups,
+ GeoJSON, [etc](https://react-leaflet.js.org/docs/api-components/).
+* ...**rest**: This is a placeholder for all other props, which are passed
+ directly to the
+ underlying `react-leaflet` [MapContainer component](https://react-leaflet.js.org/docs/api-map/#mapcontainer).
+ This means, you can also specify leaflet options in here.
+
+### Accessing Leaflet Instance
+
+If you need to access the leaflet instance directly, you can use a reference to
+it like so:
+
+```jsx
+const map = useRef();
+
+useEffect(() => {
+ if (map.current) {
+ // do stuff with map.current
+ map.current.panTo([50, 20])
+ }
+}, [map]);
+
+