Map
This commit is contained in:
File diff suppressed because one or more lines are too long
@ -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 = () => {
|
||||
}
|
||||
)
|
||||
|
||||
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>
|
||||
)
|
||||
|
@ -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,14 +867,8 @@ 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)} />
|
||||
|
||||
<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
|
||||
form='search_object'
|
||||
placeholder="Поиск"
|
||||
flex={'1'}
|
||||
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} />}
|
||||
value={searchObject}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<form>
|
||||
<Autocomplete
|
||||
placeholder="Район"
|
||||
<MantineSelect
|
||||
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"
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
value={searchCity}
|
||||
|
||||
<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}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<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'}>
|
||||
{selectedRegion && selectedDistrict &&
|
||||
<MapToolbar
|
||||
onSave={() => saveFeatures(drawingLayer)}
|
||||
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,
|
||||
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>
|
||||
<TabsPane defaultTab='objects' tabs={objectsPane} />
|
||||
<Divider />
|
||||
<TabsPane defaultTab='parameters' tabs={paramsPane} />
|
||||
</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
|
||||
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 >
|
||||
|
27
client/src/components/map/MapLayers/MapLayers.tsx
Normal file
27
client/src/components/map/MapLayers/MapLayers.tsx
Normal 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
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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
|
25
client/src/components/map/RegionSelect.tsx
Normal file
25
client/src/components/map/RegionSelect.tsx
Normal 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
|
@ -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) => {
|
||||
switch (vtable) {
|
||||
case 'vStreets':
|
||||
if (tables.includes(vtable)) {
|
||||
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:
|
||||
} else {
|
||||
return (
|
||||
<Text>
|
||||
{JSON.stringify(name)}
|
||||
|
@ -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) => ({
|
||||
|
45
client/src/components/map/TabsPane/TabsPane.tsx
Normal file
45
client/src/components/map/TabsPane/TabsPane.tsx
Normal 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
|
@ -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
|
||||
});
|
||||
|
@ -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>,
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -9,4 +9,5 @@ export type ToolType =
|
||||
"GeometryCollection" |
|
||||
"Circle" |
|
||||
"Measure" |
|
||||
"Mover" |
|
||||
null
|
@ -1,14 +1,46 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { tediousQuery } from '../../utils/tedious';
|
||||
import { GeneralDB } from '../../constants/db';
|
||||
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) => {
|
||||
try {
|
||||
const { offset, limit, search, id } = req.query
|
||||
|
||||
const result = await tediousQuery(
|
||||
`
|
||||
SELECT * FROM nGeneral..Cities
|
||||
SELECT * FROM ${GeneralDB}..Cities
|
||||
${id ? `WHERE id = '${id}'` : ''}
|
||||
${search ? `WHERE name LIKE '%${search || ''}%'` : ''}
|
||||
ORDER BY id
|
||||
@ -26,7 +58,7 @@ router.get('/types/all', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const result = await tediousQuery(
|
||||
`
|
||||
SELECT * FROM nGeneral..tTypes
|
||||
SELECT * FROM ${GeneralDB}..tTypes
|
||||
ORDER BY id
|
||||
`
|
||||
)
|
||||
@ -42,7 +74,7 @@ router.get('/objects/all', async (req: Request, res: Response) => {
|
||||
|
||||
const result = await tediousQuery(
|
||||
`
|
||||
SELECT * FROM nGeneral..vObjects
|
||||
SELECT * FROM ${GeneralDB}..vObjects
|
||||
${city_id ? `WHERE id_city = ${city_id}` : ''}
|
||||
ORDER BY object_id
|
||||
OFFSET ${Number(offset) || 0} ROWS
|
||||
@ -81,31 +113,57 @@ router.get('/objects/list', async (req: Request, res: Response) => {
|
||||
// ) = ${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
|
||||
vObjects.*,
|
||||
CASE
|
||||
WHEN vObjects.boiler_id IS NOT NULL THEN vBoilers.name
|
||||
ELSE CAST(tValues.value AS varchar(max))
|
||||
END AS name
|
||||
FROM
|
||||
vObjects
|
||||
JOIN
|
||||
vBoilers ON vBoilers.id = vObjects.boiler_id
|
||||
JOIN
|
||||
tValues ON tValues.id_param = 4 AND tValues.id_object = vObjects.object_id
|
||||
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 cte_split
|
||||
WHERE caption_params > ''
|
||||
)
|
||||
SELECT
|
||||
o.object_id,
|
||||
o.type,
|
||||
o.id_city,
|
||||
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
|
||||
vObjects.id_city = ${city_id}
|
||||
AND vObjects.year = ${year}
|
||||
AND type = ${type}
|
||||
o.id_city = ${city_id}
|
||||
AND o.year = ${year}
|
||||
AND o.type = ${type}
|
||||
AND
|
||||
(
|
||||
CASE
|
||||
WHEN TRY_CAST(vObjects.planning AS BIT) IS NOT NULL THEN TRY_CAST(vObjects.planning AS BIT)
|
||||
WHEN vObjects.planning = 'TRUE' THEN 1
|
||||
WHEN vObjects.planning = 'FALSE' THEN 0
|
||||
WHEN TRY_CAST(o.planning AS BIT) IS NOT NULL THEN TRY_CAST(o.planning AS BIT)
|
||||
WHEN o.planning = 'TRUE' THEN 1
|
||||
WHEN o.planning = 'FALSE' THEN 0
|
||||
ELSE NULL
|
||||
END
|
||||
) = ${planning};
|
||||
) = ${planning}
|
||||
GROUP BY object_id, type, id_city, year, planning;
|
||||
`
|
||||
)
|
||||
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(
|
||||
`
|
||||
SELECT * FROM nGeneral..vObjects
|
||||
SELECT * FROM ${GeneralDB}..vObjects
|
||||
${id ? `WHERE object_id = '${id}'` : ''}
|
||||
`
|
||||
)
|
||||
@ -179,9 +237,9 @@ router.get('/values/all', async (req: Request, res: Response) => {
|
||||
date_po,
|
||||
id_user
|
||||
FROM
|
||||
nGeneral..tValues v
|
||||
${GeneralDB}..tValues v
|
||||
JOIN
|
||||
nGeneral..tParameters p ON v.id_param = p.id
|
||||
${GeneralDB}..tParameters p ON v.id_param = p.id
|
||||
WHERE id_object = '${object_id}'
|
||||
`
|
||||
)
|
||||
@ -202,7 +260,7 @@ router.get('/params/all', async (req: Request, res: Response) => {
|
||||
|
||||
const result = await tediousQuery(
|
||||
`
|
||||
SELECT * FROM nGeneral..tParameters
|
||||
SELECT * FROM ${GeneralDB}..tParameters
|
||||
WHERE id = '${param_id}'
|
||||
`
|
||||
)
|
||||
@ -216,11 +274,11 @@ router.get('/params/all', async (req: Request, res: Response) => {
|
||||
const tcbParamQuery = (vtable: string, id_city: string) => {
|
||||
switch (vtable) {
|
||||
case 'vStreets':
|
||||
return `SELECT * FROM nGeneral..${vtable} WHERE id_city = ${id_city};`
|
||||
return `SELECT * FROM ${GeneralDB}..${vtable} WHERE id_city = ${id_city};`
|
||||
case 'vBoilers':
|
||||
return `SELECT * FROM nGeneral..${vtable} WHERE id_city = ${id_city};`
|
||||
return `SELECT * FROM ${GeneralDB}..${vtable} WHERE id_city = ${id_city};`
|
||||
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) {
|
||||
const result = await tediousQuery(
|
||||
`
|
||||
SELECT * FROM nGeneral..${vtable}
|
||||
SELECT * FROM ${GeneralDB}..${vtable}
|
||||
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,
|
||||
o.id_city AS id_city,
|
||||
o.year AS year
|
||||
FROM nGeneral..tValues
|
||||
JOIN nGeneral..tObjects o ON o.id = id_object
|
||||
FROM ${GeneralDB}..tValues
|
||||
JOIN ${GeneralDB}..tObjects o ON o.id = id_object
|
||||
WHERE CAST(value AS varchar(max)) LIKE '%${q}%'
|
||||
)
|
||||
SELECT
|
||||
|
@ -1,5 +1,6 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { tediousQuery } from '../../utils/tedious';
|
||||
import { GeneralDB, GisDB } from '../../constants/db';
|
||||
const router = express.Router()
|
||||
|
||||
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(
|
||||
`
|
||||
SELECT * FROM New_Gis..images
|
||||
SELECT * FROM ${GisDB}..images
|
||||
${city_id ? `WHERE city_id = ${city_id}` : ''}
|
||||
ORDER BY city_id
|
||||
OFFSET ${Number(offset) || 0} ROWS
|
||||
@ -29,8 +30,8 @@ router.get('/figures/all', async (req: Request, res: Response) => {
|
||||
|
||||
const result = await tediousQuery(
|
||||
`
|
||||
SELECT * FROM New_Gis..figures f
|
||||
JOIN nGeneral..vObjects o ON f.object_id = o.object_id WHERE o.id_city = ${city_id} AND f.year = ${year}
|
||||
SELECT * FROM ${GisDB}..figures f
|
||||
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
|
||||
OFFSET ${Number(offset) || 0} ROWS
|
||||
FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
|
||||
@ -49,8 +50,8 @@ router.get('/lines/all', async (req: Request, res: Response) => {
|
||||
|
||||
const result = await tediousQuery(
|
||||
`
|
||||
SELECT * FROM New_Gis..lines l
|
||||
JOIN nGeneral..vObjects o ON l.object_id = o.object_id WHERE o.id_city = ${city_id} AND l.year = ${year}
|
||||
SELECT * FROM ${GisDB}..lines l
|
||||
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
|
||||
OFFSET ${Number(offset) || 0} ROWS
|
||||
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
|
7
ems/src/constants/db.ts
Normal file
7
ems/src/constants/db.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const GeneralDB = 'nGeneral'
|
||||
const GisDB = 'New_Gis'
|
||||
|
||||
export {
|
||||
GeneralDB,
|
||||
GisDB
|
||||
}
|
Reference in New Issue
Block a user