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">
<head>
<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" />
<title>Dashboard</title>
<title>ИС</title>
</head>
<body>
<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 { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
import { ImageStatic, OSM, Vector as VectorSource, XYZ } from 'ol/source'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { 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 } from 'ol/extent'
import { drawingLayerStyle, highlightStyleRed, highlightStyleYellow, overlayStyle, regionsLayerStyle } from './MapStyles'
import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources'
import { mapCenter } from './MapConstants'
import ImageLayer from 'ol/layer/Image'
import VectorImageLayer from 'ol/layer/VectorImage'
import { LineString, Point } from 'ol/geom'
import { LineString, Point, SimpleGeometry } from 'ol/geom'
import { fromExtent } from 'ol/geom/Polygon'
import Collection from 'ol/Collection'
import { Coordinate } from 'ol/coordinate'
@ -24,22 +22,37 @@ import { get, transform } from 'ol/proj'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Accordion, ActionIcon, Autocomplete, Box, CloseButton, Flex, Select as MantineSelect, Text as MantineText, MantineStyleProp, rem, ScrollAreaAutosize, Slider, useMantineColorScheme, Portal, Tree, Group, TreeNodeData, Button, useTree, Timeline, Text, Stack, Overlay } from '@mantine/core'
import { IconChevronDown, IconPlus, IconSettings, IconTable, IconUpload } from '@tabler/icons-react'
import { 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 { IconPlus, IconSearch, IconSettings, IconTable, IconUpload } from '@tabler/icons-react'
import { getGridCellPosition } from './mapUtils'
import { IFigure, ILine } from '../../interfaces/gis'
import axios from 'axios'
import ObjectParameter from './ObjectParameter'
import { IObjectData, IObjectList, IObjectParam } from '../../interfaces/objects'
import ObjectData from './ObjectData'
import { IObjectParam } from '../../interfaces/objects'
import MapToolbar from './MapToolbar/MapToolbar'
import MapStatusbar from './MapStatusbar/MapStatusbar'
import { measureStyleFunction, modifyStyle } from './Measure/MeasureStyles'
import { useMapStore } from '../../store/map'
import { MapTreeCheckbox } from './MapTree/MapTreeCheckbox'
import { v4 as uuidv4 } from 'uuid'
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 { selectedCity, selectedYear, currentObjectId } = useObjectsStore()
const mapState = useMapStore()
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(),
declutter: true,
properties: {
id: uuidv4(),
name: 'Фигуры'
}
}))
const linesLayer = useRef<VectorLayer>(new VectorLayer({
const linesLayer = useRef<VectorImage>(new VectorImage({
source: new VectorSource(),
declutter: true,
properties: {
id: uuidv4(),
name: 'Линии'
}
}))
const regionsLayer = useRef<VectorImageLayer>(new VectorImageLayer({
const regionsLayer = useRef<VectorImage>(new VectorImage({
source: regionsLayerSource,
declutter: true,
style: regionsLayerStyle,
properties: {
id: uuidv4(),
@ -356,14 +372,14 @@ const MapComponent = () => {
],
target: mapElement.current as HTMLDivElement,
view: new View({
center: transform([129.7659541, 62.009504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]),
zoom: 17,
center: transform([129.7466541, 62.083504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]),
zoom: 16,
maxZoom: 21,
//extent: mapExtent,
}),
})
map.current.on('pointermove', function (e: MapBrowserEvent<any>) {
map.current.on('pointermove', function (e: MapBrowserEvent<UIEvent>) {
setCurrentCoordinate(e.coordinate)
const currentExtent = get('EPSG:3857')?.getExtent() as Extent
const { tileX, tileY } = getGridCellPosition(e.coordinate[0], e.coordinate[1], currentExtent, Number(map.current?.getView().getZoom()?.toFixed(0)))
@ -372,7 +388,7 @@ const MapComponent = () => {
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)
if (pixel) {
@ -430,7 +446,7 @@ const MapComponent = () => {
}
}, [mapState.currentTool])
const [satelliteOpacity, setSatelliteOpacity] = useState<number>(1)
const [satelliteOpacity, setSatelliteOpacity] = useState<number>(0)
const [statusText, setStatusText] = useState('')
@ -513,31 +529,14 @@ const MapComponent = () => {
}
}, [nodes])
const [currentObjectId, setCurrentObjectId] = useState<string | null>(null)
const [selectedCity, setSelectedCity] = useState<number | null>(null)
const [selectedYear, setSelectedYear] = useState<number | null>(2023)
const [citiesPage, setCitiesPage] = useState<number>(0)
const [searchCity, setSearchCity] = useState<string | undefined>("")
const throttledSearchCity = useThrottle(searchCity, 500);
const { data: existingObjectsList } = useSWR(
selectedYear && selectedCity ? `/general/objects/list?year=${selectedYear}&city_id=${selectedCity}&planning=0` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const [searchObject, setSearchObject] = useState<string | undefined>("")
const throttledSearchObject = useThrottle(searchObject, 500);
const { data: planningObjectsList } = useSWR(
selectedYear && selectedCity ? `/general/objects/list?year=${selectedYear}&city_id=${selectedCity}&planning=1` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const [objectsList, setObjectsList] = useState<TreeNodeData[] | null>(null)
const [selectedObjectList, setSelectedObjectList] = useState<number | null>(null)
useEffect(() => {
@ -549,7 +548,7 @@ const MapComponent = () => {
if (selectedObjectList == feature.get('type')) {
feature.setStyle(highlightStyleYellow);
} 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')) {
feature.setStyle(highlightStyleYellow);
} else {
feature.setStyle(null); // Reset to default style
feature.setStyle(undefined); // Reset to default style
}
})
}
}, [selectedObjectList])
useEffect(() => {
if (existingObjectsList && planningObjectsList) {
setObjectsList([
{
label: 'Существующие',
value: 'existing',
children: existingObjectsList.map((list: IObjectList) => ({
label:list.name,
count: list.count,
value: list.id,
})),
},
{
label: 'Планируемые',
value: 'planning',
children: planningObjectsList.map((list: IObjectList) => ({
label: list.name,
count: list.count,
value: list.id
}))
}
])
}
}, [existingObjectsList, planningObjectsList])
useEffect(() => {
if (currentObjectId) {
if (figuresLayer.current) {
// Reset styles and apply highlight to matching features
figuresLayer.current.getSource()?.getFeatures().forEach((feature) => {
figuresLayer.current.getSource()?.getFeatures().forEach((feature: Feature) => {
if (currentObjectId == feature.get('object_id')) {
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 {
feature.setStyle(null); // Reset to default style
feature.setStyle(undefined); // Reset to default style
}
})
}
if (linesLayer.current) {
// 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')) {
feature.setStyle(highlightStyleRed);
const geometry = feature.getGeometry()
if (geometry) {
map.current?.getView().fit(geometry as SimpleGeometry, { duration: 500, maxZoom: 18 })
}
} else {
feature.setStyle(null); // Reset to default style
feature.setStyle(undefined); // Reset to default style
}
})
}
}
}, [currentObjectId])
const { data: currentObjectData } = useSWR(
currentObjectId ? `/general/objects/${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: valuesData } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
revalidateOnFocus: false,
revalidateIfStale: false
}
)
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),
{
revalidateOnFocus: false
@ -664,16 +652,45 @@ const MapComponent = () => {
)
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)) {
figuresLayer.current.getSource()?.clear()
if (figuresData.length > 0) {
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
w: scale.w, // responseData[0].width
h: scale.h // responseData[0].width
}
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()
if (linesData.length > 0) {
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
w: scale.w, // responseData[0].width
h: scale.h // responseData[0].width
}
linesData.map((line: ILine) => {
processLine(line, scaling, mapCenter, linesLayer)
processLine(line, scaling, [offset_x, offset_y], linesLayer)
})
}
}
}, [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 (
<Box w={'100%'} h='100%' pos={'relative'}>
<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)} />
</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>
<Autocomplete
placeholder="Район"
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)}
onChange={(value) => setSearchCity(value)}
onOptionSubmit={(value) => setSelectedCity(Number(value))}
@ -729,20 +809,12 @@ const MapComponent = () => {
</form>
<MantineSelect
data={[
{ label: '2018', value: '2018' },
{ label: '2019', value: '2019' },
{ label: '2020', value: '2020' },
{ label: '2021', value: '2021' },
{ label: '2022', value: '2022' },
{ label: '2023', value: '2023' },
{ label: '2024', value: '2024' },
]}
w='84px'
data={['2018', '2019', '2020', '2021', '2022', '2023', '2024'].map(el => ({ label: el, value: el }))}
onChange={(e) => {
setSelectedYear(Number(e))
}}
defaultValue={selectedYear?.toString()}
//variant="unstyled"
allowDeselect={false}
/>
@ -766,7 +838,7 @@ const MapComponent = () => {
<Flex direction='column' mah={'86%'} pl='sm' style={{
...mapControlsStyle,
maxWidth: '300px',
maxWidth: '340px',
width: '100%',
top: '8px',
left: '8px',
@ -791,92 +863,21 @@ const MapComponent = () => {
</ActionIcon>
</Flex>
{selectedCity &&
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
<Accordion.Item key={'objects'} value={'Объекты'}>
<Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
<Accordion.Panel>
{objectsList &&
<Tree
data={objectsList}
selectOnClick
levelOffset={23}
renderNode={({ node, expanded, hasChildren, elementProps }) => (
<Group gap={6} {...elementProps} onClick={async (e) => {
elementProps.onClick(e)
if (node.value !== 'existing' && node.value !== 'planning') {
setSelectedObjectList(Number(node.value))
try {
// Fetch data from the API based on the node's value
await fetcher(`/general/objects/list?type=${node.value}&city_id=${selectedCity}&year=${selectedYear}&planning=0`, BASE_URL.ems).then(res => {
setObjectsList((prevList) => {
return prevList.map((item) => {
return {
...item,
children: [
...(item.children.map(child => {
if (child.value == node.value) {
return {
...child,
children: res.map(object => {
return {
label: object.object_id,
value: object.object_id
}
})
}
} else {
return { ...child }
}
}) || []),
],
}
}
)
}
);
})
} catch (error) {
console.error('Error fetching data:', error);
}
}
}}>
{(hasChildren || node?.count) && (
<IconChevronDown
size={18}
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
)}
<MantineText size='sm'>{`${node.label} ${node?.count ? `(${node.count})` : ''}`}</MantineText>
</Group>
)}
/>
}
<ObjectTree />
</Accordion.Panel>
</Accordion.Item>
{/* {currentObjectId &&
<Accordion.Item key={'current_object'} value={currentObjectId}>
<Accordion.Control icon={<IconTable />}>
{'Текущий объект'}
</Accordion.Control>
<Accordion.Panel>
<ObjectData {...currentObjectData as IObjectData} />
</Accordion.Panel>
</Accordion.Item>
} */}
{valuesData &&
<Accordion.Item key={'parameters'} value={'Параметры объекта'}>
<Accordion.Control icon={<IconTable />}>{'Параметры объекта'}</Accordion.Control>
<Accordion.Panel>
<Flex gap={'sm'} direction={'column'}>
{Array.isArray(valuesData) &&
// Group objects by `id_param`
Object.entries(
valuesData.reduce((acc, param) => {
if (!acc[param.id_param]) {
@ -887,7 +888,7 @@ const MapComponent = () => {
}, {} as Record<string, IObjectParam[]>)
).map(([id_param, params]) => {
// Step 1: Sort the parameters by date_s (start date) and date_po (end date)
const sortedParams = params.sort((b, a) => {
const 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();
@ -900,7 +901,7 @@ const MapComponent = () => {
active={0}
bulletSize={18}
>
{sortedParams.map((param, index) => (
{sortedParams.map((param: IObjectParam, index: number) => (
<Timeline.Item
key={index}
style={{
@ -915,7 +916,6 @@ const MapComponent = () => {
))}
</Timeline>
) : (
// Step 3: Render ObjectParameter directly if there's only one entry
<ObjectParameter key={id_param} param={sortedParams[0]} />
);
})
@ -925,31 +925,25 @@ const MapComponent = () => {
</Accordion.Item>
}
</Accordion>
}
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Слои'>
<Accordion.Item key={'objects'} value={'Слои'}>
<Accordion.Control icon={<IconTable />}>{'Слои'}</Accordion.Control>
<Accordion.Panel>
{map.current?.getLayers().getArray() &&
<Tree
data={map.current?.getLayers().getArray().map(layer => {
return {
label: layer.get('name'),
value: layer.get('id')
}
}) as TreeNodeData[]}
//selectOnClick
expandOnClick={false}
levelOffset={23}
renderNode={MapTreeCheckbox}
{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>

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

View File

@ -1,10 +1,46 @@
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Checkbox, Divider, Flex, Grid, Stack, Text } from '@mantine/core'
import { IObjectParam, IParam } from '../../interfaces/objects'
import { decodeDoubleEncodedString } from '../../utils/format'
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 {
showLabel?: boolean,
@ -12,56 +48,61 @@ interface ObjectParameterProps {
}
const ObjectParameter = ({
param,
showLabel = true
param
}: ObjectParameterProps) => {
const { data: paramData } = useSWR(
`/general/params/all?param_id=${param.id_param}`,
(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) {
case 'bit':
return (
<Flex direction='row' align='center' gap='sm'>
<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>
<TableValue value={value} name={name} type='boolean' />
)
case 'bigint':
return (
<Text>
{(value as string)}
</Text>
<TableValue value={value} name={name} type='number' />
)
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':
return (
<TCBParameter value={value as string} vtable={vtable} />
<TCBParameter value={value as string} vtable={vtable} name={name} />
)
case 'TCB':
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:
return (
<div>
{type}
{value as string}
</div>
)
}
@ -70,12 +111,7 @@ const ObjectParameter = ({
return (
<>
{paramData &&
<Stack gap={0}>
{showLabel &&
<Divider my="xs" label={paramData.name} labelPosition="left" />
}
{Parameter(paramData.format, paramData.name, param.value, paramData.vtable)}
</Stack>
Parameter(paramData.format, paramData.name, param.value, paramData.vtable, paramData.unit)
}
</>
)

View File

@ -1,30 +1,20 @@
import React from 'react'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Text } from '@mantine/core'
import TableValue from './TableValue'
interface ITCBParameterProps {
value: string,
vtable: string,
inactive?: boolean
}
interface vStreet {
id: number,
id_city: number,
name: string,
kv: number
}
interface tType {
id: number,
name: string,
inactive?: boolean,
name: string
}
const TCBParameter = ({
value,
vtable
vtable,
name
}: ITCBParameterProps) => {
//Get value
@ -32,16 +22,8 @@ const TCBParameter = ({
`/general/params/tcb?id=${value}&vtable=${vtable}`,
(url) => fetcher(url, BASE_URL.ems).then(res => res[0]),
{
revalidateOnFocus: false
}
)
//Get available values
const { data: tcbAll } = useSWR(
`/general/params/tcb?vtable=${vtable}`,
(url) => fetcher(url, BASE_URL.ems).then(res => res),
{
revalidateOnFocus: false
revalidateOnFocus: false,
revalidateIfStale: false
}
)
@ -49,43 +31,84 @@ const TCBParameter = ({
switch (vtable) {
case 'vStreets':
return (
<Text>
{JSON.stringify(tcbValue)}
</Text>
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'tTypes':
return (
<Text>
{(tcbValue as tType)?.name}
</Text>
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vPipesGround':
return (
<Text>
{(tcbValue)?.name}
</Text>
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vRepairEvent':
return (
<Text>
{(tcbValue)?.name}
</Text>
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vPipesMaterial':
return (
<Text>
{(tcbValue)?.name}
</Text>
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vBoilers':
return (
<Text>
{(tcbValue)?.name}
</Text>
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vHotWaterTypes':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vHeatingTypes':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vColdWaterTypes':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vCanalization':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vElectroSupplyTypes':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vGasSupplyTypes':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vFoundation':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vMaterialsWall':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vCovering':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vRoof':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vTechStatus':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vPipeOutDiameters':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
case 'vPipeDiameters':
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
default:
return (
<Text>
{JSON.stringify(name)}
{JSON.stringify(tcbValue)}
</Text>
)

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 Feature from "ol/Feature";
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 VectorImageLayer from "ol/layer/VectorImage";
import Map from "ol/Map";
@ -20,13 +20,15 @@ import ImageLayer from "ol/layer/Image";
import { IFigure, ILine } from "../../interfaces/gis";
import { fromCircle } from "ol/geom/Polygon";
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(
line: ILine,
scaling: { w: number, h: number },
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 y1 = line.y1 * scaling.h
@ -53,7 +55,7 @@ export function processFigure(
figure: IFigure,
scaling: { w: number, h: number },
mapCenter: Coordinate,
figuresLayer: React.MutableRefObject<VectorLayer<VectorSource<any>, any>>
figuresLayer: React.MutableRefObject<VectorImage<Feature<Geometry>, VectorSource<Feature<Geometry>>>>
) {
if (figure.figure_type_id == 1) {
const width = figure.width * scaling.w
@ -186,7 +188,6 @@ export const addInteractions = (
measureModify: React.MutableRefObject<Modify>,
) => {
const currentTool = getCurrentTool()
const showSegments = getMeasureShowSegments()
const clearPrevious = getMeasureClearPrevious()
const measureType = getMeasureType()
const tipPoint = getTipPoint()
@ -338,7 +339,7 @@ const zoomToFeature = (map: React.MutableRefObject<Map | null>, feature: Feature
}
// 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()
if (features && features.length > 0) {
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 { Outlet, useNavigate } from 'react-router-dom';
import { pages } from '../App';
@ -102,6 +102,13 @@ function DashboardLayout() {
>
Выход
</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>
</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

@ -144,7 +144,17 @@ router.get('/values/all', async (req: Request, res: Response) => {
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}'
`
)
@ -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
router.get('/params/tcb', async (req: Request, res: Response) => {
try {
const { vtable, id, offset, limit } = req.query
const { vtable, id, offset, limit, id_city } = req.query
if (!vtable) {
res.status(500)
@ -195,12 +216,13 @@ router.get('/params/tcb', async (req: Request, res: Response) => {
res.status(200).json(result)
} else {
const result = await tediousQuery(
`
SELECT * FROM nGeneral..${vtable}
ORDER BY object_id
OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
`
// `
// SELECT * FROM nGeneral..${vtable}
// ORDER BY id
// OFFSET ${Number(offset) || 0} ROWS
// FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
// `
tcbParamQuery(vtable as string, id_city as string)
)
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