diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py
index e04d95d87a..0351ac0c69 100644
--- a/src/backend/app/projects/project_routes.py
+++ b/src/backend/app/projects/project_routes.py
@@ -1024,6 +1024,7 @@ async def download_task_boundary_osm(
response = Response(content=content, media_type="application/xml")
return response
+from sqlalchemy.sql import text
@router.get("/centroid/")
async def project_centroid(
@@ -1040,11 +1041,11 @@ async def project_centroid(
List[Tuple[int, str]]: A list of tuples containing the task ID and the centroid as a string.
"""
- query = f"""SELECT id, ARRAY_AGG(ARRAY[ST_X(ST_Centroid(outline)), ST_Y(ST_Centroid(outline))]) AS centroid
+ query = text(f"""SELECT id, ARRAY_AGG(ARRAY[ST_X(ST_Centroid(outline)), ST_Y(ST_Centroid(outline))]) AS centroid
FROM projects
WHERE {f"id={project_id}" if project_id else "1=1"}
- GROUP BY id;"""
+ GROUP BY id;""")
result = db.execute(query)
- result = result.fetchall()
- return result
\ No newline at end of file
+ result_dict_list = [{"id": row[0], "centroid": row[1]} for row in result.fetchall()]
+ return result_dict_list
\ No newline at end of file
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/index.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/index.js
new file mode 100644
index 0000000000..1481d57db7
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/index.js
@@ -0,0 +1,163 @@
+/* eslint-disable react/prop-types */
+/* eslint-disable no-unused-vars */
+import 'ol-layerswitcher/dist/ol-layerswitcher.css';
+// import "../../node_modules/ol-layerswitcher/dist/ol-layerswitcher.css";
+import LayerGroup from 'ol/layer/Group';
+import LayerTile from 'ol/layer/Tile';
+import SourceOSM from 'ol/source/OSM';
+import SourceStamen from 'ol/source/Stamen';
+import LayerSwitcher from 'ol-layerswitcher';
+import React,{ useEffect } from 'react';
+
+import { XYZ } from 'ol/source';
+
+// const mapboxOutdoors = new MapboxVector({
+// styleUrl: 'mapbox://styles/geovation/ckpicg3of094w17nyqyd2ziie',
+// accessToken: 'pk.eyJ1IjoiZ2VvdmF0aW9uIiwiYSI6ImNrcGljOXBwbTBoc3oyb3BjMGsxYW9wZ2EifQ.euYtUXb6HJGLHkj4Wi3gjA',
+// });
+const osm = (visible) =>
+ new LayerTile({
+ title: 'OSM',
+ type: 'base',
+ visible: visible === 'osm',
+ source: new SourceOSM(),
+ });
+const none = (visible) =>
+ new LayerTile({
+ title: 'None',
+ type: 'base',
+ visible: visible === 'none',
+ source: null,
+ });
+const bingMaps = (visible) =>
+ new LayerTile({
+ title: 'Satellite',
+ type: 'base',
+ visible: visible === 'satellite',
+ source: new XYZ({
+ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
+ }),
+ // source: new BingMaps({
+ // key: 'AoTlmaazzog43ImdKts9HVztFzUI4PEOT0lmo2V4q7f20rfVorJGAgDREKmfQAgd',
+ // imagerySet: 'Aerial',
+ // // use maxZoom 19 to see stretched tiles instead of the BingMaps
+ // // "no photos at this zoom level" tiles
+ // maxZoom: 19,
+ // crossOrigin: 'Anonymous',
+ // }),
+ });
+const mapboxMap = (visible) =>
+ new LayerTile({
+ title: 'Mapbox Light',
+ type: 'base',
+ visible: visible === 'mapbox',
+ source: new XYZ({
+ attributions:
+ 'Tiles © ArcGIS',
+ url: 'https://api.mapbox.com/styles/v1/nishon-naxa/ckgkuy7y08rpi19qk46sces9c/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1IjoibmlzaG9uLW5heGEiLCJhIjoiY2xhYnhwbzN0MDUxYTN1bWhvcWxocWlpaSJ9.0FarR4aPxb7F9BHP31msww',
+ layer: 'topoMap',
+ maxZoom: 19,
+ crossOrigin: 'Anonymous',
+ }),
+ });
+const mapboxOutdoors = (visible) =>
+ new LayerTile({
+ title: 'Mapbox Outdoors',
+ type: 'base',
+ visible: visible === 'outdoors',
+ source: new XYZ({
+ attributions:
+ 'Tiles © ArcGIS',
+ // url:
+ // 'https://api.mapbox.com/styles/v1/geovation/ckpicg3of094w17nyqyd2ziie/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1IjoiZ2VvdmF0aW9uIiwiYSI6ImNrcGljOXBwbTBoc3oyb3BjMGsxYW9wZ2EifQ.euYtUXb6HJGLHkj4Wi3gjA',
+ url: 'https://api.mapbox.com/styles/v1/naxa-np/cl50pm1l5001814qpncu8s4ib/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1IjoibmF4YS1ucCIsImEiOiJja2E5bGp0ZDQwdHE4MnJxdnhmcGxsdGpuIn0.kB42E50iZFlFPcQiqQMClw',
+ layer: 'topoMap',
+ maxZoom: 19,
+ crossOrigin: 'Anonymous',
+ }),
+ });
+
+const topoMap = (visible = false) =>
+ new LayerTile({
+ title: 'Topo Map',
+ type: 'base',
+ visible,
+ source: new XYZ({
+ attributions:
+ 'Tiles © ArcGIS',
+ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}',
+ layer: 'topoMap',
+ maxZoom: 19,
+ crossOrigin: 'Anonymous',
+ }),
+ });
+
+const monochrome = (visible = false) =>
+ new LayerTile({
+ title: 'Monochrome',
+ type: 'base',
+ visible,
+ source: new XYZ({
+ attributions:
+ 'Tiles © ArcGIS',
+ url: 'https://api.mapbox.com/styles/v1/geovation/ckqxdp7rd0t5d17lfuxm259c7/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1IjoiZ2VvdmF0aW9uIiwiYSI6ImNrcGljOXBwbTBoc3oyb3BjMGsxYW9wZ2EifQ.euYtUXb6HJGLHkj4Wi3gjA',
+ layer: 'topomap',
+ maxZoom: 19,
+ crossOrigin: 'Anonymous',
+ }),
+ });
+
+const monochromeMidNight = (visible = false) =>
+ new LayerTile({
+ title: 'Monochrome Midnight',
+ type: 'base',
+ visible,
+ source: new XYZ({
+ attributions:
+ 'Tiles © ArcGIS',
+ url: 'https://api.mapbox.com/styles/v1/geovation/ckqxdsqu93r0417mcbdc102nb/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1IjoiZ2VvdmF0aW9uIiwiYSI6ImNrcGljOXBwbTBoc3oyb3BjMGsxYW9wZ2EifQ.euYtUXb6HJGLHkj4Wi3gjA',
+ layer: 'topomap',
+ maxZoom: 19,
+ crossOrigin: 'Anonymous',
+ }),
+ });
+
+const watercolor = new LayerTile({
+ title: 'Water color',
+ type: 'base',
+ visible: false,
+ source: new SourceStamen({
+ layer: 'watercolor',
+ }),
+});
+
+const LayerSwitcherControl = ({ map, visible = 'osm' }) => {
+ useEffect(() => {
+ if (!map) return;
+
+ const baseMaps = new LayerGroup({
+ title: 'Base maps',
+ layers: [bingMaps(visible), osm(visible), mapboxMap(visible), mapboxOutdoors(visible), none(visible)],
+ });
+
+ const layerSwitcher = new LayerSwitcher({
+ reverse: true,
+ groupSelectStyle: 'group',
+ });
+ map.addLayer(baseMaps);
+ map.addControl(layerSwitcher);
+ // eslint-disable-next-line consistent-return
+ return () => {
+ map.removeLayer(baseMaps);
+ };
+ }, [map, visible]);
+
+ return null;
+};
+
+export default LayerSwitcherControl;
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/VectorLayer.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/VectorLayer.js
new file mode 100644
index 0000000000..0d56e958e2
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/VectorLayer.js
@@ -0,0 +1,266 @@
+/* eslint-disable no-console */
+/* eslint-disable consistent-return */
+/* eslint-disable react/forbid-prop-types */
+import React,{ useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import { get } from 'ol/proj';
+import Style from 'ol/style/Style';
+import Stroke from 'ol/style/Stroke';
+import GeoJSON from 'ol/format/GeoJSON';
+import { Vector as VectorSource } from 'ol/source';
+import OLVectorLayer from 'ol/layer/Vector';
+import { defaultStyles, getStyles } from '../helpers/styleUtils';
+import { isExtentValid } from '../helpers/layerUtils';
+import {
+ Draw,
+ Modify,
+ Select,
+ defaults as defaultInteractions,
+} from 'ol/interaction.js';
+
+
+const selectElement = 'singleselect';
+
+const selectedCountry = new Style({
+ stroke: new Stroke({
+ color: '#008099',
+ width: 3,
+ }),
+ // fill: new Fill({
+ // color: 'rgba(200,20,20,0.4)',
+ // }),
+});
+let selection = {};
+const layerViewProperties = {
+ padding: [50, 50, 50, 50],
+ duration: 900,
+ constrainResolution: true,
+};
+
+const VectorLayer = ({
+ map,
+ geojson,
+ style,
+ zIndex,
+ zoomToLayer = false,
+ visibleOnMap = true,
+ properties,
+ viewProperties,
+ hoverEffect,
+ mapOnClick,
+ setStyle,
+ onModify,
+ onDraw,
+}) => {
+ const [vectorLayer, setVectorLayer] = useState(null);
+
+ useEffect(() => () => map && vectorLayer && map.removeLayer(vectorLayer), [map, vectorLayer]);
+
+ // Modify Feature
+ useEffect(() => {
+ if(!map) return;
+ if(!vectorLayer) return;
+ if(!onModify) return;
+ const select = new Select({
+ wrapX: false,
+ });
+ const vectorLayerSource = vectorLayer.getSource();
+ const modify = new Modify({
+ // features: select.getFeatures(),
+ source:vectorLayerSource
+ });
+ modify.on('modifyend',function(e){
+ var geoJSONFormat = new GeoJSON();
+
+ var geoJSONString = geoJSONFormat.writeFeatures(vectorLayer.getSource().getFeatures(),{ dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857'});
+
+ onModify(geoJSONString);
+ });
+ map.addInteraction(modify);
+ map.addInteraction(select);
+
+ return () => {
+ // map.removeInteraction(defaultInteractions().extend([select, modify]))
+ }
+ }, [map,vectorLayer,onModify])
+ // Modify Feature
+ useEffect(() => {
+ if(!map) return;
+ // if(!vectorLayer) return;
+ if(!onDraw) return;
+ const source = new VectorSource({wrapX: false});
+
+ const vector = new OLVectorLayer({
+ source: source,
+ });
+ const draw = new Draw({
+ source: source,
+ type: 'Polygon',
+ });
+ draw.on('drawend',function(e){
+ const feature = e.feature;
+ const geojsonFormat = new GeoJSON();
+ const newGeojson = geojsonFormat.writeFeature(feature,{ dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857'});
+
+ // Call your function here with the GeoJSON as an argument
+ onDraw(newGeojson);
+ // var geoJSONFormat = new GeoJSON();
+
+ // var geoJSONString = geoJSONFormat.writeFeatures(vectorLayer.getSource().getFeatures(),{ dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857'});
+ // console.log(geoJSONString,'geojsonString');
+ // onDraw(geoJSONString);
+ });
+ map.addInteraction(draw);
+
+ return () => {
+ map.removeInteraction(draw)
+ }
+ }, [map,vectorLayer,onDraw])
+
+
+ useEffect(() => {
+ if (!map) return;
+ if (!geojson) return;
+
+ const vectorLyr = new OLVectorLayer({
+ source: new VectorSource({
+ features: new GeoJSON().readFeatures(geojson, {
+ featureProjection: get('EPSG:3857'),
+ }),
+ }),
+ declutter: true,
+ });
+ map.on('click', (evt) => {
+ var pixel = evt.pixel;
+ const feature = map.forEachFeatureAtPixel(pixel, function(feature, layer) {
+
+ if (layer === vectorLyr) {
+ return feature;
+ }
+ });
+
+ // Perform an action if a feature is found
+ if (feature) {
+ // Do something with the feature
+ console.log('Clicked feature:', feature.getProperties());
+ // dispatch()
+ mapOnClick(feature.getProperties());
+ }
+ });
+ setVectorLayer(vectorLyr);
+ }, [map, geojson]);
+
+ useEffect(() => {
+ if (!map || !vectorLayer) return;
+ if (visibleOnMap) {
+ map.addLayer(vectorLayer);
+ } else {
+ map.removeLayer(vectorLayer);
+ }
+ }, [map, vectorLayer, visibleOnMap]);
+
+ useEffect(() => {
+ if (!map || !vectorLayer || !visibleOnMap || !setStyle) return;
+ vectorLayer.setStyle(setStyle);
+ }, [map, setStyle, vectorLayer, visibleOnMap]);
+
+
+ useEffect(() => {
+ if (!vectorLayer || !style.visibleOnMap) return;
+ vectorLayer.setStyle((feature, resolution) => getStyles({ style, feature, resolution }));
+ }, [vectorLayer, style]);
+
+ useEffect(() => {
+ if (!vectorLayer) return;
+ vectorLayer.setZIndex(zIndex);
+ }, [vectorLayer, zIndex]);
+
+ useEffect(() => {
+ if (!map || !vectorLayer || !zoomToLayer) return;
+ const extent = vectorLayer.getSource().getExtent();
+ if (!isExtentValid(extent)) return;
+ map.getView().fit(extent, viewProperties);
+ }, [map, vectorLayer, zoomToLayer]);
+
+ // set properties to features for identifying popup
+ useEffect(() => {
+ if (!vectorLayer || !properties) return;
+ const features = vectorLayer.getSource().getFeatures();
+ features.forEach((feat) => {
+ feat.setProperties(properties);
+ });
+ }, [vectorLayer, properties]);
+
+
+
+// style on hover
+useEffect(() => {
+ if (!map) return null;
+ if (!vectorLayer) return null;
+ if (!hoverEffect) return null;
+ const selectionLayer = new OLVectorLayer({
+ map,
+ renderMode: 'vector',
+ source: vectorLayer.getSource(),
+ // eslint-disable-next-line consistent-return
+ style: (feature) => {
+ if (feature.getId() in selection) {
+ return selectedCountry;
+ }
+ // return stylex;
+ },
+ });
+ function pointerMovefn(event) {
+ vectorLayer.getFeatures(event.pixel).then((features) => {
+ if (!features.length) {
+ selection = {};
+ selectionLayer.changed();
+ return;
+ }
+ const feature = features[0];
+ if (!feature) {
+ return;
+ }
+ const fid = feature.getId();
+ if (selectElement.startsWith('singleselect')) {
+ selection = {};
+ }
+ // add selected feature to lookup
+ selection[fid] = feature;
+
+ selectionLayer.changed();
+ });
+ }
+ map.on('pointermove', pointerMovefn);
+ return () => {
+ map.un('pointermove', pointerMovefn);
+ };
+}, [vectorLayer]);
+ return null;
+};
+
+
+
+
+
+VectorLayer.defaultProps = {
+ zIndex: 0,
+ style: { ...defaultStyles },
+ zoomToLayer: false,
+ viewProperties: layerViewProperties,
+ mapOnClick:()=>{},
+ onModify:null,
+};
+
+VectorLayer.propTypes = {
+ geojson: PropTypes.object.isRequired,
+ style: PropTypes.object,
+ zIndex: PropTypes.number,
+ zoomToLayer: PropTypes.bool,
+ viewProperties: PropTypes.object,
+ mapOnClick:PropTypes.func,
+ onModify:PropTypes.func,
+ // Context: PropTypes.object.isRequired,
+};
+
+export default VectorLayer;
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/VectorTileLayer.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/VectorTileLayer.js
new file mode 100644
index 0000000000..bd4afdeaa4
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/VectorTileLayer.js
@@ -0,0 +1,215 @@
+import React,{ useEffect, useMemo } from 'react';
+// import * as olExtent from 'ol/extent';
+import VectorTile from 'ol/layer/VectorTile';
+import MVT from 'ol/format/MVT';
+import VectorTileSource from 'ol/source/VectorTile';
+import { transformExtent } from 'ol/proj';
+import Stroke from 'ol/style/Stroke';
+import Style from 'ol/style/Style';
+import { getStyles, defaultStyles } from '../helpers/styleUtils';
+import { isExtentValid } from '../helpers/layerUtils';
+
+const selectElement = 'singleselect';
+
+const selectedCountry = new Style({
+ stroke: new Stroke({
+ color: 'rgba(255,255,255,0.8)',
+ width: 3,
+ }),
+ // fill: new Fill({
+ // color: 'rgba(200,20,20,0.4)',
+ // }),
+});
+let selection = {};
+const VectorTileLayer = ({
+ map,
+ url,
+ style = { ...defaultStyles },
+ zIndex = 1,
+ visibleOnMap = true,
+ authToken,
+ setStyle,
+ zoomToLayer = false,
+ bbox = null,
+ hoverEffect,
+ // properties,
+}) => {
+ const vectorTileLayer = useMemo(
+ () =>
+ new VectorTile({
+ renderMode: 'hybrid',
+ // declutter: true,
+ }),
+ [],
+ );
+
+ vectorTileLayer.setProperties({ name: 'site' });
+
+ // add source to layer
+ useEffect(() => {
+ if (!map) return;
+
+ const requestHeader = new Headers();
+ if (authToken) {
+ requestHeader.append('Authorization', `Token ${authToken}`);
+ }
+
+ const vectorTileSource = new VectorTileSource({
+ // format: new MVT({ featureClass: Feature }),
+ format: new MVT({ idProperty: 'id' }),
+ maxZoom: 19,
+ url,
+ transition: 0,
+
+ tileLoadFunction: (tile, vtUrl) => {
+ tile.setLoader((extent, resolution, projection) => {
+ fetch(vtUrl, {
+ headers: requestHeader,
+ }).then((response) => {
+ response.arrayBuffer().then((data) => {
+ const format = tile.getFormat();
+ const features = format.readFeatures(data, {
+ extent,
+ featureProjection: projection,
+ });
+ tile.setFeatures(features);
+ });
+ });
+ });
+ },
+ });
+ vectorTileLayer.setSource(vectorTileSource);
+ }, [map, url, authToken, vectorTileLayer]);
+
+ // add layer to map
+ useEffect(() => {
+ if (!map) return;
+ if (visibleOnMap) {
+ map.addLayer(vectorTileLayer);
+ } else {
+ map.removeLayer(vectorTileLayer);
+ }
+ }, [map, visibleOnMap, vectorTileLayer]);
+
+ // // set style
+ useEffect(() => {
+ if (!map || !visibleOnMap || !setStyle) return;
+ vectorTileLayer.setStyle(setStyle);
+ }, [map, setStyle, vectorTileLayer, visibleOnMap]);
+
+ // set style
+ useEffect(() => {
+ if (!map || !visibleOnMap || setStyle) return;
+ vectorTileLayer.setStyle((feature, resolution) => getStyles({ style, feature, resolution }));
+ }, [map, style, vectorTileLayer, visibleOnMap, setStyle]);
+
+ // set z-index
+ useEffect(() => {
+ if (!map) return;
+ vectorTileLayer.setZIndex(zIndex);
+ }, [map, zIndex, vectorTileLayer]);
+
+ // // set properties to features for identifying popup
+ // useEffect(() => {
+ // if (!vectorTileLayer || !properties) return;
+ // vectorTileLayer.getSource().on('tileloadend', (evt) => {
+ // // const z = evt.tile.getTileCoord()[0];
+ // const features = evt.tile.getFeatures();
+ // features.forEach((feat) => {
+ // feat.setProperties(properties);
+ // });
+ // });
+ // // // console.log(vectorTileLayer.getSource(), 'sourcex');
+ // // const features = vectorTileLayer.getSource().getFeatures();
+ // // features.forEach((feat) => {
+ // // feat.setProperties(properties);
+ // // });
+ // }, [vectorTileLayer, properties]);
+
+ // useEffect(() => {
+ // const featuresForZ = [];
+ // vectorTileLayer.getSource().on('tileloadend', evt => {
+ // const z = evt.tile.getTileCoord()[0];
+ // const feature = evt.tile.getFeatures();
+ // if (!Array.isArray(featuresForZ[z])) {
+ // featuresForZ[z] = [];
+ // }
+ // featuresForZ[z] = featuresForZ[z].concat(feature);
+ // });
+ // setFeatures(featuresForZ);
+ // }, []);
+
+ // useEffect(() => {
+ // if (!map) return;
+ // const extent = olExtent.createEmpty();
+ // if (isExtentValid(extent)) {
+ // map.getView().fit(extent, {
+ // padding: [50, 50, 50, 50],
+ // duration: 500,
+ // constrainResolution: true,
+ // });
+ // }
+ // }, [map]);
+
+ // style on hover
+ useEffect(() => {
+ if (!map) return null;
+ if (!hoverEffect) return null;
+ const selectionLayer = new VectorTile({
+ map,
+ renderMode: 'vector',
+ source: vectorTileLayer.getSource(),
+ // eslint-disable-next-line consistent-return
+ style: (feature) => {
+ if (feature.getId() in selection) {
+ return selectedCountry;
+ }
+ // return stylex;
+ },
+ });
+ function pointerMovefn(event) {
+ vectorTileLayer.getFeatures(event.pixel).then((features) => {
+ if (!features.length) {
+ selection = {};
+ selectionLayer.changed();
+ return;
+ }
+ const feature = features[0];
+ if (!feature) {
+ return;
+ }
+ const fid = feature.getId();
+ if (selectElement.startsWith('singleselect')) {
+ selection = {};
+ }
+ // add selected feature to lookup
+ selection[fid] = feature;
+
+ selectionLayer.changed();
+ });
+ }
+ map.on('pointermove', pointerMovefn);
+ return () => {
+ map.un('pointermove', pointerMovefn);
+ };
+ }, [vectorTileLayer]);
+
+ // zoom to layer
+ useEffect(() => {
+ if (!map || !vectorTileLayer || !zoomToLayer || !bbox) return;
+ const transformedExtent = transformExtent(bbox, 'EPSG:4326', 'EPSG:3857');
+ if (!isExtentValid(transformedExtent)) return;
+ map.getView().fit(transformedExtent, {
+ padding: [50, 50, 50, 50],
+ duration: 900,
+ constrainResolution: true,
+ });
+ }, [map, vectorTileLayer, zoomToLayer, bbox]);
+
+ // cleanup
+ useEffect(() => () => map && map.removeLayer(vectorTileLayer), [map, vectorTileLayer]);
+
+ return null;
+};
+
+export default VectorTileLayer;
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/index.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/index.js
new file mode 100644
index 0000000000..8292e0e2a4
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/index.js
@@ -0,0 +1,3 @@
+export { default as VectorTileLayer } from './VectorTileLayer';
+
+export { default as VectorLayer } from './VectorLayer';
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/MapContainer/index.jsx b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/MapContainer/index.jsx
new file mode 100644
index 0000000000..b7abafa404
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/MapContainer/index.jsx
@@ -0,0 +1,37 @@
+/* eslint-disable no-nested-ternary */
+/* eslint-disable react/jsx-props-no-spreading */
+/* eslint-disable react/jsx-no-useless-fragment */
+import React from 'react';
+import PropTypes from 'prop-types';
+import '../map.scss';
+
+const { Children, cloneElement, forwardRef } = React;
+
+const MapContainer = forwardRef(({ children, mapInstance, ...rest }, ref) => {
+ const childrenCount = Children.count(children);
+ const props = {
+ map: mapInstance,
+ };
+ return (
+
+ {childrenCount < 1 ? (
+ <>>
+ ) : childrenCount > 1 ? (
+ Children.map(children, (child) => (child ? cloneElement(child, { ...props }) : <>>))
+ ) : (
+ cloneElement(children, { ...props })
+ )}
+
+ );
+});
+
+MapContainer.defaultProps = {
+ mapInstance: null,
+};
+
+MapContainer.propTypes = {
+ children: PropTypes.node.isRequired,
+ mapInstance: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
+};
+
+export default MapContainer;
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/index.jsx b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/index.jsx
new file mode 100644
index 0000000000..9457978187
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/index.jsx
@@ -0,0 +1,72 @@
+/* eslint-disable react/prop-types */
+/* eslint-disable jsx-a11y/anchor-has-content */
+/* eslint-disable func-names */
+/* eslint-disable jsx-a11y/control-has-associated-label */
+import React, { useEffect } from 'react';
+import Overlay from 'ol/Overlay';
+import './popup.scss';
+
+const Popup = ({ map, except }) => {
+ useEffect(() => {
+ if (!map) return;
+
+ const container = document.getElementById('popup');
+ const content = document.getElementById('popup-content');
+ const closer = document.getElementById('popup-closer');
+
+ const overlay = new Overlay({
+ element: container,
+ autoPan: true,
+ autoPanAnimation: {
+ duration: 250,
+ },
+ });
+
+ closer.onclick = function () {
+ overlay.setPosition(undefined);
+ closer.blur();
+ return false;
+ };
+
+ map.on('singleclick', (evt) => {
+ const { coordinate } = evt;
+ const features = map.getFeaturesAtPixel(evt.pixel);
+ if (features.length < 1) {
+ overlay.setPosition(undefined);
+ closer.blur();
+ content.innerHTML = '';
+ return;
+ }
+ const properties = features[0].getProperties();
+ const { layerId } = properties;
+ if (layerId === except) {
+ overlay.setPosition(undefined);
+ closer.blur();
+ return;
+ }
+ content.innerHTML = `
+ ${Object.keys(properties).reduce(
+ (str, key) =>
+ `${str}
+
+ ${key} |
+ ${properties[key]}
+ |
+
`,
+ '',
+ )}
+
`;
+ overlay.setPosition(coordinate);
+ map.addOverlay(overlay);
+ });
+ }, [map]);
+
+ return (
+
+ );
+};
+
+export default Popup;
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.css b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.css
new file mode 100644
index 0000000000..bdd5110d88
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.css
@@ -0,0 +1,49 @@
+@charset "UTF-8";
+.ol-popup {
+ position: absolute;
+ background-color: white;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+ padding: 15px;
+ border-radius: 3px;
+ border: 1px solid #cccccc;
+ margin-top: 12px;
+ bottom: 12px;
+ left: -50px;
+ min-width: 280px;
+}
+
+.ol-popup:after,
+.ol-popup:before {
+ top: 100%;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+
+.ol-popup:after {
+ border-top-color: white;
+ border-width: 10px;
+ left: 48px;
+ margin-left: -10px;
+}
+
+.ol-popup:before {
+ border-top-color: #cccccc;
+ border-width: 11px;
+ left: 48px;
+ margin-left: -11px;
+}
+
+.ol-popup-closer {
+ text-decoration: none;
+ position: absolute;
+ top: 2px;
+ right: 8px;
+}
+
+.ol-popup-closer:after {
+ content: "✖";
+}/*# sourceMappingURL=popup.css.map */
\ No newline at end of file
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.css.map b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.css.map
new file mode 100644
index 0000000000..d2b27a5434
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["popup.css","popup.scss"],"names":[],"mappings":"AAAA,gBAAgB;ACAhB;EACE,kBAAA;EACA,uBAAA;EACA,wCAAA;EACA,aAAA;EACA,kBAAA;EACA,yBAAA;EAEA,gBAAA;EAEA,YAAA;EACA,WAAA;EACA,gBAAA;ADAF;;ACEA;;EAEE,SAAA;EACA,yBAAA;EACA,YAAA;EACA,SAAA;EACA,QAAA;EACA,kBAAA;EACA,oBAAA;ADCF;;ACCA;EACE,uBAAA;EACA,kBAAA;EACA,UAAA;EACA,kBAAA;ADEF;;ACAA;EACE,yBAAA;EACA,kBAAA;EACA,UAAA;EACA,kBAAA;ADGF;;ACDA;EACE,qBAAA;EACA,kBAAA;EACA,QAAA;EACA,UAAA;ADIF;;ACFA;EACE,YAAA;ADKF","file":"popup.css"}
\ No newline at end of file
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.scss b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.scss
new file mode 100644
index 0000000000..2890e1b352
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.scss
@@ -0,0 +1,50 @@
+.ol-popup {
+ position: absolute;
+ background-color: white;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+ // padding: 15px;
+ border-radius: 20px;
+ border: 1px solid #BDBDBD;
+ min-height: fit-content;
+ // top: 6px;
+ margin-top: 12px;
+ // min-height: 400px;
+ bottom: 12px;
+ left: -50px;
+ min-width: 280px;
+}
+.ol-popup:after,
+.ol-popup:before {
+ top: 100%;
+ border: solid transparent;
+ content: ' ';
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+.ol-popup:after {
+ border-top-color: white;
+ border-width: 10px;
+ left: 48px;
+ margin-left: -10px;
+}
+.ol-popup:before {
+ border-top-color: #cccccc;
+ border-width: 11px;
+ left: 48px;
+ margin-left: -11px;
+}
+.ol-popup-closer {
+ text-decoration: none;
+ position: absolute;
+ top: 12px;
+ right: 16px;
+}
+.ol-popup-closer:after {
+ content: '✖';
+}
+
+#popup-content {
+ height: fit-content;
+}
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/helpers/getFeatureGeojson.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/helpers/getFeatureGeojson.js
new file mode 100644
index 0000000000..953546f484
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/helpers/getFeatureGeojson.js
@@ -0,0 +1,13 @@
+import GeoJSON from 'ol/format/GeoJSON';
+
+export default function getFeatureGeojson(
+ feature,
+ options = {
+ dataProjection: 'EPSG:4326',
+ featureProjection: 'EPSG:3857',
+ },
+) {
+ const format = new GeoJSON();
+ const geoJsonStr = format.writeFeature(feature, options);
+ return JSON.parse(geoJsonStr);
+}
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/helpers/layerUtils.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/helpers/layerUtils.js
new file mode 100644
index 0000000000..565bf5649e
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/helpers/layerUtils.js
@@ -0,0 +1,62 @@
+/* eslint-disable no-promise-executor-return */
+import * as olExtent from 'ol/extent';
+import { Point } from 'ol/geom';
+import VectorLayer from 'ol/layer/Vector';
+import VectorTileLayer from 'ol/layer/VectorTile';
+import { fromLonLat } from 'ol/proj';
+
+export function zoomToLatLng(map, latLng, options = {}) {
+ const [lat, lng] = latLng;
+ map.getView().fit(new Point(fromLonLat([lng, lat])), {
+ padding: [50, 50, 50, 50],
+ duration: 900,
+ constrainResolution: true,
+ maxZoom: 18,
+ ...options,
+ });
+}
+
+export function getCurrentMapExtent(map) {
+ return map.getView().calculateExtent(map.getSize());
+}
+
+export function getOverallLayerExtentx(map) {
+ const extent = olExtent.createEmpty();
+ const layers = map.getLayers();
+ layers.forEach((layer) => {
+ if (layer instanceof VectorLayer) {
+ olExtent.extend(extent, layer.getSource().getExtent());
+ }
+ });
+ return extent;
+}
+
+export function isExtentValid(extent) {
+ return !extent.some((value) => value === Infinity || Number.isNaN(value));
+}
+
+export async function getOverallLayerExtent(map) {
+ const extent = olExtent.createEmpty();
+ const layerArr = map.getLayers().getArray();
+ for (let i = 0; i < layerArr.length; i += 1) {
+ const layer = layerArr[i];
+ if (layer instanceof VectorTileLayer) {
+ // eslint-disable-next-line
+ await new Promise((resolve) =>
+ layerArr[i].getSource().on('tileloadend', async (evt) => {
+ const feature = await evt.tile.getFeatures();
+ feature.forEach((feat) => {
+ const featureExtent = feat.getExtent();
+ olExtent.extend(extent, featureExtent);
+ });
+ resolve();
+ // setTimeout(() => resolve(), 1000);
+ }),
+ );
+ } else if (layer instanceof VectorLayer) {
+ const featExtent = layer.getSource().getExtent();
+ olExtent.extend(extent, featExtent);
+ }
+ }
+ return extent;
+}
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/helpers/styleUtils.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/helpers/styleUtils.js
new file mode 100644
index 0000000000..4c14f44fe2
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/helpers/styleUtils.js
@@ -0,0 +1,240 @@
+import { Fill, Stroke, Style, Icon, Circle, Text } from 'ol/style';
+
+export function hexToRgba(hex, opacity = 100) {
+ if (!hex) return '';
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ const r = parseInt(result[1], 16);
+ const g = parseInt(result[2], 16);
+ const b = parseInt(result[3], 16);
+ const a = opacity * 0.01;
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+}
+
+export function getPropertiesField(geojson) {
+ return Object.keys(geojson.features[0].properties);
+}
+
+export function getGeometryType(geojson) {
+ return geojson.features[0]?.geometry?.type;
+}
+
+// https://stackoverflow.com/questions/1152024/best-way-to-generate-a-random-color-in-javascript/1152508
+export function getRandomColor() {
+ return `#${`000000${Math.floor(Math.random() * 16777216).toString(16)}`.substr(-6)}`;
+}
+
+export const defaultStyles = {
+ lineColor: '#000000',
+ lineOpacity: 70,
+ fillColor: '#1a2fa2',
+ fillOpacity: 50,
+ lineThickness: 1,
+ circleRadius: 3,
+ dashline: 0,
+ showLabel: false,
+ customLabelText: null,
+ labelField: '',
+ labelFont: 'Calibri',
+ labelFontSize: 14,
+ labelColor: '#000000',
+ labelOpacity: 100,
+ labelOutlineWidth: 3,
+ labelOutlineColor: '#ffffff',
+ labelOffsetX: 0,
+ labelOffsetY: 0,
+ labelText: 'normal',
+ labelMaxResolution: 400,
+ labelAlign: 'center',
+ labelBaseline: 'middle',
+ labelRotationDegree: 0,
+ labelFontWeight: 'normal',
+ labelPlacement: 'point',
+ labelMaxAngleDegree: 45.0,
+ labelOverflow: false,
+ labelLineHeight: 1,
+ visibleOnMap: true,
+ icon: {},
+ showSublayer: false,
+ sublayerColumnName: '',
+ sublayer: {},
+};
+
+export const municipalStyles = {
+ ...defaultStyles,
+ fillOpacity: 0,
+ lineColor: '#ffffff',
+};
+
+export const defaultRasterStyles = {
+ opacity: 100,
+};
+
+export const sublayerKeysException = ['showSublayer', 'sublayerColumnName', 'sublayer'];
+
+function createIconMarker(style) {
+ const {
+ icon: { url, scale },
+ } = style;
+ // fetch(url)
+ // .then(res => res.blob())
+ return new Icon({
+ // anchor: [0.5, 46],
+ // anchorXUnits: 'fraction',
+ // anchorYUnits: 'pixels',
+ scale: scale || 0.9,
+ crossOrigin: 'anonymous',
+ // imgSize: [1500, 1500],
+ src: url,
+ });
+}
+
+function createCircleMarker(style) {
+ const { lineColor, lineOpacity, fillColor, fillOpacity, lineThickness, circleRadius } = style;
+ return new Circle({
+ radius: circleRadius,
+ stroke: new Stroke({
+ color: hexToRgba(lineColor, lineOpacity),
+ width: lineThickness,
+ }),
+ fill: new Fill({
+ color: hexToRgba(fillColor, fillOpacity),
+ }),
+ });
+}
+
+function stringDivider(str, width, spaceReplacer) {
+ if (str.length > width) {
+ let p = width;
+ while (p > 0 && str[p] !== ' ' && str[p] !== '-') {
+ p -= 1;
+ }
+ if (p > 0) {
+ let left;
+ if (str.substring(p, p + 1) === '-') {
+ left = str.substring(0, p + 1);
+ } else {
+ left = str.substring(0, p);
+ }
+ const right = str.substring(p + 1);
+ return left + spaceReplacer + stringDivider(right, width, spaceReplacer);
+ }
+ }
+ return str;
+}
+
+function truncString(str, n) {
+ return str.length > n ? `${str.substr(0, n - 1)}...` : str.substr(0);
+}
+
+function getText(style, feature, resolution) {
+ const type = style.labelText;
+ const maxResolution = style.labelMaxResolution;
+ let text = style.showLabel ? style.customLabelText || feature.get(style.labelField)?.toString() || '' : '';
+
+ if (resolution > maxResolution) {
+ text = '';
+ } else if (type === 'hide') {
+ text = '';
+ } else if (type === 'shorten') {
+ text = truncString(text, 12);
+ } else if (type === 'wrap' && (!style.labelPlacement || style.labelPlacement !== 'line')) {
+ text = stringDivider(text, 16, '\n');
+ }
+
+ return text;
+}
+
+function createTextStyle(style, feature, resolution) {
+ const {
+ labelFont,
+ labelFontSize,
+ labelColor,
+ labelOpacity,
+ labelOutlineWidth,
+ labelOutlineColor,
+ labelOffsetX,
+ labelOffsetY,
+ labelAlign,
+ labelBaseline,
+ labelFontWeight,
+ labelLineHeight,
+ labelPlacement,
+ labelMaxAngleDegree,
+ labelRotationDegree,
+ } = style;
+ const font = labelFont
+ ? `${labelFontWeight} ${labelFontSize}px/${labelLineHeight || 1} ${labelFont}`
+ : '14px Calibri';
+ return new Text({
+ text: getText(style, feature, resolution),
+ font,
+ overflow: true,
+ fill: new Fill({
+ color: hexToRgba(labelColor, labelOpacity),
+ }),
+ stroke: new Stroke({
+ color: hexToRgba(labelOutlineColor, labelOpacity),
+ width: parseInt(labelOutlineWidth, 10),
+ }),
+ offsetX: labelOffsetX,
+ offsetY: labelOffsetY,
+ textAlign: labelAlign || undefined,
+ textBaseline: labelBaseline,
+ placement: labelPlacement,
+ maxAngle: labelMaxAngleDegree,
+ rotation: parseFloat(labelRotationDegree) || 0,
+ });
+}
+
+export function generateLayerStylePoint(style, feature, resolution) {
+ const { icon } = style;
+ return new Style({
+ image: icon?.url ? createIconMarker(style) : createCircleMarker(style),
+ text: createTextStyle(style, feature, resolution),
+ });
+}
+
+export function generateLayerStylePolygon(style, feature, resolution) {
+ const { lineColor, lineOpacity, fillColor, fillOpacity, lineThickness, dashline } = style;
+ return new Style({
+ stroke: new Stroke({
+ color: hexToRgba(lineColor, lineOpacity),
+ width: lineThickness,
+ lineDash: [dashline],
+ }),
+ fill: new Fill({
+ color: hexToRgba(fillColor, fillOpacity),
+ }),
+ text: createTextStyle(style, feature, resolution),
+ });
+}
+
+export function generateLayerStyleLine(style, feature, resolution) {
+ return new Style({
+ stroke: new Stroke({
+ color: hexToRgba(style.lineColor, style.lineOpacity),
+ width: style.lineThickness,
+ }),
+ fill: new Fill({
+ color: hexToRgba(style.fillColor, style.fillOpacity),
+ }),
+ text: createTextStyle(style, feature, resolution),
+ });
+}
+
+export function getStyles({ style, feature, resolution }) {
+ const geometryType = feature.getGeometry().getType();
+ switch (geometryType) {
+ case 'Point':
+ return generateLayerStylePoint(style, feature, resolution);
+ case 'Polygon':
+ return generateLayerStylePolygon(style, feature, resolution);
+ case 'LineString':
+ return generateLayerStyleLine(style, feature, resolution);
+ case 'MultiLineString':
+ return generateLayerStyleLine(style, feature, resolution);
+
+ default:
+ return generateLayerStylePolygon(style, feature, resolution);
+ }
+}
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/index.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/index.js
new file mode 100644
index 0000000000..590d8e71b3
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/index.js
@@ -0,0 +1,3 @@
+export { default as MapContainer } from './MapContainer';
+
+export { default as useOLMap } from './useOLMap';
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.css b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.css
new file mode 100644
index 0000000000..d84aeb99db
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.css
@@ -0,0 +1,97 @@
+.ol-map {
+ width: 100%;
+ height: 100%;
+}
+
+.ol-control button {
+ color: #757575 !important;
+ font-size: 1.2rem !important;
+ width: 30px !important;
+ height: 30px !important;
+ line-height: 38px !important;
+ margin: 0 auto !important;
+ padding: 0 !important;
+ background-color: rgb(253, 254, 255) !important;
+ border: 1px solid #bdbdbd !important;
+ border-radius: 5px 5px 0 0 !important !important;
+ font-weight: normal !important;
+}
+
+.ol-control.ol-zoom button {
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+}
+
+.ol-control button:hover,
+.ol-control button:focus {
+ text-decoration: none !important;
+ background-color: #9da6e2 !important;
+ color: #011b8e !important;
+}
+
+.ol-zoom {
+ bottom: 1rem !important;
+ right: 1rem !important;
+ top: unset !important;
+ left: unset !important;
+}
+
+.ol-zoom-in {
+ border-radius: 5px 5px 0 0 !important;
+}
+
+.ol-zoom-out {
+ border-radius: 0 0 5px 5px !important;
+}
+
+.main__map {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0px;
+ left: 0px;
+ border: none;
+ z-index: -1;
+}
+
+.ol-scale-bar {
+ position: absolute !important;
+ bottom: 10px !important;
+ left: -70% !important;
+ margin: 55px 0 !important;
+ padding: 0px 15px !important;
+ overflow: visible !important;
+}
+
+.ol-scale-line {
+ background: rgb(66, 66, 66) !important;
+ position: absolute !important;
+ bottom: 10px !important;
+ left: -70% !important;
+}
+
+.layer-switcher {
+ bottom: 5.5rem !important;
+ right: 1rem !important;
+ top: unset !important;
+ left: unset !important;
+}
+.layer-switcher .panel {
+ z-index: 2;
+}
+.layer-switcher button {
+ background-image: none;
+ background: #fff;
+ border-radius: 5px !important;
+}
+.layer-switcher button::before {
+ font-family: "Material Icons";
+ content: "layers";
+ position: relative;
+ top: -3px;
+}
+
+.ol-attribution {
+ display: none !important;
+}/*# sourceMappingURL=map.css.map */
\ No newline at end of file
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.scss b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.scss
new file mode 100644
index 0000000000..9176fd6cb2
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.scss
@@ -0,0 +1,134 @@
+// .ol-map {
+// width: 100%;
+// height: 100%;
+// // min-width: 600px;
+// // min-height: 500px;
+// // border-radius: 10px;
+// }
+
+// .ol-control button {
+// color: #757575 !important;
+// font-size: 1.2rem !important;
+// width: 30px !important;
+// height: 30px !important;
+// line-height: 38px !important;
+// margin: 0 auto !important;
+// padding: 0 !important;
+// background-color: rgb(253, 254, 255) !important;
+// border: 1px solid #bdbdbd !important;
+// border-radius: 5px 5px 0 0 !important;
+// font-weight: normal !important;
+// }
+// .ol-control.ol-zoom button {
+// display: flex !important;
+// align-items: center !important;
+// justify-content: center !important;
+// }
+
+// .ol-control button:hover,
+// .ol-control button:focus {
+// text-decoration: none !important;
+// background-color: #9da6e2 !important;
+// color: #011b8e !important;
+// }
+
+// .ol-zoom {
+// bottom: 1rem !important;
+// right: 1rem !important;
+// top: unset !important;
+// left: unset !important;
+// }
+// .ol-zoom-in {
+// border-radius: 5px 5px 0 0 !important;
+// }
+// .ol-zoom-out {
+// border-radius: 0 0 5px 5px !important;
+// }
+
+// .main__map {
+// position: absolute;
+// width: 100%;
+// height: 100%;
+// top: 0px;
+// left: 0px;
+// border: none;
+// z-index: -1;
+// }
+
+// .ol-scale-bar {
+// position: absolute !important;
+// bottom: 10px !important;
+// left: -70% !important;
+// margin: 55px 0 !important;
+// padding: 0px 15px !important;
+// overflow: visible !important;
+
+// // .ol-scale-text {
+// // bottom: 35px !important;
+// // }
+
+// // .ol-scale-step-text {
+// // bottom: 0px !important;
+// // }
+// }
+
+// .ol-scale-line {
+// background: rgba(66, 66, 66, 1) !important;
+// position: absolute !important;
+// bottom: 8px !important;
+// left: 24% !important;
+// padding: 5px;
+// }
+// .ol-scale-line-inner {
+// // border: 1px solid #eee;
+// border-top: none;
+// color: #eee;
+// font-size: 10px;
+// text-align: center;
+// margin: 1px;
+// will-change: contents, width;
+// transition: all 0.25s;
+// }
+
+// .layer-switcher {
+// bottom: 5.5rem !important;
+// right: 1rem !important;
+// top: unset !important;
+// left: unset !important;
+
+// .panel {
+// z-index: 2;
+// }
+
+// button {
+// // display: block !important;
+// background-image: none;
+// background: #fff;
+// border-radius: 5px !important;
+// &::before {
+// font-family: 'Material Icons';
+// content: 'layers';
+// position: relative;
+// top: -3px;
+// }
+// }
+// }
+
+// .ol-attribution {
+// display: none !important;
+// }
+@keyframes blink {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+ }
+
+.blink {
+ animation: blink 1s infinite;
+}
\ No newline at end of file
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/useOLMap/index.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/useOLMap/index.js
new file mode 100644
index 0000000000..487a5b80c8
--- /dev/null
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/useOLMap/index.js
@@ -0,0 +1,68 @@
+/* eslint-disable consistent-return */
+import React,{ useRef, useState, useEffect } from 'react';
+import Map from 'ol/Map';
+import { View } from 'ol';
+import * as olExtent from 'ol/extent';
+import VectorLayer from 'ol/layer/Vector';
+
+const defaultProps = {
+ center: [0, 0],
+ zoom: 2,
+ maxZoom: 20,
+};
+
+const useOLMap = (props) => {
+ const settings = { ...defaultProps, ...props };
+ const { center, zoom, maxZoom } = settings;
+
+ const mapRef = useRef(null);
+ const [map, setMap] = useState(null);
+ const [renderComplete, setRenderComplete] = useState(null);
+
+ useEffect(() => {
+ const options = {
+ view: new View({
+ center,
+ zoom,
+ maxZoom,
+ }),
+ target: mapRef.current,
+ };
+ const mapInstance = new Map(options);
+ setMap(mapInstance);
+
+ return () => mapInstance.setTarget(undefined);
+ // eslint-disable-next-line
+ }, []);
+
+ useEffect(() => {
+ if (!map) return;
+
+ function onRenderComplete() {
+ const extent = olExtent.createEmpty();
+ const layers = map.getLayers();
+ layers.forEach((layer) => {
+ if (layer instanceof VectorLayer) {
+ olExtent.extend(extent, layer.getSource().getExtent());
+ }
+ });
+ if (extent[0] !== Infinity) {
+ setTimeout(() => {
+ setRenderComplete(true);
+ }, 500);
+ map.un('rendercomplete', onRenderComplete);
+ }
+ }
+ map.on('rendercomplete', onRenderComplete);
+
+ return () => {
+ if (map) {
+ map.un('rendercomplete', onRenderComplete);
+ }
+ };
+ }, [map]);
+
+ return { mapRef, map, renderComplete };
+};
+
+export default useOLMap;
diff --git a/src/frontend/main/src/components/home/ProjectListMap.tsx b/src/frontend/main/src/components/home/ProjectListMap.tsx
new file mode 100644
index 0000000000..639ae08da4
--- /dev/null
+++ b/src/frontend/main/src/components/home/ProjectListMap.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import PropTypes from "prop-types";
+
+const ProjectListMap = (props) => {
+ return (
+
+
Test
+
+ );
+};
+
+ProjectListMap.propTypes = {};
+
+export default ProjectListMap;
diff --git a/src/frontend/main/src/views/Home.jsx b/src/frontend/main/src/views/Home.jsx
index 9717447e92..45d95c2e40 100755
--- a/src/frontend/main/src/views/Home.jsx
+++ b/src/frontend/main/src/views/Home.jsx
@@ -8,6 +8,7 @@ import ProjectCardSkeleton from '../components/home/ProjectCardSkeleton';
import HomePageFilters from '../components/home/HomePageFilters';
import CoreModules from '../shared/CoreModules';
import AssetModules from '../shared/AssetModules';
+import ProjectListMap from '../components/home/ProjectListMap';
const Home = () => {
const [searchQuery, setSearchQuery] = useState('');
@@ -85,7 +86,7 @@ const Home = () => {
)}
{showMapStatus && (
-
+
)}
) : (