diff --git a/client/package-lock.json b/client/package-lock.json index 876f7eb..3ca22df 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -55,6 +55,7 @@ "react-router-dom": "^6.23.1", "recharts": "^2.12.7", "swr": "^2.2.5", + "uuid": "^11.0.3", "zustand": "^4.5.2" }, "devDependencies": { @@ -12121,6 +12122,18 @@ "base64-arraybuffer": "^1.0.2" } }, + "node_modules/uuid": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/varint": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", diff --git a/client/package.json b/client/package.json index 253a52b..8269566 100644 --- a/client/package.json +++ b/client/package.json @@ -58,6 +58,7 @@ "react-router-dom": "^6.23.1", "recharts": "^2.12.7", "swr": "^2.2.5", + "uuid": "^11.0.3", "zustand": "^4.5.2" }, "devDependencies": { diff --git a/client/src/components/map/MapComponent.tsx b/client/src/components/map/MapComponent.tsx index 43c14c3..b0baf5e 100644 --- a/client/src/components/map/MapComponent.tsx +++ b/client/src/components/map/MapComponent.tsx @@ -1,45 +1,47 @@ import { useCallback, useEffect, useRef, useState } from 'react' -import GeoJSON from 'ol/format/GeoJSON' import 'ol/ol.css' 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 { Type } from 'ol/geom/Geometry' -import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition' +import { click, never, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition' import Feature from 'ol/Feature' -import { IGeometryType, SatelliteMapsProvider } from '../../interfaces/map' -import { containsExtent, Extent } from 'ol/extent' -import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles' +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 { Circle, LineString, MultiPoint, Point, Polygon, SimpleGeometry } from 'ol/geom' -import { fromCircle, fromExtent } from 'ol/geom/Polygon' +import { LineString, Point } from 'ol/geom' +import { fromExtent } from 'ol/geom/Polygon' import Collection from 'ol/Collection' import { Coordinate } from 'ol/coordinate' -import { Stroke, Fill, Circle as CircleStyle, Style, Text } from 'ol/style' -import { calculateCenter, calculateExtent, calculateRotationAngle, rotateProjection } from './mapUtils' +import { addInteractions, calculateCenter, loadFeatures, processFigure, processLine, regionsInit, saveFeatures, updateImageSource } from './mapUtils' import MapBrowserEvent from 'ol/MapBrowserEvent' 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, Divider, Portal, Tree, Group, TreeNodeData } from '@mantine/core' -import { IconApi, IconArrowBackUp, IconArrowsMove, IconChevronDown, IconCircle, IconExclamationCircle, IconLine, IconPlus, IconPoint, IconPolygon, IconRuler, IconSettings, IconTable, IconUpload } from '@tabler/icons-react' +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 { 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 { uploadCoordinates } from '../../actions/map' 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' const MapComponent = () => { + const mapState = useMapStore() + const [currentCoordinate, setCurrentCoordinate] = useState(null) const [currentZ, setCurrentZ] = useState(undefined) const [currentX, setCurrentX] = useState(undefined) @@ -47,13 +49,30 @@ const MapComponent = () => { const [file, setFile] = useState(null) const [polygonExtent, setPolygonExtent] = useState(undefined) - const [bottomLeft, setBottomLeft] = useState(undefined) - const [topLeft, setTopLeft] = useState(undefined) - const [topRight, setTopRight] = useState(undefined) - const [bottomRight, setBottomRight] = useState(undefined) + + const [rectCoords, setRectCoords] = useState(undefined) + + // Measure + const measureSource = useRef(new VectorSource()) + const measureModify = useRef(new Modify({ source: measureSource.current, style: modifyStyle })) + + const measureLayer = useRef(new VectorLayer({ + source: measureSource.current, + style: function (feature) { + return measureStyleFunction(feature); + }, + properties: { + id: uuidv4(), + type: 'measure', + name: 'Линейка' + } + })) + + const measureDraw = useRef(null) + + ///// const mapElement = useRef(null) - const [currentTool, setCurrentTool] = useState(null) const map = useRef(null) @@ -70,6 +89,10 @@ const MapComponent = () => { const satLayer = useRef(new TileLayer({ source: gMapsSatSource.current, + properties: { + id: uuidv4(), + name: 'Спутник' + } })) const draw = useRef(null) @@ -91,127 +114,54 @@ const MapComponent = () => { const drawingLayerSource = useRef(new VectorSource()) const citiesLayer = useRef(new VectorLayer({ - source: new VectorSource() + source: new VectorSource(), + properties: { + id: uuidv4(), + name: 'Города' + } })) const figuresLayer = useRef(new VectorLayer({ - source: new VectorSource() + source: new VectorSource(), + properties: { + id: uuidv4(), + name: 'Фигуры' + } })) const linesLayer = useRef(new VectorLayer({ - source: new VectorSource() + source: new VectorSource(), + properties: { + id: uuidv4(), + name: 'Линии' + } })) const regionsLayer = useRef(new VectorImageLayer({ source: regionsLayerSource, - style: regionsLayerStyle + style: regionsLayerStyle, + properties: { + id: uuidv4(), + name: 'Регион' + } })) const selectedRegion = useRef(null) const baseLayer = useRef(new TileLayer({ source: new OSM(), + properties: { + id: uuidv4(), + name: 'OpenStreetMap' + } })) - const imageLayer = useRef>(new ImageLayer()) - - const addInteractions = () => { - if (currentTool) { - draw.current = new Draw({ - source: drawingLayerSource.current, - type: currentTool, - condition: noModifierKeys - }) - - draw.current.on('drawend', function (s) { - console.log(s.feature.getGeometry()?.getType()) - let type: IGeometryType = 'POLYGON' - - switch (s.feature.getGeometry()?.getType()) { - case 'LineString': - type = 'LINE' - break - case 'Polygon': - type = 'POLYGON' - break - default: - type = 'POLYGON' - break - } - const coordinates = (s.feature.getGeometry() as SimpleGeometry).getCoordinates() as Coordinate[] - uploadCoordinates(coordinates, type) - }) - - map?.current?.addInteraction(draw.current) - snap.current = new Snap({ source: drawingLayerSource.current }) - map?.current?.addInteraction(snap.current) + const imageLayer = useRef>(new ImageLayer({ + properties: { + id: uuidv4(), + name: 'Изображение' } - } - - // Function to save features to localStorage - const saveFeatures = () => { - const features = drawingLayer.current?.getSource()?.getFeatures() - if (features && features.length > 0) { - const geoJSON = new GeoJSON() - const featuresJSON = geoJSON.writeFeatures(features) - localStorage.setItem('savedFeatures', featuresJSON) - } - - console.log(drawingLayer.current?.getSource()?.getFeatures()) - } - - // Function to load features from localStorage - const loadFeatures = () => { - const savedFeatures = localStorage.getItem('savedFeatures') - if (savedFeatures) { - const geoJSON = new GeoJSON() - const features = geoJSON.readFeatures(savedFeatures, { - featureProjection: 'EPSG:4326', // Ensure the projection is correct - }) - drawingLayerSource.current?.addFeatures(features) // Add features to the vector source - //drawingLayer.current?.getSource()?.changed() - } - } - - const handleToolSelect = (tool: Type) => { - if (currentTool == tool) { - setCurrentTool(null) - } else { - setCurrentTool(tool) - } - } - - const zoomToFeature = (feature: Feature) => { - const geometry = feature.getGeometry() - const extent = geometry?.getExtent() - - if (map.current && extent) { - map.current.getView().fit(extent, { - duration: 300, - maxZoom: 19, - }) - } - } - - const style = new Style({ - geometry: function (feature) { - const modifyGeometry = feature.get('modifyGeometry'); - return modifyGeometry ? modifyGeometry.geometry : feature.getGeometry(); - }, - fill: new Fill({ - color: 'rgba(255, 255, 255, 0.2)', - }), - stroke: new Stroke({ - color: '#ffcc33', - width: 2, - }), - image: new CircleStyle({ - radius: 7, - fill: new Fill({ - color: '#ffcc33', - }), - }), - }); + })) // tile processing const handleImageDrop = useCallback((event: DragEvent) => { @@ -327,100 +277,7 @@ const MapComponent = () => { } }); - // Function to update the image layer with a new source when extent changes - const updateImageSource = () => { - const newExtent = polygonFeature.getGeometry()?.getExtent(); - - const bottomLeft = polygonFeature.getGeometry()?.getCoordinates()[0][0] - const topLeft = polygonFeature.getGeometry()?.getCoordinates()[0][1] - const topRight = polygonFeature.getGeometry()?.getCoordinates()[0][2] - const bottomRight = polygonFeature.getGeometry()?.getCoordinates()[0][3] - - setPolygonExtent(newExtent) - setBottomLeft(bottomLeft) - setTopLeft(topLeft) - setTopRight(topRight) - setBottomRight(bottomRight) - - if (newExtent && bottomLeft && bottomRight && topRight && topLeft) { - const originalExtent = calculateExtent(bottomLeft, topLeft, topRight, bottomRight) - - const worldExtent = get('EPSG:3857')?.getExtent() as Extent - const zoomLevel = Number(map.current?.getView().getZoom()?.toFixed(0)) - const { tileX: blX, tileY: blY } = getGridCellPosition(bottomLeft[0], bottomLeft[1], worldExtent, zoomLevel) - const { tileX: tlX, tileY: tlY } = getGridCellPosition(topLeft[0], topLeft[1], worldExtent, zoomLevel) - const { tileX: trX, tileY: trY } = getGridCellPosition(topRight[0], topRight[1], worldExtent, zoomLevel) - const { tileX: brX, tileY: brY } = getGridCellPosition(bottomRight[0], topRight[1], worldExtent, zoomLevel) - const minX = Math.min(blX, tlX, trX, brX) - const maxX = Math.max(blX, tlX, trX, brX) - const minY = Math.min(blY, tlY, trY, brY) - const maxY = Math.max(blY, tlY, trY, brY) - - const mapWidth = Math.abs(worldExtent[0] - worldExtent[2]) - const mapHeight = Math.abs(worldExtent[1] - worldExtent[3]) - - const tilesH = Math.sqrt(Math.pow(4, zoomLevel)) - const tileWidth = mapWidth / (Math.sqrt(Math.pow(4, zoomLevel))) - const tileHeight = mapHeight / (Math.sqrt(Math.pow(4, zoomLevel))) - - const minPosX = minX - (tilesH / 2) - const maxPosX = maxX - (tilesH / 2) + 1 - const minPosY = -(minY - (tilesH / 2)) - const maxPosY = -(maxY - (tilesH / 2) + 1) - console.log(`tileWidth: ${tileWidth} minPosX: ${minPosX} maxPosX: ${maxPosX} minPosY: ${minPosY} maxPosY: ${maxPosY}`) - - const newMinX = tileWidth * minPosX - const newMaxX = tileWidth * maxPosX - const newMinY = tileHeight * maxPosY - const newMaxY = tileHeight * minPosY - - console.log('Tile slippy bounds: ', minX, maxX, minY, maxY) - console.log('Tile bounds: ', newMinX, newMaxX, newMinY, newMaxY) - - const angleDegrees = calculateRotationAngle(bottomLeft, bottomRight) * 180 / Math.PI - - const paddingLeft = Math.abs(newExtent[0] - newMinX) - const paddingRight = Math.abs(newExtent[2] - newMaxX) - const paddingTop = Math.abs(newExtent[3] - newMaxY) - const paddingBottom = Math.abs(newExtent[1] - newMinY) - - const pixelWidth = Math.abs(minX - (maxX + 1)) * 256 - //const pixelHeight = Math.abs(minY - (maxY + 1)) * 256 - - const width = Math.abs(newMinX - newMaxX) - const perPixel = width / pixelWidth - - const paddingLeftPixel = paddingLeft / perPixel - const paddingRightPixel = paddingRight / perPixel - const paddingTopPixel = paddingTop / perPixel - const paddingBottomPixel = paddingBottom / perPixel - - console.log('Rotation angle degrees: ', angleDegrees) - - console.log('Padding top pixel: ', paddingTopPixel) - console.log('Padding left pixel: ', paddingLeftPixel) - console.log('Padding right pixel: ', paddingRightPixel) - console.log('Padding bottom pixel: ', paddingBottomPixel) - - console.log('Per pixel: ', width / pixelWidth) - - const boundsWidthPixel = Math.abs(newExtent[0] - newExtent[2]) / perPixel - const boundsHeightPixel = Math.abs(newExtent[1] - newExtent[3]) / perPixel - console.log('Bounds width pixel', boundsWidthPixel) - console.log('Bounds height pixel', boundsHeightPixel) - - // Result will be sharp rotate(angleDegrees), resize(boundsWidthPixel), extend() - - const newImageSource = new ImageStatic({ - url: imageUrl, - imageExtent: originalExtent, - projection: rotateProjection('EPSG:3857', calculateRotationAngle(bottomLeft, bottomRight), originalExtent) - }); - imageLayer.current.setSource(newImageSource); - } - }; - - translate.on('translateend', updateImageSource); + translate.on('translateend', () => updateImageSource(imageUrl, imageLayer, polygonFeature, setPolygonExtent, setRectCoords)); //modify.on('modifyend', updateImageSource); modify.on('modifystart', function (event) { @@ -441,7 +298,7 @@ const MapComponent = () => { feature.unset('modifyGeometry', true); } }) - updateImageSource() + updateImageSource(imageUrl, imageLayer, polygonFeature, setPolygonExtent, setRectCoords) }) map.current.addInteraction(translate); @@ -454,129 +311,49 @@ const MapComponent = () => { } }, []) - function regionsInit() { - map.current?.on('click', function (e) { - if (selectedRegion.current !== null) { - selectedRegion.current = null - } - - if (map.current) { - map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) { - if (layer === regionsLayer.current) { - selectedRegion.current = feature as Feature - // Zoom to the selected feature - zoomToFeature(selectedRegion.current) - - return true - } else return false - }); - } - }) - - // Show current selected region - map.current?.on('pointermove', function (e) { - if (selectedRegion.current !== null) { - selectedRegion.current.setStyle(undefined) - selectedRegion.current = null - } - - if (map.current) { - map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) { - if (layer === regionsLayer.current) { - selectedRegion.current = feature as Feature - selectedRegion.current.setStyle(selectStyle) - - if (feature.get('district')) { - setStatusText(feature.get('district')) - } - - return true - } else return false - }) - } - }) - - // Hide regions layer when fully visible - map.current?.on('moveend', function () { - const viewExtent = map.current?.getView().calculateExtent(map.current.getSize()) - const features = regionsLayer.current.getSource()?.getFeatures() - - let isViewCovered = false - - features?.forEach((feature: Feature) => { - const featureExtent = feature?.getGeometry()?.getExtent() - if (viewExtent && featureExtent) { - if (containsExtent(featureExtent, viewExtent)) { - isViewCovered = true - } - } - }) - - regionsLayer.current.setVisible(!isViewCovered) - }) - } - useEffect(() => { drawingLayer.current = new VectorLayer({ source: drawingLayerSource.current, - style: drawingLayerStyle + style: drawingLayerStyle, + properties: { + id: uuidv4(), + name: 'Чертеж' + } }) overlayLayer.current = new VectorLayer({ source: overlayLayerSource.current, - style: function (feature) { - const styles = [style] - const modifyGeometry = feature.get('modifyGeometry') - const geometry = modifyGeometry ? modifyGeometry.geometry : feature.getGeometry() - const result = calculateCenter(geometry) - const center = result.center - if (center) { - styles.push( - new Style({ - geometry: new Point(center), - image: new CircleStyle({ - radius: 4, - fill: new Fill({ - color: '#ff3333' - }) - }) - }) - ) - const coordinates = result.coordinates - if (coordinates) { - const minRadius = result.minRadius - const sqDistances = result.sqDistances - const rsq = minRadius * minRadius - if (Array.isArray(sqDistances)) { - const points = coordinates.filter(function (_coordinate, index) { - return sqDistances[index] > rsq - }) - styles.push( - new Style({ - geometry: new MultiPoint(points), - image: new CircleStyle({ - radius: 4, - fill: new Fill({ - color: '#33cc33' - }) - }) - }) - ) - } - } - } - return styles - }, + style: overlayStyle, + properties: { + id: uuidv4(), + name: 'Наложения' + } }) nodeLayer.current = new VectorLayer({ source: nodeLayerSource.current, - style: drawingLayerStyle + style: drawingLayerStyle, + properties: { + id: uuidv4(), + name: 'Узлы' + } }) map.current = new Map({ controls: [], - layers: [baseLayer.current, satLayer.current, regionsLayer.current, citiesLayer.current, linesLayer.current, figuresLayer.current, drawingLayer.current, imageLayer.current, overlayLayer.current, nodeLayer.current], + layers: [ + baseLayer.current, + satLayer.current, + regionsLayer.current, + citiesLayer.current, + linesLayer.current, + figuresLayer.current, + drawingLayer.current, + imageLayer.current, + overlayLayer.current, + nodeLayer.current, + measureLayer.current + ], target: mapElement.current as HTMLDivElement, view: new View({ center: transform([129.7659541, 62.009504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]), @@ -589,7 +366,6 @@ const MapComponent = () => { map.current.on('pointermove', function (e: MapBrowserEvent) { 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))) setCurrentZ(Number(map.current?.getView().getZoom()?.toFixed(0))) setCurrentX(tileX) @@ -599,9 +375,11 @@ const MapComponent = () => { map.current.on('click', function (e: MapBrowserEvent) { const pixel = map.current?.getEventPixel(e.originalEvent) - map.current?.forEachFeatureAtPixel(pixel, function (feature) { - setCurrentObjectId(feature.get('object_id')) - }) + if (pixel) { + map.current?.forEachFeatureAtPixel(pixel, function (feature) { + setCurrentObjectId(feature.get('object_id')) + }) + } }) const modify = new Modify({ source: drawingLayerSource.current }) @@ -619,9 +397,9 @@ const MapComponent = () => { } }) - loadFeatures() + loadFeatures(drawingLayerSource) - regionsInit() + regionsInit(map, selectedRegion, regionsLayer, setStatusText) if (mapElement.current) { mapElement.current.addEventListener('dragover', (e) => { @@ -641,15 +419,16 @@ const MapComponent = () => { }, []) useEffect(() => { - if (currentTool) { + if (mapState.currentTool) { if (draw.current) map?.current?.removeInteraction(draw.current) - if (snap.current) map?.current?.removeInteraction(snap.current) - addInteractions() + //if (snap.current) map?.current?.removeInteraction(snap.current) + addInteractions(drawingLayerSource, draw, map, snap, measureDraw, measureSource, measureModify) } else { if (draw.current) map?.current?.removeInteraction(draw.current) if (snap.current) map?.current?.removeInteraction(snap.current) + if (measureDraw.current) map?.current?.removeInteraction(measureDraw.current) } - }, [currentTool]) + }, [mapState.currentTool]) const [satelliteOpacity, setSatelliteOpacity] = useState(1) @@ -678,21 +457,21 @@ const MapComponent = () => { }, [satMapsProvider]) const submitOverlay = async () => { - if (file && polygonExtent && bottomLeft && topLeft && topRight && bottomRight) { + if (file && polygonExtent && rectCoords?.bl && rectCoords?.tl && rectCoords?.tr && rectCoords?.br) { const formData = new FormData() formData.append('file', file) formData.append('extentMinX', polygonExtent[0].toString()) formData.append('extentMinY', polygonExtent[1].toString()) formData.append('extentMaxX', polygonExtent[2].toString()) formData.append('extentMaxY', polygonExtent[3].toString()) - formData.append('blX', bottomLeft[0].toString()) - formData.append('blY', bottomLeft[1].toString()) - formData.append('tlX', topLeft[0].toString()) - formData.append('tlY', topLeft[1].toString()) - formData.append('trX', topRight[0].toString()) - formData.append('trY', topRight[1].toString()) - formData.append('brX', bottomRight[0].toString()) - formData.append('brY', bottomRight[1].toString()) + formData.append('blX', rectCoords?.bl[0].toString()) + formData.append('blY', rectCoords?.bl[1].toString()) + formData.append('tlX', rectCoords?.tl[0].toString()) + formData.append('tlY', rectCoords?.tl[1].toString()) + formData.append('trX', rectCoords?.tr[0].toString()) + formData.append('trY', rectCoords?.tr[1].toString()) + formData.append('brX', rectCoords?.br[0].toString()) + formData.append('brY', rectCoords?.br[1].toString()) await fetch(`${import.meta.env.VITE_API_EMS_URL}/tiles/upload`, { method: 'POST', body: formData }) } @@ -709,7 +488,8 @@ const MapComponent = () => { // ? '#FFFFFFAA' // : '#000000AA', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', - backdropFilter: 'blur(8px)' + backdropFilter: 'blur(8px)', + border: '1px solid #00000022' } const { data: nodes } = useSWR('/nodes/all', () => fetcher('/nodes/all', BASE_URL.ems), { revalidateOnFocus: false }) @@ -733,102 +513,6 @@ const MapComponent = () => { } }, [nodes]) - function styleFunction(feature: Feature) { - return [ - new Style({ - fill: new Fill({ - color: 'rgba(255,255,255,0.4)' - }), - stroke: new Stroke({ - color: '#3399CC', - width: 1.25 - }), - text: new Text({ - font: '12px Calibri,sans-serif', - fill: new Fill({ color: '#000' }), - stroke: new Stroke({ - color: '#fff', width: 2 - }), - // get the text from the feature - `this` is ol.Feature - // and show only under certain resolution - text: feature.get('object_id') - }) - }) - ]; - } - - function fourthStyleFunction(feature: Feature) { - return [ - new Style({ - fill: new Fill({ - color: 'rgba(255,255,255,0.4)' - }), - stroke: new Stroke({ - color: '#3399CC', - width: 1.25 - }), - text: new Text({ - font: '12px Calibri,sans-serif', - fill: new Fill({ color: '#000' }), - stroke: new Stroke({ - color: '#fff', width: 2 - }), - // get the text from the feature - `this` is ol.Feature - // and show only under certain resolution - text: `${feature.get('object_id')}\n ${feature.get('angle')}` - }) - }) - ]; - } - - function thirdStyleFunction(feature: Feature) { - return [ - new Style({ - fill: new Fill({ - color: 'rgba(255,255,255,0.4)' - }), - stroke: new Stroke({ - color: '#33ccb3', - width: 1.25 - }), - text: new Text({ - font: '12px Calibri,sans-serif', - fill: new Fill({ color: '#000' }), - stroke: new Stroke({ - color: '#fff', width: 2 - }), - // get the text from the feature - `this` is ol.Feature - // and show only under certain resolution - text: feature.get('object_id') - }) - }) - ]; - } - - function firstStyleFunction(feature: Feature) { - return [ - new Style({ - fill: new Fill({ - color: 'rgba(255,255,255,0.4)' - }), - stroke: new Stroke({ - color: 'red', - width: 1.25 - }), - text: new Text({ - font: '12px Calibri,sans-serif', - fill: new Fill({ color: '#000' }), - stroke: new Stroke({ - color: '#fff', width: 2 - }), - // get the text from the feature - `this` is ol.Feature - // and show only under certain resolution - text: feature.get('object_id') - }) - }) - ]; - } - const [currentObjectId, setCurrentObjectId] = useState(null) const [selectedCity, setSelectedCity] = useState(null) @@ -859,22 +543,11 @@ const MapComponent = () => { useEffect(() => { if (!selectedObjectList || !map.current) return; - // Define the highlight style - const highlightStyle = new Style({ - stroke: new Stroke({ - color: 'yellow', - width: 3, - }), - fill: new Fill({ - color: 'rgba(255, 255, 0, 0.3)', - }), - }); - if (figuresLayer.current) { // Reset styles and apply highlight to matching features - figuresLayer.current.getSource().getFeatures().forEach((feature) => { + figuresLayer.current.getSource()?.getFeatures().forEach((feature) => { if (selectedObjectList == feature.get('type')) { - feature.setStyle(highlightStyle); + feature.setStyle(highlightStyleYellow); } else { feature.setStyle(null); // Reset to default style } @@ -883,9 +556,9 @@ const MapComponent = () => { if (linesLayer.current) { // Reset styles and apply highlight to matching features - linesLayer.current.getSource().getFeatures().forEach((feature) => { + linesLayer.current.getSource()?.getFeatures().forEach((feature) => { if (selectedObjectList == feature.get('type')) { - feature.setStyle(highlightStyle); + feature.setStyle(highlightStyleYellow); } else { feature.setStyle(null); // Reset to default style } @@ -900,7 +573,8 @@ const MapComponent = () => { label: 'Существующие', value: 'existing', children: existingObjectsList.map((list: IObjectList) => ({ - label: `${list.name} (${list.count})`, + label:list.name, + count: list.count, value: list.id, })), }, @@ -908,7 +582,8 @@ const MapComponent = () => { label: 'Планируемые', value: 'planning', children: planningObjectsList.map((list: IObjectList) => ({ - label: `${list.name} (${list.count})`, + label: list.name, + count: list.count, value: list.id })) } @@ -918,22 +593,11 @@ const MapComponent = () => { useEffect(() => { if (currentObjectId) { - // Define the highlight style - const highlightStyle = new Style({ - stroke: new Stroke({ - color: 'red', - width: 3, - }), - fill: new Fill({ - color: 'rgba(255, 255, 0, 0.3)', - }), - }); - if (figuresLayer.current) { // Reset styles and apply highlight to matching features - figuresLayer.current.getSource().getFeatures().forEach((feature) => { + figuresLayer.current.getSource()?.getFeatures().forEach((feature) => { if (currentObjectId == feature.get('object_id')) { - feature.setStyle(highlightStyle); + feature.setStyle(highlightStyleRed); } else { feature.setStyle(null); // Reset to default style } @@ -942,9 +606,9 @@ const MapComponent = () => { if (linesLayer.current) { // Reset styles and apply highlight to matching features - linesLayer.current.getSource().getFeatures().forEach((feature) => { + linesLayer.current.getSource()?.getFeatures().forEach((feature) => { if (currentObjectId == feature.get('object_id')) { - feature.setStyle(highlightStyle); + feature.setStyle(highlightStyleRed); } else { feature.setStyle(null); // Reset to default style } @@ -1009,90 +673,7 @@ const MapComponent = () => { } figuresData.map((figure: IFigure) => { - if (figure.figure_type_id == 1) { - const width = figure.width * scaling.w - const height = figure.height * scaling.h - - const left = figure.left * scaling.w - const top = figure.top * scaling.h - - const centerX = mapCenter[0] + left + (width / 2) - const centerY = mapCenter[1] - top - (height / 2) - - const radius = width / 2; - const circleGeom = new Circle([centerX, centerY], radius) - - const ellipseGeom = fromCircle(circleGeom, 64) - ellipseGeom.scale(1, height / width) - - const feature = new Feature(ellipseGeom) - - feature.setStyle(firstStyleFunction(feature)) - feature.set('type', figure.type) - feature.set('object_id', figure.object_id) - feature.set('planning', figure.planning) - figuresLayer.current?.getSource()?.addFeature(feature) - } - - if (figure.figure_type_id == 3) { - const x = figure.left * scaling.w - const y = figure.top * scaling.h - - const center = [mapCenter[0] + x, mapCenter[1] - y] - - const coords = figure.points?.split(' ').map(pair => { - const [x, y] = pair.split(';').map(Number) - return [ - center[0] + (x * scaling.w), - center[1] - (y * scaling.h) - ] - }) - - if (coords) { - const polygon = new Polygon([coords]) - - const feature = new Feature({ - geometry: polygon - }) - - feature.set('object_id', figure.object_id) - feature.set('planning', figure.planning) - feature.set('type', figure.type) - feature.setStyle(thirdStyleFunction(feature)) - figuresLayer.current?.getSource()?.addFeature(feature) - } - } - - if (figure.figure_type_id == 4) { - const width = figure.width * scaling.w - const height = figure.height * scaling.h - const left = figure.left * scaling.w - const top = figure.top * scaling.h - - const halfWidth = width / 2 - const halfHeight = height / 2 - - const center = [mapCenter[0] + left + halfWidth, mapCenter[1] - top - halfHeight] - - const testCoords = [ - [center[0] - halfWidth, center[1] - halfHeight], - [center[0] - halfWidth, center[1] + halfHeight], - [center[0] + halfWidth, center[1] + halfHeight], - [center[0] + halfWidth, center[1] - halfHeight], - [center[0] - halfWidth, center[1] - halfHeight] - ] - - const geometry1 = new Polygon([testCoords]) - const anchor1 = center - geometry1.rotate(-figure.angle * Math.PI / 180, anchor1) - const feature1 = new Feature(geometry1) - feature1.set('object_id', figure.object_id) - feature1.set('planning', figure.planning) - feature1.set('type', figure.type) - feature1.set('angle', figure.angle) - feature1.setStyle(fourthStyleFunction(feature1)) - figuresLayer.current?.getSource()?.addFeature(feature1) - } + processFigure(figure, scaling, mapCenter, figuresLayer) }) } } @@ -1106,25 +687,7 @@ const MapComponent = () => { } linesData.map((line: ILine) => { - const x1 = line.x1 * scaling.w - const y1 = line.y1 * scaling.h - const x2 = line.x2 * scaling.w - const y2 = line.y2 * scaling.h - - const center = [mapCenter[0], mapCenter[1]] - - const testCoords = [ - [center[0] + x1, center[1] - y1], - [center[0] + x2, center[1] - y2], - ] - - const feature = new Feature(new LineString(testCoords)) - feature.setStyle(styleFunction(feature)) - feature.set('type', line.type) - feature.set('planning', line.planning) - feature.set('object_id', line.object_id) - - linesLayer.current?.getSource()?.addFeature(feature) + processLine(line, scaling, mapCenter, linesLayer) }) } } @@ -1195,10 +758,8 @@ const MapComponent = () => { saveFeatures()} + onSave={() => saveFeatures(drawingLayer)} onRemove={() => draw.current?.removeLastPoint()} - handleToolSelect={handleToolSelect} onMover={() => map?.current?.addInteraction(new Translate())} colorScheme={colorScheme} /> @@ -1211,84 +772,185 @@ const MapComponent = () => { left: '8px', }}> - - submitOverlay()} - > - - + + + submitOverlay()} + > + + - - - - + + + + - - - }>{'Объекты'} - - {objectsList && - ( - { - elementProps.onClick(e) - if (node.value !== 'existing' && node.value !== 'planning') { - setSelectedObjectList(Number(node.value)) + + + }>{'Объекты'} + + {objectsList && + ( + { + 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) && ( + + )} + {`${node.label} ${node?.count ? `(${node.count})` : ''}`} + + )} + /> + } + + + + + {/* {currentObjectId && + + }> + {'Текущий объект'} + + + + + + } */} + + {valuesData && + + }>{'Параметры объекта'} + + + {Array.isArray(valuesData) && + // Group objects by `id_param` + Object.entries( + valuesData.reduce((acc, param) => { + if (!acc[param.id_param]) { + acc[param.id_param] = []; + } + acc[param.id_param].push(param); + return acc; + }, {} as Record) + ).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 dateA = new Date(a.date_s || 0); + const dateB = new Date(b.date_s || 0); + return dateA.getTime() - dateB.getTime(); + }); + + return sortedParams.length > 1 ? ( + // Step 2: Render Mantine Timeline for multiple entries with the same `id_param` + + {sortedParams.map((param, index) => ( + + + + {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('.')} + + + ))} + + ) : ( + // Step 3: Render ObjectParameter directly if there's only one entry + + ); + }) + } + + + + } + + + + + }>{'Слои'} + + {map.current?.getLayers().getArray() && + { + return { + label: layer.get('name'), + value: layer.get('id') } - }}> - {hasChildren && ( - - )} + }) as TreeNodeData[]} + //selectOnClick + expandOnClick={false} + levelOffset={23} + renderNode={MapTreeCheckbox} + /> + } - {node.label} - - )} - /> - } - - - - - {currentObjectId && - - }> - {'Текущий объект'} - - - - } + + - {valuesData && - - }>{'Параметры объекта'} - - - {Array.isArray(valuesData) && valuesData.map((param: IObjectParam) => { - return ( - - ) - })} - - - - } - - { /> -
rsq + }) + styles.push( + new Style({ + geometry: new MultiPoint(points), + image: new CircleStyle({ + radius: 4, + fill: new Fill({ + color: '#33cc33' + }) + }) + }) + ) + } + } + } + return styles +} + +export function styleFunction(feature: Feature) { + return [ + new Style({ + fill: new Fill({ + color: 'rgba(255,255,255,0.4)' + }), + stroke: new Stroke({ + color: '#3399CC', + width: 1.25 + }), + text: new Text({ + font: '12px Calibri,sans-serif', + fill: new Fill({ color: '#000' }), + stroke: new Stroke({ + color: '#fff', width: 2 + }), + // get the text from the feature - `this` is ol.Feature + // and show only under certain resolution + text: feature.get('object_id') + }) + }) + ]; +} + +export function firstStyleFunction(feature: Feature) { + return [ + new Style({ + fill: new Fill({ + color: 'rgba(255,255,255,0.4)' + }), + stroke: new Stroke({ + color: 'red', + width: 1.25 + }), + text: new Text({ + font: '12px Calibri,sans-serif', + fill: new Fill({ color: '#000' }), + stroke: new Stroke({ + color: '#fff', width: 2 + }), + // get the text from the feature - `this` is ol.Feature + // and show only under certain resolution + text: feature.get('object_id') + }) + }) + ]; +} + +export function thirdStyleFunction(feature: Feature) { + return [ + new Style({ + fill: new Fill({ + color: 'rgba(255,255,255,0.4)' + }), + stroke: new Stroke({ + color: '#33ccb3', + width: 1.25 + }), + text: new Text({ + font: '12px Calibri,sans-serif', + fill: new Fill({ color: '#000' }), + stroke: new Stroke({ + color: '#fff', width: 2 + }), + // get the text from the feature - `this` is ol.Feature + // and show only under certain resolution + text: feature.get('object_id') + }) + }) + ]; +} + +export function fourthStyleFunction(feature: Feature) { + return [ + new Style({ + fill: new Fill({ + color: 'rgba(255,255,255,0.4)' + }), + stroke: new Stroke({ + color: '#3399CC', + width: 1.25 + }), + text: new Text({ + font: '12px Calibri,sans-serif', + fill: new Fill({ color: '#000' }), + stroke: new Stroke({ + color: '#fff', width: 2 + }), + // get the text from the feature - `this` is ol.Feature + // and show only under certain resolution + text: `${feature.get('object_id')}\n ${feature.get('angle')}` + }) + }) + ]; +} const drawingLayerStyle: FlatStyleLike = { 'fill-color': 'rgba(255, 255, 255, 0.2)', diff --git a/client/src/components/map/MapToolbar/MapToolbar.tsx b/client/src/components/map/MapToolbar/MapToolbar.tsx index 173f245..ed1306a 100644 --- a/client/src/components/map/MapToolbar/MapToolbar.tsx +++ b/client/src/components/map/MapToolbar/MapToolbar.tsx @@ -1,25 +1,22 @@ import { ActionIcon, MantineColorScheme } from '@mantine/core' import { IconApi, IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPoint, IconPolygon, IconRuler } from '@tabler/icons-react' -import { Type } from 'ol/geom/Geometry' -import React from 'react' +import { setCurrentTool, useMapStore } from '../../../store/map'; interface IToolbarProps { - currentTool: Type | null; onSave: () => void; onRemove: () => void; - handleToolSelect: (tool: Type) => void; onMover: () => void; colorScheme: MantineColorScheme; } const MapToolbar = ({ - currentTool, onSave, onRemove, - handleToolSelect, onMover, colorScheme }: IToolbarProps) => { + const mapState = useMapStore() + return ( { @@ -38,36 +35,36 @@ const MapToolbar = ({ { - handleToolSelect('Point') + setCurrentTool('Point') }}> { - handleToolSelect('LineString') + setCurrentTool('LineString') }}> { - handleToolSelect('Polygon') + setCurrentTool('Polygon') }}> { - handleToolSelect('Circle') + setCurrentTool('Circle') }}> @@ -82,8 +79,10 @@ const MapToolbar = ({ + variant={mapState.currentTool === 'Measure' ? 'filled' : 'transparent'} + onClick={() => { + setCurrentTool('Measure') + }}> diff --git a/client/src/components/map/MapTree/MapTreeCheckbox.tsx b/client/src/components/map/MapTree/MapTreeCheckbox.tsx new file mode 100644 index 0000000..c7318bf --- /dev/null +++ b/client/src/components/map/MapTree/MapTreeCheckbox.tsx @@ -0,0 +1,39 @@ +import { Checkbox, Group, RenderTreeNodePayload } from "@mantine/core"; +import { IconChevronDown } from "@tabler/icons-react"; +import { useEffect } from "react"; + +export const MapTreeCheckbox = ({ + node, + expanded, + hasChildren, + elementProps, + tree, +}: RenderTreeNodePayload) => { + const checked = tree.isNodeChecked(node.value); + const indeterminate = tree.isNodeIndeterminate(node.value); + + useEffect(() => { + console.log(node.value) + }, [checked]) + + return ( + + (!checked ? tree.checkNode(node.value) : tree.uncheckNode(node.value))} + /> + + tree.toggleExpanded(node.value)}> + {node.label} + + {hasChildren && ( + + )} + + + ); +}; \ No newline at end of file diff --git a/client/src/components/map/Measure/MeasureStyles.ts b/client/src/components/map/Measure/MeasureStyles.ts new file mode 100644 index 0000000..50dcdb4 --- /dev/null +++ b/client/src/components/map/Measure/MeasureStyles.ts @@ -0,0 +1,199 @@ +import { FeatureLike } from "ol/Feature"; +import { LineString, Point, Polygon } from "ol/geom"; +import Geometry, { Type } from "ol/geom/Geometry"; +import { Fill, RegularShape, Stroke, Style, Text } from "ol/style"; +import CircleStyle from "ol/style/Circle"; +import { getArea, getLength } from 'ol/sphere' +import { Modify } from "ol/interaction"; +import { getMeasureShowSegments } from "../../../store/map"; + +export const style = new Style({ + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.2)', + }), + stroke: new Stroke({ + color: 'rgba(0, 0, 0, 0.5)', + lineDash: [10, 10], + width: 2, + }), + image: new CircleStyle({ + radius: 5, + stroke: new Stroke({ + color: 'rgba(0, 0, 0, 0.7)', + }), + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.2)', + }), + }), +}); + +export const labelStyle = new Style({ + text: new Text({ + font: '14px Calibri,sans-serif', + fill: new Fill({ + color: 'rgba(255, 255, 255, 1)', + }), + backgroundFill: new Fill({ + color: 'rgba(0, 0, 0, 0.7)', + }), + padding: [3, 3, 3, 3], + textBaseline: 'bottom', + offsetY: -15, + }), + image: new RegularShape({ + radius: 8, + points: 3, + angle: Math.PI, + displacement: [0, 10], + fill: new Fill({ + color: 'rgba(0, 0, 0, 0.7)', + }), + }), +}); + +export const tipStyle = new Style({ + text: new Text({ + font: '12px Calibri,sans-serif', + fill: new Fill({ + color: 'rgba(255, 255, 255, 1)', + }), + backgroundFill: new Fill({ + color: 'rgba(0, 0, 0, 0.4)', + }), + padding: [2, 2, 2, 2], + textAlign: 'left', + offsetX: 15, + }), +}); + +export const modifyStyle = new Style({ + image: new CircleStyle({ + radius: 5, + stroke: new Stroke({ + color: 'rgba(0, 0, 0, 0.7)', + }), + fill: new Fill({ + color: 'rgba(0, 0, 0, 0.4)', + }), + }), + text: new Text({ + text: 'Drag to modify', + font: '12px Calibri,sans-serif', + fill: new Fill({ + color: 'rgba(255, 255, 255, 1)', + }), + backgroundFill: new Fill({ + color: 'rgba(0, 0, 0, 0.7)', + }), + padding: [2, 2, 2, 2], + textAlign: 'left', + offsetX: 15, + }), +}); + +export const segmentStyle = new Style({ + text: new Text({ + font: '12px Calibri,sans-serif', + fill: new Fill({ + color: 'rgba(255, 255, 255, 1)', + }), + backgroundFill: new Fill({ + color: 'rgba(0, 0, 0, 0.4)', + }), + padding: [2, 2, 2, 2], + textBaseline: 'bottom', + offsetY: -12, + }), + image: new RegularShape({ + radius: 6, + points: 3, + angle: Math.PI, + displacement: [0, 8], + fill: new Fill({ + color: 'rgba(0, 0, 0, 0.4)', + }), + }), +}); + +const formatLength = function (line: Geometry) { + const length = getLength(line); + let output; + if (length > 100) { + output = Math.round((length / 1000) * 100) / 100 + ' km'; + } else { + output = Math.round(length * 100) / 100 + ' m'; + } + return output; +}; + +const formatArea = function (polygon: Geometry) { + const area = getArea(polygon); + let output; + if (area > 10000) { + output = Math.round((area / 1000000) * 100) / 100 + ' km\xB2'; + } else { + output = Math.round(area * 100) / 100 + ' m\xB2'; + } + return output; +}; + +export function measureStyleFunction( + feature: FeatureLike, + drawType?: Type, + tip?: string, + setTipPoint?: React.Dispatch>, + modify?: React.MutableRefObject +) { + const styles = []; + const geometry = feature.getGeometry(); + const type = geometry?.getType(); + const segmentStyles = [segmentStyle]; + + const segments = getMeasureShowSegments() + + if (!geometry) return + + let point, label, line; + if (!drawType || drawType === type || type === 'Point') { + styles.push(style); + if (type === 'Polygon') { + point = (geometry as Polygon).getInteriorPoint(); + label = formatArea(geometry as Polygon); + line = new LineString((geometry as Polygon).getCoordinates()[0]); + } else if (type === 'LineString') { + point = new Point((geometry as Polygon).getLastCoordinate()); + label = formatLength(geometry as LineString); + line = geometry; + } + } + if (segments && line) { + let count = 0; + (line as LineString).forEachSegment(function (a, b) { + const segment = new LineString([a, b]); + const label = formatLength(segment); + if (segmentStyles.length - 1 < count) { + segmentStyles.push(segmentStyle.clone()); + } + const segmentPoint = new Point(segment.getCoordinateAt(0.5)); + segmentStyles[count].setGeometry(segmentPoint); + segmentStyles[count].getText()?.setText(label); + styles.push(segmentStyles[count]); + count++; + }); + } + if (label) { + labelStyle.setGeometry(point as Geometry); + labelStyle.getText()?.setText(label); + styles.push(labelStyle); + } + if ( + tip && + type === 'Point' && + !modify?.current.getOverlay()?.getSource()?.getFeatures().length + ) { + setTipPoint?.(geometry as Point); + tipStyle.getText()?.setText(tip); + styles.push(tipStyle); + } + return styles; +} diff --git a/client/src/components/map/ObjectParameter.tsx b/client/src/components/map/ObjectParameter.tsx index 04c818f..17c6a9b 100644 --- a/client/src/components/map/ObjectParameter.tsx +++ b/client/src/components/map/ObjectParameter.tsx @@ -1,49 +1,83 @@ import useSWR from 'swr' import { fetcher } from '../../http/axiosInstance' import { BASE_URL } from '../../constants' -import { Checkbox, Grid } from '@mantine/core' +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' + +interface ObjectParameterProps { + showLabel?: boolean, + param: IObjectParam, +} const ObjectParameter = ({ - id_param, - value -}: IObjectParam) => { + param, + showLabel = true +}: ObjectParameterProps) => { const { data: paramData } = useSWR( - `/general/params/all?param_id=${id_param}`, + `/general/params/all?param_id=${param.id_param}`, (url) => fetcher(url, BASE_URL.ems).then(res => res[0] as IParam), { revalidateOnFocus: false } ) - const Parameter = (type: string, name: string, value: unknown) => { + const Parameter = (type: string, name: string, value: unknown, vtable: string) => { switch (type) { case 'bit': return ( - - - - - -

{name}

-
-
+ + + {name} + + ) + case 'varchar(200)': + return ( + + {decodeDoubleEncodedString(value as string)} + + ) + case 'varchar(5)': + return ( + + {decodeDoubleEncodedString(value as string)} + + ) + case 'bigint': + return ( + + {(value as string)} + + ) + case 'GTCB': + return ( + + ) + case 'TCB': + return ( + ) default: return (
- Неподдерживаемый параметр + {type}
) } } return ( -
+ <> {paramData && - Parameter(paramData.format, paramData.name, value) + + {showLabel && + + } + {Parameter(paramData.format, paramData.name, param.value, paramData.vtable)} + } -
+ ) } diff --git a/client/src/components/map/TCBParameter.tsx b/client/src/components/map/TCBParameter.tsx new file mode 100644 index 0000000..9f49fab --- /dev/null +++ b/client/src/components/map/TCBParameter.tsx @@ -0,0 +1,100 @@ +import React from 'react' +import useSWR from 'swr' +import { fetcher } from '../../http/axiosInstance' +import { BASE_URL } from '../../constants' +import { Text } from '@mantine/core' + +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, +} + +const TCBParameter = ({ + value, + vtable +}: ITCBParameterProps) => { + + //Get value + const { data: tcbValue } = useSWR( + `/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 + } + ) + + const TCBValue = (vtable: string) => { + switch (vtable) { + case 'vStreets': + return ( + + {JSON.stringify(tcbValue)} + + ) + case 'tTypes': + return ( + + {(tcbValue as tType)?.name} + + ) + case 'vPipesGround': + return ( + + {(tcbValue)?.name} + + ) + case 'vRepairEvent': + return ( + + {(tcbValue)?.name} + + ) + case 'vPipesMaterial': + return ( + + {(tcbValue)?.name} + + ) + case 'vBoilers': + return ( + + {(tcbValue)?.name} + + ) + default: + return ( + + {JSON.stringify(tcbValue)} + + ) + } + } + + return ( + TCBValue(vtable) + ) +} + +export default TCBParameter \ No newline at end of file diff --git a/client/src/components/map/mapUtils.ts b/client/src/components/map/mapUtils.ts index 03bafda..0479b11 100644 --- a/client/src/components/map/mapUtils.ts +++ b/client/src/components/map/mapUtils.ts @@ -1,12 +1,368 @@ import { Coordinate, distance, rotate } from "ol/coordinate"; -import { Extent, getCenter, getHeight, getWidth } from "ol/extent"; -import { LineString, Polygon, SimpleGeometry } from "ol/geom"; +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 VectorLayer from "ol/layer/Vector"; +import VectorImageLayer from "ol/layer/VectorImage"; +import Map from "ol/Map"; import { addCoordinateTransforms, addProjection, get, getTransform, Projection, ProjectionLike, transform } from "ol/proj"; +import VectorSource from "ol/source/Vector"; import proj4 from "proj4"; +import { firstStyleFunction, fourthStyleFunction, selectStyle, styleFunction, thirdStyleFunction } from "./MapStyles"; +import { Type } from "ol/geom/Geometry"; +import { Draw, Modify, Snap } from "ol/interaction"; +import { noModifierKeys } from "ol/events/condition"; +import { IGeometryType, IRectCoords } from "../../interfaces/map"; +import { uploadCoordinates } from "../../actions/map"; +import { ImageStatic } from "ol/source"; +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"; + +export function processLine( + line: ILine, + scaling: { w: number, h: number }, + mapCenter: Coordinate, + linesLayer: React.MutableRefObject, any>> +) { + const x1 = line.x1 * scaling.w + const y1 = line.y1 * scaling.h + const x2 = line.x2 * scaling.w + const y2 = line.y2 * scaling.h + + const center = [mapCenter[0], mapCenter[1]] + + const testCoords = [ + [center[0] + x1, center[1] - y1], + [center[0] + x2, center[1] - y2], + ] + + const feature = new Feature(new LineString(testCoords)) + feature.setStyle(styleFunction(feature)) + feature.set('type', line.type) + feature.set('planning', line.planning) + feature.set('object_id', line.object_id) + + linesLayer.current?.getSource()?.addFeature(feature) +} + +export function processFigure( + figure: IFigure, + scaling: { w: number, h: number }, + mapCenter: Coordinate, + figuresLayer: React.MutableRefObject, any>> +) { + if (figure.figure_type_id == 1) { + const width = figure.width * scaling.w + const height = figure.height * scaling.h + + const left = figure.left * scaling.w + const top = figure.top * scaling.h + + const centerX = mapCenter[0] + left + (width / 2) + const centerY = mapCenter[1] - top - (height / 2) + + const radius = width / 2; + const circleGeom = new Circle([centerX, centerY], radius) + + const ellipseGeom = fromCircle(circleGeom, 64) + ellipseGeom.scale(1, height / width) + + const feature = new Feature(ellipseGeom) + + feature.setStyle(firstStyleFunction(feature)) + feature.set('type', figure.type) + feature.set('object_id', figure.object_id) + feature.set('planning', figure.planning) + figuresLayer.current?.getSource()?.addFeature(feature) + } + + if (figure.figure_type_id == 3) { + const x = figure.left * scaling.w + const y = figure.top * scaling.h + + const center = [mapCenter[0] + x, mapCenter[1] - y] + + const coords = figure.points?.split(' ').map(pair => { + const [x, y] = pair.split(';').map(Number) + return [ + center[0] + (x * scaling.w), + center[1] - (y * scaling.h) + ] + }) + + if (coords) { + const polygon = new Polygon([coords]) + + const feature = new Feature({ + geometry: polygon + }) + + feature.set('object_id', figure.object_id) + feature.set('planning', figure.planning) + feature.set('type', figure.type) + feature.setStyle(thirdStyleFunction(feature)) + figuresLayer.current?.getSource()?.addFeature(feature) + } + } + + if (figure.figure_type_id == 4) { + const width = figure.width * scaling.w + const height = figure.height * scaling.h + const left = figure.left * scaling.w + const top = figure.top * scaling.h + + const halfWidth = width / 2 + const halfHeight = height / 2 + + const center = [mapCenter[0] + left + halfWidth, mapCenter[1] - top - halfHeight] + + const testCoords = [ + [center[0] - halfWidth, center[1] - halfHeight], + [center[0] - halfWidth, center[1] + halfHeight], + [center[0] + halfWidth, center[1] + halfHeight], + [center[0] + halfWidth, center[1] - halfHeight], + [center[0] - halfWidth, center[1] - halfHeight] + ] + + const geometry1 = new Polygon([testCoords]) + const anchor1 = center + geometry1.rotate(-figure.angle * Math.PI / 180, anchor1) + const feature1 = new Feature(geometry1) + feature1.set('object_id', figure.object_id) + feature1.set('planning', figure.planning) + feature1.set('type', figure.type) + feature1.set('angle', figure.angle) + feature1.setStyle(fourthStyleFunction(feature1)) + figuresLayer.current?.getSource()?.addFeature(feature1) + } +} + +// Function to update the image layer with a new source when extent changes +export const updateImageSource = ( + imageUrl: string, + imageLayer: React.MutableRefObject>, + polygonFeature: Feature, + setPolygonExtent: (value: React.SetStateAction) => void, + setRectCoords: React.Dispatch> +) => { + const newExtent = polygonFeature.getGeometry()?.getExtent(); + + const bottomLeft = polygonFeature.getGeometry()?.getCoordinates()[0][0] + const topLeft = polygonFeature.getGeometry()?.getCoordinates()[0][1] + const topRight = polygonFeature.getGeometry()?.getCoordinates()[0][2] + const bottomRight = polygonFeature.getGeometry()?.getCoordinates()[0][3] + + setRectCoords({ + bl: bottomLeft, + tl: topLeft, + tr: topRight, + br: bottomRight + }) + + setPolygonExtent(newExtent) + + if (newExtent && bottomLeft && bottomRight && topRight && topLeft) { + const originalExtent = calculateExtent(bottomLeft, topLeft, topRight, bottomRight) + const newImageSource = new ImageStatic({ + url: imageUrl, + imageExtent: originalExtent, + projection: rotateProjection('EPSG:3857', calculateRotationAngle(bottomLeft, bottomRight), originalExtent) + }); + imageLayer.current.setSource(newImageSource); + } +}; + +export const addInteractions = ( + drawingLayerSource: React.MutableRefObject>>, + draw: React.MutableRefObject, + map: React.MutableRefObject, + snap: React.MutableRefObject, + measureDraw: React.MutableRefObject, + measureSource: React.MutableRefObject>>, + measureModify: React.MutableRefObject, +) => { + const currentTool = getCurrentTool() + const showSegments = getMeasureShowSegments() + const clearPrevious = getMeasureClearPrevious() + const measureType = getMeasureType() + const tipPoint = getTipPoint() + + if (currentTool !== 'Measure') { + draw.current = new Draw({ + source: drawingLayerSource.current, + type: currentTool as Type, + condition: noModifierKeys + }) + + draw.current.on('drawend', function (s) { + console.log(s.feature.getGeometry()?.getType()) + let type: IGeometryType = 'POLYGON' + + switch (s.feature.getGeometry()?.getType()) { + case 'LineString': + type = 'LINE' + break + case 'Polygon': + type = 'POLYGON' + break + default: + type = 'POLYGON' + break + } + const coordinates = (s.feature.getGeometry() as SimpleGeometry).getCoordinates() as Coordinate[] + uploadCoordinates(coordinates, type) + }) + + map?.current?.addInteraction(draw.current) + snap.current = new Snap({ source: drawingLayerSource.current }) + map?.current?.addInteraction(snap.current) + } + + if (currentTool == 'Measure') { + const drawType = measureType; + const activeTip = + 'Кликните, чтобы продолжить рисовать ' + + (drawType === 'Polygon' ? 'многоугольник' : 'линию'); + const idleTip = 'Кликните, чтобы начать измерение'; + let tip = idleTip; + + measureDraw.current = new Draw({ + source: measureSource.current, + type: drawType, + style: function (feature) { + return measureStyleFunction(feature, drawType, tip); + }, + }); + measureDraw.current.on('drawstart', function () { + if (clearPrevious) { + measureSource.current.clear(); + } + measureModify.current.setActive(false); + tip = activeTip; + }); + measureDraw.current.on('drawend', function () { + modifyStyle.setGeometry(tipPoint as Geometry); + measureModify.current.setActive(true); + map.current?.once('pointermove', function () { + modifyStyle.setGeometry(''); + }); + tip = idleTip; + }); + measureModify.current.setActive(true); + map.current?.addInteraction(measureDraw.current); + } +} + +export function regionsInit( + map: React.MutableRefObject, + selectedRegion: React.MutableRefObject | null>, + regionsLayer: React.MutableRefObject, VectorSource>>>, + setStatusText: (value: React.SetStateAction) => void, + +) { + map.current?.on('click', function (e) { + if (selectedRegion.current !== null) { + selectedRegion.current = null + } + + if (map.current) { + map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) { + if (layer === regionsLayer.current) { + selectedRegion.current = feature as Feature + // Zoom to the selected feature + zoomToFeature(map, selectedRegion.current) + + return true + } else return false + }); + } + }) + + // Show current selected region + map.current?.on('pointermove', function (e) { + if (selectedRegion.current !== null) { + selectedRegion.current.setStyle(undefined) + selectedRegion.current = null + } + + if (map.current) { + map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) { + if (layer === regionsLayer.current) { + selectedRegion.current = feature as Feature + selectedRegion.current.setStyle(selectStyle) + + if (feature.get('district')) { + setStatusText(feature.get('district')) + } + + return true + } else return false + }) + } + }) + + // Hide regions layer when fully visible + map.current?.on('moveend', function () { + const viewExtent = map.current?.getView().calculateExtent(map.current.getSize()) + const features = regionsLayer.current.getSource()?.getFeatures() + + let isViewCovered = false + + features?.forEach((feature: Feature) => { + const featureExtent = feature?.getGeometry()?.getExtent() + if (viewExtent && featureExtent) { + if (containsExtent(featureExtent, viewExtent)) { + isViewCovered = true + } + } + }) + + regionsLayer.current.setVisible(!isViewCovered) + }) +} + +const zoomToFeature = (map: React.MutableRefObject, feature: Feature) => { + const geometry = feature.getGeometry() + const extent = geometry?.getExtent() + + if (map.current && extent) { + map.current.getView().fit(extent, { + duration: 300, + maxZoom: 19, + }) + } +} + +// Function to save features to localStorage +export const saveFeatures = (layerRef: React.MutableRefObject, any> | null>) => { + const features = layerRef.current?.getSource()?.getFeatures() + if (features && features.length > 0) { + const geoJSON = new GeoJSON() + const featuresJSON = geoJSON.writeFeatures(features) + localStorage.setItem('savedFeatures', featuresJSON) + } +} + +// Function to load features from localStorage +export const loadFeatures = (layerSource: React.MutableRefObject>>) => { + const savedFeatures = localStorage.getItem('savedFeatures') + if (savedFeatures) { + const geoJSON = new GeoJSON() + const features = geoJSON.readFeatures(savedFeatures, { + featureProjection: 'EPSG:4326', // Ensure the projection is correct + }) + layerSource.current?.addFeatures(features) // Add features to the vector source + //drawingLayer.current?.getSource()?.changed() + } +} function rotateProjection(projection: ProjectionLike, angle: number, extent: Extent) { function rotateCoordinate(coordinate: Coordinate, angle: number, anchor: Coordinate) { - var coord = rotate( + const coord = rotate( [coordinate[0] - anchor[0], coordinate[1] - anchor[1]], angle ); @@ -21,10 +377,10 @@ function rotateProjection(projection: ProjectionLike, angle: number, extent: Ext return rotateCoordinate(coordinate, -angle, getCenter(extent)); } - var normalProjection = get(projection); + const normalProjection = get(projection); if (normalProjection) { - var rotatedProjection = new Projection({ + const rotatedProjection = new Projection({ code: normalProjection.getCode() + ":" + angle.toString() + ":" + extent.toString(), units: normalProjection.getUnits(), extent: extent @@ -55,9 +411,9 @@ function rotateProjection(projection: ProjectionLike, angle: number, extent: Ext // also set up transforms with any projections defined using proj4 if (typeof proj4 !== "undefined") { - var projCodes = Object.keys(proj4.defs); + const projCodes = Object.keys(proj4.defs); projCodes.forEach(function (code) { - var proj4Projection = get(code) as Projection; + const proj4Projection = get(code) as Projection; if (proj4Projection) { if (!getTransform(proj4Projection, rotatedProjection)) { addCoordinateTransforms( @@ -177,7 +533,7 @@ function calculateCenter(geometry: SimpleGeometry) { const dy = coordinate[1] - center[1]; return dx * dx + dy * dy; }); - minRadius = Math.sqrt(Math.max.apply(Math, sqDistances)) / 3; + minRadius = Math.sqrt(Math.max(...sqDistances)) / 3; } else { minRadius = Math.max( diff --git a/client/src/interfaces/map.ts b/client/src/interfaces/map.ts index 042460a..1aa438d 100644 --- a/client/src/interfaces/map.ts +++ b/client/src/interfaces/map.ts @@ -1,3 +1,5 @@ +import { Coordinate } from "ol/coordinate"; + export interface SatelliteMapsProviders { google: 'google'; yandex: 'yandex'; @@ -11,3 +13,10 @@ export interface IGeometryTypes { } export type IGeometryType = IGeometryTypes[keyof IGeometryTypes] + +export interface IRectCoords { + bl: Coordinate | undefined, + tl: Coordinate | undefined, + tr: Coordinate | undefined, + br: Coordinate | undefined +} \ No newline at end of file diff --git a/client/src/main.tsx b/client/src/main.tsx index 65dbc81..c1f202a 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -12,4 +12,4 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( , -) +) \ No newline at end of file diff --git a/client/src/store/map.ts b/client/src/store/map.ts new file mode 100644 index 0000000..a1003f8 --- /dev/null +++ b/client/src/store/map.ts @@ -0,0 +1,85 @@ +import { create } from 'zustand'; +import { ToolType } from '../types/tools'; +import { Point } from 'ol/geom'; +import Map from 'ol/Map'; + +interface MapState { + currentTool: ToolType, + measureType: "LineString" | "Polygon", + measureShowSegments: boolean, + measureClearPrevious: boolean, + tipPoint: Point | null, + map: Map | null +} + +export const useMapStore = create(() => ({ + currentTool: null, + measureType: "LineString", + measureShowSegments: true, + measureClearPrevious: true, + tipPoint: null, + map: null +})); + +const getMap = () => { + return useMapStore.getState().map +} + +const setMap = (map: Map | null) => { + useMapStore.setState(() => ({ map: map })) +} + +const setTipPoint = (tipPoint: Point | null) => { + useMapStore.setState(() => ({ tipPoint: tipPoint })) +} + +const getTipPoint = () => { + return useMapStore.getState().tipPoint +} + +const setMeasureType = (tool: "LineString" | "Polygon") => { + useMapStore.setState(() => ({ measureType: tool })) +} + +const getMeasureType = () => { + return useMapStore.getState().measureType +} + +const setCurrentTool = (tool: ToolType) => { + tool === useMapStore.getState().currentTool + ? useMapStore.setState(() => ({ currentTool: null })) + : useMapStore.setState(() => ({ currentTool: tool })) +} + +const getCurrentTool = () => { + return useMapStore.getState().currentTool +} + +const getMeasureShowSegments = () => { + return useMapStore.getState().measureShowSegments +} + +const getMeasureClearPrevious = () => { + return useMapStore.getState().measureClearPrevious +} + +const setMeasureShowSegments = (bool: boolean) => { + useMapStore.setState(() => ({ measureShowSegments: bool })) +} + +const setMeasureClearPrevious = (bool: boolean) => { + useMapStore.setState(() => ({ measureClearPrevious: bool })) +} + +export { + setCurrentTool, + getCurrentTool, + setMeasureShowSegments, + setMeasureClearPrevious, + getMeasureShowSegments, + getMeasureClearPrevious, + setMeasureType, + getMeasureType, + getTipPoint, + setTipPoint +} \ No newline at end of file diff --git a/client/src/types/tools.ts b/client/src/types/tools.ts new file mode 100644 index 0000000..6890ed7 --- /dev/null +++ b/client/src/types/tools.ts @@ -0,0 +1,12 @@ +export type ToolType = + "Point" | + "LineString" | + "LinearRing" | + "Polygon" | + "MultiPoint" | + "MultiLineString" | + "MultiPolygon" | + "GeometryCollection" | + "Circle" | + "Measure" | + null \ No newline at end of file diff --git a/client/src/utils/format.ts b/client/src/utils/format.ts new file mode 100644 index 0000000..9f6811a --- /dev/null +++ b/client/src/utils/format.ts @@ -0,0 +1,44 @@ +// CP437 Character Map +const CP437_MAP = [ + '\0', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '◙', '♂', '♀', '♪', '♫', '☼', '►', + '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '→', '←', '∟', '↔', '▲', '▼', ' ', '!', '"', + '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', + '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', + '}', '~', '⌂', 'Ç', 'ü', 'é', 'â', 'ä', 'à', 'å', 'ç', 'ê', 'ë', 'è', 'ï', 'î', 'ì', 'Ä', + 'Å', 'É', 'æ', 'Æ', 'ô', 'ö', 'ò', 'û', 'ù', 'ÿ', 'Ö', 'Ü', '¢', '£', '¥', '₧', 'ƒ', 'á', + 'í', 'ó', 'ú', 'ñ', 'Ñ', 'ª', 'º', '¿', '⌐', '¬', '½', '¼', '¡', '«', '»', '░', '▒', '▓', + '│', '┤', '╡', '╢', '╖', '╕', '╣', '║', '╗', '╝', '╜', '╛', '┐', '└', '┴', '┬', '├', '─', + '┼', '╞', '╟', '╚', '╔', '╩', '╦', '╠', '═', '╬', '╧', '╨', '╤', '╥', '╙', '╘', '╒', '╓', + '╫', '╪', '┘', '┌', '█', '▄', '▌', '▐', '▀', 'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ', 'Φ', + 'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩', '≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·', + '√', 'ⁿ', '²', '■', ' ' +]; + +function decodeCP437ToBytes(garbledString: string) { + const bytes = []; + for (const char of garbledString) { + const byte = CP437_MAP.indexOf(char); + if (byte === -1) { + //console.warn(`Character '${char}' not found in CP437 map`); + bytes.push(63); // '?' as a placeholder + } + bytes.push(byte); + } + return Uint8Array.from(bytes); +} + +function decodeWindows1251FromBytes(byteArray: any) { + const decoder = new TextDecoder('windows-1251'); + return decoder.decode(byteArray); +} + +export function decodeDoubleEncodedString(garbledString: string) { + // Step 1: Decode from CP437 to bytes + const bytes = decodeCP437ToBytes(garbledString); + + // Step 2: Decode bytes as WINDOWS-1251 + return decodeWindows1251FromBytes(bytes); +} \ No newline at end of file diff --git a/client/yarn.lock b/client/yarn.lock index ea6e600..27b7499 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -6592,6 +6592,11 @@ utrie@^1.0.2: dependencies: base64-arraybuffer "^1.0.2" +uuid@^11.0.3: + version "11.0.3" + resolved "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz" + integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg== + varint@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz" diff --git a/ems/src/api/general/index.ts b/ems/src/api/general/index.ts index bb91559..010b694 100644 --- a/ems/src/api/general/index.ts +++ b/ems/src/api/general/index.ts @@ -57,35 +57,60 @@ router.get('/objects/all', async (req: Request, res: Response) => { router.get('/objects/list', async (req: Request, res: Response) => { try { - const { city_id, year, planning } = req.query + const { city_id, year, planning, type } = req.query - const result = await tediousQuery( - ` - SELECT - tTypes.id AS id, - tTypes.name AS name, - COUNT(vObjects.type) AS count - FROM - vObjects - JOIN - tTypes ON vObjects.type = tTypes.id - WHERE - vObjects.id_city = ${city_id} AND vObjects.year = ${year} - AND - ( - CASE - WHEN TRY_CAST(vObjects.planning AS BIT) IS NOT NULL THEN TRY_CAST(vObjects.planning AS BIT) - WHEN vObjects.planning = 'TRUE' THEN 1 - WHEN vObjects.planning = 'FALSE' THEN 0 - ELSE NULL - END - ) = ${planning} - GROUP BY - tTypes.id, - tTypes.name; - ` - ) - res.status(200).json(result) + if (type) { + const result = await tediousQuery( + ` + SELECT + * + FROM + vObjects + WHERE + vObjects.id_city = ${city_id} + AND vObjects.year = ${year} + AND type = ${type} + AND + ( + CASE + WHEN TRY_CAST(vObjects.planning AS BIT) IS NOT NULL THEN TRY_CAST(vObjects.planning AS BIT) + WHEN vObjects.planning = 'TRUE' THEN 1 + WHEN vObjects.planning = 'FALSE' THEN 0 + ELSE NULL + END + ) = ${planning}; + ` + ) + res.status(200).json(result) + } else { + const result = await tediousQuery( + ` + SELECT + tTypes.id AS id, + tTypes.name AS name, + COUNT(vObjects.type) AS count + FROM + vObjects + JOIN + tTypes ON vObjects.type = tTypes.id + WHERE + vObjects.id_city = ${city_id} AND vObjects.year = ${year} + AND + ( + CASE + WHEN TRY_CAST(vObjects.planning AS BIT) IS NOT NULL THEN TRY_CAST(vObjects.planning AS BIT) + WHEN vObjects.planning = 'TRUE' THEN 1 + WHEN vObjects.planning = 'FALSE' THEN 0 + ELSE NULL + END + ) = ${planning} + GROUP BY + tTypes.id, + tTypes.name; + ` + ) + res.status(200).json(result) + } } catch (err) { res.status(500) } @@ -151,4 +176,37 @@ router.get('/params/all', async (req: Request, res: Response) => { } }) +// Get value from TCB parameter +router.get('/params/tcb', async (req: Request, res: Response) => { + try { + const { vtable, id, offset, limit } = req.query + + if (!vtable) { + res.status(500) + } + + if (id) { + const result = await tediousQuery( + ` + SELECT * FROM nGeneral..${vtable} + WHERE id = '${id}' + ` + ) + 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; + ` + ) + res.status(200).json(result) + } + } catch (err) { + res.status(500) + } +}) + export default router \ No newline at end of file