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: }, { title: 'Неразмещенные', value: 'unplaced', view: <>> }, { title: 'Другие', value: 'other', view: <>> }, ] // const paramsPane: ITabsPane[] = [ // { title: 'История изменений', value: 'history', view: <>> }, // { title: 'Параметры', value: 'parameters', view: }, // { title: 'Вычисляемые', value: 'calculated', view: <>> } // ] // Map const mapElement = useRef(null) // Get type roles useSWR(`/gis/type-roles`, (url) => fetcher(url, BASE_URL.nest).then(res => { if (Array.isArray(res)) { setTypeRoles(id, res) } return res }), swrOptions) const { data: regionsData } = useSWR(`/general/regions`, (url) => fetcher(url, BASE_URL.nest).then(res => { setRegionsData(res) return res }), swrOptions) // Bounds: region const { data: regionBoundsData } = useSWR(`/gis/bounds/region`, (url) => fetcher(url, BASE_URL.nest), 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.nest}/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.nest).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.nest }).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.nest }).then((res) => res.data), swrOptions ) const { data: districtData } = useSWR( selectedDistrict ? `/gis/images/all?city_id=${selectedDistrict}` : null, (url) => axios.get(url, { baseURL: BASE_URL.nest }).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.nest}/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(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 ( {active && } appearance={alignMode ? 'primary' : 'transparent'} onClick={() => setAlignMode(id, !alignMode)} /> } e.preventDefault()} onDrop={(e) => handleImageDrop(e, id)}> {statusText && active && !selectedYear && {/* {statusText} */} } {/* */} {!!selectedRegion && !!selectedDistrict && !!selectedYear && } style={{ zIndex: '1', display: 'flex', height: 'min-content' }} appearance='subtle' onClick={() => setLeftPaneHidden(!leftPaneHidden)} /> } {selectedDistrict && selectedYear && } {!!selectedRegion && !!selectedDistrict && !!selectedYear && } {(linesValidating || figuresValidating) && ( )} ) } export default MapComponent