Files
tests/client/src/components/map/MapComponent.tsx
2024-08-26 16:11:37 +09:00

337 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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