This commit is contained in:
cracklesparkle
2024-12-06 12:42:34 +09:00
parent bd0a317e76
commit e9595f9703
16 changed files with 770 additions and 390 deletions

View File

@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/logo2.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dashboard</title> <title>ИС</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

34
client/public/logo1.svg Normal file
View File

@ -0,0 +1,34 @@
<svg width="252" height="252" viewBox="0 0 252 252" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M61 36C47.1929 36 36 47.1929 36 61V191C36 204.807 47.1929 216 61 216H191C204.807 216 216 204.807 216 191V61C216 47.1929 204.807 36 191 36H61ZM73 54C61.9543 54 53 62.9543 53 74V179C53 190.046 61.9543 199 73 199H178C189.046 199 198 190.046 198 179V74C198 62.9543 189.046 54 178 54H73Z" fill="#D9D9D9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M61 36C47.1929 36 36 47.1929 36 61V191C36 204.807 47.1929 216 61 216H191C204.807 216 216 204.807 216 191V61C216 47.1929 204.807 36 191 36H61ZM73 54C61.9543 54 53 62.9543 53 74V179C53 190.046 61.9543 199 73 199H178C189.046 199 198 190.046 198 179V74C198 62.9543 189.046 54 178 54H73Z" fill="url(#paint0_linear_2_2)"/>
<path d="M67.4996 81.0781L66.5525 172H115.5V126C115.5 119.096 121.096 113.5 128 113.5H236V80H67.434C67.481 80.3523 67.5034 80.7123 67.4996 81.0781Z" fill="#D9D9D9"/>
<path d="M67.4996 81.0781L66.5525 172H115.5V126C115.5 119.096 121.096 113.5 128 113.5H236V80H67.434C67.481 80.3523 67.5034 80.7123 67.4996 81.0781Z" fill="url(#paint1_linear_2_2)"/>
<path d="M236 138.5H158.178L191.678 172H236V138.5Z" fill="#D9D9D9"/>
<path d="M236 138.5H158.178L191.678 172H236V138.5Z" fill="url(#paint2_linear_2_2)"/>
<path d="M156.322 172L140.5 156.178V172H156.322Z" fill="#D9D9D9"/>
<path d="M156.322 172L140.5 156.178V172H156.322Z" fill="url(#paint3_linear_2_2)"/>
<path d="M15 172H51.5517L52.5004 80.9219C52.5037 80.6096 52.526 80.3019 52.5662 80H15V172Z" fill="#D9D9D9"/>
<path d="M15 172H51.5517L52.5004 80.9219C52.5037 80.6096 52.526 80.3019 52.5662 80H15V172Z" fill="url(#paint4_linear_2_2)"/>
<defs>
<linearGradient id="paint0_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-color="#007B91"/>
</linearGradient>
<linearGradient id="paint1_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-color="#007B91"/>
</linearGradient>
<linearGradient id="paint2_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-color="#007B91"/>
</linearGradient>
<linearGradient id="paint3_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-color="#007B91"/>
</linearGradient>
<linearGradient id="paint4_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-color="#007B91"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

34
client/public/logo2.svg Normal file
View File

@ -0,0 +1,34 @@
<svg width="252" height="252" viewBox="0 0 252 252" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M61 36C47.1929 36 36 47.1929 36 61V191C36 204.807 47.1929 216 61 216H191C204.807 216 216 204.807 216 191V61C216 47.1929 204.807 36 191 36H61ZM73 54C61.9543 54 53 62.9543 53 74V179C53 190.046 61.9543 199 73 199H178C189.046 199 198 190.046 198 179V74C198 62.9543 189.046 54 178 54H73Z" fill="#D9D9D9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M61 36C47.1929 36 36 47.1929 36 61V191C36 204.807 47.1929 216 61 216H191C204.807 216 216 204.807 216 191V61C216 47.1929 204.807 36 191 36H61ZM73 54C61.9543 54 53 62.9543 53 74V179C53 190.046 61.9543 199 73 199H178C189.046 199 198 190.046 198 179V74C198 62.9543 189.046 54 178 54H73Z" fill="url(#paint0_linear_2_2)"/>
<path d="M67.4996 81.0781L66.5525 172H115.5V126C115.5 119.096 121.096 113.5 128 113.5H236V80H67.434C67.481 80.3523 67.5034 80.7123 67.4996 81.0781Z" fill="#D9D9D9"/>
<path d="M67.4996 81.0781L66.5525 172H115.5V126C115.5 119.096 121.096 113.5 128 113.5H236V80H67.434C67.481 80.3523 67.5034 80.7123 67.4996 81.0781Z" fill="url(#paint1_linear_2_2)"/>
<path d="M236 138.5H158.178L191.678 172H236V138.5Z" fill="#D9D9D9"/>
<path d="M236 138.5H158.178L191.678 172H236V138.5Z" fill="url(#paint2_linear_2_2)"/>
<path d="M156.322 172L140.5 156.178V172H156.322Z" fill="#D9D9D9"/>
<path d="M156.322 172L140.5 156.178V172H156.322Z" fill="url(#paint3_linear_2_2)"/>
<path d="M15 172H51.5517L52.5004 80.9219C52.5037 80.6096 52.526 80.3019 52.5662 80H15V172Z" fill="#D9D9D9"/>
<path d="M15 172H51.5517L52.5004 80.9219C52.5037 80.6096 52.526 80.3019 52.5662 80H15V172Z" fill="url(#paint4_linear_2_2)"/>
<defs>
<linearGradient id="paint0_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D9FF" stop-opacity="0"/>
<stop offset="1" stop-color="#00D9FF"/>
</linearGradient>
<linearGradient id="paint1_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D9FF" stop-opacity="0"/>
<stop offset="1" stop-color="#00D9FF"/>
</linearGradient>
<linearGradient id="paint2_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D9FF" stop-opacity="0"/>
<stop offset="1" stop-color="#00D9FF"/>
</linearGradient>
<linearGradient id="paint3_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D9FF" stop-opacity="0"/>
<stop offset="1" stop-color="#00D9FF"/>
</linearGradient>
<linearGradient id="paint4_linear_2_2" x1="236" y1="36" x2="15" y2="216" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D9FF" stop-opacity="0"/>
<stop offset="1" stop-color="#00D9FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,21 +0,0 @@
import { useState, useEffect, useMemo } from 'react'
import axiosInstance from '../http/axiosInstance'
export function useDataFetching<T>(url: string, initData: T): T {
const [data, setData] = useState<T>(initData)
useEffect(() => {
const fetchData = async () => {
const response = await axiosInstance.get(url)
const result = await response.data
setData(result)
}
fetchData()
}, [url])
// Memoize the data value
const memoizedData = useMemo<T>(() => data, [data])
return memoizedData
}
export default useDataFetching;

View File

@ -0,0 +1,110 @@
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 { setCurrentObjectId, useObjectsStore } from '../../store/objects';
const ObjectTree = () => {
const { selectedCity, 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,
(url) => fetcher(url, BASE_URL.ems).then(res => {
if (Array.isArray(res)) {
let count = 0
res.forEach(el => {
count = count + el.count
})
setExistingCount(count)
}
return res
}),
{
revalidateOnFocus: false
}
)
const { data: planningObjectsList } = useSWR(
selectedYear && selectedCity ? `/general/objects/list?year=${selectedYear}&city_id=${selectedCity}&planning=1` : null,
(url) => fetcher(url, BASE_URL.ems).then(res => {
if (Array.isArray(res)) {
let count = 0
res.forEach(el => {
count = count + el.count
})
setPlanningCount(count)
}
return res
}),
{
revalidateOnFocus: false
}
)
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>
)
}
interface TypeTreeProps {
label: string;
value: string;
count: number;
objectList: unknown;
planning: number;
}
const TypeTree = ({
label,
objectList,
count,
planning
}: TypeTreeProps) => {
return (
<NavLink p={0} label={`${label} ${count ? `(${count})` : ''}`}>
{Array.isArray(objectList) && objectList.map(list => (
<ObjectList key={list.id} label={list.name} id={list.id} planning={planning} count={list.count} />
))}
</NavLink>
)
}
interface IObjectList {
label: string;
id: number;
planning: number;
count: number;
}
const ObjectList = ({
label,
id,
planning,
count
}: IObjectList) => {
const { selectedCity, selectedYear } = useObjectsStore()
const { data } = useSWR(
selectedCity && selectedYear ? `/general/objects/list?type=${id}&city_id=${selectedCity}&year=${selectedYear}&planning=${planning}` : null,
(url) => fetcher(url, BASE_URL.ems),
{ revalidateOnFocus: false }
)
return (
<NavLink p={0} label={`${label} ${count ? `(${count})` : ''}`}>
{Array.isArray(data) && data.map((type) => (
<NavLink key={type.object_id} label={type.object_id} p={0} onClick={() => setCurrentObjectId(type.object_id)} />
))}
</NavLink>
)
}
export default ObjectTree

View File

@ -1,18 +0,0 @@
import { useEffect, useMemo, useState } from "react";
import UserService from "../services/UserService";
export default function useUserData<T>(token: string, initData: T): T {
const [userData, setUserData] = useState<T>(initData)
useEffect(()=> {
const fetchUserData = async (token: string) => {
const response = await UserService.getCurrentUser(token)
setUserData(response.data)
}
fetchUserData(token)
}, [token])
const memoizedData = useMemo<T>(() => userData, [userData])
return memoizedData
}

View File

@ -4,17 +4,15 @@ import Map from 'ol/Map'
import View from 'ol/View' import View from 'ol/View'
import { 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, 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 } 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 { mapCenter } from './MapConstants'
import ImageLayer from 'ol/layer/Image' import ImageLayer from 'ol/layer/Image'
import VectorImageLayer from 'ol/layer/VectorImage' import { LineString, Point, SimpleGeometry } from 'ol/geom'
import { LineString, Point } from 'ol/geom'
import { fromExtent } from 'ol/geom/Polygon' import { fromExtent } from 'ol/geom/Polygon'
import Collection from 'ol/Collection' import Collection from 'ol/Collection'
import { Coordinate } from 'ol/coordinate' import { Coordinate } from 'ol/coordinate'
@ -24,22 +22,37 @@ 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, Text as MantineText, MantineStyleProp, rem, ScrollAreaAutosize, Slider, useMantineColorScheme, Portal, Tree, Group, TreeNodeData, Button, useTree, Timeline, Text, Stack, Overlay } from '@mantine/core' import { Accordion, ActionIcon, Autocomplete, Box, CloseButton, Flex, Select as MantineSelect, MantineStyleProp, rem, ScrollAreaAutosize, Slider, useMantineColorScheme, Portal, Timeline, Text, Stack, NavLink, Grid, Checkbox } from '@mantine/core'
import { IconChevronDown, IconPlus, IconSettings, IconTable, IconUpload } from '@tabler/icons-react' import { IconPlus, IconSearch, IconSettings, IconTable, 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 ObjectParameter from './ObjectParameter'
import { IObjectData, IObjectList, IObjectParam } from '../../interfaces/objects' import { IObjectParam } from '../../interfaces/objects'
import ObjectData from './ObjectData'
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'
import { useMapStore } from '../../store/map' import { useMapStore } from '../../store/map'
import { MapTreeCheckbox } from './MapTree/MapTreeCheckbox'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { useThrottle } from '@uidotdev/usehooks'
import ObjectTree from '../Tree/ObjectTree'
import { setCurrentObjectId, setSelectedCity, setSelectedYear, useObjectsStore } from '../../store/objects'
// Settings for cities
const citySettings = [
{
city_id: 145,
// scale: 10000,
scale: 9000,
offset_x: 14442665.697619518,
offset_y: 8884520.63524492,
rotation: 0
}
]
const MapComponent = () => { const MapComponent = () => {
const { selectedCity, selectedYear, currentObjectId } = useObjectsStore()
const mapState = useMapStore() const mapState = useMapStore()
const [currentCoordinate, setCurrentCoordinate] = useState<Coordinate | null>(null) const [currentCoordinate, setCurrentCoordinate] = useState<Coordinate | null>(null)
@ -121,24 +134,27 @@ const MapComponent = () => {
} }
})) }))
const figuresLayer = useRef<VectorLayer>(new VectorLayer({ const figuresLayer = useRef<VectorImage>(new VectorImage({
source: new VectorSource(), source: new VectorSource(),
declutter: true,
properties: { properties: {
id: uuidv4(), id: uuidv4(),
name: 'Фигуры' name: 'Фигуры'
} }
})) }))
const linesLayer = useRef<VectorLayer>(new VectorLayer({ const linesLayer = useRef<VectorImage>(new VectorImage({
source: new VectorSource(), source: new VectorSource(),
declutter: true,
properties: { properties: {
id: uuidv4(), id: uuidv4(),
name: 'Линии' name: 'Линии'
} }
})) }))
const regionsLayer = useRef<VectorImageLayer>(new VectorImageLayer({ const regionsLayer = useRef<VectorImage>(new VectorImage({
source: regionsLayerSource, source: regionsLayerSource,
declutter: true,
style: regionsLayerStyle, style: regionsLayerStyle,
properties: { properties: {
id: uuidv4(), id: uuidv4(),
@ -356,14 +372,14 @@ const MapComponent = () => {
], ],
target: mapElement.current as HTMLDivElement, target: mapElement.current as HTMLDivElement,
view: new View({ view: new View({
center: transform([129.7659541, 62.009504], '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: 17, zoom: 16,
maxZoom: 21, maxZoom: 21,
//extent: mapExtent, //extent: mapExtent,
}), }),
}) })
map.current.on('pointermove', function (e: MapBrowserEvent<any>) { map.current.on('pointermove', function (e: MapBrowserEvent<UIEvent>) {
setCurrentCoordinate(e.coordinate) setCurrentCoordinate(e.coordinate)
const currentExtent = get('EPSG:3857')?.getExtent() as Extent const currentExtent = get('EPSG:3857')?.getExtent() as Extent
const { tileX, tileY } = getGridCellPosition(e.coordinate[0], e.coordinate[1], currentExtent, Number(map.current?.getView().getZoom()?.toFixed(0))) const { tileX, tileY } = getGridCellPosition(e.coordinate[0], e.coordinate[1], currentExtent, Number(map.current?.getView().getZoom()?.toFixed(0)))
@ -372,7 +388,7 @@ const MapComponent = () => {
setCurrentY(tileY) setCurrentY(tileY)
}) })
map.current.on('click', function (e: MapBrowserEvent<any>) { map.current.on('click', function (e: MapBrowserEvent<UIEvent>) {
const pixel = map.current?.getEventPixel(e.originalEvent) const pixel = map.current?.getEventPixel(e.originalEvent)
if (pixel) { if (pixel) {
@ -430,7 +446,7 @@ const MapComponent = () => {
} }
}, [mapState.currentTool]) }, [mapState.currentTool])
const [satelliteOpacity, setSatelliteOpacity] = useState<number>(1) const [satelliteOpacity, setSatelliteOpacity] = useState<number>(0)
const [statusText, setStatusText] = useState('') const [statusText, setStatusText] = useState('')
@ -513,31 +529,14 @@ const MapComponent = () => {
} }
}, [nodes]) }, [nodes])
const [currentObjectId, setCurrentObjectId] = useState<string | null>(null)
const [selectedCity, setSelectedCity] = useState<number | null>(null)
const [selectedYear, setSelectedYear] = useState<number | null>(2023)
const [citiesPage, setCitiesPage] = useState<number>(0) const [citiesPage, setCitiesPage] = useState<number>(0)
const [searchCity, setSearchCity] = useState<string | undefined>("") const [searchCity, setSearchCity] = useState<string | undefined>("")
const throttledSearchCity = useThrottle(searchCity, 500);
const { data: existingObjectsList } = useSWR( const [searchObject, setSearchObject] = useState<string | undefined>("")
selectedYear && selectedCity ? `/general/objects/list?year=${selectedYear}&city_id=${selectedCity}&planning=0` : null, const throttledSearchObject = useThrottle(searchObject, 500);
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: planningObjectsList } = useSWR(
selectedYear && selectedCity ? `/general/objects/list?year=${selectedYear}&city_id=${selectedCity}&planning=1` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const [objectsList, setObjectsList] = useState<TreeNodeData[] | null>(null)
const [selectedObjectList, setSelectedObjectList] = useState<number | null>(null) const [selectedObjectList, setSelectedObjectList] = useState<number | null>(null)
useEffect(() => { useEffect(() => {
@ -549,7 +548,7 @@ const MapComponent = () => {
if (selectedObjectList == feature.get('type')) { if (selectedObjectList == feature.get('type')) {
feature.setStyle(highlightStyleYellow); feature.setStyle(highlightStyleYellow);
} else { } else {
feature.setStyle(null); // Reset to default style feature.setStyle(undefined); // Reset to default style
} }
}) })
} }
@ -560,81 +559,70 @@ const MapComponent = () => {
if (selectedObjectList == feature.get('type')) { if (selectedObjectList == feature.get('type')) {
feature.setStyle(highlightStyleYellow); feature.setStyle(highlightStyleYellow);
} else { } else {
feature.setStyle(null); // Reset to default style feature.setStyle(undefined); // Reset to default style
} }
}) })
} }
}, [selectedObjectList]) }, [selectedObjectList])
useEffect(() => {
if (existingObjectsList && planningObjectsList) {
setObjectsList([
{
label: 'Существующие',
value: 'existing',
children: existingObjectsList.map((list: IObjectList) => ({
label:list.name,
count: list.count,
value: list.id,
})),
},
{
label: 'Планируемые',
value: 'planning',
children: planningObjectsList.map((list: IObjectList) => ({
label: list.name,
count: list.count,
value: list.id
}))
}
])
}
}, [existingObjectsList, planningObjectsList])
useEffect(() => { useEffect(() => {
if (currentObjectId) { if (currentObjectId) {
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: Feature) => {
if (currentObjectId == feature.get('object_id')) { if (currentObjectId == feature.get('object_id')) {
feature.setStyle(highlightStyleRed); feature.setStyle(highlightStyleRed);
const geometry = feature.getGeometry()
if (geometry) {
map.current?.getView().fit(geometry as SimpleGeometry, { duration: 500, maxZoom: 18 })
//map.current?.getView().animate({ center: calculateCenter(geometry as SimpleGeometry).center })
}
} else { } else {
feature.setStyle(null); // Reset to default style feature.setStyle(undefined); // Reset to default style
} }
}) })
} }
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: Feature) => {
if (currentObjectId == feature.get('object_id')) { if (currentObjectId == feature.get('object_id')) {
feature.setStyle(highlightStyleRed); feature.setStyle(highlightStyleRed);
const geometry = feature.getGeometry()
if (geometry) {
map.current?.getView().fit(geometry as SimpleGeometry, { duration: 500, maxZoom: 18 })
}
} else { } else {
feature.setStyle(null); // Reset to default style feature.setStyle(undefined); // Reset to default style
} }
}) })
} }
} }
}, [currentObjectId]) }, [currentObjectId])
const { data: currentObjectData } = useSWR(
currentObjectId ? `/general/objects/${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: valuesData } = useSWR( const { data: valuesData } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null, currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems), (url) => fetcher(url, BASE_URL.ems),
{ {
revalidateOnFocus: false revalidateOnFocus: false,
revalidateIfStale: false
} }
) )
const { data: citiesData } = useSWR( const { data: citiesData } = useSWR(
`/general/cities/all?limit=${10}&offset=${citiesPage || 0}${searchCity ? `&search=${searchCity}` : ''}`, `/general/cities/all?limit=${10}&offset=${citiesPage || 0}${throttledSearchCity ? `&search=${throttledSearchCity}` : ''}`,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: searchData } = useSWR(
throttledSearchObject !== "" && selectedCity && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedCity}&year=${selectedYear}` : null,
(url) => fetcher(url, BASE_URL.ems), (url) => fetcher(url, BASE_URL.ems),
{ {
revalidateOnFocus: false revalidateOnFocus: false
@ -664,16 +652,45 @@ const MapComponent = () => {
) )
useEffect(() => { useEffect(() => {
let offset_x = 14444582.697619518
let offset_y = 8866450.63524492
let scale = {
w: 100000,
h: 100000
}
let rotation = 0
if (citySettings.find(el => el.city_id === selectedCity)) {
console.log("City settings found")
if (citySettings.find(el => el.city_id === selectedCity)?.scale) {
scale = {
w: citySettings.find(el => el.city_id === selectedCity).scale,
h: citySettings.find(el => el.city_id === selectedCity).scale
}
}
if (citySettings.find(el => el.city_id === selectedCity)?.offset_x && citySettings.find(el => el.city_id === selectedCity)?.offset_y) {
offset_x = citySettings.find(el => el.city_id === selectedCity).offset_x
offset_y = citySettings.find(el => el.city_id === selectedCity).offset_y
}
if (citySettings.find(el => el.city_id === selectedCity)?.rotation) {
rotation = citySettings.find(el => el.city_id === selectedCity)?.rotation
}
} else {
console.log("City settings NOT found")
}
if (Array.isArray(figuresData)) { if (Array.isArray(figuresData)) {
figuresLayer.current.getSource()?.clear() figuresLayer.current.getSource()?.clear()
if (figuresData.length > 0) { if (figuresData.length > 0) {
const scaling = { const scaling = {
w: 10000, // responseData[0].width w: scale.w, // responseData[0].width
h: 10000 // responseData[0].width h: scale.h // responseData[0].width
} }
figuresData.map((figure: IFigure) => { figuresData.map((figure: IFigure) => {
processFigure(figure, scaling, mapCenter, figuresLayer) processFigure(figure, scaling, [offset_x, offset_y], figuresLayer)
}) })
} }
} }
@ -682,17 +699,54 @@ const MapComponent = () => {
linesLayer.current.getSource()?.clear() linesLayer.current.getSource()?.clear()
if (linesData.length > 0) { if (linesData.length > 0) {
const scaling = { const scaling = {
w: 10000, // responseData[0].width w: scale.w, // responseData[0].width
h: 10000 // responseData[0].width h: scale.h // responseData[0].width
} }
linesData.map((line: ILine) => { linesData.map((line: ILine) => {
processLine(line, scaling, mapCenter, linesLayer) processLine(line, scaling, [offset_x, offset_y], linesLayer)
}) })
} }
} }
}, [figuresData, linesData, selectedCity, selectedYear]) }, [figuresData, linesData, selectedCity, selectedYear])
useEffect(() => {
// if (map.current) {
// map.current.on('postcompose', function () {
// if (colorScheme === 'dark') {
// document.querySelector('canvas').style.filter = 'grayscale(80%) invert(100%) hue-rotate(180deg)'
// } else {
// document.querySelector('canvas').style.filter = 'grayscale(0%) invert(0%) hue-rotate(0deg)'
// }
// })
// }
if (baseLayer.current) {
baseLayer.current.on('prerender', function (e) {
if (colorScheme === 'dark') {
if (e.context) {
const context = e.context as CanvasRenderingContext2D;
context.filter = 'grayscale(80%) invert(100%) hue-rotate(180deg) ';
context.globalCompositeOperation = 'source-over';
}
} else {
if (e.context) {
const context = e.context as CanvasRenderingContext2D;
context.filter = 'none';
}
}
})
baseLayer.current.on('postrender', function (e) {
if (e.context) {
const context = e.context as CanvasRenderingContext2D;
context.filter = 'none';
}
})
}
}, [colorScheme])
return ( return (
<Box w={'100%'} h='100%' pos={'relative'}> <Box w={'100%'} h='100%' pos={'relative'}>
<Portal target='#header-portal'> <Portal target='#header-portal'>
@ -703,11 +757,37 @@ const MapComponent = () => {
<MantineSelect variant='filled' value={satMapsProvider} data={[{ label: 'Google', value: 'google' }, { label: 'Yandex', value: 'yandex' }, { label: 'Custom', value: 'custom' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} /> <MantineSelect variant='filled' value={satMapsProvider} data={[{ label: 'Google', value: 'google' }, { label: 'Yandex', value: 'yandex' }, { label: 'Custom', value: 'custom' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} />
</Flex> </Flex>
<form>
<Autocomplete
placeholder="Поиск"
flex={'1'}
data={searchData ? searchData.map((item: { value: string, id_object: string }) => ({ label: item.value, value: item.id_object.toString() })) : []}
//onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearchObject(value)}
onOptionSubmit={(value) => setCurrentObjectId(value)}
rightSection={
searchObject !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearchObject('')
//setSelectedCity(null)
}}
aria-label="Clear value"
/>
)
}
leftSection={<IconSearch size={16} />}
value={searchObject}
/>
</form>
<form> <form>
<Autocomplete <Autocomplete
placeholder="Район" placeholder="Район"
flex={'1'} flex={'1'}
data={citiesData ? citiesData.map((item: any) => ({ label: item.name, value: item.id.toString() })) : []} data={citiesData ? citiesData.map((item: { name: string, id: number }) => ({ label: item.name, value: item.id.toString() })) : []}
//onSelect={(e) => console.log(e.currentTarget.value)} //onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearchCity(value)} onChange={(value) => setSearchCity(value)}
onOptionSubmit={(value) => setSelectedCity(Number(value))} onOptionSubmit={(value) => setSelectedCity(Number(value))}
@ -729,20 +809,12 @@ const MapComponent = () => {
</form> </form>
<MantineSelect <MantineSelect
data={[ w='84px'
{ label: '2018', value: '2018' }, data={['2018', '2019', '2020', '2021', '2022', '2023', '2024'].map(el => ({ label: el, value: el }))}
{ label: '2019', value: '2019' },
{ label: '2020', value: '2020' },
{ label: '2021', value: '2021' },
{ label: '2022', value: '2022' },
{ label: '2023', value: '2023' },
{ label: '2024', value: '2024' },
]}
onChange={(e) => { onChange={(e) => {
setSelectedYear(Number(e)) setSelectedYear(Number(e))
}} }}
defaultValue={selectedYear?.toString()} defaultValue={selectedYear?.toString()}
//variant="unstyled"
allowDeselect={false} allowDeselect={false}
/> />
@ -766,7 +838,7 @@ const MapComponent = () => {
<Flex direction='column' mah={'86%'} pl='sm' style={{ <Flex direction='column' mah={'86%'} pl='sm' style={{
...mapControlsStyle, ...mapControlsStyle,
maxWidth: '300px', maxWidth: '340px',
width: '100%', width: '100%',
top: '8px', top: '8px',
left: '8px', left: '8px',
@ -791,165 +863,87 @@ const MapComponent = () => {
</ActionIcon> </ActionIcon>
</Flex> </Flex>
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'> {selectedCity &&
<Accordion.Item key={'objects'} value={'Объекты'}> <Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
<Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control> <Accordion.Item key={'objects'} value={'Объекты'}>
<Accordion.Panel> <Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
{objectsList &&
<Tree
data={objectsList}
selectOnClick
levelOffset={23}
renderNode={({ node, expanded, hasChildren, elementProps }) => (
<Group gap={6} {...elementProps} onClick={async (e) => {
elementProps.onClick(e)
if (node.value !== 'existing' && node.value !== 'planning') {
setSelectedObjectList(Number(node.value))
try {
// Fetch data from the API based on the node's value
await fetcher(`/general/objects/list?type=${node.value}&city_id=${selectedCity}&year=${selectedYear}&planning=0`, BASE_URL.ems).then(res => {
setObjectsList((prevList) => {
return prevList.map((item) => {
return {
...item,
children: [
...(item.children.map(child => {
if (child.value == node.value) {
return {
...child,
children: res.map(object => {
return {
label: object.object_id,
value: object.object_id
}
})
}
} else {
return { ...child }
}
}) || []),
],
}
}
)
}
);
})
} catch (error) {
console.error('Error fetching data:', error);
}
}
}}>
{(hasChildren || node?.count) && (
<IconChevronDown
size={18}
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
)}
<MantineText size='sm'>{`${node.label} ${node?.count ? `(${node.count})` : ''}`}</MantineText>
</Group>
)}
/>
}
</Accordion.Panel>
</Accordion.Item>
{/* {currentObjectId &&
<Accordion.Item key={'current_object'} value={currentObjectId}>
<Accordion.Control icon={<IconTable />}>
{'Текущий объект'}
</Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
<ObjectData {...currentObjectData as IObjectData} /> <ObjectTree />
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
} */}
{valuesData && {valuesData &&
<Accordion.Item key={'parameters'} value={'Параметры объекта'}> <Accordion.Item key={'parameters'} value={'Параметры объекта'}>
<Accordion.Control icon={<IconTable />}>{'Параметры объекта'}</Accordion.Control> <Accordion.Control icon={<IconTable />}>{'Параметры объекта'}</Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
<Flex gap={'sm'} direction={'column'}> <Flex gap={'sm'} direction={'column'}>
{Array.isArray(valuesData) && {Array.isArray(valuesData) &&
// Group objects by `id_param` Object.entries(
Object.entries( valuesData.reduce((acc, param) => {
valuesData.reduce((acc, param) => { if (!acc[param.id_param]) {
if (!acc[param.id_param]) { acc[param.id_param] = [];
acc[param.id_param] = []; }
} acc[param.id_param].push(param);
acc[param.id_param].push(param); return acc;
return acc; }, {} as Record<string, IObjectParam[]>)
}, {} as Record<string, IObjectParam[]>) ).map(([id_param, params]) => {
).map(([id_param, params]) => { // Step 1: Sort the parameters by date_s (start date) and date_po (end date)
// Step 1: Sort the parameters by date_s (start date) and date_po (end date) const sortedParams = (params as IObjectParam[]).sort((b, a) => {
const sortedParams = params.sort((b, a) => { const dateA = new Date(a.date_s || 0);
const dateA = new Date(a.date_s || 0); const dateB = new Date(b.date_s || 0);
const dateB = new Date(b.date_s || 0); return dateA.getTime() - dateB.getTime();
return dateA.getTime() - dateB.getTime(); });
});
return sortedParams.length > 1 ? ( return sortedParams.length > 1 ? (
// Step 2: Render Mantine Timeline for multiple entries with the same `id_param` // Step 2: Render Mantine Timeline for multiple entries with the same `id_param`
<Timeline <Timeline
key={id_param} key={id_param}
active={0} active={0}
bulletSize={18} bulletSize={18}
> >
{sortedParams.map((param, index) => ( {sortedParams.map((param: IObjectParam, index: number) => (
<Timeline.Item <Timeline.Item
key={index} key={index}
style={{ style={{
filter: index !== 0 ? 'grayscale(100%) opacity(50%)' : 'none' filter: index !== 0 ? 'grayscale(100%) opacity(50%)' : 'none'
}} }}
> >
<ObjectParameter param={param} showLabel={false} /> <ObjectParameter param={param} showLabel={false} />
<Text size='xs'> <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('.')} {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> </Text>
</Timeline.Item> </Timeline.Item>
))} ))}
</Timeline> </Timeline>
) : ( ) : (
// Step 3: Render ObjectParameter directly if there's only one entry <ObjectParameter key={id_param} param={sortedParams[0]} />
<ObjectParameter key={id_param} param={sortedParams[0]} /> );
); })
}) }
} </Flex>
</Flex> </Accordion.Panel>
</Accordion.Panel> </Accordion.Item>
</Accordion.Item> }
} </Accordion>
</Accordion> }
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Слои'> <Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Слои'>
<Accordion.Item key={'objects'} value={'Слои'}> <Accordion.Item key={'objects'} value={'Слои'}>
<Accordion.Control icon={<IconTable />}>{'Слои'}</Accordion.Control> <Accordion.Control icon={<IconTable />}>{'Слои'}</Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
{map.current?.getLayers().getArray() && {map.current?.getLayers().getArray() && map.current?.getLayers().getArray().map((layer, index) => (
<Tree <Flex key={`layer-${index}`} gap='xs' align='center'>
data={map.current?.getLayers().getArray().map(layer => { <Checkbox.Indicator
return { checked={layer.getLayerState().visible}
label: layer.get('name'), onClick={() => layer.getLayerState().visible ? layer.setVisible(false) : layer.setVisible(true)}
value: layer.get('id') />
} <NavLink p={0} label={layer.get('name')} onClick={() => { console.log(layer.getLayerState()) }} />
}) as TreeNodeData[]} </Flex>
//selectOnClick ))}
expandOnClick={false}
levelOffset={23}
renderNode={MapTreeCheckbox}
/>
}
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
</Accordion> </Accordion>
</Stack> </Stack>
</ScrollAreaAutosize> </ScrollAreaAutosize>
</Flex> </Flex>

View File

@ -1,6 +1,5 @@
import { Checkbox, Group, RenderTreeNodePayload } from "@mantine/core"; import { Checkbox, Group, RenderTreeNodePayload, Text } from "@mantine/core";
import { IconChevronDown } from "@tabler/icons-react"; import { IconChevronDown } from "@tabler/icons-react";
import { useEffect } from "react";
export const MapTreeCheckbox = ({ export const MapTreeCheckbox = ({
node, node,
@ -12,10 +11,6 @@ export const MapTreeCheckbox = ({
const checked = tree.isNodeChecked(node.value); const checked = tree.isNodeChecked(node.value);
const indeterminate = tree.isNodeIndeterminate(node.value); const indeterminate = tree.isNodeIndeterminate(node.value);
useEffect(() => {
console.log(node.value)
}, [checked])
return ( return (
<Group gap="xs" {...elementProps}> <Group gap="xs" {...elementProps}>
<Checkbox.Indicator <Checkbox.Indicator
@ -25,7 +20,7 @@ export const MapTreeCheckbox = ({
/> />
<Group gap={5} onClick={() => tree.toggleExpanded(node.value)}> <Group gap={5} onClick={() => tree.toggleExpanded(node.value)}>
<span>{node.label}</span> <Text size="xs">{node.label}</Text>
{hasChildren && ( {hasChildren && (
<IconChevronDown <IconChevronDown

View File

@ -1,10 +1,46 @@
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 { Checkbox, Divider, Flex, Grid, Stack, Text } from '@mantine/core'
import { IObjectParam, IParam } from '../../interfaces/objects' import { IObjectParam, IParam } from '../../interfaces/objects'
import { decodeDoubleEncodedString } from '../../utils/format'
import TCBParameter from './TCBParameter' import TCBParameter from './TCBParameter'
import TableValue from './TableValue'
export function formatNumericValue(format: string, value: string) {
// Extract precision and scale from the format string
const regex = /numeric\((\d+),(\d+)\)/;
const match = format.match(regex);
if (!match) return value; // return original if format is not correct
const precision = parseInt(match[1], 10); // Total number of digits
const scale = parseInt(match[2], 10); // Number of digits after the decimal point
// Convert value to a number and handle cases like empty value or invalid input
const numericValue = parseFloat(value);
if (isNaN(numericValue)) {
return '0'.padStart(precision - scale, '0') + '.' + '0'.repeat(scale); // fallback in case of invalid value
}
// Ensure the value has the correct number of digits after the decimal point (scale)
let formattedValue = numericValue.toFixed(scale);
// Ensure the total length doesn't exceed the precision
const totalDigits = formattedValue.replace('.', '').length;
if (totalDigits > precision) {
// Truncate the value if it exceeds the total precision
formattedValue = numericValue.toPrecision(precision);
}
// Pad with leading zeros if necessary (if it's an integer and the precision is greater than the scale)
const [integerPart, decimalPart] = formattedValue.split('.');
// Ensure the integer part doesn't exceed the precision
const paddedInteger = integerPart.padStart(precision - scale, '0');
// Reassemble the number
return `${paddedInteger}.${decimalPart.padEnd(scale, '0')}`;
}
interface ObjectParameterProps { interface ObjectParameterProps {
showLabel?: boolean, showLabel?: boolean,
@ -12,56 +48,61 @@ interface ObjectParameterProps {
} }
const ObjectParameter = ({ const ObjectParameter = ({
param, param
showLabel = true
}: ObjectParameterProps) => { }: ObjectParameterProps) => {
const { data: paramData } = useSWR( const { data: paramData } = useSWR(
`/general/params/all?param_id=${param.id_param}`, `/general/params/all?param_id=${param.id_param}`,
(url) => fetcher(url, BASE_URL.ems).then(res => res[0] as IParam), (url) => fetcher(url, BASE_URL.ems).then(res => res[0] as IParam),
{ {
revalidateOnFocus: false revalidateOnFocus: false,
revalidateIfStale: false
} }
) )
const Parameter = (type: string, name: string, value: unknown, vtable: string) => { const Parameter = (type: string, name: string, value: unknown, vtable: string, unit: string | null) => {
switch (type) { switch (type) {
case 'bit': case 'bit':
return ( return (
<Flex direction='row' align='center' gap='sm'> <TableValue value={value} name={name} type='boolean' />
<Checkbox defaultChecked={value as boolean} />
<Text>{name}</Text>
</Flex>
)
case 'varchar(200)':
return (
<Text>
{decodeDoubleEncodedString(value as string)}
</Text>
)
case 'varchar(5)':
return (
<Text>
{decodeDoubleEncodedString(value as string)}
</Text>
) )
case 'bigint': case 'bigint':
return ( return (
<Text> <TableValue value={value} name={name} type='number' />
{(value as string)} )
</Text> case 'tinyint':
return (
<TableValue value={value} name={name} type='number' />
)
// TODO: Calculate from calc procedures
case 'calculate':
return (
<TableValue value={value} name={name} type='value' />
) )
case 'GTCB': case 'GTCB':
return ( return (
<TCBParameter value={value as string} vtable={vtable} /> <TCBParameter value={value as string} vtable={vtable} name={name} />
) )
case 'TCB': case 'TCB':
return ( return (
<TCBParameter value={value as string} vtable={vtable} /> <TCBParameter value={value as string} vtable={vtable} name={name} />
)
case type.match(/varchar\((\d+)\)/)?.input:
return (
<TableValue value={value} name={name} type='string' />
)
case type.match(/numeric\((\d+),(\d+)\)/)?.input:
return (
<TableValue value={value} name={name} type='number' unit={unit} />
)
case 'year':
return (
<TableValue value={value} name={name} type='number' />
) )
default: default:
return ( return (
<div> <div>
{type} {type}
{value as string}
</div> </div>
) )
} }
@ -70,12 +111,7 @@ const ObjectParameter = ({
return ( return (
<> <>
{paramData && {paramData &&
<Stack gap={0}> Parameter(paramData.format, paramData.name, param.value, paramData.vtable, paramData.unit)
{showLabel &&
<Divider my="xs" label={paramData.name} labelPosition="left" />
}
{Parameter(paramData.format, paramData.name, param.value, paramData.vtable)}
</Stack>
} }
</> </>
) )

View File

@ -1,30 +1,20 @@
import React 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 { Text } from '@mantine/core' import { Text } from '@mantine/core'
import TableValue from './TableValue'
interface ITCBParameterProps { interface ITCBParameterProps {
value: string, value: string,
vtable: string, vtable: string,
inactive?: boolean inactive?: boolean,
} name: string
interface vStreet {
id: number,
id_city: number,
name: string,
kv: number
}
interface tType {
id: number,
name: string,
} }
const TCBParameter = ({ const TCBParameter = ({
value, value,
vtable vtable,
name
}: ITCBParameterProps) => { }: ITCBParameterProps) => {
//Get value //Get value
@ -32,16 +22,8 @@ const TCBParameter = ({
`/general/params/tcb?id=${value}&vtable=${vtable}`, `/general/params/tcb?id=${value}&vtable=${vtable}`,
(url) => fetcher(url, BASE_URL.ems).then(res => res[0]), (url) => fetcher(url, BASE_URL.ems).then(res => res[0]),
{ {
revalidateOnFocus: false revalidateOnFocus: false,
} revalidateIfStale: false
)
//Get available values
const { data: tcbAll } = useSWR(
`/general/params/tcb?vtable=${vtable}`,
(url) => fetcher(url, BASE_URL.ems).then(res => res),
{
revalidateOnFocus: false
} }
) )
@ -49,43 +31,84 @@ const TCBParameter = ({
switch (vtable) { switch (vtable) {
case 'vStreets': case 'vStreets':
return ( return (
<Text> <TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
{JSON.stringify(tcbValue)}
</Text>
) )
case 'tTypes': case 'tTypes':
return ( return (
<Text> <TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
{(tcbValue as tType)?.name}
</Text>
) )
case 'vPipesGround': case 'vPipesGround':
return ( return (
<Text> <TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
{(tcbValue)?.name}
</Text>
) )
case 'vRepairEvent': case 'vRepairEvent':
return ( return (
<Text> <TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
{(tcbValue)?.name}
</Text>
) )
case 'vPipesMaterial': case 'vPipesMaterial':
return ( return (
<Text> <TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
{(tcbValue)?.name}
</Text>
) )
case 'vBoilers': case 'vBoilers':
return ( return (
<Text> <TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
{(tcbValue)?.name} )
</Text> 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: default:
return ( return (
<Text> <Text>
{JSON.stringify(name)}
{JSON.stringify(tcbValue)} {JSON.stringify(tcbValue)}
</Text> </Text>
) )

View File

@ -0,0 +1,71 @@
import { Checkbox, ComboboxData, Grid, NumberInput, Select, Text, Textarea } from '@mantine/core';
import useSWR from 'swr';
import { fetcher } from '../../http/axiosInstance';
import { BASE_URL } from '../../constants';
import { useObjectsStore } from '../../store/objects';
interface TableValueProps {
name: string;
value: unknown;
type: 'value' | 'boolean' | 'number' | 'select' | 'string';
unit?: string | null | undefined;
vtable?: string;
}
const TableValue = ({
name,
value,
type,
unit,
vtable
}: TableValueProps) => {
const { selectedCity } = useObjectsStore()
//Get available values
const { data: tcbAll, isValidating } = useSWR(
type === 'select' && selectedCity ? `/general/params/tcb?vtable=${vtable}&id_city=${selectedCity}` : null,
(url) => fetcher(url, BASE_URL.ems).then(res => {
if (Array.isArray(res)) {
return res.map((el) => ({
label: el.name || "",
value: JSON.stringify(el.id)
})) as ComboboxData
}
}),
{
revalidateOnFocus: false,
revalidateIfStale: false
}
)
return (
<Grid>
<Grid.Col span={4} style={{ display: 'flex', alignItems: 'center' }}>
<Text size='xs' style={{ textWrap: 'wrap' }}>{name as string}</Text>
</Grid.Col>
<Grid.Col span={8}>
{type === 'boolean' ?
<Checkbox defaultChecked={value as boolean} />
:
type === 'number' ?
<NumberInput
size='xs'
value={value as number}
onChange={() => { }}
suffix={unit ? ` ${unit}` : ''}
/>
:
type === 'select' && !isValidating && tcbAll ?
<Select size='xs' data={tcbAll} defaultValue={JSON.stringify(value)} />
:
type === 'string' ?
<Textarea size='xs' defaultValue={value as string} autosize minRows={1} />
:
<Text size='xs'>{value as string}</Text>
}
</Grid.Col>
</Grid>
)
}
export default TableValue

View File

@ -2,7 +2,7 @@ import { Coordinate, distance, rotate } from "ol/coordinate";
import { containsExtent, Extent, getCenter, getHeight, getWidth } from "ol/extent"; import { containsExtent, Extent, getCenter, getHeight, getWidth } from "ol/extent";
import Feature from "ol/Feature"; import Feature from "ol/Feature";
import GeoJSON from "ol/format/GeoJSON"; import GeoJSON from "ol/format/GeoJSON";
import { Circle, Geometry, LineString, Point, Polygon, SimpleGeometry } from "ol/geom"; import { Circle, Geometry, LineString, Polygon, SimpleGeometry } from "ol/geom";
import VectorLayer from "ol/layer/Vector"; import VectorLayer from "ol/layer/Vector";
import VectorImageLayer from "ol/layer/VectorImage"; import VectorImageLayer from "ol/layer/VectorImage";
import Map from "ol/Map"; import Map from "ol/Map";
@ -20,13 +20,15 @@ import ImageLayer from "ol/layer/Image";
import { IFigure, ILine } from "../../interfaces/gis"; import { IFigure, ILine } from "../../interfaces/gis";
import { fromCircle } from "ol/geom/Polygon"; import { fromCircle } from "ol/geom/Polygon";
import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles"; import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles";
import { getCurrentTool, getMeasureClearPrevious, getMeasureShowSegments, getMeasureType, getTipPoint } from "../../store/map"; import { getCurrentTool, getMeasureClearPrevious, getMeasureType, getTipPoint } from "../../store/map";
import { VectorImage } from "ol/layer";
import { MutableRefObject } from "react";
export function processLine( export function processLine(
line: ILine, line: ILine,
scaling: { w: number, h: number }, scaling: { w: number, h: number },
mapCenter: Coordinate, mapCenter: Coordinate,
linesLayer: React.MutableRefObject<VectorLayer<VectorSource<any>, any>> linesLayer: React.MutableRefObject<VectorImage<Feature<Geometry>, VectorSource<Feature<Geometry>>>>
) { ) {
const x1 = line.x1 * scaling.w const x1 = line.x1 * scaling.w
const y1 = line.y1 * scaling.h const y1 = line.y1 * scaling.h
@ -53,7 +55,7 @@ export function processFigure(
figure: IFigure, figure: IFigure,
scaling: { w: number, h: number }, scaling: { w: number, h: number },
mapCenter: Coordinate, mapCenter: Coordinate,
figuresLayer: React.MutableRefObject<VectorLayer<VectorSource<any>, any>> figuresLayer: React.MutableRefObject<VectorImage<Feature<Geometry>, VectorSource<Feature<Geometry>>>>
) { ) {
if (figure.figure_type_id == 1) { if (figure.figure_type_id == 1) {
const width = figure.width * scaling.w const width = figure.width * scaling.w
@ -186,7 +188,6 @@ export const addInteractions = (
measureModify: React.MutableRefObject<Modify>, measureModify: React.MutableRefObject<Modify>,
) => { ) => {
const currentTool = getCurrentTool() const currentTool = getCurrentTool()
const showSegments = getMeasureShowSegments()
const clearPrevious = getMeasureClearPrevious() const clearPrevious = getMeasureClearPrevious()
const measureType = getMeasureType() const measureType = getMeasureType()
const tipPoint = getTipPoint() const tipPoint = getTipPoint()
@ -338,7 +339,7 @@ const zoomToFeature = (map: React.MutableRefObject<Map | null>, feature: Feature
} }
// Function to save features to localStorage // Function to save features to localStorage
export const saveFeatures = (layerRef: React.MutableRefObject<VectorLayer<VectorSource<any>, any> | null>) => { export const saveFeatures = (layerRef: MutableRefObject<VectorLayer<VectorSource> | null>) => {
const features = layerRef.current?.getSource()?.getFeatures() const features = layerRef.current?.getSource()?.getFeatures()
if (features && features.length > 0) { if (features && features.length > 0) {
const geoJSON = new GeoJSON() const geoJSON = new GeoJSON()

View File

@ -1,4 +1,4 @@
import { AppShell, Avatar, Burger, Button, Flex, Group, Menu, NavLink, rem, Text, useMantineColorScheme } from '@mantine/core'; import { AppShell, Avatar, Burger, Button, Flex, Group, Image, Menu, NavLink, rem, Text, useMantineColorScheme } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { Outlet, useNavigate } from 'react-router-dom'; import { Outlet, useNavigate } from 'react-router-dom';
import { pages } from '../App'; import { pages } from '../App';
@ -102,6 +102,13 @@ function DashboardLayout() {
> >
Выход Выход
</Menu.Item> </Menu.Item>
<Menu.Item>
<Flex gap='sm' align='center'>
<Image src={'/logo2.svg'} w={32} />
<Text>0.1.0</Text>
</Flex>
</Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
</Group> </Group>

View File

@ -0,0 +1,46 @@
import { create } from 'zustand';
interface ObjectsState {
selectedCity: number | null;
selectedYear: number | null;
currentObjectId: string | null;
}
export const useObjectsStore = create<ObjectsState>(() => ({
selectedCity: null,
selectedYear: 2023,
currentObjectId: null
}));
const getSelectedCity = () => {
return useObjectsStore.getState().selectedCity
}
const setSelectedCity = (city: number | null) => {
useObjectsStore.setState(() => ({ selectedCity: city }))
}
const getSelectedYear = () => {
return useObjectsStore.getState().selectedYear
}
const setSelectedYear = (year: number | null) => {
useObjectsStore.setState(() => ({ selectedYear: year }))
}
const getCurrentObjectId = () => {
return useObjectsStore.getState().currentObjectId
}
const setCurrentObjectId = (objectId: string | null) => {
useObjectsStore.setState(() => ({ currentObjectId: objectId }))
}
export {
getSelectedCity,
setSelectedCity,
getSelectedYear,
setSelectedYear,
getCurrentObjectId,
setCurrentObjectId
}

3
client/src/utils/math.ts Normal file
View File

@ -0,0 +1,3 @@
export function deg2rad(degrees: number) {
return degrees * (Math.PI / 180)
}

View File

@ -62,17 +62,17 @@ router.get('/objects/list', async (req: Request, res: Response) => {
if (type) { if (type) {
const result = await tediousQuery( const result = await tediousQuery(
` `
SELECT SELECT
* *
FROM FROM
vObjects vObjects
WHERE WHERE
vObjects.id_city = ${city_id} vObjects.id_city = ${city_id}
AND vObjects.year = ${year} AND vObjects.year = ${year}
AND type = ${type} AND 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(vObjects.planning AS BIT) IS NOT NULL THEN TRY_CAST(vObjects.planning AS BIT)
WHEN vObjects.planning = 'TRUE' THEN 1 WHEN vObjects.planning = 'TRUE' THEN 1
WHEN vObjects.planning = 'FALSE' THEN 0 WHEN vObjects.planning = 'FALSE' THEN 0
@ -91,12 +91,12 @@ router.get('/objects/list', async (req: Request, res: Response) => {
COUNT(vObjects.type) AS count COUNT(vObjects.type) AS count
FROM FROM
vObjects vObjects
JOIN JOIN
tTypes ON vObjects.type = tTypes.id tTypes ON vObjects.type = tTypes.id
WHERE WHERE
vObjects.id_city = ${city_id} AND vObjects.year = ${year} vObjects.id_city = ${city_id} AND vObjects.year = ${year}
AND AND
( (
CASE CASE
WHEN TRY_CAST(vObjects.planning AS BIT) IS NOT NULL THEN TRY_CAST(vObjects.planning AS BIT) 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 = 'TRUE' THEN 1
@ -144,7 +144,17 @@ router.get('/values/all', async (req: Request, res: Response) => {
const result = await tediousQuery( const result = await tediousQuery(
` `
SELECT * FROM nGeneral..tValues SELECT
id_object,
id_param,
CAST(v.value AS varchar(max)) AS value,
date_s,
date_po,
id_user
FROM
nGeneral..tValues v
JOIN
nGeneral..tParameters p ON v.id_param = p.id
WHERE id_object = '${object_id}' WHERE id_object = '${object_id}'
` `
) )
@ -176,10 +186,21 @@ 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};`
case 'vBoilers':
return `SELECT * FROM nGeneral..${vtable} WHERE id_city = ${id_city};`
default:
return `SELECT * FROM nGeneral..${vtable};`
}
}
// Get value from TCB parameter // Get value from TCB parameter
router.get('/params/tcb', async (req: Request, res: Response) => { router.get('/params/tcb', async (req: Request, res: Response) => {
try { try {
const { vtable, id, offset, limit } = req.query const { vtable, id, offset, limit, id_city } = req.query
if (!vtable) { if (!vtable) {
res.status(500) res.status(500)
@ -195,12 +216,13 @@ router.get('/params/tcb', async (req: Request, res: Response) => {
res.status(200).json(result) res.status(200).json(result)
} else { } else {
const result = await tediousQuery( const result = await tediousQuery(
` // `
SELECT * FROM nGeneral..${vtable} // SELECT * FROM nGeneral..${vtable}
ORDER BY object_id // ORDER BY id
OFFSET ${Number(offset) || 0} ROWS // OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY; // FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
` // `
tcbParamQuery(vtable as string, id_city as string)
) )
res.status(200).json(result) res.status(200).json(result)
} }
@ -209,4 +231,47 @@ router.get('/params/tcb', async (req: Request, res: Response) => {
} }
}) })
router.get('/search/objects', async (req: Request, res: Response) => {
try {
const { q, id_city, year } = req.query
if (q && id_city && year) {
const result = await tediousQuery(
`
WITH RankedValues AS (
SELECT
id_object,
date_s,
CAST(value AS varchar(max)) AS value,
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
WHERE CAST(value AS varchar(max)) LIKE '%${q}%'
)
SELECT
id_object,
date_s,
value,
id_city,
year
FROM RankedValues
WHERE rn = 1 AND id_city = ${id_city} AND year = ${year};
`
)
res.status(200).json(result)
} else {
res.status(400).json("Bad request")
}
} catch (err) {
res.status(500).json({
message: "Error",
error: err
})
}
})
export default router export default router