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 useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance' import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants' 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 { setCurrentObjectId, useObjectsStore } from '../../store/objects';
import { IconChevronDown } from '@tabler/icons-react';
import { setSelectedObjectType } from '../../store/map';
const ObjectTree = () => { const ObjectTree = () => {
const { selectedCity, selectedYear } = useObjectsStore() const { selectedDistrict, selectedYear } = useObjectsStore()
const [existingCount, setExistingCount] = useState(0) const [existingCount, setExistingCount] = useState(0)
const [planningCount, setPlanningCount] = useState(0) const [planningCount, setPlanningCount] = useState(0)
const { data: existingObjectsList } = useSWR( 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 => { (url) => fetcher(url, BASE_URL.ems).then(res => {
if (Array.isArray(res)) { if (Array.isArray(res)) {
let count = 0 let count = 0
@ -29,7 +31,7 @@ const ObjectTree = () => {
) )
const { data: planningObjectsList } = useSWR( 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 => { (url) => fetcher(url, BASE_URL.ems).then(res => {
if (Array.isArray(res)) { if (Array.isArray(res)) {
let count = 0 let count = 0
@ -45,12 +47,20 @@ const ObjectTree = () => {
} }
) )
if (selectedDistrict) {
return ( return (
<Accordion multiple chevronPosition='left'> <Accordion multiple chevronPosition='left'>
<TypeTree label='Существующие' value={'existing'} count={existingCount} objectList={existingObjectsList} planning={0} /> <TypeTree label='Существующие' value={'existing'} count={existingCount} objectList={existingObjectsList} planning={0} />
<TypeTree label='Планируемые' value={'planning'} count={planningCount} objectList={planningObjectsList} planning={1} /> <TypeTree label='Планируемые' value={'planning'} count={planningCount} objectList={planningObjectsList} planning={1} />
</Accordion> </Accordion>
) )
} else {
return (
<Text size='xs'>Выберите регион и населённый пункт, чтобы увидеть список объектов.</Text>
)
}
} }
interface TypeTreeProps { interface TypeTreeProps {
@ -90,18 +100,23 @@ const ObjectList = ({
planning, planning,
count count
}: IObjectList) => { }: IObjectList) => {
const { selectedCity, selectedYear } = useObjectsStore() const { selectedDistrict, selectedYear } = useObjectsStore()
const { data } = useSWR( 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), (url) => fetcher(url, BASE_URL.ems),
{ revalidateOnFocus: false } {
revalidateOnFocus: false,
revalidateIfStale: false
}
) )
return ( 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) => ( {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> </NavLink>
) )

View File

@ -2,13 +2,13 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import 'ol/ol.css' import 'ol/ol.css'
import Map from 'ol/Map' import Map from 'ol/Map'
import View from 'ol/View' 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 { ImageStatic, OSM, Vector as VectorSource, XYZ } from 'ol/source'
import { Tile as TileLayer, VectorImage, Vector as VectorLayer } from 'ol/layer' import { Tile as TileLayer, VectorImage, Vector as VectorLayer } from 'ol/layer'
import { click, never, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition' import { click, never, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
import Feature from 'ol/Feature' import Feature from 'ol/Feature'
import { IRectCoords, SatelliteMapsProvider } from '../../interfaces/map' 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 { drawingLayerStyle, highlightStyleRed, highlightStyleYellow, overlayStyle, regionsLayerStyle } from './MapStyles'
import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources' import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources'
import ImageLayer from 'ol/layer/Image' import ImageLayer from 'ol/layer/Image'
@ -22,13 +22,11 @@ import { get, transform } from 'ol/proj'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance' import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants' 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 { ActionIcon, Autocomplete, Box, CloseButton, Flex, Select as MantineSelect, MantineStyleProp, rem, Slider, useMantineColorScheme, Portal, Menu, Button, Group, Divider, LoadingOverlay } from '@mantine/core'
import { IconPlus, IconSearch, IconSettings, IconTable, IconUpload } from '@tabler/icons-react' import { IconBoxMultiple, IconChevronDown, IconPlus, IconSearch, IconSettings, IconUpload } from '@tabler/icons-react'
import { getGridCellPosition } from './mapUtils' import { getGridCellPosition } from './mapUtils'
import { IFigure, ILine } from '../../interfaces/gis' import { IFigure, ILine } from '../../interfaces/gis'
import axios from 'axios' import axios from 'axios'
import ObjectParameter from './ObjectParameter'
import { IObjectParam } from '../../interfaces/objects'
import MapToolbar from './MapToolbar/MapToolbar' import MapToolbar from './MapToolbar/MapToolbar'
import MapStatusbar from './MapStatusbar/MapStatusbar' import MapStatusbar from './MapStatusbar/MapStatusbar'
import { measureStyleFunction, modifyStyle } from './Measure/MeasureStyles' import { measureStyleFunction, modifyStyle } from './Measure/MeasureStyles'
@ -36,7 +34,48 @@ import { setCurrentCoordinate, setCurrentX, setCurrentY, setCurrentZ, setSatMaps
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { useThrottle } from '@uidotdev/usehooks' import { useThrottle } from '@uidotdev/usehooks'
import ObjectTree from '../Tree/ObjectTree' 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 // Settings for cities
const citySettings = [ const citySettings = [
@ -53,10 +92,11 @@ const citySettings = [
] ]
const MapComponent = () => { const MapComponent = () => {
const { colorScheme } = useMantineColorScheme();
// States // States
const { selectedCity, selectedYear, currentObjectId } = useObjectsStore() const { selectedYear, currentObjectId, selectedRegion, selectedDistrict } = useObjectsStore()
const { currentTool, satMapsProvider } = useMapStore() const { currentTool, satMapsProvider, selectedObjectType } = useMapStore()
/// ///
const [file, setFile] = useState<File | null>(null) const [file, setFile] = useState<File | null>(null)
@ -82,83 +122,6 @@ const MapComponent = () => {
const measureDraw = useRef<Draw | null>(null) 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 mapElement = useRef<HTMLDivElement | null>(null)
const map = useRef<Map | null>(null) const map = useRef<Map | null>(null)
@ -182,6 +145,7 @@ const MapComponent = () => {
const draw = useRef<Draw | null>(null) const draw = useRef<Draw | null>(null)
const snap = useRef<Snap | null>(null) const snap = useRef<Snap | null>(null)
const translate = useRef<Translate | null>(null)
const selectFeature = useRef<Select>(new Select({ const selectFeature = useRef<Select>(new Select({
condition: function (mapBrowserEvent) { 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({ const baseLayer = useRef<TileLayer>(new TileLayer({
source: new OSM(), source: new OSM(),
@ -258,7 +222,6 @@ const MapComponent = () => {
} }
})) }))
// tile processing // tile processing
const handleImageDrop = useCallback((event: DragEvent) => { const handleImageDrop = useCallback((event: DragEvent) => {
event.preventDefault(); event.preventDefault();
@ -455,9 +418,9 @@ const MapComponent = () => {
view: new View({ view: new View({
center: transform([129.7466541, 62.083504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]), center: transform([129.7466541, 62.083504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]),
//zoom: 16, //zoom: 16,
zoom: 15, zoom: 5,
maxZoom: 21, maxZoom: 21,
//extent: mapExtent, extent: mapExtent,
}), }),
}) })
@ -497,7 +460,7 @@ const MapComponent = () => {
loadFeatures(drawingLayerSource) loadFeatures(drawingLayerSource)
regionsInit(map, selectedRegion, regionsLayer) regionsInit(map, selectedArea, regionsLayer)
if (mapElement.current) { if (mapElement.current) {
mapElement.current.addEventListener('dragover', (e) => { mapElement.current.addEventListener('dragover', (e) => {
@ -520,8 +483,9 @@ const MapComponent = () => {
if (currentTool) { if (currentTool) {
if (draw.current) map?.current?.removeInteraction(draw.current) if (draw.current) map?.current?.removeInteraction(draw.current)
//if (snap.current) map?.current?.removeInteraction(snap.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 { } else {
if (translate.current) map?.current?.removeInteraction(translate.current)
if (draw.current) map?.current?.removeInteraction(draw.current) if (draw.current) map?.current?.removeInteraction(draw.current)
if (snap.current) map?.current?.removeInteraction(snap.current) if (snap.current) map?.current?.removeInteraction(snap.current)
if (measureDraw.current) map?.current?.removeInteraction(measureDraw.current) if (measureDraw.current) map?.current?.removeInteraction(measureDraw.current)
@ -577,17 +541,11 @@ const MapComponent = () => {
} }
} }
const { colorScheme } = useMantineColorScheme();
const mapControlsStyle: MantineStyleProp = { const mapControlsStyle: MantineStyleProp = {
borderRadius: '4px', borderRadius: '4px',
position: 'absolute', position: 'absolute',
zIndex: '1', zIndex: '1',
// backgroundColor: (theme) => backgroundColor: colorScheme === 'light' ? '#F0F0F0CC' : '#000000CC',
// theme.palette.mode === 'light'
// ? '#FFFFFFAA'
// : '#000000AA',
backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA',
backdropFilter: 'blur(8px)', backdropFilter: 'blur(8px)',
border: '1px solid #00000022' border: '1px solid #00000022'
} }
@ -613,23 +571,16 @@ const MapComponent = () => {
} }
}, [nodes]) }, [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 [searchObject, setSearchObject] = useState<string | undefined>("")
const throttledSearchObject = useThrottle(searchObject, 500); const throttledSearchObject = useThrottle(searchObject, 500);
const [selectedObjectList, setSelectedObjectList] = useState<number | null>(null)
useEffect(() => { useEffect(() => {
if (!selectedObjectList || !map.current) return; if (!selectedObjectType || !map.current) return;
if (figuresLayer.current) { if (figuresLayer.current) {
// Reset styles and apply highlight to matching features // Reset styles and apply highlight to matching features
figuresLayer.current.getSource()?.getFeatures().forEach((feature) => { figuresLayer.current.getSource()?.getFeatures().forEach((feature) => {
if (selectedObjectList == feature.get('type')) { if (selectedObjectType == feature.get('type')) {
feature.setStyle(highlightStyleYellow); feature.setStyle(highlightStyleYellow);
} else { } else {
feature.setStyle(undefined); // Reset to default style feature.setStyle(undefined); // Reset to default style
@ -640,14 +591,14 @@ const MapComponent = () => {
if (linesLayer.current) { if (linesLayer.current) {
// Reset styles and apply highlight to matching features // Reset styles and apply highlight to matching features
linesLayer.current.getSource()?.getFeatures().forEach((feature) => { linesLayer.current.getSource()?.getFeatures().forEach((feature) => {
if (selectedObjectList == feature.get('type')) { if (selectedObjectType == feature.get('type')) {
feature.setStyle(highlightStyleYellow); feature.setStyle(highlightStyleYellow);
} else { } else {
feature.setStyle(undefined); // Reset to default style feature.setStyle(undefined); // Reset to default style
} }
}) })
} }
}, [selectedObjectList]) }, [selectedObjectType])
useEffect(() => { useEffect(() => {
if (currentObjectId) { if (currentObjectId) {
@ -688,17 +639,16 @@ const MapComponent = () => {
} }
}, [currentObjectId]) }, [currentObjectId])
const { data: valuesData } = useSWR( const { data: regionsData } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null, `/general/regions/all`,
(url) => fetcher(url, BASE_URL.ems), (url) => fetcher(url, BASE_URL.ems),
{ {
revalidateOnFocus: false, revalidateOnFocus: false
revalidateIfStale: false
} }
) )
const { data: citiesData } = useSWR( const { data: districtsData } = useSWR(
`/general/cities/all?limit=${10}&offset=${citiesPage || 0}${throttledSearchCity ? `&search=${throttledSearchCity}` : ''}`, selectedRegion ? `/general/districts/all?region_id=${selectedRegion}` : null,
(url) => fetcher(url, BASE_URL.ems), (url) => fetcher(url, BASE_URL.ems),
{ {
revalidateOnFocus: false revalidateOnFocus: false
@ -706,7 +656,7 @@ const MapComponent = () => {
) )
const { data: searchData } = useSWR( 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), (url) => fetcher(url, BASE_URL.ems),
{ {
revalidateOnFocus: false revalidateOnFocus: false
@ -714,7 +664,7 @@ const MapComponent = () => {
) )
const { data: figuresData, isValidating: figuresValidating } = useSWR( 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, { (url) => axios.get(url, {
baseURL: BASE_URL.ems baseURL: BASE_URL.ems
}).then((res) => res.data), }).then((res) => res.data),
@ -723,8 +673,8 @@ const MapComponent = () => {
} }
) )
const { data: linesData } = useSWR( const { data: linesData, isValidating: linesValidating } = useSWR(
!figuresValidating && selectedCity && selectedYear ? `/gis/lines/all?city_id=${selectedCity}&year=${selectedYear}&offset=0&limit=${10000}` : null, !figuresValidating && selectedDistrict && selectedYear ? `/gis/lines/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
(url) => axios.get(url, { (url) => axios.get(url, {
baseURL: BASE_URL.ems baseURL: BASE_URL.ems
}).then((res) => { }).then((res) => {
@ -744,7 +694,7 @@ const MapComponent = () => {
} }
//let rotation = 0 //let rotation = 0
const settings = citySettings.find(el => el.city_id === selectedCity) const settings = citySettings.find(el => el.city_id === selectedDistrict)
if (settings) { if (settings) {
console.log("City settings found") console.log("City settings found")
@ -778,6 +728,14 @@ const MapComponent = () => {
figuresData.map((figure: IFigure) => { figuresData.map((figure: IFigure) => {
processFigure(figure, scaling, [offset_x, offset_y], figuresLayer) 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(() => { useEffect(() => {
if (selectedCity) { if (selectedDistrict) {
let offset_x = 14442665.697619518 let offset_x = 14442665.697619518
let offset_y = 8884520.63524492 let offset_y = 8884520.63524492
//let scale = 9000 //let scale = 9000
@ -805,7 +763,7 @@ const MapComponent = () => {
//let image_width = 8500 //let image_width = 8500
//let image_height = 12544 //let image_height = 12544
const settings = citySettings.find(el => el.city_id === selectedCity) const settings = citySettings.find(el => el.city_id === selectedDistrict)
if (settings) { if (settings) {
console.log("City settings found") console.log("City settings found")
@ -830,7 +788,7 @@ const MapComponent = () => {
console.log("City settings NOT found") 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(); const img = new Image();
img.src = imageUrl; img.src = imageUrl;
img.onload = () => { img.onload = () => {
@ -867,7 +825,7 @@ const MapComponent = () => {
} }
}; };
} }
}, [selectedCity]) }, [selectedDistrict])
useEffect(() => { useEffect(() => {
// if (map.current) { // if (map.current) {
@ -909,14 +867,8 @@ const MapComponent = () => {
<Box w={'100%'} h='100%' pos={'relative'}> <Box w={'100%'} h='100%' pos={'relative'}>
<Portal target='#header-portal'> <Portal target='#header-portal'>
<Flex gap={'sm'} direction={'row'}> <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' }, { label: 'Static', value: 'static' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} />
</Flex>
<form>
<Autocomplete <Autocomplete
form='search_object'
placeholder="Поиск" placeholder="Поиск"
flex={'1'} flex={'1'}
data={searchData ? searchData.map((item: { value: string, id_object: string }) => ({ label: item.value, value: item.id_object.toString() })) : []} data={searchData ? searchData.map((item: { value: string, id_object: string }) => ({ label: item.value, value: item.id_object.toString() })) : []}
@ -939,32 +891,26 @@ const MapComponent = () => {
leftSection={<IconSearch size={16} />} leftSection={<IconSearch size={16} />}
value={searchObject} value={searchObject}
/> />
</form>
<form> <MantineSelect
<Autocomplete placeholder="Регион"
placeholder="Район"
flex={'1'} flex={'1'}
data={citiesData ? citiesData.map((item: { name: string, id: number }) => ({ label: item.name, value: item.id.toString() })) : []} data={regionsData ? regionsData.map((item: { name: string, id: number }) => ({ label: item.name, value: item.id.toString() })) : []}
//onSelect={(e) => console.log(e.currentTarget.value)} onChange={(value) => setSelectedRegion(Number(value))}
onChange={(value) => setSearchCity(value)} clearable
onOptionSubmit={(value) => setSelectedCity(Number(value))} searchable
rightSection={ value={selectedRegion ? selectedRegion.toString() : null}
searchCity !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearchCity('')
setSelectedCity(null)
}}
aria-label="Clear value"
/> />
)
} <MantineSelect
value={searchCity} 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}
/> />
</form>
<MantineSelect <MantineSelect
w='84px' w='84px'
@ -976,134 +922,71 @@ const MapComponent = () => {
allowDeselect={false} allowDeselect={false}
/> />
<ActionIcon <Menu
size='lg' position="bottom-end"
variant='transparent' transitionProps={{ transition: 'pop-top-right' }}
title='Настройки ИКС' 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) }} /> <IconSettings style={{ width: rem(20), height: rem(20) }} />
</ActionIcon> </ActionIcon>
</Flex> </Flex>
</Portal> </Portal>
<LoadingOverlay visible={linesValidating || figuresValidating} />
<Flex w={'100%'} h={'100%'} pos={'absolute'}> <Flex w={'100%'} h={'100%'} pos={'absolute'}>
{selectedRegion && selectedDistrict &&
<MapToolbar <MapToolbar
onSave={() => saveFeatures(drawingLayer)} onSave={() => saveFeatures(drawingLayer)}
onRemove={() => draw.current?.removeLastPoint()} onRemove={() => draw.current?.removeLastPoint()}
onMover={() => map?.current?.addInteraction(new Translate())}
colorScheme={colorScheme}
/> />
}
<Flex direction='column' mah={'86%'} pl='sm' style={{ {selectedRegion && selectedDistrict &&
<Flex direction='column' mah={'94%'} h={'100%'} pl='sm' style={{
...mapControlsStyle, ...mapControlsStyle,
maxWidth: '340px', maxWidth: '340px',
width: '100%', width: '100%',
top: '8px', top: '8px',
left: '8px', left: '8px',
}}> }}>
<ScrollAreaAutosize offsetScrollbars> <TabsPane defaultTab='objects' tabs={objectsPane} />
<Stack> <Divider />
<Flex direction='row'> <TabsPane defaultTab='parameters' tabs={paramsPane} />
<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> </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>
<MapStatusbar <MapStatusbar
mapControlsStyle={mapControlsStyle} mapControlsStyle={mapControlsStyle}
@ -1113,13 +996,7 @@ const MapComponent = () => {
<div <div
id="map-container" id="map-container"
ref={mapElement} ref={mapElement}
style={{ style={{ width: '100%', height: '100%', maxHeight: '100%', position: 'fixed', flexGrow: 1 }}
width: '100%',
height: '100%',
maxHeight: '100%',
position: 'fixed',
flexGrow: 1
}}
> >
</div> </div>
</Box > </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 { IconApi, IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPoint, IconPolygon, IconRuler } from '@tabler/icons-react'
import { setCurrentTool, useMapStore } from '../../../store/map'; import { setCurrentTool, useMapStore } from '../../../store/map';
interface IToolbarProps { interface IToolbarProps {
onSave: () => void; onSave: () => void;
onRemove: () => void; onRemove: () => void;
onMover: () => void;
colorScheme: MantineColorScheme;
} }
const MapToolbar = ({ const MapToolbar = ({
onSave, onSave,
onRemove, onRemove,
onMover,
colorScheme
}: IToolbarProps) => { }: IToolbarProps) => {
const mapState = useMapStore() const mapState = useMapStore()
const { colorScheme } = useMantineColorScheme();
return ( return (
<ActionIcon.Group orientation='vertical' pos='absolute' top='8px' right='8px' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}> <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 <ActionIcon
size='lg' size='lg'
variant='transparent' variant={mapState.currentTool === 'Mover' ? 'filled' : 'transparent'}
onClick={onMover} onClick={() => {
setCurrentTool('Mover')
}}
> >
<IconArrowsMove /> <IconArrowsMove />
</ActionIcon> </ActionIcon>

View File

@ -98,10 +98,14 @@ const ObjectParameter = ({
return ( return (
<TableValue value={value} name={name} type='number' /> <TableValue value={value} name={name} type='number' />
) )
case 'uniqueidentifier':
return (
<TableValue value={value} name={name} type='value'/>
)
default: default:
return ( return (
<div> <div>
{type} {name}
{value as string} {value as string}
</div> </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,85 +27,36 @@ 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) => { const TCBValue = (vtable: string) => {
switch (vtable) { if (tables.includes(vtable)) {
case 'vStreets':
return ( return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} /> <TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
) )
case 'tTypes': } else {
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 ( return (
<Text> <Text>
{JSON.stringify(name)} {JSON.stringify(name)}

View File

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

View File

@ -4,11 +4,21 @@ import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.tsx' import App from './App.tsx'
import './index.css' 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( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode> <React.StrictMode>
<MantineProvider defaultColorScheme="light"> <MantineProvider theme={theme}>
<App /> <App />
</MantineProvider> </MantineProvider>
</React.StrictMode>, </React.StrictMode>,

View File

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

View File

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

View File

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

View File

@ -1,14 +1,46 @@
import express, { Request, Response } from 'express'; import express, { Request, Response } from 'express';
import { tediousQuery } from '../../utils/tedious'; import { tediousQuery } from '../../utils/tedious';
import { GeneralDB } from '../../constants/db';
const router = express.Router() const router = express.Router()
router.get('/regions/all', async (req: Request, res: Response) => {
try {
const result = await tediousQuery(
`
SELECT * FROM ${GeneralDB}..vRegions;
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
router.get('/districts/all', async (req: Request, res: Response) => {
try {
const { region_id } = req.query
const result = await tediousQuery(
`
SELECT c.*, d.name AS district_name
FROM ${GeneralDB}..vCities c
JOIN ${GeneralDB}..vDistricts d ON d.id_region = c.id_region AND d.id = c.id_district
WHERE c.id_region = ${region_id};
`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
router.get('/cities/all', async (req: Request, res: Response) => { router.get('/cities/all', async (req: Request, res: Response) => {
try { try {
const { offset, limit, search, id } = req.query const { offset, limit, search, id } = req.query
const result = await tediousQuery( const result = await tediousQuery(
` `
SELECT * FROM nGeneral..Cities SELECT * FROM ${GeneralDB}..Cities
${id ? `WHERE id = '${id}'` : ''} ${id ? `WHERE id = '${id}'` : ''}
${search ? `WHERE name LIKE '%${search || ''}%'` : ''} ${search ? `WHERE name LIKE '%${search || ''}%'` : ''}
ORDER BY id ORDER BY id
@ -26,7 +58,7 @@ router.get('/types/all', async (req: Request, res: Response) => {
try { try {
const result = await tediousQuery( const result = await tediousQuery(
` `
SELECT * FROM nGeneral..tTypes SELECT * FROM ${GeneralDB}..tTypes
ORDER BY id ORDER BY id
` `
) )
@ -42,7 +74,7 @@ router.get('/objects/all', async (req: Request, res: Response) => {
const result = await tediousQuery( const result = await tediousQuery(
` `
SELECT * FROM nGeneral..vObjects SELECT * FROM ${GeneralDB}..vObjects
${city_id ? `WHERE id_city = ${city_id}` : ''} ${city_id ? `WHERE id_city = ${city_id}` : ''}
ORDER BY object_id ORDER BY object_id
OFFSET ${Number(offset) || 0} ROWS OFFSET ${Number(offset) || 0} ROWS
@ -81,31 +113,57 @@ router.get('/objects/list', async (req: Request, res: Response) => {
// ) = ${planning}; // ) = ${planning};
// ` // `
` `
WITH cte_split(type_id, split_value, caption_params) AS
(
-- anchor member
SELECT DISTINCT
type_id,
CAST(LEFT(caption_params, CHARINDEX(',', caption_params + ',') - 1) AS VARCHAR(255)), -- Explicitly casting to VARCHAR
STUFF(caption_params, 1, CHARINDEX(',', caption_params + ','), '')
FROM New_Gis..caption_params
WHERE city_id = -1 AND user_id = -1
UNION ALL
-- recursive member
SELECT SELECT
vObjects.*, type_id,
CASE CAST(LEFT(caption_params, CHARINDEX(',', caption_params + ',') - 1) AS VARCHAR(255)), -- Explicitly casting to VARCHAR
WHEN vObjects.boiler_id IS NOT NULL THEN vBoilers.name STUFF(caption_params, 1, CHARINDEX(',', caption_params + ','), '')
ELSE CAST(tValues.value AS varchar(max)) FROM cte_split
END AS name WHERE caption_params > ''
FROM )
vObjects SELECT
JOIN o.object_id,
vBoilers ON vBoilers.id = vObjects.boiler_id o.type,
JOIN o.id_city,
tValues ON tValues.id_param = 4 AND tValues.id_object = vObjects.object_id o.year,
o.planning,
string_agg(cast(v.value as varchar), ',') as caption
FROM ${GeneralDB}..vObjects o
JOIN cte_split c ON o.type = c.type_id
JOIN ${GeneralDB}..tParameters p ON p.id = split_value
JOIN ${GeneralDB}..tValues v
ON
v.id_param = split_value
AND v.id_object = o.object_id
AND (v.date_po IS NULL)
AND (v.date_s < DATEFROMPARTS(${Number(year) + 1},01,01))
WHERE WHERE
vObjects.id_city = ${city_id} o.id_city = ${city_id}
AND vObjects.year = ${year} AND o.year = ${year}
AND type = ${type} AND o.type = ${type}
AND AND
( (
CASE CASE
WHEN TRY_CAST(vObjects.planning AS BIT) IS NOT NULL THEN TRY_CAST(vObjects.planning AS BIT) WHEN TRY_CAST(o.planning AS BIT) IS NOT NULL THEN TRY_CAST(o.planning AS BIT)
WHEN vObjects.planning = 'TRUE' THEN 1 WHEN o.planning = 'TRUE' THEN 1
WHEN vObjects.planning = 'FALSE' THEN 0 WHEN o.planning = 'FALSE' THEN 0
ELSE NULL ELSE NULL
END END
) = ${planning}; ) = ${planning}
GROUP BY object_id, type, id_city, year, planning;
` `
) )
res.status(200).json(result) res.status(200).json(result)
@ -149,7 +207,7 @@ router.get('/objects/:id([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F
const result = await tediousQuery( const result = await tediousQuery(
` `
SELECT * FROM nGeneral..vObjects SELECT * FROM ${GeneralDB}..vObjects
${id ? `WHERE object_id = '${id}'` : ''} ${id ? `WHERE object_id = '${id}'` : ''}
` `
) )
@ -179,9 +237,9 @@ router.get('/values/all', async (req: Request, res: Response) => {
date_po, date_po,
id_user id_user
FROM FROM
nGeneral..tValues v ${GeneralDB}..tValues v
JOIN JOIN
nGeneral..tParameters p ON v.id_param = p.id ${GeneralDB}..tParameters p ON v.id_param = p.id
WHERE id_object = '${object_id}' WHERE id_object = '${object_id}'
` `
) )
@ -202,7 +260,7 @@ router.get('/params/all', async (req: Request, res: Response) => {
const result = await tediousQuery( const result = await tediousQuery(
` `
SELECT * FROM nGeneral..tParameters SELECT * FROM ${GeneralDB}..tParameters
WHERE id = '${param_id}' WHERE id = '${param_id}'
` `
) )
@ -216,11 +274,11 @@ router.get('/params/all', async (req: Request, res: Response) => {
const tcbParamQuery = (vtable: string, id_city: string) => { const tcbParamQuery = (vtable: string, id_city: string) => {
switch (vtable) { switch (vtable) {
case 'vStreets': case 'vStreets':
return `SELECT * FROM nGeneral..${vtable} WHERE id_city = ${id_city};` return `SELECT * FROM ${GeneralDB}..${vtable} WHERE id_city = ${id_city};`
case 'vBoilers': case 'vBoilers':
return `SELECT * FROM nGeneral..${vtable} WHERE id_city = ${id_city};` return `SELECT * FROM ${GeneralDB}..${vtable} WHERE id_city = ${id_city};`
default: default:
return `SELECT * FROM nGeneral..${vtable};` return `SELECT * FROM ${GeneralDB}..${vtable};`
} }
} }
@ -236,7 +294,7 @@ router.get('/params/tcb', async (req: Request, res: Response) => {
if (id) { if (id) {
const result = await tediousQuery( const result = await tediousQuery(
` `
SELECT * FROM nGeneral..${vtable} SELECT * FROM ${GeneralDB}..${vtable}
WHERE id = '${id}' WHERE id = '${id}'
` `
) )
@ -273,8 +331,8 @@ router.get('/search/objects', async (req: Request, res: Response) => {
ROW_NUMBER() OVER (PARTITION BY id_object ORDER BY date_s DESC) AS rn, ROW_NUMBER() OVER (PARTITION BY id_object ORDER BY date_s DESC) AS rn,
o.id_city AS id_city, o.id_city AS id_city,
o.year AS year o.year AS year
FROM nGeneral..tValues FROM ${GeneralDB}..tValues
JOIN nGeneral..tObjects o ON o.id = id_object JOIN ${GeneralDB}..tObjects o ON o.id = id_object
WHERE CAST(value AS varchar(max)) LIKE '%${q}%' WHERE CAST(value AS varchar(max)) LIKE '%${q}%'
) )
SELECT SELECT

View File

@ -1,5 +1,6 @@
import express, { Request, Response } from 'express'; import express, { Request, Response } from 'express';
import { tediousQuery } from '../../utils/tedious'; import { tediousQuery } from '../../utils/tedious';
import { GeneralDB, GisDB } from '../../constants/db';
const router = express.Router() const router = express.Router()
router.get('/images/all', async (req: Request, res: Response) => { router.get('/images/all', async (req: Request, res: Response) => {
@ -8,7 +9,7 @@ router.get('/images/all', async (req: Request, res: Response) => {
const result = await tediousQuery( const result = await tediousQuery(
` `
SELECT * FROM New_Gis..images SELECT * FROM ${GisDB}..images
${city_id ? `WHERE city_id = ${city_id}` : ''} ${city_id ? `WHERE city_id = ${city_id}` : ''}
ORDER BY city_id ORDER BY city_id
OFFSET ${Number(offset) || 0} ROWS OFFSET ${Number(offset) || 0} ROWS
@ -29,8 +30,8 @@ router.get('/figures/all', async (req: Request, res: Response) => {
const result = await tediousQuery( const result = await tediousQuery(
` `
SELECT * FROM New_Gis..figures f SELECT * FROM ${GisDB}..figures f
JOIN nGeneral..vObjects o ON f.object_id = o.object_id WHERE o.id_city = ${city_id} AND f.year = ${year} JOIN ${GeneralDB}..vObjects o ON f.object_id = o.object_id WHERE o.id_city = ${city_id} AND f.year = ${year}
ORDER BY f.year ORDER BY f.year
OFFSET ${Number(offset) || 0} ROWS OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY; FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
@ -49,8 +50,8 @@ router.get('/lines/all', async (req: Request, res: Response) => {
const result = await tediousQuery( const result = await tediousQuery(
` `
SELECT * FROM New_Gis..lines l SELECT * FROM ${GisDB}..lines l
JOIN nGeneral..vObjects o ON l.object_id = o.object_id WHERE o.id_city = ${city_id} AND l.year = ${year} JOIN ${GeneralDB}..vObjects o ON l.object_id = o.object_id WHERE o.id_city = ${city_id} AND l.year = ${year}
ORDER BY l.year ORDER BY l.year
OFFSET ${Number(offset) || 0} ROWS OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY; FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
@ -62,4 +63,16 @@ router.get('/lines/all', async (req: Request, res: Response) => {
} }
}) })
router.get('/regions/borders', async (req: Request, res: Response) => {
try {
const result = await tediousQuery(
`
SELECT * FROM ${GisDB}..visual_regions`
)
res.status(200).json(result)
} catch (err) {
res.status(500)
}
})
export default router export default router

7
ems/src/constants/db.ts Normal file
View File

@ -0,0 +1,7 @@
const GeneralDB = 'nGeneral'
const GisDB = 'New_Gis'
export {
GeneralDB,
GisDB
}