This commit is contained in:
cracklesparkle
2024-12-16 10:50:35 +09:00
parent eeae97288a
commit 87866e4e51
19 changed files with 2419487 additions and 499 deletions

File diff suppressed because one or more lines are too long

View File

@ -2,17 +2,19 @@ import { useState } from 'react'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Accordion, NavLink } from '@mantine/core';
import { Accordion, NavLink, Text } from '@mantine/core';
import { setCurrentObjectId, useObjectsStore } from '../../store/objects';
import { IconChevronDown } from '@tabler/icons-react';
import { setSelectedObjectType } from '../../store/map';
const ObjectTree = () => {
const { selectedCity, selectedYear } = useObjectsStore()
const { selectedDistrict, selectedYear } = useObjectsStore()
const [existingCount, setExistingCount] = useState(0)
const [planningCount, setPlanningCount] = useState(0)
const { data: existingObjectsList } = useSWR(
selectedYear && selectedCity ? `/general/objects/list?year=${selectedYear}&city_id=${selectedCity}&planning=0` : null,
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null,
(url) => fetcher(url, BASE_URL.ems).then(res => {
if (Array.isArray(res)) {
let count = 0
@ -29,7 +31,7 @@ const ObjectTree = () => {
)
const { data: planningObjectsList } = useSWR(
selectedYear && selectedCity ? `/general/objects/list?year=${selectedYear}&city_id=${selectedCity}&planning=1` : null,
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null,
(url) => fetcher(url, BASE_URL.ems).then(res => {
if (Array.isArray(res)) {
let count = 0
@ -45,12 +47,20 @@ const ObjectTree = () => {
}
)
return (
<Accordion multiple chevronPosition='left'>
<TypeTree label='Существующие' value={'existing'} count={existingCount} objectList={existingObjectsList} planning={0} />
<TypeTree label='Планируемые' value={'planning'} count={planningCount} objectList={planningObjectsList} planning={1} />
</Accordion>
)
if (selectedDistrict) {
return (
<Accordion multiple chevronPosition='left'>
<TypeTree label='Существующие' value={'existing'} count={existingCount} objectList={existingObjectsList} planning={0} />
<TypeTree label='Планируемые' value={'planning'} count={planningCount} objectList={planningObjectsList} planning={1} />
</Accordion>
)
} else {
return (
<Text size='xs'>Выберите регион и населённый пункт, чтобы увидеть список объектов.</Text>
)
}
}
interface TypeTreeProps {
@ -90,18 +100,23 @@ const ObjectList = ({
planning,
count
}: IObjectList) => {
const { selectedCity, selectedYear } = useObjectsStore()
const { selectedDistrict, selectedYear } = useObjectsStore()
const { data } = useSWR(
selectedCity && selectedYear ? `/general/objects/list?type=${id}&city_id=${selectedCity}&year=${selectedYear}&planning=${planning}` : null,
selectedDistrict && selectedYear ? `/general/objects/list?type=${id}&city_id=${selectedDistrict}&year=${selectedYear}&planning=${planning}` : null,
(url) => fetcher(url, BASE_URL.ems),
{ revalidateOnFocus: false }
{
revalidateOnFocus: false,
revalidateIfStale: false
}
)
return (
<NavLink p={0} label={`${label} ${count ? `(${count})` : ''}`}>
<NavLink onClick={() => {
setSelectedObjectType(id)
}} rightSection={<IconChevronDown size={14} />} p={0} label={`${id} ${label} ${count ? `(${count})` : ''}`}>
{Array.isArray(data) && data.map((type) => (
<NavLink key={type.object_id} label={type.name} p={0} onClick={() => setCurrentObjectId(type.object_id)} />
<NavLink key={type.object_id} label={type.caption ? type.caption : 'Без названия'} p={0} onClick={() => setCurrentObjectId(type.object_id)} />
))}
</NavLink>
)

View File

@ -2,13 +2,13 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import 'ol/ol.css'
import Map from 'ol/Map'
import View from 'ol/View'
import { DragBox, Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
import { ImageStatic, OSM, Vector as VectorSource, XYZ } from 'ol/source'
import { Tile as TileLayer, VectorImage, 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, getWidth } from 'ol/extent'
import { Extent } from 'ol/extent'
import { drawingLayerStyle, highlightStyleRed, highlightStyleYellow, overlayStyle, regionsLayerStyle } from './MapStyles'
import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources'
import ImageLayer from 'ol/layer/Image'
@ -22,13 +22,11 @@ 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, MantineStyleProp, rem, ScrollAreaAutosize, Slider, useMantineColorScheme, Portal, Timeline, Text, Stack, NavLink, Checkbox } from '@mantine/core'
import { IconPlus, IconSearch, IconSettings, IconTable, IconUpload } from '@tabler/icons-react'
import { ActionIcon, Autocomplete, Box, CloseButton, Flex, Select as MantineSelect, MantineStyleProp, rem, Slider, useMantineColorScheme, Portal, Menu, Button, Group, Divider, LoadingOverlay } from '@mantine/core'
import { IconBoxMultiple, IconChevronDown, IconPlus, IconSearch, IconSettings, IconUpload } from '@tabler/icons-react'
import { getGridCellPosition } from './mapUtils'
import { IFigure, ILine } from '../../interfaces/gis'
import axios from 'axios'
import ObjectParameter from './ObjectParameter'
import { IObjectParam } from '../../interfaces/objects'
import MapToolbar from './MapToolbar/MapToolbar'
import MapStatusbar from './MapStatusbar/MapStatusbar'
import { measureStyleFunction, modifyStyle } from './Measure/MeasureStyles'
@ -36,7 +34,48 @@ import { setCurrentCoordinate, setCurrentX, setCurrentY, setCurrentZ, setSatMaps
import { v4 as uuidv4 } from 'uuid'
import { useThrottle } from '@uidotdev/usehooks'
import ObjectTree from '../Tree/ObjectTree'
import { setCurrentObjectId, setSelectedCity, setSelectedYear, useObjectsStore } from '../../store/objects'
import { setCurrentObjectId, setSelectedDistrict, setSelectedRegion, setSelectedYear, useObjectsStore } from '../../store/objects'
import MapLayers from './MapLayers/MapLayers'
import ObjectParameters from './ObjectParameters/ObjectParameters'
import TabsPane, { ITabsPane } from './TabsPane/TabsPane'
import { mapExtent } from './MapConstants'
// Tab settings
const objectsPane: ITabsPane[] = [
{
title: 'Объекты',
value: 'objects',
view: <ObjectTree />
},
{
title: 'Неразмещенные',
value: 'unplaced',
view: <></>
},
{
title: 'Другие',
value: 'other',
view: <></>
},
]
const paramsPane: ITabsPane[] = [
{
title: 'История изменений',
value: 'history',
view: <></>
},
{
title: 'Параметры',
value: 'parameters',
view: <ObjectParameters />
},
{
title: 'Вычисляемые',
value: 'calculated',
view: <></>
},
]
// Settings for cities
const citySettings = [
@ -53,10 +92,11 @@ const citySettings = [
]
const MapComponent = () => {
const { colorScheme } = useMantineColorScheme();
// States
const { selectedCity, selectedYear, currentObjectId } = useObjectsStore()
const { currentTool, satMapsProvider } = useMapStore()
const { selectedYear, currentObjectId, selectedRegion, selectedDistrict } = useObjectsStore()
const { currentTool, satMapsProvider, selectedObjectType } = useMapStore()
///
const [file, setFile] = useState<File | null>(null)
@ -82,83 +122,6 @@ const MapComponent = () => {
const measureDraw = useRef<Draw | null>(null)
/////
// Box selection
const dragBox = useRef(new DragBox({
condition: platformModifierKeyOnly
}))
useEffect(() => {
if (dragBox.current) {
dragBox.current.on('boxend', function () {
const boxExtent = dragBox.current.getGeometry().getExtent();
// if the extent crosses the antimeridian process each world separately
const worldExtent = map.current?.getView().getProjection().getExtent();
if (worldExtent) {
const worldWidth = getWidth(worldExtent);
const startWorld = Math.floor((boxExtent[0] - worldExtent[0]) / worldWidth);
const endWorld = Math.floor((boxExtent[2] - worldExtent[0]) / worldWidth);
for (let world = startWorld; world <= endWorld; ++world) {
const left = Math.max(boxExtent[0] - world * worldWidth, worldExtent[0]);
const right = Math.min(boxExtent[2] - world * worldWidth, worldExtent[2]);
const extent = [left, boxExtent[1], right, boxExtent[3]];
const boxFeatures = vectorSource
.getFeaturesInExtent(extent)
.filter(
(feature) =>
!selectedFeatures.getArray().includes(feature) &&
feature.getGeometry().intersectsExtent(extent),
);
// features that intersect the box geometry are added to the
// collection of selected features
// if the view is not obliquely rotated the box geometry and
// its extent are equalivalent so intersecting features can
// be added directly to the collection
const rotation = map.getView().getRotation();
const oblique = rotation % (Math.PI / 2) !== 0;
// when the view is obliquely rotated the box extent will
// exceed its geometry so both the box and the candidate
// feature geometries are rotated around a common anchor
// to confirm that, with the box geometry aligned with its
// extent, the geometries intersect
if (oblique) {
const anchor = [0, 0];
const geometry = dragBox.getGeometry().clone();
geometry.translate(-world * worldWidth, 0);
geometry.rotate(-rotation, anchor);
const extent = geometry.getExtent();
boxFeatures.forEach(function (feature) {
const geometry = feature.getGeometry().clone();
geometry.rotate(-rotation, anchor);
if (geometry.intersectsExtent(extent)) {
selectedFeatures.push(feature);
}
});
} else {
selectedFeatures.extend(boxFeatures);
}
}
}
})
// clear selection when drawing a new box and when clicking on the map
dragBox.current.on('boxstart', function () {
selectedFeatures.clear();
})
}
}, [])
/////
const mapElement = useRef<HTMLDivElement | null>(null)
const map = useRef<Map | null>(null)
@ -182,6 +145,7 @@ const MapComponent = () => {
const draw = useRef<Draw | null>(null)
const snap = useRef<Snap | null>(null)
const translate = useRef<Translate | null>(null)
const selectFeature = useRef<Select>(new Select({
condition: function (mapBrowserEvent) {
@ -234,7 +198,7 @@ const MapComponent = () => {
}
}))
const selectedRegion = useRef<Feature | null>(null)
const selectedArea = useRef<Feature | null>(null)
const baseLayer = useRef<TileLayer>(new TileLayer({
source: new OSM(),
@ -258,7 +222,6 @@ const MapComponent = () => {
}
}))
// tile processing
const handleImageDrop = useCallback((event: DragEvent) => {
event.preventDefault();
@ -455,9 +418,9 @@ const MapComponent = () => {
view: new View({
center: transform([129.7466541, 62.083504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]),
//zoom: 16,
zoom: 15,
zoom: 5,
maxZoom: 21,
//extent: mapExtent,
extent: mapExtent,
}),
})
@ -497,7 +460,7 @@ const MapComponent = () => {
loadFeatures(drawingLayerSource)
regionsInit(map, selectedRegion, regionsLayer)
regionsInit(map, selectedArea, regionsLayer)
if (mapElement.current) {
mapElement.current.addEventListener('dragover', (e) => {
@ -520,8 +483,9 @@ const MapComponent = () => {
if (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)
addInteractions(drawingLayerSource, translate, draw, map, snap, measureDraw, measureSource, measureModify)
} else {
if (translate.current) map?.current?.removeInteraction(translate.current)
if (draw.current) map?.current?.removeInteraction(draw.current)
if (snap.current) map?.current?.removeInteraction(snap.current)
if (measureDraw.current) map?.current?.removeInteraction(measureDraw.current)
@ -577,17 +541,11 @@ const MapComponent = () => {
}
}
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',
backgroundColor: colorScheme === 'light' ? '#F0F0F0CC' : '#000000CC',
backdropFilter: 'blur(8px)',
border: '1px solid #00000022'
}
@ -613,23 +571,16 @@ const MapComponent = () => {
}
}, [nodes])
const [citiesPage, setCitiesPage] = useState<number>(0)
const [searchCity, setSearchCity] = useState<string | undefined>("")
const throttledSearchCity = useThrottle(searchCity, 500);
const [searchObject, setSearchObject] = useState<string | undefined>("")
const throttledSearchObject = useThrottle(searchObject, 500);
const [selectedObjectList, setSelectedObjectList] = useState<number | null>(null)
useEffect(() => {
if (!selectedObjectList || !map.current) return;
if (!selectedObjectType || !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')) {
if (selectedObjectType == feature.get('type')) {
feature.setStyle(highlightStyleYellow);
} else {
feature.setStyle(undefined); // Reset to default style
@ -640,14 +591,14 @@ const MapComponent = () => {
if (linesLayer.current) {
// Reset styles and apply highlight to matching features
linesLayer.current.getSource()?.getFeatures().forEach((feature) => {
if (selectedObjectList == feature.get('type')) {
if (selectedObjectType == feature.get('type')) {
feature.setStyle(highlightStyleYellow);
} else {
feature.setStyle(undefined); // Reset to default style
}
})
}
}, [selectedObjectList])
}, [selectedObjectType])
useEffect(() => {
if (currentObjectId) {
@ -688,17 +639,16 @@ const MapComponent = () => {
}
}, [currentObjectId])
const { data: valuesData } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null,
const { data: regionsData } = useSWR(
`/general/regions/all`,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false,
revalidateIfStale: false
revalidateOnFocus: false
}
)
const { data: citiesData } = useSWR(
`/general/cities/all?limit=${10}&offset=${citiesPage || 0}${throttledSearchCity ? `&search=${throttledSearchCity}` : ''}`,
const { data: districtsData } = useSWR(
selectedRegion ? `/general/districts/all?region_id=${selectedRegion}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
@ -706,7 +656,7 @@ const MapComponent = () => {
)
const { data: searchData } = useSWR(
throttledSearchObject !== "" && selectedCity && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedCity}&year=${selectedYear}` : null,
throttledSearchObject !== "" && selectedDistrict && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedDistrict}&year=${selectedYear}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
@ -714,7 +664,7 @@ const MapComponent = () => {
)
const { data: figuresData, isValidating: figuresValidating } = useSWR(
selectedCity && selectedYear ? `/gis/figures/all?city_id=${selectedCity}&year=${selectedYear}&offset=0&limit=${10000}` : null,
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),
@ -723,8 +673,8 @@ const MapComponent = () => {
}
)
const { data: linesData } = useSWR(
!figuresValidating && selectedCity && selectedYear ? `/gis/lines/all?city_id=${selectedCity}&year=${selectedYear}&offset=0&limit=${10000}` : null,
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) => {
@ -744,7 +694,7 @@ const MapComponent = () => {
}
//let rotation = 0
const settings = citySettings.find(el => el.city_id === selectedCity)
const settings = citySettings.find(el => el.city_id === selectedDistrict)
if (settings) {
console.log("City settings found")
@ -778,6 +728,14 @@ const MapComponent = () => {
figuresData.map((figure: IFigure) => {
processFigure(figure, scaling, [offset_x, offset_y], figuresLayer)
})
if (map.current) {
const extent = figuresLayer.current.getSource()?.getExtent()
if (extent) {
map.current.getView().fit(fromExtent(extent), { duration: 500 })
}
}
}
}
@ -794,10 +752,10 @@ const MapComponent = () => {
})
}
}
}, [figuresData, linesData, selectedCity, selectedYear])
}, [figuresData, linesData, selectedDistrict, selectedYear])
useEffect(() => {
if (selectedCity) {
if (selectedDistrict) {
let offset_x = 14442665.697619518
let offset_y = 8884520.63524492
//let scale = 9000
@ -805,7 +763,7 @@ const MapComponent = () => {
//let image_width = 8500
//let image_height = 12544
const settings = citySettings.find(el => el.city_id === selectedCity)
const settings = citySettings.find(el => el.city_id === selectedDistrict)
if (settings) {
console.log("City settings found")
@ -830,7 +788,7 @@ const MapComponent = () => {
console.log("City settings NOT found")
}
const imageUrl = `${import.meta.env.VITE_API_EMS_URL}/static/${selectedCity}`;
const imageUrl = `${import.meta.env.VITE_API_EMS_URL}/static/${selectedDistrict}`;
const img = new Image();
img.src = imageUrl;
img.onload = () => {
@ -867,7 +825,7 @@ const MapComponent = () => {
}
};
}
}, [selectedCity])
}, [selectedDistrict])
useEffect(() => {
// if (map.current) {
@ -909,62 +867,50 @@ const MapComponent = () => {
<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)} />
<Autocomplete
form='search_object'
placeholder="Поиск"
flex={'1'}
data={searchData ? searchData.map((item: { value: string, id_object: string }) => ({ label: item.value, value: item.id_object.toString() })) : []}
//onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearchObject(value)}
onOptionSubmit={(value) => setCurrentObjectId(value)}
rightSection={
searchObject !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearchObject('')
//setSelectedCity(null)
}}
aria-label="Clear value"
/>
)
}
leftSection={<IconSearch size={16} />}
value={searchObject}
/>
<MantineSelect variant='filled' value={satMapsProvider} data={[{ label: 'Google', value: 'google' }, { label: 'Yandex', value: 'yandex' }, { label: 'Custom', value: 'custom' }, { label: 'Static', value: 'static' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} />
</Flex>
<MantineSelect
placeholder="Регион"
flex={'1'}
data={regionsData ? regionsData.map((item: { name: string, id: number }) => ({ label: item.name, value: item.id.toString() })) : []}
onChange={(value) => setSelectedRegion(Number(value))}
clearable
searchable
value={selectedRegion ? selectedRegion.toString() : null}
/>
<form>
<Autocomplete
placeholder="Поиск"
flex={'1'}
data={searchData ? searchData.map((item: { value: string, id_object: string }) => ({ label: item.value, value: item.id_object.toString() })) : []}
//onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearchObject(value)}
onOptionSubmit={(value) => setCurrentObjectId(value)}
rightSection={
searchObject !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearchObject('')
//setSelectedCity(null)
}}
aria-label="Clear value"
/>
)
}
leftSection={<IconSearch size={16} />}
value={searchObject}
/>
</form>
<form>
<Autocomplete
placeholder="Район"
flex={'1'}
data={citiesData ? citiesData.map((item: { name: string, id: number }) => ({ 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
placeholder="Населённый пункт"
flex={'1'}
data={districtsData ? districtsData.map((item: { name: string, id: number, district_name: string }) => ({ label: [item.name, item.district_name].join(' - '), value: item.id.toString() })) : []}
onChange={(value) => setSelectedDistrict(Number(value))}
clearable
searchable
value={selectedDistrict ? selectedDistrict.toString() : null}
/>
<MantineSelect
w='84px'
@ -976,134 +922,71 @@ const MapComponent = () => {
allowDeselect={false}
/>
<ActionIcon
size='lg'
variant='transparent'
title='Настройки ИКС'
<Menu
position="bottom-end"
transitionProps={{ transition: 'pop-top-right' }}
withinPortal
>
<Menu.Target>
<Button variant='transparent'>
<Group gap={7} wrap='nowrap' style={{ flexShrink: 0 }} title='Слои'>
<IconBoxMultiple style={{ width: rem(20), height: rem(20) }} />
<IconChevronDown style={{ width: rem(12), height: rem(12) }} stroke={1.5} />
</Group>
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>{'Настройка видимости слоёв'}</Menu.Label>
<Flex p='sm' direction='column' gap='xs'>
<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: 'Яндекс', value: 'yandex' }, { label: 'Подложка', value: 'custom' }, { label: 'Static', value: 'static' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} />
</Flex>
<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>
<MapLayers map={map} />
</Flex>
</Menu.Dropdown>
</Menu>
<ActionIcon size='lg' variant='transparent' title='Настройки ИКС'>
<IconSettings style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
</Flex>
</Portal>
<LoadingOverlay visible={linesValidating || figuresValidating} />
<Flex w={'100%'} h={'100%'} pos={'absolute'}>
<MapToolbar
onSave={() => saveFeatures(drawingLayer)}
onRemove={() => draw.current?.removeLastPoint()}
onMover={() => map?.current?.addInteraction(new Translate())}
colorScheme={colorScheme}
/>
{selectedRegion && selectedDistrict &&
<MapToolbar
onSave={() => saveFeatures(drawingLayer)}
onRemove={() => draw.current?.removeLastPoint()}
/>
}
<Flex direction='column' mah={'86%'} pl='sm' style={{
...mapControlsStyle,
maxWidth: '340px',
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>
{selectedCity &&
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
<Accordion.Item key={'objects'} value={'Объекты'}>
<Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
<Accordion.Panel>
<ObjectTree />
</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) &&
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 as IObjectParam[]).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: IObjectParam, index: number) => (
<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>
) : (
<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() && map.current?.getLayers().getArray().map((layer, index) => (
<Flex key={`layer-${index}`} gap='xs' align='center'>
<Checkbox.Indicator
checked={layer.getLayerState().visible}
onClick={() => layer.getLayerState().visible ? layer.setVisible(false) : layer.setVisible(true)}
/>
<NavLink p={0} label={layer.get('name')} onClick={() => { console.log(layer.getLayerState()) }} />
</Flex>
))}
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Stack>
</ScrollAreaAutosize>
</Flex>
{selectedRegion && selectedDistrict &&
<Flex direction='column' mah={'94%'} h={'100%'} pl='sm' style={{
...mapControlsStyle,
maxWidth: '340px',
width: '100%',
top: '8px',
left: '8px',
}}>
<TabsPane defaultTab='objects' tabs={objectsPane} />
<Divider />
<TabsPane defaultTab='parameters' tabs={paramsPane} />
</Flex>
}
<MapStatusbar
mapControlsStyle={mapControlsStyle}
@ -1113,13 +996,7 @@ const MapComponent = () => {
<div
id="map-container"
ref={mapElement}
style={{
width: '100%',
height: '100%',
maxHeight: '100%',
position: 'fixed',
flexGrow: 1
}}
style={{ width: '100%', height: '100%', maxHeight: '100%', position: 'fixed', flexGrow: 1 }}
>
</div>
</Box >

View File

@ -0,0 +1,27 @@
import { Checkbox, Flex, NavLink, Stack } from '@mantine/core'
import Map from 'ol/Map'
import React from 'react'
interface MapLayersProps {
map: React.MutableRefObject<Map | null>
}
const MapLayers = ({
map
}: MapLayersProps) => {
return (
<Stack gap='0'>
{map.current?.getLayers().getArray() && map.current?.getLayers().getArray().map((layer, index) => (
<Flex key={`layer-${index}`} gap='xs' align='center'>
<Checkbox.Indicator
checked={layer.getLayerState().visible}
onClick={() => layer.getLayerState().visible ? layer.setVisible(false) : layer.setVisible(true)}
/>
<NavLink p={0} label={layer.get('name')} onClick={() => { console.log(layer.getLayerState()) }} />
</Flex>
))}
</Stack>
)
}
export default MapLayers

View File

@ -1,21 +1,18 @@
import { ActionIcon, MantineColorScheme } from '@mantine/core'
import { ActionIcon, useMantineColorScheme } from '@mantine/core'
import { IconApi, IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPoint, IconPolygon, IconRuler } from '@tabler/icons-react'
import { setCurrentTool, useMapStore } from '../../../store/map';
interface IToolbarProps {
onSave: () => void;
onRemove: () => void;
onMover: () => void;
colorScheme: MantineColorScheme;
}
const MapToolbar = ({
onSave,
onRemove,
onMover,
colorScheme
}: IToolbarProps) => {
const mapState = useMapStore()
const { colorScheme } = useMantineColorScheme();
return (
<ActionIcon.Group orientation='vertical' pos='absolute' top='8px' right='8px' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
@ -71,8 +68,10 @@ const MapToolbar = ({
<ActionIcon
size='lg'
variant='transparent'
onClick={onMover}
variant={mapState.currentTool === 'Mover' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Mover')
}}
>
<IconArrowsMove />
</ActionIcon>

View File

@ -98,10 +98,14 @@ const ObjectParameter = ({
return (
<TableValue value={value} name={name} type='number' />
)
case 'uniqueidentifier':
return (
<TableValue value={value} name={name} type='value'/>
)
default:
return (
<div>
{type}
{name}
{value as string}
</div>
)

View File

@ -0,0 +1,59 @@
import { Flex, LoadingOverlay } from '@mantine/core';
import { IObjectParam } from '../../../interfaces/objects';
import ObjectParameter from '../ObjectParameter';
import useSWR from 'swr';
import { BASE_URL } from '../../../constants';
import { fetcher } from '../../../http/axiosInstance';
import { useObjectsStore } from '../../../store/objects';
const ObjectParameters = () => {
const { currentObjectId } = useObjectsStore()
const { data: valuesData, isValidating: valuesValidating } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false,
revalidateIfStale: false
}
)
return (
<Flex gap={'sm'} direction={'column'} pos='relative'>
<LoadingOverlay visible={valuesValidating} />
{Array.isArray(valuesData) &&
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 as IObjectParam[]).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 ? (
sortedParams.map((param: IObjectParam) => {
if (param.date_po == null) {
return (
<ObjectParameter key={id_param} param={param} showLabel={false} />
)
}
}
)
) : (
<ObjectParameter key={id_param} param={sortedParams[0]} />
);
})
}
</Flex>
)
}
export default ObjectParameters

View File

@ -0,0 +1,25 @@
import useSWR from 'swr'
import { BASE_URL } from '../../constants'
import { fetcher } from '../../http/axiosInstance'
import { Flex } from '@mantine/core'
const RegionSelect = () => {
const { data } = useSWR(`/gis/regions/borders`, (url) => fetcher(url, BASE_URL.ems), {
revalidateOnFocus: false,
revalidateIfStale: false
})
return (
<Flex align='center' justify='center'>
{Array.isArray(data) &&
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width='100%' height='100vh' transform='scale(1, -1)'>
{data.map((el, index) => (
<path key={`path-${index}`} d={el.path} fill="white" stroke="black" />
))}
</svg>
}
</Flex>
)
}
export default RegionSelect

View File

@ -27,91 +27,42 @@ const TCBParameter = ({
}
)
const tables = [
'vStreets',
'tTypes',
'vPipesGround',
'vPipesLayer',
'vPipesIsolation',
'vRepairEvent',
'vPipesMaterial',
'vBoilers',
'vHotWaterTypes',
'vHeatingTypes',
'vColdWaterTypes',
'vCanalization',
'vElectroSupplyTypes',
'vGasSupplyTypes',
'vFoundation',
'vMaterialsWall',
'vCovering',
'vRoof',
'vTechStatus',
'vPipeOutDiameters',
'vPipeDiameters',
]
const TCBValue = (vtable: string) => {
switch (vtable) {
case 'vStreets':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'tTypes':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vPipesGround':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vRepairEvent':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vPipesMaterial':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vBoilers':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vHotWaterTypes':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vHeatingTypes':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vColdWaterTypes':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vCanalization':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vElectroSupplyTypes':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vGasSupplyTypes':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vFoundation':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vMaterialsWall':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vCovering':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vRoof':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vTechStatus':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vPipeOutDiameters':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vPipeDiameters':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
default:
return (
<Text>
{JSON.stringify(name)}
{JSON.stringify(tcbValue)}
</Text>
)
if (tables.includes(vtable)) {
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
} else {
return (
<Text>
{JSON.stringify(name)}
{JSON.stringify(tcbValue)}
</Text>
)
}
}

View File

@ -19,11 +19,11 @@ const TableValue = ({
unit,
vtable
}: TableValueProps) => {
const { selectedCity } = useObjectsStore()
const { selectedDistrict } = useObjectsStore()
//Get available values
const { data: tcbAll, isValidating } = useSWR(
type === 'select' && selectedCity ? `/general/params/tcb?vtable=${vtable}&id_city=${selectedCity}` : null,
type === 'select' && selectedDistrict ? `/general/params/tcb?vtable=${vtable}&id_city=${selectedDistrict}` : null,
(url) => fetcher(url, BASE_URL.ems).then(res => {
if (Array.isArray(res)) {
return res.map((el) => ({

View File

@ -0,0 +1,45 @@
import { ScrollAreaAutosize, Tabs } from '@mantine/core';
export interface ITabsPane {
title: string;
value: string;
view: JSX.Element;
}
export interface TabsPaneProps {
defaultTab: string;
tabs: ITabsPane[];
}
const TabsPane = ({
defaultTab,
tabs
}: TabsPaneProps) => {
return (
<Tabs defaultValue={defaultTab} mah='50%' h={'100%'} style={{
display: 'grid',
gridTemplateRows: 'min-content auto'
}}>
<ScrollAreaAutosize>
<Tabs.List>
{tabs.map((tab) => (
<Tabs.Tab key={tab.value} value={tab.value}>
{tab.title}
</Tabs.Tab>
))}
</Tabs.List>
</ScrollAreaAutosize>
<ScrollAreaAutosize h='100%' offsetScrollbars p='xs'>
{tabs.map(tab => (
<Tabs.Panel key={tab.value} value={tab.value}>
{tab.view}
</Tabs.Panel>
))}
</ScrollAreaAutosize>
</Tabs>
)
}
export default TabsPane

View File

@ -11,7 +11,7 @@ import VectorSource from "ol/source/Vector";
import proj4 from "proj4";
import { firstStyleFunction, fourthStyleFunction, selectStyle, styleFunction, thirdStyleFunction } from "./MapStyles";
import { Type } from "ol/geom/Geometry";
import { Draw, Modify, Snap } from "ol/interaction";
import { Draw, Modify, Snap, Translate } from "ol/interaction";
import { noModifierKeys } from "ol/events/condition";
import { IGeometryType, IRectCoords } from "../../interfaces/map";
import { uploadCoordinates } from "../../actions/map";
@ -22,6 +22,7 @@ import { fromCircle } from "ol/geom/Polygon";
import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles";
import { getCurrentTool, getMeasureClearPrevious, getMeasureType, getTipPoint, setStatusText } from "../../store/map";
import { MutableRefObject } from "react";
import { setSelectedRegion } from "../../store/objects";
export function processLine(
line: ILine,
@ -179,6 +180,7 @@ export const updateImageSource = (
export const addInteractions = (
drawingLayerSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>,
translate: React.MutableRefObject<Translate | null>,
draw: React.MutableRefObject<Draw | null>,
map: React.MutableRefObject<Map | null>,
snap: React.MutableRefObject<Snap | null>,
@ -191,7 +193,7 @@ export const addInteractions = (
const measureType = getMeasureType()
const tipPoint = getTipPoint()
if (currentTool !== 'Measure') {
if (currentTool !== 'Measure' && currentTool !== 'Mover') {
draw.current = new Draw({
source: drawingLayerSource.current,
type: currentTool as Type,
@ -255,6 +257,11 @@ export const addInteractions = (
measureModify.current.setActive(true);
map.current?.addInteraction(measureDraw.current);
}
if (currentTool == 'Mover') {
translate.current = new Translate()
map?.current?.addInteraction(translate.current)
}
}
export function regionsInit(
@ -274,6 +281,9 @@ export function regionsInit(
// Zoom to the selected feature
zoomToFeature(map, selectedRegion.current)
if (feature.get('id')) {
setSelectedRegion(feature.get('id'))
}
return true
} else return false
});

View File

@ -4,11 +4,21 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { MantineProvider } from '@mantine/core';
import { createTheme, DEFAULT_THEME, MantineProvider, mergeMantineTheme } from '@mantine/core';
const overrides = createTheme({
// Set this color to `--mantine-color-body` CSS variable
white: '#F0F0F0',
colors: {
// ...
},
})
const theme = mergeMantineTheme(DEFAULT_THEME, overrides);
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<MantineProvider defaultColorScheme="light">
<MantineProvider theme={theme}>
<App />
</MantineProvider>
</React.StrictMode>,

View File

@ -18,6 +18,7 @@ interface MapState {
currentCoordinate: Coordinate | null;
statusText: string;
satMapsProvider: SatelliteMapsProvider;
selectedObjectType: number | null;
}
export const useMapStore = create<MapState>(() => ({
@ -32,7 +33,8 @@ export const useMapStore = create<MapState>(() => ({
currentY: undefined,
currentCoordinate: null,
statusText: '',
satMapsProvider: 'custom'
satMapsProvider: 'custom',
selectedObjectType: null,
}));
const setCurrentZ = (z: number | undefined) => useMapStore.setState(() => ({ currentZ: z }))
@ -41,6 +43,8 @@ const setCurrentY = (y: number | undefined) => useMapStore.setState(() => ({ cur
const setCurrentCoordinate = (c: Coordinate | null) => useMapStore.setState(() => ({ currentCoordinate: c }))
const setStatusText = (t: string) => useMapStore.setState(() => ({ statusText: t }))
const setSatMapsProvider = (p: SatelliteMapsProvider) => useMapStore.setState(() => ({ satMapsProvider: p }))
const setSelectedObjectType = (t: number | null) => useMapStore.setState(() => ({ selectedObjectType: t }))
const setMap = (m: Map | null) => useMapStore.setState(() => ({ map: m }))
const setTipPoint = (tipPoint: Point | null) => {
useMapStore.setState(() => ({ tipPoint: tipPoint }))
@ -50,6 +54,10 @@ const getTipPoint = () => {
return useMapStore.getState().tipPoint
}
const getMap = () => {
return useMapStore.getState().map
}
const setMeasureType = (tool: "LineString" | "Polygon") => {
useMapStore.setState(() => ({ measureType: tool }))
}
@ -100,5 +108,8 @@ export {
setCurrentY,
setCurrentCoordinate,
setStatusText,
setSatMapsProvider
setSatMapsProvider,
setSelectedObjectType,
setMap,
getMap
}

View File

@ -1,12 +1,16 @@
import { create } from 'zustand';
interface ObjectsState {
selectedRegion: number | null;
selectedDistrict: number | null;
selectedCity: number | null;
selectedYear: number | null;
currentObjectId: string | null;
}
export const useObjectsStore = create<ObjectsState>(() => ({
selectedRegion: null,
selectedDistrict: null,
selectedCity: null,
selectedYear: 2023,
currentObjectId: null
@ -16,6 +20,14 @@ const getSelectedCity = () => {
return useObjectsStore.getState().selectedCity
}
const setSelectedRegion = (region: number | null) => {
useObjectsStore.setState(() => ({ selectedRegion: region }))
}
const setSelectedDistrict = (district: number | null) => {
useObjectsStore.setState(() => ({ selectedDistrict: district }))
}
const setSelectedCity = (city: number | null) => {
useObjectsStore.setState(() => ({ selectedCity: city }))
}
@ -42,5 +54,7 @@ export {
getSelectedYear,
setSelectedYear,
getCurrentObjectId,
setCurrentObjectId
setCurrentObjectId,
setSelectedRegion,
setSelectedDistrict
}

View File

@ -9,4 +9,5 @@ export type ToolType =
"GeometryCollection" |
"Circle" |
"Measure" |
"Mover" |
null