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 ( + {showMapStatus && ( -
+ )} ) : (