588 lines
23 KiB
TypeScript
588 lines
23 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import 'ol/ol.css'
|
|
import { Modify } from 'ol/interaction'
|
|
import { ImageStatic, Vector as VectorSource } from 'ol/source'
|
|
import Feature from 'ol/Feature'
|
|
import { getCenter } from 'ol/extent'
|
|
import { highlightStyleRed, highlightStyleYellow } from './MapStyles'
|
|
import { customMapSource, googleMapsSatelliteSource, yandexMapsSatelliteSource } from './MapSources'
|
|
import { Geometry } from 'ol/geom'
|
|
import { fromExtent } from 'ol/geom/Polygon'
|
|
import { Coordinate } from 'ol/coordinate'
|
|
import { addFigures, addInteractions, addLines, getCitySettings, getFeatureByEntityId, handleImageDrop, loadFeatures, loadRegionBounds, loadRegionCapitals, zoomToFeature } from './mapUtils'
|
|
import useSWR, { SWRConfiguration } from 'swr'
|
|
import { fetcher } from '../../http/axiosInstance'
|
|
import { BASE_URL } from '../../constants'
|
|
import { IconBoxPadding, IconChevronLeft, } from '@tabler/icons-react'
|
|
import axios from 'axios'
|
|
import MapToolbar from './MapToolbar/MapToolbar'
|
|
import MapStatusbar from './MapStatusbar/MapStatusbar'
|
|
import { setAlignMode, setTypeRoles, useMapStore, setMapLabel, } from '../../store/map'
|
|
import ObjectTree from '../Tree/ObjectTree'
|
|
import { setSelectedDistrict, setSelectedRegion, setSelectedYear, useObjectsStore } from '../../store/objects'
|
|
import ObjectParameters from './ObjectParameters/ObjectParameters'
|
|
import TabsPane, { ITabsPane } from './TabsPane/TabsPane'
|
|
//import { useSearchParams } from 'react-router-dom'
|
|
import GeoJSON from 'ol/format/GeoJSON'
|
|
import MapLegend from './MapLegend/MapLegend'
|
|
import MapMode from './MapMode'
|
|
import MapPrint from './MapPrint/MapPrint'
|
|
import { Button, Divider, Spinner, Portal, Tooltip, Drawer } from '@fluentui/react-components'
|
|
import { useAppStore } from '../../store/app'
|
|
import { setDistrictsData, setRegionsData } from '../../store/regions'
|
|
import View from 'ol/View'
|
|
import { Style } from 'ol/style'
|
|
import MapRegionSelect from './MapRegionSelect/MapRegionSelect'
|
|
import MapLayersSelect from './MapLayers/MapLayersSelect'
|
|
import MapObjectSearch from './MapObjectSearch/MapObjectSearch'
|
|
import { useCapitals } from '../../hooks/map/useCapitals'
|
|
|
|
const swrOptions: SWRConfiguration = {
|
|
revalidateOnFocus: false
|
|
}
|
|
|
|
const MapComponent = ({
|
|
id,
|
|
active = false
|
|
}: {
|
|
id: string,
|
|
active: boolean,
|
|
}) => {
|
|
const { colorScheme } = useAppStore()
|
|
|
|
// Store
|
|
const { selectedYear, currentObjectId, selectedRegion, selectedDistrict } = useObjectsStore().id[id]
|
|
const {
|
|
mode, map, currentTool, alignMode, satMapsProvider,
|
|
selectedObjectType, measureDraw, draw, snap, translate,
|
|
drawingLayerSource,
|
|
satLayer, staticMapLayer, figuresLayer, linesLayer,
|
|
regionsLayer, districtBoundLayer, districtsLayer,
|
|
printAreaDraw, statusText, statusTextPosition, regionSelect, districtSelect,
|
|
capitalsLayer
|
|
} = useMapStore().id[id]
|
|
|
|
// Tab settings
|
|
const objectsPane: ITabsPane[] = [
|
|
{ title: 'Объекты', value: 'objects', view: <ObjectTree map_id={id} /> },
|
|
{ title: 'Неразмещенные', value: 'unplaced', view: <></> },
|
|
{ title: 'Другие', value: 'other', view: <></> },
|
|
]
|
|
// const paramsPane: ITabsPane[] = [
|
|
// { title: 'История изменений', value: 'history', view: <></> },
|
|
// { title: 'Параметры', value: 'parameters', view: <ObjectParameters map_id={id} /> },
|
|
// { title: 'Вычисляемые', value: 'calculated', view: <></> }
|
|
// ]
|
|
|
|
// Map
|
|
const mapElement = useRef<HTMLDivElement | null>(null)
|
|
|
|
// Get type roles
|
|
useSWR(`/gis/type-roles`, (url) => fetcher(url, BASE_URL.ems).then(res => {
|
|
if (Array.isArray(res)) {
|
|
setTypeRoles(id, res)
|
|
}
|
|
return res
|
|
}), swrOptions)
|
|
|
|
const { data: regionsData } = useSWR(`/general/regions`, (url) => fetcher(url, BASE_URL.ems).then(res => {
|
|
setRegionsData(res)
|
|
return res
|
|
}), swrOptions)
|
|
|
|
// Bounds: region
|
|
const { data: regionBoundsData } = useSWR(`/gis/bounds/region`, (url) => fetcher(url, BASE_URL.ems), swrOptions)
|
|
|
|
// Map init
|
|
useEffect(() => {
|
|
map?.setTarget(mapElement.current as HTMLDivElement)
|
|
|
|
if (drawingLayerSource) {
|
|
const modify = new Modify({ source: drawingLayerSource })
|
|
map?.addInteraction(modify)
|
|
}
|
|
|
|
loadFeatures(id)
|
|
}, [])
|
|
|
|
// First step: On region bounds loaded
|
|
useEffect(() => {
|
|
if (regionsLayer && regionBoundsData) {
|
|
loadRegionBounds(regionBoundsData, regionsLayer)
|
|
}
|
|
|
|
return () => {
|
|
if (regionsLayer) {
|
|
regionsLayer.getSource()?.clear()
|
|
}
|
|
}
|
|
}, [regionBoundsData])
|
|
|
|
useEffect(() => {
|
|
if (regionsData && Array.isArray(regionsData) && capitalsLayer) {
|
|
loadRegionCapitals(regionsData, capitalsLayer)
|
|
}
|
|
|
|
return () => {
|
|
if (capitalsLayer) {
|
|
capitalsLayer.getSource()?.clear()
|
|
}
|
|
}
|
|
}, [regionsData, capitalsLayer])
|
|
|
|
useEffect(() => {
|
|
if (selectedRegion === null && regionBoundsData) {
|
|
if (map) {
|
|
const extent = regionsLayer.getSource()?.getExtent()
|
|
|
|
if (extent) {
|
|
map?.setView(new View({
|
|
extent: extent,
|
|
showFullExtent: true,
|
|
}))
|
|
|
|
map.getView().fit(fromExtent(extent), { padding: [100, 100, 100, 100] })
|
|
}
|
|
|
|
regionsLayer.getSource()?.forEachFeature((feature) => {
|
|
if (feature.getProperties()['entity_id'] !== selectedRegion) {
|
|
feature.setStyle()
|
|
}
|
|
})
|
|
|
|
map.addInteraction(regionSelect)
|
|
}
|
|
}
|
|
}, [regionBoundsData, selectedRegion, map, regionsLayer])
|
|
|
|
useEffect(() => {
|
|
if (selectedRegion && map) {
|
|
regionsLayer.getSource()?.forEachFeature((feature) => {
|
|
if (feature.getProperties()['entity_id'] !== selectedRegion) {
|
|
feature.setStyle(new Style())
|
|
}
|
|
})
|
|
}
|
|
}, [selectedRegion, map])
|
|
|
|
// Last step: once selected scheme
|
|
useEffect(() => {
|
|
if (selectedDistrict && selectedYear && districtBoundLayer) {
|
|
const bounds = new VectorSource({
|
|
url: `${BASE_URL.ems}/gis/bounds/city/${selectedDistrict}`,
|
|
format: new GeoJSON(),
|
|
})
|
|
|
|
districtBoundLayer.setSource(bounds)
|
|
|
|
//
|
|
// bounds.on('featuresloadend', function () {
|
|
// map?.setView(new View({
|
|
// extent: bounds.getExtent()
|
|
// }))
|
|
// })
|
|
}
|
|
|
|
return () => {
|
|
if (districtBoundLayer) {
|
|
districtBoundLayer.getSource()?.clear()
|
|
}
|
|
}
|
|
}, [selectedDistrict, selectedYear, districtBoundLayer])
|
|
|
|
// Edit Mode: add interaction on tool change
|
|
useEffect(() => {
|
|
if (currentTool) {
|
|
if (draw) map?.removeInteraction(draw)
|
|
//if (snap.current) map?.current?.removeInteraction(snap.current)
|
|
addInteractions(id)
|
|
} else {
|
|
//map?.getInteractions().clear()
|
|
//addInteractions(id)
|
|
if (translate) map?.removeInteraction(translate)
|
|
if (draw) map?.removeInteraction(draw)
|
|
if (snap) map?.removeInteraction(snap)
|
|
if (measureDraw) map?.removeInteraction(measureDraw)
|
|
}
|
|
}, [currentTool])
|
|
|
|
// Satellite tiles setting
|
|
useEffect(() => {
|
|
if (satLayer) {
|
|
if (satMapsProvider === 'google') {
|
|
satLayer.setSource(googleMapsSatelliteSource)
|
|
}
|
|
|
|
if (satMapsProvider === 'yandex') {
|
|
satLayer.setSource(yandexMapsSatelliteSource)
|
|
}
|
|
|
|
if (satMapsProvider === 'custom') {
|
|
satLayer.setSource(customMapSource)
|
|
}
|
|
}
|
|
}, [satMapsProvider, satLayer])
|
|
|
|
useEffect(() => {
|
|
if (!selectedObjectType || !map) return
|
|
|
|
if (figuresLayer) {
|
|
// Reset styles and apply highlight to matching features
|
|
figuresLayer.getSource()?.getFeatures().forEach((feature) => {
|
|
if (selectedObjectType == feature.get('type')) {
|
|
feature.setStyle(highlightStyleYellow)
|
|
} else {
|
|
feature.setStyle(undefined) // Reset to default style
|
|
}
|
|
})
|
|
}
|
|
|
|
if (linesLayer) {
|
|
// Reset styles and apply highlight to matching features
|
|
linesLayer.getSource()?.getFeatures().forEach((feature) => {
|
|
if (selectedObjectType == feature.get('type')) {
|
|
feature.setStyle(highlightStyleYellow)
|
|
} else {
|
|
feature.setStyle(undefined) // Reset to default style
|
|
}
|
|
})
|
|
}
|
|
}, [selectedObjectType])
|
|
|
|
useEffect(() => {
|
|
if (currentObjectId) {
|
|
if (figuresLayer) {
|
|
// Reset styles and apply highlight to matching features
|
|
figuresLayer.getSource()?.getFeatures().forEach((feature: Feature) => {
|
|
if (Array.isArray(feature.get('object_id')) ? feature.get('object_id')[0] === currentObjectId : currentObjectId === feature.get('object_id')) {
|
|
feature.setStyle(highlightStyleRed)
|
|
|
|
zoomToFeature(id, feature)
|
|
} else {
|
|
feature.setStyle(undefined) // Reset to default style
|
|
}
|
|
})
|
|
}
|
|
|
|
if (linesLayer) {
|
|
// Reset styles and apply highlight to matching features
|
|
linesLayer.getSource()?.getFeatures().forEach((feature: Feature) => {
|
|
if (Array.isArray(feature.get('object_id')) ? feature.get('object_id')[0] === currentObjectId : currentObjectId === feature.get('object_id')) {
|
|
feature.setStyle(highlightStyleRed)
|
|
|
|
zoomToFeature(id, feature)
|
|
} else {
|
|
feature.setStyle(undefined) // Reset to default style
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}, [currentObjectId, figuresLayer, linesLayer])
|
|
|
|
const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.ems).then(res => {
|
|
setDistrictsData(res)
|
|
return res
|
|
}), swrOptions)
|
|
|
|
const { data: figuresData, isValidating: figuresValidating } = useSWR(
|
|
selectedDistrict && selectedYear ? `/gis/figures/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
|
|
(url) => axios.get(url, {
|
|
baseURL: BASE_URL.ems
|
|
}).then((res) => res.data),
|
|
swrOptions
|
|
)
|
|
|
|
const { data: linesData, isValidating: linesValidating } = useSWR(
|
|
!figuresValidating && selectedDistrict && selectedYear ? `/gis/lines/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
|
|
(url) => axios.get(url, {
|
|
baseURL: BASE_URL.ems
|
|
}).then((res) => res.data),
|
|
swrOptions
|
|
)
|
|
|
|
const { data: districtData } = useSWR(
|
|
selectedDistrict ? `/gis/images/all?city_id=${selectedDistrict}` : null,
|
|
(url) => axios.get(url, {
|
|
baseURL: BASE_URL.ems
|
|
}).then((res) => Array.isArray(res.data) ? res.data[0] : null),
|
|
swrOptions
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (Array.isArray(districtsData)) {
|
|
districtsData.map((district) => {
|
|
if (district.id === selectedDistrict) {
|
|
setMapLabel(id, [district.name, district.district_name, selectedYear].join(' - '))
|
|
}
|
|
})
|
|
}
|
|
}, [districtsData, id, selectedDistrict, selectedYear])
|
|
|
|
useEffect(() => {
|
|
if (selectedDistrict === null) {
|
|
setSelectedYear(id, null)
|
|
|
|
map?.addInteraction(districtSelect)
|
|
}
|
|
|
|
if (selectedRegion === null) {
|
|
setSelectedYear(id, null)
|
|
setSelectedDistrict(id, null)
|
|
}
|
|
}, [selectedDistrict, selectedRegion, id, map])
|
|
|
|
const [leftPaneHidden, setLeftPaneHidden] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const districtBoundSource = districtBoundLayer.getSource()
|
|
|
|
if (map && selectedDistrict && districtBoundSource && districtBoundSource.getFeatures().length > 0) {
|
|
const center: Coordinate = getCenter(districtBoundSource.getExtent())
|
|
const settings = getCitySettings()
|
|
|
|
addFigures(center, figuresData, figuresLayer, settings, map)
|
|
|
|
addLines(center, linesData, linesLayer, settings)
|
|
}
|
|
|
|
}, [map, figuresData, linesData, selectedDistrict, selectedYear, districtBoundLayer])
|
|
|
|
useEffect(() => {
|
|
if (selectedDistrict && districtData) {
|
|
const settings = getCitySettings()
|
|
|
|
const imageUrl = `${import.meta.env.VITE_API_EMS_URL}/tiles/static/${selectedDistrict}`
|
|
const img = new Image()
|
|
img.src = imageUrl
|
|
img.onload = () => {
|
|
if (map) {
|
|
const width = img.naturalWidth
|
|
const height = img.naturalHeight
|
|
//const k = (width < height ? width / height : height / width)
|
|
const k = settings.image_scale
|
|
|
|
const wk = width * k
|
|
const hk = height * k
|
|
|
|
const center = [settings.offset_x + (wk), settings.offset_y - (hk)]
|
|
|
|
const extent = [center[0] - (wk), center[1] - (hk), center[0] + (wk), center[1] + (hk)]
|
|
|
|
// Set up the initial image layer with the extent
|
|
const imageSource = new ImageStatic({
|
|
url: imageUrl,
|
|
imageExtent: extent,
|
|
})
|
|
staticMapLayer.setSource(imageSource)
|
|
|
|
//map.current.addLayer(imageLayer.current)
|
|
}
|
|
}
|
|
}
|
|
}, [selectedDistrict, districtData, staticMapLayer])
|
|
|
|
useEffect(() => {
|
|
if (map) {
|
|
if (mode === 'print') {
|
|
map.addInteraction(printAreaDraw)
|
|
} else {
|
|
map.removeInteraction(printAreaDraw)
|
|
}
|
|
}
|
|
}, [mode, map, printAreaDraw])
|
|
|
|
useEffect(() => {
|
|
if (districtsData && Array.isArray(districtsData)) {
|
|
const list: Number[] = []
|
|
districtsData.map(district => {
|
|
list.push(district.id as Number)
|
|
})
|
|
|
|
fetch(`${BASE_URL.ems}/gis/bounds/city`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ list })
|
|
}).then(async response => {
|
|
const data = await response.json()
|
|
|
|
if (Array.isArray(data)) {
|
|
data.map(bound => {
|
|
const geoJson = new GeoJSON() //new GeoJSON({ featureProjection: 'EPSG:3857' })
|
|
const geometry = geoJson.readGeometry(bound) as Geometry
|
|
|
|
const feature = new Feature(geometry)
|
|
feature.setProperties(bound.properties)
|
|
|
|
districtsLayer.getSource()?.addFeature(feature)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}, [districtsData])
|
|
|
|
const mapTooltipRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (mapTooltipRef.current) {
|
|
const leftOffset = 30
|
|
const topOffset = -30
|
|
mapTooltipRef.current.style.left = (statusTextPosition[0] + leftOffset).toString() + 'px'
|
|
mapTooltipRef.current.style.top = (statusTextPosition[1] + topOffset).toString() + 'px'
|
|
}
|
|
}, [statusTextPosition, mapTooltipRef])
|
|
|
|
// zoom on region select
|
|
useEffect(() => {
|
|
if (selectedRegion && !selectedDistrict) {
|
|
const feature = getFeatureByEntityId(selectedRegion, regionsLayer)
|
|
|
|
if (feature) {
|
|
regionSelect.getFeatures().push(feature)
|
|
map?.setView(new View({
|
|
extent: feature?.getGeometry()?.getExtent(),
|
|
showFullExtent: true,
|
|
}))
|
|
}
|
|
|
|
zoomToFeature(id, feature)
|
|
} else if (selectedDistrict) {
|
|
// zoom on district select
|
|
const feature = getFeatureByEntityId(selectedDistrict, districtsLayer)
|
|
|
|
if (feature) {
|
|
districtSelect.getFeatures().push(feature)
|
|
|
|
regionsLayer.setOpacity(0)
|
|
}
|
|
|
|
// map?.setView(new View({
|
|
// extent: feature?.getGeometry()?.getExtent(),
|
|
// showFullExtent: true,
|
|
// }))
|
|
|
|
zoomToFeature(id, feature)
|
|
} else if (!selectedRegion) {
|
|
setSelectedRegion(id, null)
|
|
setSelectedYear(id, null)
|
|
}
|
|
}, [selectedRegion, selectedDistrict, id])
|
|
|
|
useCapitals(capitalsLayer, !!selectedDistrict)
|
|
|
|
useEffect(() => {
|
|
if (selectedYear) {
|
|
regionsLayer.setOpacity(0)
|
|
districtsLayer.setOpacity(0)
|
|
}
|
|
}, [selectedYear])
|
|
|
|
|
|
return (
|
|
<div style={{ display: 'grid', gridTemplateRows: 'auto min-content', position: 'relative', width: '100%', height: '100%' }}>
|
|
<MapPrint id={id} mapElement={mapElement} />
|
|
|
|
{active &&
|
|
<Portal mountNode={document.querySelector('#header-portal')}>
|
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
|
<MapObjectSearch map_id={id} />
|
|
|
|
<Button icon={<IconBoxPadding />} appearance={alignMode ? 'primary' : 'transparent'} onClick={() => setAlignMode(id, !alignMode)} />
|
|
|
|
<MapLayersSelect map_id={id} />
|
|
</div>
|
|
</Portal >
|
|
}
|
|
|
|
<div style={{ gridRow: '1 / span 1', display: 'grid', gridTemplateColumns: 'min-content auto', position: 'relative' }}>
|
|
<div style={{ gridColumn: '1 / span 1', width: !selectedRegion || (!!selectedRegion && !selectedYear) ? '300px' : 'min-content', height: '100%', position: 'relative', display: 'flex', zIndex: '2' }}>
|
|
<MapRegionSelect map_id={id} />
|
|
</div>
|
|
|
|
<div ref={mapElement} id={id} key={id} style={{ gridColumn: '2 / span 1', position: 'relative', width: '100%', height: '100%', maxHeight: '100%', zIndex: '1', filter: colorScheme === 'dark' ? 'invert(100%) hue-rotate(180deg)' : 'unset' }} onDragOver={(e) => e.preventDefault()} onDrop={(e) => handleImageDrop(e, id)}>
|
|
<div>
|
|
{statusText && active && !selectedYear &&
|
|
<Tooltip hideDelay={0} showDelay={0} content={statusText} relationship='description' visible>
|
|
<div style={{ position: 'absolute', zIndex: 9999, userSelect: 'none', pointerEvents: 'none' }} ref={mapTooltipRef}>
|
|
{/* {statusText} */}
|
|
</div>
|
|
</Tooltip>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ position: 'absolute', display: 'flex', flexDirection: 'column', width: '100%', height: '100%' }}>
|
|
<div style={{ display: 'flex', height: '94%', padding: '0.5rem', flexGrow: 1 }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', maxWidth: '380px' }}>
|
|
<div style={{ display: 'flex', width: '100%', height: '100%', gap: '0.5rem' }}>
|
|
<Drawer style={{ borderRadius: '0.25rem', height: '100%', zIndex: 1 }} type='inline' open={!!selectedRegion && !!selectedDistrict && !!selectedYear && !leftPaneHidden}>
|
|
<TabsPane defaultTab='objects' tabs={objectsPane} />
|
|
<Divider />
|
|
<ObjectParameters map_id={id} />
|
|
{/* <TabsPane defaultTab='parameters' tabs={paramsPane} /> */}
|
|
</Drawer>
|
|
|
|
{!!selectedRegion && !!selectedDistrict && !!selectedYear &&
|
|
<Button
|
|
icon={<IconChevronLeft size={16}
|
|
style={{
|
|
transform: `${leftPaneHidden ? 'rotate(180deg)' : ''}`,
|
|
}} />}
|
|
|
|
style={{
|
|
zIndex: '1',
|
|
display: 'flex',
|
|
height: 'min-content'
|
|
}}
|
|
appearance='subtle'
|
|
onClick={() => setLeftPaneHidden(!leftPaneHidden)}
|
|
/>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', alignItems: 'center' }} >
|
|
<div style={{ display: 'flex', flexDirection: 'column', width: 'fit-content' }}>
|
|
{selectedDistrict && selectedYear && <MapMode map_id={id} />}
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', maxWidth: '340px', alignItems: 'flex-end', justifyContent: 'space-between', gap: '1rem' }}>
|
|
<MapToolbar map_id={id} />
|
|
|
|
{!!selectedRegion && !!selectedDistrict && !!selectedYear &&
|
|
<MapLegend selectedDistrict={selectedDistrict} selectedYear={selectedYear} />
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{(linesValidating || figuresValidating) && (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
inset: 0,
|
|
backgroundColor: "rgba(255, 255, 255, 0.6)",
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
zIndex: 9999,
|
|
}}
|
|
>
|
|
<Spinner size="large" label="Загрузка..." />
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ gridRow: '2 / span 1', display: 'flex', bottom: '0', width: '100%' }}>
|
|
<MapStatusbar
|
|
map_id={id}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default MapComponent
|