Files
tests/client/src/components/map/MapComponent.tsx
cracklesparkle bd0a317e76 Object data
2024-11-26 18:00:18 +09:00

983 lines
46 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react'
import 'ol/ol.css'
import Map from 'ol/Map'
import View from 'ol/View'
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
import { ImageStatic, OSM, Vector as VectorSource, XYZ } from 'ol/source'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { click, never, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
import Feature from 'ol/Feature'
import { IRectCoords, SatelliteMapsProvider } from '../../interfaces/map'
import { Extent } from 'ol/extent'
import { drawingLayerStyle, highlightStyleRed, highlightStyleYellow, overlayStyle, regionsLayerStyle } from './MapStyles'
import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources'
import { mapCenter } from './MapConstants'
import ImageLayer from 'ol/layer/Image'
import VectorImageLayer from 'ol/layer/VectorImage'
import { LineString, Point } from 'ol/geom'
import { fromExtent } from 'ol/geom/Polygon'
import Collection from 'ol/Collection'
import { Coordinate } from 'ol/coordinate'
import { addInteractions, calculateCenter, loadFeatures, processFigure, processLine, regionsInit, saveFeatures, updateImageSource } from './mapUtils'
import MapBrowserEvent from 'ol/MapBrowserEvent'
import { get, transform } from 'ol/proj'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Accordion, ActionIcon, Autocomplete, Box, CloseButton, Flex, Select as MantineSelect, Text as MantineText, MantineStyleProp, rem, ScrollAreaAutosize, Slider, useMantineColorScheme, Portal, Tree, Group, TreeNodeData, Button, useTree, Timeline, Text, Stack, Overlay } from '@mantine/core'
import { IconChevronDown, IconPlus, IconSettings, IconTable, IconUpload } from '@tabler/icons-react'
import { getGridCellPosition } from './mapUtils'
import { IFigure, ILine } from '../../interfaces/gis'
import axios from 'axios'
import ObjectParameter from './ObjectParameter'
import { IObjectData, IObjectList, IObjectParam } from '../../interfaces/objects'
import ObjectData from './ObjectData'
import MapToolbar from './MapToolbar/MapToolbar'
import MapStatusbar from './MapStatusbar/MapStatusbar'
import { measureStyleFunction, modifyStyle } from './Measure/MeasureStyles'
import { useMapStore } from '../../store/map'
import { MapTreeCheckbox } from './MapTree/MapTreeCheckbox'
import { v4 as uuidv4 } from 'uuid'
const MapComponent = () => {
const mapState = useMapStore()
const [currentCoordinate, setCurrentCoordinate] = useState<Coordinate | null>(null)
const [currentZ, setCurrentZ] = useState<number | undefined>(undefined)
const [currentX, setCurrentX] = useState<number | undefined>(undefined)
const [currentY, setCurrentY] = useState<number | undefined>(undefined)
const [file, setFile] = useState<File | null>(null)
const [polygonExtent, setPolygonExtent] = useState<Extent | undefined>(undefined)
const [rectCoords, setRectCoords] = useState<IRectCoords | undefined>(undefined)
// Measure
const measureSource = useRef(new VectorSource())
const measureModify = useRef(new Modify({ source: measureSource.current, style: modifyStyle }))
const measureLayer = useRef(new VectorLayer({
source: measureSource.current,
style: function (feature) {
return measureStyleFunction(feature);
},
properties: {
id: uuidv4(),
type: 'measure',
name: 'Линейка'
}
}))
const measureDraw = useRef<Draw | null>(null)
/////
const mapElement = useRef<HTMLDivElement | null>(null)
const map = useRef<Map | null>(null)
const [satMapsProvider, setSatMapsProvider] = useState<SatelliteMapsProvider>('custom')
const gMapsSatSource = useRef<XYZ>(googleMapsSatelliteSource)
const customMapSource = useRef<XYZ>(new XYZ({
url: `${import.meta.env.VITE_API_EMS_URL}/tiles/tile/custom/{z}/{x}/{y}`,
attributions: 'Custom map data'
}))
const yMapsSatSource = useRef<XYZ>(yandexMapsSatelliteSource)
const satLayer = useRef<TileLayer>(new TileLayer({
source: gMapsSatSource.current,
properties: {
id: uuidv4(),
name: 'Спутник'
}
}))
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 nodeLayer = useRef<VectorLayer | null>(null)
const nodeLayerSource = useRef<VectorSource>(new VectorSource())
const overlayLayer = useRef<VectorLayer | null>(null)
const overlayLayerSource = useRef<VectorSource>(new VectorSource())
const drawingLayer = useRef<VectorLayer | null>(null)
const drawingLayerSource = useRef<VectorSource>(new VectorSource())
const citiesLayer = useRef<VectorLayer>(new VectorLayer({
source: new VectorSource(),
properties: {
id: uuidv4(),
name: 'Города'
}
}))
const figuresLayer = useRef<VectorLayer>(new VectorLayer({
source: new VectorSource(),
properties: {
id: uuidv4(),
name: 'Фигуры'
}
}))
const linesLayer = useRef<VectorLayer>(new VectorLayer({
source: new VectorSource(),
properties: {
id: uuidv4(),
name: 'Линии'
}
}))
const regionsLayer = useRef<VectorImageLayer>(new VectorImageLayer({
source: regionsLayerSource,
style: regionsLayerStyle,
properties: {
id: uuidv4(),
name: 'Регион'
}
}))
const selectedRegion = useRef<Feature | null>(null)
const baseLayer = useRef<TileLayer>(new TileLayer({
source: new OSM(),
properties: {
id: uuidv4(),
name: 'OpenStreetMap'
}
}))
const imageLayer = useRef<ImageLayer<ImageStatic>>(new ImageLayer({
properties: {
id: uuidv4(),
name: 'Изображение'
}
}))
// tile processing
const handleImageDrop = useCallback((event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
if (!event.dataTransfer?.files) return
const files = event.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
setFile(file)
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = () => {
const imageUrl = reader.result as string;
const img = new Image();
img.src = imageUrl;
img.onload = () => {
if (map.current) {
const view = map.current.getView();
const center = view.getCenter() || [0, 0];
const width = img.naturalWidth;
const height = img.naturalHeight;
const resolution = view.getResolution() || 0;
const extent = [
center[0] - (width * resolution) / 20,
center[1] - (height * resolution) / 20,
center[0] + (width * resolution) / 20,
center[1] + (height * resolution) / 20,
];
// Create a polygon feature with the same extent as the image
const polygonFeature = new Feature({
geometry: fromExtent(extent),
});
// Add the polygon feature to the drawing layer source
overlayLayerSource.current?.addFeature(polygonFeature);
// Set up the initial image layer with the extent
const imageSource = new ImageStatic({
url: imageUrl,
imageExtent: extent,
});
imageLayer.current.setSource(imageSource);
//map.current.addLayer(imageLayer.current);
// Add interactions for translation and scaling
const translate = new Translate({
layers: [imageLayer.current],
features: new Collection([polygonFeature]),
});
const defaultStyle = new Modify({ source: overlayLayerSource.current })
.getOverlay()
.getStyleFunction();
const modify = new Modify({
insertVertexCondition: never,
source: overlayLayerSource.current,
condition: function (event) {
return primaryAction(event) && !platformModifierKeyOnly(event);
},
deleteCondition: never,
features: new Collection([polygonFeature]),
style: function (feature) {
feature.get('features').forEach(function (modifyFeature: Feature) {
const modifyGeometry = modifyFeature.get('modifyGeometry')
if (modifyGeometry) {
const point = (feature.getGeometry() as Point).getCoordinates()
let modifyPoint = modifyGeometry.point
if (!modifyPoint) {
// save the initial geometry and vertex position
modifyPoint = point;
modifyGeometry.point = modifyPoint;
modifyGeometry.geometry0 = modifyGeometry.geometry;
// get anchor and minimum radius of vertices to be used
const result = calculateCenter(modifyGeometry.geometry0);
modifyGeometry.center = result.center;
modifyGeometry.minRadius = result.minRadius;
}
const center = modifyGeometry.center;
const minRadius = modifyGeometry.minRadius;
let dx, dy;
dx = modifyPoint[0] - center[0];
dy = modifyPoint[1] - center[1];
const initialRadius = Math.sqrt(dx * dx + dy * dy);
if (initialRadius > minRadius) {
const initialAngle = Math.atan2(dy, dx);
dx = point[0] - center[0];
dy = point[1] - center[1];
const currentRadius = Math.sqrt(dx * dx + dy * dy);
if (currentRadius > 0) {
const currentAngle = Math.atan2(dy, dx);
const geometry = modifyGeometry.geometry0.clone();
geometry.scale(currentRadius / initialRadius, undefined, center);
geometry.rotate(currentAngle - initialAngle, center);
modifyGeometry.geometry = geometry;
}
}
}
})
const res = map?.current?.getView()?.getResolution()
if (typeof res === 'number' && feature && defaultStyle) {
return defaultStyle(feature, res)
}
}
});
translate.on('translateend', () => updateImageSource(imageUrl, imageLayer, polygonFeature, setPolygonExtent, setRectCoords));
//modify.on('modifyend', updateImageSource);
modify.on('modifystart', function (event) {
event.features.forEach(function (feature) {
feature.set(
'modifyGeometry',
{ geometry: feature.getGeometry()?.clone() },
true,
);
});
});
modify.on('modifyend', function (event) {
event.features.forEach(function (feature) {
const modifyGeometry = feature.get('modifyGeometry');
if (modifyGeometry) {
feature.setGeometry(modifyGeometry.geometry);
feature.unset('modifyGeometry', true);
}
})
updateImageSource(imageUrl, imageLayer, polygonFeature, setPolygonExtent, setRectCoords)
})
map.current.addInteraction(translate);
map.current.addInteraction(modify);
}
};
};
reader.readAsDataURL(file);
}
}
}, [])
useEffect(() => {
drawingLayer.current = new VectorLayer({
source: drawingLayerSource.current,
style: drawingLayerStyle,
properties: {
id: uuidv4(),
name: 'Чертеж'
}
})
overlayLayer.current = new VectorLayer({
source: overlayLayerSource.current,
style: overlayStyle,
properties: {
id: uuidv4(),
name: 'Наложения'
}
})
nodeLayer.current = new VectorLayer({
source: nodeLayerSource.current,
style: drawingLayerStyle,
properties: {
id: uuidv4(),
name: 'Узлы'
}
})
map.current = new Map({
controls: [],
layers: [
baseLayer.current,
satLayer.current,
regionsLayer.current,
citiesLayer.current,
linesLayer.current,
figuresLayer.current,
drawingLayer.current,
imageLayer.current,
overlayLayer.current,
nodeLayer.current,
measureLayer.current
],
target: mapElement.current as HTMLDivElement,
view: new View({
center: transform([129.7659541, 62.009504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]),
zoom: 17,
maxZoom: 21,
//extent: mapExtent,
}),
})
map.current.on('pointermove', function (e: MapBrowserEvent<any>) {
setCurrentCoordinate(e.coordinate)
const currentExtent = get('EPSG:3857')?.getExtent() as Extent
const { tileX, tileY } = getGridCellPosition(e.coordinate[0], e.coordinate[1], currentExtent, Number(map.current?.getView().getZoom()?.toFixed(0)))
setCurrentZ(Number(map.current?.getView().getZoom()?.toFixed(0)))
setCurrentX(tileX)
setCurrentY(tileY)
})
map.current.on('click', function (e: MapBrowserEvent<any>) {
const pixel = map.current?.getEventPixel(e.originalEvent)
if (pixel) {
map.current?.forEachFeatureAtPixel(pixel, function (feature) {
setCurrentObjectId(feature.get('object_id'))
})
}
})
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(drawingLayerSource)
regionsInit(map, selectedRegion, regionsLayer, setStatusText)
if (mapElement.current) {
mapElement.current.addEventListener('dragover', (e) => {
e.preventDefault()
})
mapElement.current.addEventListener('drop', handleImageDrop)
}
return () => {
map?.current?.setTarget(undefined)
if (mapElement.current) {
mapElement.current.removeEventListener('drop', handleImageDrop)
}
}
}, [])
useEffect(() => {
if (mapState.currentTool) {
if (draw.current) map?.current?.removeInteraction(draw.current)
//if (snap.current) map?.current?.removeInteraction(snap.current)
addInteractions(drawingLayerSource, draw, map, snap, measureDraw, measureSource, measureModify)
} else {
if (draw.current) map?.current?.removeInteraction(draw.current)
if (snap.current) map?.current?.removeInteraction(snap.current)
if (measureDraw.current) map?.current?.removeInteraction(measureDraw.current)
}
}, [mapState.currentTool])
const [satelliteOpacity, setSatelliteOpacity] = useState<number>(1)
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 : satMapsProvider == 'custom' ? customMapSource.current : gMapsSatSource.current)
satLayer.current?.getSource()?.refresh()
}, [satMapsProvider])
const submitOverlay = async () => {
if (file && polygonExtent && rectCoords?.bl && rectCoords?.tl && rectCoords?.tr && rectCoords?.br) {
const formData = new FormData()
formData.append('file', file)
formData.append('extentMinX', polygonExtent[0].toString())
formData.append('extentMinY', polygonExtent[1].toString())
formData.append('extentMaxX', polygonExtent[2].toString())
formData.append('extentMaxY', polygonExtent[3].toString())
formData.append('blX', rectCoords?.bl[0].toString())
formData.append('blY', rectCoords?.bl[1].toString())
formData.append('tlX', rectCoords?.tl[0].toString())
formData.append('tlY', rectCoords?.tl[1].toString())
formData.append('trX', rectCoords?.tr[0].toString())
formData.append('trY', rectCoords?.tr[1].toString())
formData.append('brX', rectCoords?.br[0].toString())
formData.append('brY', rectCoords?.br[1].toString())
await fetch(`${import.meta.env.VITE_API_EMS_URL}/tiles/upload`, { method: 'POST', body: formData })
}
}
const { colorScheme } = useMantineColorScheme();
const mapControlsStyle: MantineStyleProp = {
borderRadius: '4px',
position: 'absolute',
zIndex: '1',
// backgroundColor: (theme) =>
// theme.palette.mode === 'light'
// ? '#FFFFFFAA'
// : '#000000AA',
backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA',
backdropFilter: 'blur(8px)',
border: '1px solid #00000022'
}
const { data: nodes } = useSWR('/nodes/all', () => fetcher('/nodes/all', BASE_URL.ems), { revalidateOnFocus: false })
useEffect(() => {
// Draw features based on database data
if (Array.isArray(nodes)) {
nodes.map(node => {
if (node.shape_type === 'LINE') {
const coordinates: Coordinate[] = []
if (Array.isArray(node.shape)) {
node.shape.map((point: any) => {
const coordinate = [point.x as number, point.y as number] as Coordinate
coordinates.push(coordinate)
})
}
//console.log(coordinates)
nodeLayerSource.current.addFeature(new Feature({ geometry: new LineString(coordinates) }))
}
})
}
}, [nodes])
const [currentObjectId, setCurrentObjectId] = useState<string | null>(null)
const [selectedCity, setSelectedCity] = useState<number | null>(null)
const [selectedYear, setSelectedYear] = useState<number | null>(2023)
const [citiesPage, setCitiesPage] = useState<number>(0)
const [searchCity, setSearchCity] = useState<string | undefined>("")
const { data: existingObjectsList } = useSWR(
selectedYear && selectedCity ? `/general/objects/list?year=${selectedYear}&city_id=${selectedCity}&planning=0` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: planningObjectsList } = useSWR(
selectedYear && selectedCity ? `/general/objects/list?year=${selectedYear}&city_id=${selectedCity}&planning=1` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const [objectsList, setObjectsList] = useState<TreeNodeData[] | null>(null)
const [selectedObjectList, setSelectedObjectList] = useState<number | null>(null)
useEffect(() => {
if (!selectedObjectList || !map.current) return;
if (figuresLayer.current) {
// Reset styles and apply highlight to matching features
figuresLayer.current.getSource()?.getFeatures().forEach((feature) => {
if (selectedObjectList == feature.get('type')) {
feature.setStyle(highlightStyleYellow);
} else {
feature.setStyle(null); // Reset to default style
}
})
}
if (linesLayer.current) {
// Reset styles and apply highlight to matching features
linesLayer.current.getSource()?.getFeatures().forEach((feature) => {
if (selectedObjectList == feature.get('type')) {
feature.setStyle(highlightStyleYellow);
} else {
feature.setStyle(null); // Reset to default style
}
})
}
}, [selectedObjectList])
useEffect(() => {
if (existingObjectsList && planningObjectsList) {
setObjectsList([
{
label: 'Существующие',
value: 'existing',
children: existingObjectsList.map((list: IObjectList) => ({
label:list.name,
count: list.count,
value: list.id,
})),
},
{
label: 'Планируемые',
value: 'planning',
children: planningObjectsList.map((list: IObjectList) => ({
label: list.name,
count: list.count,
value: list.id
}))
}
])
}
}, [existingObjectsList, planningObjectsList])
useEffect(() => {
if (currentObjectId) {
if (figuresLayer.current) {
// Reset styles and apply highlight to matching features
figuresLayer.current.getSource()?.getFeatures().forEach((feature) => {
if (currentObjectId == feature.get('object_id')) {
feature.setStyle(highlightStyleRed);
} else {
feature.setStyle(null); // Reset to default style
}
})
}
if (linesLayer.current) {
// Reset styles and apply highlight to matching features
linesLayer.current.getSource()?.getFeatures().forEach((feature) => {
if (currentObjectId == feature.get('object_id')) {
feature.setStyle(highlightStyleRed);
} else {
feature.setStyle(null); // Reset to default style
}
})
}
}
}, [currentObjectId])
const { data: currentObjectData } = useSWR(
currentObjectId ? `/general/objects/${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: valuesData } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: citiesData } = useSWR(
`/general/cities/all?limit=${10}&offset=${citiesPage || 0}${searchCity ? `&search=${searchCity}` : ''}`,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: figuresData, isValidating: figuresValidating } = useSWR(
selectedCity && selectedYear ? `/gis/figures/all?city_id=${selectedCity}&year=${selectedYear}&offset=0&limit=${10000}` : null,
(url) => axios.get(url, {
baseURL: BASE_URL.ems
}).then((res) => res.data),
{
revalidateOnFocus: false
}
)
const { data: linesData } = useSWR(
!figuresValidating && selectedCity && selectedYear ? `/gis/lines/all?city_id=${selectedCity}&year=${selectedYear}&offset=0&limit=${10000}` : null,
(url) => axios.get(url, {
baseURL: BASE_URL.ems
}).then((res) => {
return res.data
}),
{
revalidateOnFocus: false
}
)
useEffect(() => {
if (Array.isArray(figuresData)) {
figuresLayer.current.getSource()?.clear()
if (figuresData.length > 0) {
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
}
figuresData.map((figure: IFigure) => {
processFigure(figure, scaling, mapCenter, figuresLayer)
})
}
}
if (Array.isArray(linesData)) {
linesLayer.current.getSource()?.clear()
if (linesData.length > 0) {
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
}
linesData.map((line: ILine) => {
processLine(line, scaling, mapCenter, linesLayer)
})
}
}
}, [figuresData, linesData, selectedCity, selectedYear])
return (
<Box w={'100%'} h='100%' pos={'relative'}>
<Portal target='#header-portal'>
<Flex gap={'sm'} direction={'row'}>
<Flex align='center' direction='row' gap='sm'>
<Slider w='100%' min={0} max={1} step={0.001} value={satelliteOpacity} defaultValue={satelliteOpacity} onChange={(value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
<MantineSelect variant='filled' value={satMapsProvider} data={[{ label: 'Google', value: 'google' }, { label: 'Yandex', value: 'yandex' }, { label: 'Custom', value: 'custom' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} />
</Flex>
<form>
<Autocomplete
placeholder="Район"
flex={'1'}
data={citiesData ? citiesData.map((item: any) => ({ label: item.name, value: item.id.toString() })) : []}
//onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearchCity(value)}
onOptionSubmit={(value) => setSelectedCity(Number(value))}
rightSection={
searchCity !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearchCity('')
setSelectedCity(null)
}}
aria-label="Clear value"
/>
)
}
value={searchCity}
/>
</form>
<MantineSelect
data={[
{ label: '2018', value: '2018' },
{ label: '2019', value: '2019' },
{ label: '2020', value: '2020' },
{ label: '2021', value: '2021' },
{ label: '2022', value: '2022' },
{ label: '2023', value: '2023' },
{ label: '2024', value: '2024' },
]}
onChange={(e) => {
setSelectedYear(Number(e))
}}
defaultValue={selectedYear?.toString()}
//variant="unstyled"
allowDeselect={false}
/>
<ActionIcon
size='lg'
variant='transparent'
title='Настройки ИКС'
>
<IconSettings style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
</Flex>
</Portal>
<Flex w={'100%'} h={'100%'} pos={'absolute'}>
<MapToolbar
onSave={() => saveFeatures(drawingLayer)}
onRemove={() => draw.current?.removeLastPoint()}
onMover={() => map?.current?.addInteraction(new Translate())}
colorScheme={colorScheme}
/>
<Flex direction='column' mah={'86%'} pl='sm' style={{
...mapControlsStyle,
maxWidth: '300px',
width: '100%',
top: '8px',
left: '8px',
}}>
<ScrollAreaAutosize offsetScrollbars>
<Stack>
<Flex direction='row'>
<ActionIcon
size='lg'
variant='transparent'
onClick={() => submitOverlay()}
>
<IconUpload style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
<ActionIcon
size='lg'
variant='transparent'
title='Добавить подложку'
>
<IconPlus style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
</Flex>
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
<Accordion.Item key={'objects'} value={'Объекты'}>
<Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
<Accordion.Panel>
{objectsList &&
<Tree
data={objectsList}
selectOnClick
levelOffset={23}
renderNode={({ node, expanded, hasChildren, elementProps }) => (
<Group gap={6} {...elementProps} onClick={async (e) => {
elementProps.onClick(e)
if (node.value !== 'existing' && node.value !== 'planning') {
setSelectedObjectList(Number(node.value))
try {
// Fetch data from the API based on the node's value
await fetcher(`/general/objects/list?type=${node.value}&city_id=${selectedCity}&year=${selectedYear}&planning=0`, BASE_URL.ems).then(res => {
setObjectsList((prevList) => {
return prevList.map((item) => {
return {
...item,
children: [
...(item.children.map(child => {
if (child.value == node.value) {
return {
...child,
children: res.map(object => {
return {
label: object.object_id,
value: object.object_id
}
})
}
} else {
return { ...child }
}
}) || []),
],
}
}
)
}
);
})
} catch (error) {
console.error('Error fetching data:', error);
}
}
}}>
{(hasChildren || node?.count) && (
<IconChevronDown
size={18}
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
)}
<MantineText size='sm'>{`${node.label} ${node?.count ? `(${node.count})` : ''}`}</MantineText>
</Group>
)}
/>
}
</Accordion.Panel>
</Accordion.Item>
{/* {currentObjectId &&
<Accordion.Item key={'current_object'} value={currentObjectId}>
<Accordion.Control icon={<IconTable />}>
{'Текущий объект'}
</Accordion.Control>
<Accordion.Panel>
<ObjectData {...currentObjectData as IObjectData} />
</Accordion.Panel>
</Accordion.Item>
} */}
{valuesData &&
<Accordion.Item key={'parameters'} value={'Параметры объекта'}>
<Accordion.Control icon={<IconTable />}>{'Параметры объекта'}</Accordion.Control>
<Accordion.Panel>
<Flex gap={'sm'} direction={'column'}>
{Array.isArray(valuesData) &&
// Group objects by `id_param`
Object.entries(
valuesData.reduce((acc, param) => {
if (!acc[param.id_param]) {
acc[param.id_param] = [];
}
acc[param.id_param].push(param);
return acc;
}, {} as Record<string, IObjectParam[]>)
).map(([id_param, params]) => {
// Step 1: Sort the parameters by date_s (start date) and date_po (end date)
const sortedParams = params.sort((b, a) => {
const dateA = new Date(a.date_s || 0);
const dateB = new Date(b.date_s || 0);
return dateA.getTime() - dateB.getTime();
});
return sortedParams.length > 1 ? (
// Step 2: Render Mantine Timeline for multiple entries with the same `id_param`
<Timeline
key={id_param}
active={0}
bulletSize={18}
>
{sortedParams.map((param, index) => (
<Timeline.Item
key={index}
style={{
filter: index !== 0 ? 'grayscale(100%) opacity(50%)' : 'none'
}}
>
<ObjectParameter param={param} showLabel={false} />
<Text size='xs'>
{new Date(param.date_s).toLocaleDateString('en-GB').split('/').join('.')} - {new Date(param.date_po).toLocaleDateString('en-GB') === '01/01/1970' ? 'По сей день' : new Date(param.date_po).toLocaleDateString('en-GB').split('/').join('.')}
</Text>
</Timeline.Item>
))}
</Timeline>
) : (
// Step 3: Render ObjectParameter directly if there's only one entry
<ObjectParameter key={id_param} param={sortedParams[0]} />
);
})
}
</Flex>
</Accordion.Panel>
</Accordion.Item>
}
</Accordion>
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Слои'>
<Accordion.Item key={'objects'} value={'Слои'}>
<Accordion.Control icon={<IconTable />}>{'Слои'}</Accordion.Control>
<Accordion.Panel>
{map.current?.getLayers().getArray() &&
<Tree
data={map.current?.getLayers().getArray().map(layer => {
return {
label: layer.get('name'),
value: layer.get('id')
}
}) as TreeNodeData[]}
//selectOnClick
expandOnClick={false}
levelOffset={23}
renderNode={MapTreeCheckbox}
/>
}
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Stack>
</ScrollAreaAutosize>
</Flex>
<MapStatusbar
mapControlsStyle={mapControlsStyle}
currentCoordinate={currentCoordinate}
currentX={currentX}
currentY={currentY}
currentZ={currentZ}
statusText={statusText}
/>
</Flex>
<div
id="map-container"
ref={mapElement}
style={{
width: '100%',
height: '100%',
maxHeight: '100%',
position: 'fixed',
flexGrow: 1
}}
>
</div>
</Box >
);
};
export default MapComponent