forked from VinokurovVE/tests
337 lines
12 KiB
TypeScript
337 lines
12 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
||
import GeoJSON from 'ol/format/GeoJSON'
|
||
import 'ol/ol.css'
|
||
import Map from 'ol/Map'
|
||
import View from 'ol/View'
|
||
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
|
||
import { OSM, Vector as VectorSource, XYZ } from 'ol/source'
|
||
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
|
||
import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box } from '@mui/material'
|
||
import { Adjust, Api, CircleOutlined, OpenWith, RectangleOutlined, Rule, Straighten, Timeline, Undo, Warning } from '@mui/icons-material'
|
||
import { Type } from 'ol/geom/Geometry'
|
||
import { click, noModifierKeys, shiftKeyOnly } from 'ol/events/condition'
|
||
import Feature from 'ol/Feature'
|
||
import { SatelliteMapsProvider } from '../../interfaces/map'
|
||
import { containsExtent } from 'ol/extent'
|
||
import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles'
|
||
import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources'
|
||
import { mapCenter, mapExtent } from './MapConstants'
|
||
|
||
const MapComponent = () => {
|
||
const mapElement = useRef<HTMLDivElement | null>(null)
|
||
const [currentTool, setCurrentTool] = useState<Type | null>(null)
|
||
|
||
const map = useRef<Map | null>(null)
|
||
|
||
const [satMapsProvider, setSatMapsProvider] = useState<SatelliteMapsProvider>('yandex')
|
||
|
||
const gMapsSatSource = useRef<XYZ>(googleMapsSatelliteSource)
|
||
|
||
const yMapsSatSource = useRef<XYZ>(yandexMapsSatelliteSource)
|
||
|
||
const satLayer = useRef<TileLayer>(new TileLayer({
|
||
source: gMapsSatSource.current,
|
||
}))
|
||
|
||
const draw = useRef<Draw | null>(null)
|
||
const snap = useRef<Snap | null>(null)
|
||
|
||
const selectFeature = useRef<Select>(new Select({
|
||
condition: function (mapBrowserEvent) {
|
||
return click(mapBrowserEvent) && shiftKeyOnly(mapBrowserEvent);
|
||
},
|
||
}))
|
||
|
||
const drawingLayer = useRef<VectorLayer | null>(null)
|
||
const drawingLayerSource = useRef<VectorSource>(new VectorSource())
|
||
|
||
const regionsLayer = useRef<VectorLayer>(new VectorLayer({
|
||
source: regionsLayerSource,
|
||
style: regionsLayerStyle
|
||
}))
|
||
|
||
const selectedRegion = useRef<Feature | null>(null)
|
||
|
||
const baseLayer = useRef<TileLayer>(new TileLayer({
|
||
source: new OSM(),
|
||
}))
|
||
|
||
const addInteractions = () => {
|
||
if (currentTool) {
|
||
draw.current = new Draw({
|
||
source: drawingLayerSource.current,
|
||
type: currentTool,
|
||
condition: noModifierKeys
|
||
})
|
||
map?.current?.addInteraction(draw.current)
|
||
snap.current = new Snap({ source: drawingLayerSource.current })
|
||
map?.current?.addInteraction(snap.current)
|
||
}
|
||
}
|
||
|
||
// Function to save features to localStorage
|
||
const saveFeatures = () => {
|
||
const features = drawingLayer.current?.getSource()?.getFeatures()
|
||
if (features && features.length > 0) {
|
||
const geoJSON = new GeoJSON()
|
||
const featuresJSON = geoJSON.writeFeatures(features)
|
||
localStorage.setItem('savedFeatures', featuresJSON)
|
||
}
|
||
|
||
console.log(drawingLayer.current?.getSource()?.getFeatures())
|
||
}
|
||
|
||
// Function to load features from localStorage
|
||
const loadFeatures = () => {
|
||
const savedFeatures = localStorage.getItem('savedFeatures')
|
||
if (savedFeatures) {
|
||
const geoJSON = new GeoJSON()
|
||
const features = geoJSON.readFeatures(savedFeatures, {
|
||
featureProjection: 'EPSG:4326', // Ensure the projection is correct
|
||
})
|
||
drawingLayerSource.current?.addFeatures(features) // Add features to the vector source
|
||
//drawingLayer.current?.getSource()?.changed()
|
||
}
|
||
}
|
||
|
||
const handleToolSelect = (tool: Type) => {
|
||
if (currentTool == tool) {
|
||
setCurrentTool(null)
|
||
} else {
|
||
setCurrentTool(tool)
|
||
}
|
||
}
|
||
|
||
const zoomToFeature = (feature: Feature) => {
|
||
const geometry = feature.getGeometry()
|
||
const extent = geometry?.getExtent()
|
||
|
||
if (map.current && extent) {
|
||
map.current.getView().fit(extent, {
|
||
duration: 300,
|
||
maxZoom: 19,
|
||
})
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
drawingLayer.current = new VectorLayer({
|
||
source: drawingLayerSource.current,
|
||
style: drawingLayerStyle,
|
||
})
|
||
|
||
map.current = new Map({
|
||
layers: [baseLayer.current, satLayer.current, regionsLayer.current, drawingLayer.current],
|
||
target: mapElement.current as HTMLDivElement,
|
||
view: new View({
|
||
center: mapCenter,
|
||
zoom: 2,
|
||
maxZoom: 21,
|
||
extent: mapExtent,
|
||
}),
|
||
})
|
||
|
||
const modify = new Modify({ source: drawingLayerSource.current })
|
||
map.current.addInteraction(modify)
|
||
|
||
map.current.addInteraction(selectFeature.current)
|
||
|
||
selectFeature.current.on('select', (e) => {
|
||
const selectedFeatures = e.selected
|
||
|
||
if (selectedFeatures.length > 0) {
|
||
selectedFeatures.forEach((feature) => {
|
||
drawingLayerSource.current?.removeFeature(feature)
|
||
})
|
||
}
|
||
})
|
||
|
||
loadFeatures()
|
||
|
||
// Show current selected region
|
||
map.current.on('pointermove', function (e) {
|
||
if (selectedRegion.current !== null) {
|
||
selectedRegion.current.setStyle(undefined)
|
||
selectedRegion.current = null
|
||
}
|
||
|
||
if (map.current) {
|
||
map.current.forEachFeatureAtPixel(e.pixel, function (f) {
|
||
selectedRegion.current = f as Feature
|
||
selectedRegion.current.setStyle(selectStyle)
|
||
|
||
if (f.get('district')) {
|
||
setStatusText(f.get('district'))
|
||
}
|
||
|
||
return true
|
||
})
|
||
}
|
||
})
|
||
|
||
map.current.on('click', function (e) {
|
||
if (selectedRegion.current !== null) {
|
||
selectedRegion.current = null
|
||
}
|
||
|
||
if (map.current) {
|
||
map.current.forEachFeatureAtPixel(e.pixel, function (f) {
|
||
selectedRegion.current = f as Feature
|
||
// Zoom to the selected feature
|
||
zoomToFeature(selectedRegion.current)
|
||
|
||
return true
|
||
});
|
||
}
|
||
|
||
})
|
||
|
||
// Hide regions layer when fully visible
|
||
map.current.on('moveend', function () {
|
||
const viewExtent = map.current?.getView().calculateExtent(map.current.getSize())
|
||
const features = regionsLayer.current.getSource()?.getFeatures()
|
||
|
||
let isViewCovered = false
|
||
|
||
features?.forEach((feature: Feature) => {
|
||
const featureExtent = feature?.getGeometry()?.getExtent()
|
||
if (viewExtent && featureExtent) {
|
||
if (containsExtent(featureExtent, viewExtent)) {
|
||
isViewCovered = true
|
||
}
|
||
}
|
||
})
|
||
|
||
regionsLayer.current.setVisible(!isViewCovered)
|
||
})
|
||
|
||
return () => {
|
||
map?.current?.setTarget(undefined)
|
||
}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
if (currentTool) {
|
||
if (draw.current) map?.current?.removeInteraction(draw.current)
|
||
if (snap.current) map?.current?.removeInteraction(snap.current)
|
||
addInteractions()
|
||
} else {
|
||
if (draw.current) map?.current?.removeInteraction(draw.current)
|
||
if (snap.current) map?.current?.removeInteraction(snap.current)
|
||
}
|
||
}, [currentTool])
|
||
|
||
const [satelliteOpacity, setSatelliteOpacity] = useState<number>(0)
|
||
|
||
const [statusText, setStatusText] = useState('')
|
||
|
||
// Visibility setting
|
||
useEffect(() => {
|
||
satLayer.current?.setOpacity(satelliteOpacity)
|
||
|
||
if (satelliteOpacity == 0) {
|
||
baseLayer.current?.setVisible(true)
|
||
satLayer.current?.setVisible(false)
|
||
} if (satelliteOpacity == 1) {
|
||
baseLayer.current?.setVisible(false)
|
||
satLayer.current?.setVisible(true)
|
||
} else if (satelliteOpacity > 0 && satelliteOpacity < 1) {
|
||
baseLayer.current?.setVisible(true)
|
||
satLayer.current?.setVisible(true)
|
||
}
|
||
}, [satelliteOpacity])
|
||
|
||
// Satellite tiles setting
|
||
useEffect(() => {
|
||
satLayer.current?.setSource(satMapsProvider == 'google' ? gMapsSatSource.current : satMapsProvider == 'yandex' ? yMapsSatSource.current : gMapsSatSource.current)
|
||
satLayer.current?.getSource()?.refresh()
|
||
}, [satMapsProvider])
|
||
|
||
return (
|
||
<Stack flex={1} flexDirection='column'>
|
||
<Stack my={1} spacing={1} direction='row' divider={<Divider orientation='vertical' flexItem />}>
|
||
|
||
<Stack flex={1} alignItems='center' justifyContent='center'>
|
||
<Slider aria-label="Opacity" min={0} max={1} step={0.001} defaultValue={satelliteOpacity} value={satelliteOpacity} onChange={(_, value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
|
||
</Stack>
|
||
|
||
<MUISelect
|
||
variant='standard'
|
||
labelId="demo-simple-select-label"
|
||
id="demo-simple-select"
|
||
value={satMapsProvider}
|
||
label="Satellite Provider"
|
||
onChange={(e) => setSatMapsProvider(e.target.value as SatelliteMapsProvider)}
|
||
>
|
||
<MenuItem value={'google'}>Google</MenuItem>
|
||
<MenuItem value={'yandex'}>Яндекс</MenuItem>
|
||
</MUISelect>
|
||
|
||
|
||
<IconButton onClick={() => {
|
||
fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res))
|
||
}}>
|
||
<Api />
|
||
</IconButton>
|
||
|
||
<IconButton onClick={() => {
|
||
saveFeatures()
|
||
}}>
|
||
<Warning />
|
||
</IconButton>
|
||
|
||
<IconButton
|
||
onClick={() => {
|
||
draw.current?.removeLastPoint()
|
||
}}>
|
||
<Undo />
|
||
</IconButton>
|
||
|
||
<IconButton
|
||
sx={{ backgroundColor: currentTool === 'Point' ? 'Highlight' : 'transparent' }}
|
||
onClick={() => handleToolSelect('Point')}>
|
||
<Adjust />
|
||
</IconButton>
|
||
|
||
<IconButton
|
||
sx={{ backgroundColor: currentTool === 'LineString' ? 'Highlight' : 'transparent' }}
|
||
onClick={() => handleToolSelect('LineString')}>
|
||
<Timeline />
|
||
</IconButton>
|
||
|
||
<IconButton
|
||
sx={{ backgroundColor: currentTool === 'Polygon' ? 'Highlight' : 'transparent' }}
|
||
onClick={() => handleToolSelect('Polygon')}>
|
||
<RectangleOutlined />
|
||
</IconButton>
|
||
|
||
<IconButton
|
||
sx={{ backgroundColor: currentTool === 'Circle' ? 'Highlight' : 'transparent' }}
|
||
onClick={() => handleToolSelect('Circle')}>
|
||
<CircleOutlined />
|
||
</IconButton>
|
||
|
||
<IconButton
|
||
onClick={() => map?.current?.addInteraction(new Translate())}
|
||
>
|
||
<OpenWith />
|
||
</IconButton>
|
||
|
||
<IconButton>
|
||
<Straighten />
|
||
</IconButton>
|
||
</Stack>
|
||
|
||
<Box>
|
||
<div id="map-container" ref={mapElement} style={{ width: '100%', height: '600px', maxHeight: '100%', position: 'relative', flexGrow: 1 }}></div>
|
||
</Box>
|
||
|
||
<Stack>
|
||
{statusText}
|
||
</Stack>
|
||
</Stack>
|
||
);
|
||
};
|
||
|
||
export default MapComponent
|