diff --git a/client/src/App.tsx b/client/src/App.tsx index bea8403..85176e9 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -18,9 +18,10 @@ import PasswordReset from "./pages/auth/PasswordReset" import MapTest from "./pages/MapTest" import MonitorPage from "./pages/MonitorPage" import DashboardLayout from "./layouts/DashboardLayout" -import { IconApi, IconBuildingFactory2, IconDeviceDesktopAnalytics, IconFiles, IconHome, IconLogin, IconLogin2, IconMap, IconPassword, IconReport, IconServer, IconSettings, IconShield, IconTable, IconUsers } from "@tabler/icons-react" +import { IconApi, IconBuildingFactory2, IconComponents, IconDeviceDesktopAnalytics, IconFiles, IconHome, IconLogin, IconLogin2, IconMap, IconPassword, IconReport, IconServer, IconSettings, IconShield, IconTable, IconUsers } from "@tabler/icons-react" import { Box, Loader } from "@mantine/core" import TableTest from "./pages/TableTest" +import ComponentTest from "./pages/ComponentTest" // Определение страниц с путями и компонентом для рендера export const pages = [ @@ -148,7 +149,7 @@ export const pages = [ component: , drawer: true, dashboard: true, - enabled: false, + enabled: true, }, { label: "Table test", @@ -159,6 +160,15 @@ export const pages = [ dashboard: true, enabled: true, }, + { + label: "Component test", + path: "/component-test", + icon: , + component: , + drawer: true, + dashboard: true, + enabled: true, + }, ] function App() { diff --git a/client/src/actions/map.ts b/client/src/actions/map.ts new file mode 100644 index 0000000..43834b3 --- /dev/null +++ b/client/src/actions/map.ts @@ -0,0 +1,23 @@ +import { Coordinate } from "ol/coordinate"; +import { IGeometryType } from "../interfaces/map"; + +export const uploadCoordinates = async (coordinates: Coordinate[], type: IGeometryType) => { + try { + const response = await fetch(`${import.meta.env.VITE_API_EMS_URL}/nodes`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ coordinates, object_id: 1, type: type }) // Replace with actual object_id + }); + + if (response.ok) { + const data = await response.json(); + console.log('Node created:', data); + } else { + console.error('Failed to upload coordinates'); + } + } catch (error) { + console.error('Error:', error); + } +}; \ No newline at end of file diff --git a/client/src/components/CardInfo/CardInfo.tsx b/client/src/components/CardInfo/CardInfo.tsx index db6586a..dc8c579 100644 --- a/client/src/components/CardInfo/CardInfo.tsx +++ b/client/src/components/CardInfo/CardInfo.tsx @@ -1,4 +1,4 @@ -import { Divider, Paper, Typography } from '@mui/material' +import { Divider, Flex, Text } from '@mantine/core'; import { PropsWithChildren } from 'react' interface CardInfoProps extends PropsWithChildren { @@ -10,14 +10,14 @@ export default function CardInfo({ label }: CardInfoProps) { return ( - - + + {label} - + {children} - + ) } \ No newline at end of file diff --git a/client/src/components/CardInfo/CardInfoChip.tsx b/client/src/components/CardInfo/CardInfoChip.tsx index 5c662ed..de1b7b0 100644 --- a/client/src/components/CardInfo/CardInfoChip.tsx +++ b/client/src/components/CardInfo/CardInfoChip.tsx @@ -1,4 +1,4 @@ -import { Chip } from '@mui/material' +import { Chip } from '@mantine/core'; import { ReactElement } from 'react' interface CardInfoChipProps { @@ -17,9 +17,10 @@ export default function CardInfoChip({ return ( + variant='outline' + > + {label} + ) } \ No newline at end of file diff --git a/client/src/components/CardInfo/CardInfoLabel.tsx b/client/src/components/CardInfo/CardInfoLabel.tsx index 0004ca4..fe3ea03 100644 --- a/client/src/components/CardInfo/CardInfoLabel.tsx +++ b/client/src/components/CardInfo/CardInfoLabel.tsx @@ -1,4 +1,4 @@ -import { Box, Typography } from '@mui/material' +import { Flex, Text } from '@mantine/core'; interface CardInfoLabelProps { label: string; value: string | number; @@ -9,14 +9,14 @@ export default function CardInfoLabel({ value }: CardInfoLabelProps) { return ( - - + + {label} - + - + {value} - - + + ) } \ No newline at end of file diff --git a/client/src/components/FolderViewer.tsx b/client/src/components/FolderViewer.tsx index b3dc891..8d3a58f 100644 --- a/client/src/components/FolderViewer.tsx +++ b/client/src/components/FolderViewer.tsx @@ -1,12 +1,11 @@ import { useDocuments, useDownload, useFolders } from '../hooks/swrHooks' import { IDocument, IDocumentFolder } from '../interfaces/documents' -import { Box, CircularProgress, Divider, SxProps } from '@mui/material' import { Folder, InsertDriveFile } from '@mui/icons-material' import React, { useEffect, useState } from 'react' import DocumentService from '../services/DocumentService' import { mutate } from 'swr' import FileViewer from './modals/FileViewer' -import { ActionIcon, Anchor, Breadcrumbs, Button, FileButton, Flex, Loader, RingProgress, ScrollAreaAutosize, Table, Text } from '@mantine/core' +import { ActionIcon, Anchor, Breadcrumbs, Button, Divider, FileButton, Flex, Loader, MantineStyleProp, RingProgress, ScrollAreaAutosize, Table, Text } from '@mantine/core' import { IconCancel, IconDownload, IconFile, IconFilePlus, IconFileUpload, IconX } from '@tabler/icons-react' interface FolderProps { @@ -21,7 +20,7 @@ interface DocumentProps { handleDocumentClick: (index: number) => void; } -const FileItemStyle: SxProps = { +const FileItemStyle: MantineStyleProp = { cursor: 'pointer', display: 'flex', width: '100%', @@ -36,13 +35,13 @@ function ItemFolder({ folder, handleFolderClick, ...props }: FolderProps) { handleFolderClick(folder)} > - {folder.name} - + ) } @@ -72,15 +71,15 @@ function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentPro return ( - handleDocumentClick(index)} {...props} > {doc.name} - - + + { if (!isLoading) { @@ -94,7 +93,7 @@ function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentPro } - + ) } @@ -171,7 +170,7 @@ export default function FolderViewer() { if (foldersLoading || documentsLoading) { return ( - + ) } @@ -205,16 +204,12 @@ export default function FolderViewer() { {currentFolder && - - + 0 ? '1px dashed gray' : 'none', borderRadius: '8px', - p: '16px' }}> - + {(props) => } @@ -240,7 +235,7 @@ export default function FolderViewer() { } - + @@ -264,8 +259,8 @@ export default function FolderViewer() { ))} } - - + + } + {serverIps && //
} - + ) } diff --git a/client/src/components/ServerHardware.tsx b/client/src/components/ServerHardware.tsx index db7e898..c2abe7c 100644 --- a/client/src/components/ServerHardware.tsx +++ b/client/src/components/ServerHardware.tsx @@ -1,8 +1,7 @@ import { AppBar, CircularProgress, Dialog, IconButton, Toolbar } from '@mui/material' -import { Fragment, useState } from 'react' +import { useState } from 'react' import { IRegion } from '../interfaces/fuel' import { useHardwares, useServers } from '../hooks/swrHooks' -import ServerService from '../services/ServersService' import { GridColDef } from '@mui/x-data-grid' import { Close } from '@mui/icons-material' import ServerData from './ServerData' diff --git a/client/src/components/ServerIpsView.tsx b/client/src/components/ServerIpsView.tsx index b4994e6..80ec1c5 100644 --- a/client/src/components/ServerIpsView.tsx +++ b/client/src/components/ServerIpsView.tsx @@ -1,5 +1,5 @@ -import { AppBar, CircularProgress, Dialog, IconButton, TextField, Toolbar } from '@mui/material' -import { Fragment, useState } from 'react' +import { AppBar, CircularProgress, Dialog, IconButton, Toolbar } from '@mui/material' +import { useState } from 'react' import { IRegion } from '../interfaces/fuel' import { useServerIps, useServers } from '../hooks/swrHooks' import ServerService from '../services/ServersService' @@ -92,7 +92,7 @@ export default function ServerIpsView() { /> ) } - //value={search} + //value={search} /> diff --git a/client/src/components/map/MapComponent.tsx b/client/src/components/map/MapComponent.tsx index db9f38b..43c14c3 100644 --- a/client/src/components/map/MapComponent.tsx +++ b/client/src/components/map/MapComponent.tsx @@ -9,7 +9,7 @@ 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 Feature from 'ol/Feature' -import { SatelliteMapsProvider } from '../../interfaces/map' +import { IGeometryType, SatelliteMapsProvider } from '../../interfaces/map' import { containsExtent, Extent } from 'ol/extent' import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles' import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources' @@ -24,36 +24,28 @@ import { Stroke, Fill, Circle as CircleStyle, Style, Text } from 'ol/style' import { calculateCenter, calculateExtent, calculateRotationAngle, rotateProjection } from './mapUtils' import MapBrowserEvent from 'ol/MapBrowserEvent' import { get, transform } from 'ol/proj' -import { useCities } from '../../hooks/swrHooks' import useSWR from 'swr' import { fetcher } from '../../http/axiosInstance' import { BASE_URL } from '../../constants' -import { Accordion, ActionIcon, Autocomplete, Box, Button, CloseButton, Flex, Grid, Select as MantineSelect, Text as MantineText, MantineStyleProp, rem, ScrollAreaAutosize, Slider, useMantineColorScheme, Divider, Portal } from '@mantine/core' -import { IconApi, IconArrowBackUp, IconArrowsMove, 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, 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 { getGridCellPosition } from './mapUtils' import { IFigure, ILine } from '../../interfaces/gis' import axios from 'axios' import ObjectParameter from './ObjectParameter' -import { IObjectData, IObjectParam } from '../../interfaces/objects' +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' const MapComponent = () => { - //const { cities } = useCities(100, 1) - - // useEffect(() => { - // if (cities) { - // cities.map((city: any) => { - // citiesLayer.current?.getSource()?.addFeature(new Feature(new Point(fromLonLat([city.longitude, city.width])))) - // }) - // } - // }, [cities]) - const [currentCoordinate, setCurrentCoordinate] = useState(null) const [currentZ, setCurrentZ] = useState(undefined) const [currentX, setCurrentX] = useState(undefined) const [currentY, setCurrentY] = useState(undefined) - const [file, setFile] = useState(null) + const [file, setFile] = useState(null) const [polygonExtent, setPolygonExtent] = useState(undefined) const [bottomLeft, setBottomLeft] = useState(undefined) const [topLeft, setTopLeft] = useState(undefined) @@ -70,7 +62,7 @@ const MapComponent = () => { const gMapsSatSource = useRef(googleMapsSatelliteSource) const customMapSource = useRef(new XYZ({ - url: `${import.meta.env.VITE_API_EMS_URL}/tile/custom/{z}/{x}/{y}`, + url: `${import.meta.env.VITE_API_EMS_URL}/tiles/tile/custom/{z}/{x}/{y}`, attributions: 'Custom map data' })) @@ -133,7 +125,7 @@ const MapComponent = () => { draw.current.on('drawend', function (s) { console.log(s.feature.getGeometry()?.getType()) - let type = 'POLYGON' + let type: IGeometryType = 'POLYGON' switch (s.feature.getGeometry()?.getType()) { case 'LineString': @@ -146,7 +138,7 @@ const MapComponent = () => { type = 'POLYGON' break } - const coordinates = (s.feature.getGeometry() as SimpleGeometry).getCoordinates() + const coordinates = (s.feature.getGeometry() as SimpleGeometry).getCoordinates() as Coordinate[] uploadCoordinates(coordinates, type) }) @@ -222,10 +214,12 @@ const MapComponent = () => { }); // tile processing - const handleImageDrop = useCallback((event: any) => { + const handleImageDrop = useCallback((event: DragEvent) => { event.preventDefault(); event.stopPropagation(); + if (!event.dataTransfer?.files) return + const files = event.dataTransfer.files; if (files.length > 0) { const file = files[0]; @@ -657,27 +651,6 @@ const MapComponent = () => { } }, [currentTool]) - const uploadCoordinates = async (coordinates: any, type: any) => { - try { - const response = await fetch(`${import.meta.env.VITE_API_EMS_URL}/nodes`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ coordinates, object_id: 1, type: type }) // Replace with actual object_id - }); - - if (response.ok) { - const data = await response.json(); - console.log('Node created:', data); - } else { - console.error('Failed to upload coordinates'); - } - } catch (error) { - console.error('Error:', error); - } - }; - const [satelliteOpacity, setSatelliteOpacity] = useState(1) const [statusText, setStatusText] = useState('') @@ -721,7 +694,7 @@ const MapComponent = () => { formData.append('brX', bottomRight[0].toString()) formData.append('brY', bottomRight[1].toString()) - await fetch(`${import.meta.env.VITE_API_EMS_URL}/upload`, { method: 'POST', body: formData }) + await fetch(`${import.meta.env.VITE_API_EMS_URL}/tiles/upload`, { method: 'POST', body: formData }) } } @@ -864,6 +837,122 @@ const MapComponent = () => { const [searchCity, setSearchCity] = useState("") + 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 { 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(null) + const [selectedObjectList, setSelectedObjectList] = useState(null) + + 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) => { + if (selectedObjectList == feature.get('type')) { + feature.setStyle(highlightStyle); + } else { + feature.setStyle(null); // Reset to default style + } + }) + } + + if (linesLayer.current) { + // Reset styles and apply highlight to matching features + linesLayer.current.getSource().getFeatures().forEach((feature) => { + if (selectedObjectList == feature.get('type')) { + feature.setStyle(highlightStyle); + } else { + feature.setStyle(null); // Reset to default style + } + }) + } + }, [selectedObjectList]) + + useEffect(() => { + if (existingObjectsList && planningObjectsList) { + setObjectsList([ + { + label: 'Существующие', + value: 'existing', + children: existingObjectsList.map((list: IObjectList) => ({ + label: `${list.name} (${list.count})`, + value: list.id, + })), + }, + { + label: 'Планируемые', + value: 'planning', + children: planningObjectsList.map((list: IObjectList) => ({ + label: `${list.name} (${list.count})`, + value: list.id + })) + } + ]) + } + }, [existingObjectsList, planningObjectsList]) + + 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) => { + if (currentObjectId == feature.get('object_id')) { + feature.setStyle(highlightStyle); + } else { + feature.setStyle(null); // Reset to default style + } + }) + } + + if (linesLayer.current) { + // Reset styles and apply highlight to matching features + linesLayer.current.getSource().getFeatures().forEach((feature) => { + if (currentObjectId == feature.get('object_id')) { + feature.setStyle(highlightStyle); + } else { + feature.setStyle(null); // Reset to default style + } + }) + } + } + }, [currentObjectId]) + const { data: currentObjectData } = useSWR( currentObjectId ? `/general/objects/${currentObjectId}` : null, (url) => fetcher(url, BASE_URL.ems), @@ -939,7 +1028,9 @@ const MapComponent = () => { 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) } @@ -965,6 +1056,8 @@ const MapComponent = () => { }) 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) } @@ -994,6 +1087,8 @@ const MapComponent = () => { 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) @@ -1025,6 +1120,8 @@ const MapComponent = () => { 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) @@ -1037,6 +1134,12 @@ const MapComponent = () => { + + setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} /> + + setSatMapsProvider(value as SatelliteMapsProvider)} /> + +
{ - - { - fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res)) - }}> - - - - { - saveFeatures() - }}> - - - - { - draw.current?.removeLastPoint() - }}> - - - - { - handleToolSelect('Point') - }}> - - - - { - handleToolSelect('LineString') - }}> - - - - { - handleToolSelect('Polygon') - }}> - - - - { - handleToolSelect('Circle') - }}> - - - - map?.current?.addInteraction(new Translate())} - > - - - - - - - + saveFeatures()} + onRemove={() => draw.current?.removeLastPoint()} + handleToolSelect={handleToolSelect} + onMover={() => map?.current?.addInteraction(new Translate())} + colorScheme={colorScheme} + /> { - - setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} /> - - setSatMapsProvider(value as SatelliteMapsProvider)} /> - - }>{'Объекты'} - + {objectsList && + ( + { + elementProps.onClick(e) + if (node.value !== 'existing' && node.value !== 'planning') { + setSelectedObjectList(Number(node.value)) + } + }}> + {hasChildren && ( + + )} + + {node.label} + + )} + /> + } + - - }> - {'Текущий объект'} - - - - - + {currentObjectId && + + }> + {'Текущий объект'} + + + + + + } - {valuesData && - }>{'Параметры объекта'} - - - {Array.isArray(valuesData) && valuesData.map((param: IObjectParam) => { - return ( - - ) - })} - - - } + {valuesData && + + }>{'Параметры объекта'} + + + {Array.isArray(valuesData) && valuesData.map((param: IObjectParam) => { + return ( + + ) + })} + + + + } - - - x: {currentCoordinate?.[0]} - - - - y: {currentCoordinate?.[1]} - - - - - - Z={currentZ} - - - - X={currentX} - - - - Y={currentY} - - - - {statusText} - - + diff --git a/client/src/components/map/MapSources.ts b/client/src/components/map/MapSources.ts index 7403867..e398ac9 100644 --- a/client/src/components/map/MapSources.ts +++ b/client/src/components/map/MapSources.ts @@ -10,12 +10,12 @@ register(proj4); const yandexProjection = get('EPSG:3395')?.setExtent([-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]) || 'EPSG:3395' const googleMapsSatelliteSource = new XYZ({ - url: `${import.meta.env.VITE_API_EMS_URL}/tile/google/{z}/{x}/{y}`, + url: `${import.meta.env.VITE_API_EMS_URL}/tiles/tile/google/{z}/{x}/{y}`, attributions: 'Map data © Google' }) const yandexMapsSatelliteSource = new XYZ({ - url: `${import.meta.env.VITE_API_EMS_URL}/tile/yandex/{z}/{x}/{y}`, + url: `${import.meta.env.VITE_API_EMS_URL}/tiles/tile/yandex/{z}/{x}/{y}`, attributions: 'Map data © Yandex', projection: yandexProjection, }) diff --git a/client/src/components/map/MapStatusbar/MapStatusbar.tsx b/client/src/components/map/MapStatusbar/MapStatusbar.tsx new file mode 100644 index 0000000..50ac8a5 --- /dev/null +++ b/client/src/components/map/MapStatusbar/MapStatusbar.tsx @@ -0,0 +1,53 @@ +import { Divider, Flex, rem, Text } from '@mantine/core' +import { Coordinate } from 'ol/coordinate'; +import React, { CSSProperties } from 'react' + +interface IMapStatusbarProps { + mapControlsStyle: CSSProperties; + currentCoordinate: Coordinate | null; + currentX: number | undefined; + currentY: number | undefined; + currentZ: number | undefined; + statusText: string; +} + +const MapStatusbar = ({ + mapControlsStyle, + currentCoordinate, + currentX, + currentY, + currentZ, + statusText +}: IMapStatusbarProps) => { + return ( + + + x: {currentCoordinate?.[0]} + + + + y: {currentCoordinate?.[1]} + + + + + + Z={currentZ} + + + + X={currentX} + + + + Y={currentY} + + + + {statusText} + + + ) +} + +export default MapStatusbar \ No newline at end of file diff --git a/client/src/components/map/MapToolbar/MapToolbar.tsx b/client/src/components/map/MapToolbar/MapToolbar.tsx new file mode 100644 index 0000000..173f245 --- /dev/null +++ b/client/src/components/map/MapToolbar/MapToolbar.tsx @@ -0,0 +1,93 @@ +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' + +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) => { + return ( + + { + fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res)) + }}> + + + + + + + + + + + + { + handleToolSelect('Point') + }}> + + + + { + handleToolSelect('LineString') + }}> + + + + { + handleToolSelect('Polygon') + }}> + + + + { + handleToolSelect('Circle') + }}> + + + + + + + + + + + + ) +} + +export default MapToolbar \ No newline at end of file diff --git a/client/src/components/map/ObjectData.tsx b/client/src/components/map/ObjectData.tsx index 7f03109..18e0b44 100644 --- a/client/src/components/map/ObjectData.tsx +++ b/client/src/components/map/ObjectData.tsx @@ -1,4 +1,4 @@ -import { Flex, Table } from '@mantine/core' +import { Flex } from '@mantine/core' import { IObjectData, IObjectType } from '../../interfaces/objects' import useSWR from 'swr' import { fetcher } from '../../http/axiosInstance' @@ -17,7 +17,7 @@ const ObjectData = (object_data: IObjectData) => { {Array.isArray(typeData) && (typeData.find(type => Number(type.id) === Number(object_data.type)) as IObjectType).name} - + ) } diff --git a/client/src/components/map/ObjectParameter.tsx b/client/src/components/map/ObjectParameter.tsx index 23473bd..04c818f 100644 --- a/client/src/components/map/ObjectParameter.tsx +++ b/client/src/components/map/ObjectParameter.tsx @@ -1,8 +1,7 @@ -import React from 'react' import useSWR from 'swr' import { fetcher } from '../../http/axiosInstance' import { BASE_URL } from '../../constants' -import { Checkbox, Flex, Grid } from '@mantine/core' +import { Checkbox, Grid } from '@mantine/core' import { IObjectParam, IParam } from '../../interfaces/objects' const ObjectParameter = ({ diff --git a/client/src/interfaces/gis.ts b/client/src/interfaces/gis.ts index 8b1f863..7b47f33 100644 --- a/client/src/interfaces/gis.ts +++ b/client/src/interfaces/gis.ts @@ -11,7 +11,9 @@ export interface IFigure { label_top: number | null, label_angle: number | null, label_size: number | null, - year: number + year: number, + type: number, + planning: boolean } export interface ILine { @@ -28,5 +30,7 @@ export interface ILine { label_sizes: string | null, label_angels: string | null, label_positions: string | null, - year: number + year: number, + type: number, + planning: boolean } \ No newline at end of file diff --git a/client/src/interfaces/map.ts b/client/src/interfaces/map.ts index e8c7e1a..042460a 100644 --- a/client/src/interfaces/map.ts +++ b/client/src/interfaces/map.ts @@ -3,4 +3,11 @@ export interface SatelliteMapsProviders { yandex: 'yandex'; custom: 'custom'; } -export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders] \ No newline at end of file +export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders] + +export interface IGeometryTypes { + LINE: 'LINE' + POLYGON: 'POLYGON' +} + +export type IGeometryType = IGeometryTypes[keyof IGeometryTypes] diff --git a/client/src/interfaces/objects.ts b/client/src/interfaces/objects.ts index 19b5907..134a104 100644 --- a/client/src/interfaces/objects.ts +++ b/client/src/interfaces/objects.ts @@ -1,3 +1,9 @@ +export interface IObjectList { + id: number, + name: string, + count: number +} + export interface IObjectData { object_id: string, id_city: number, diff --git a/client/src/pages/ApiTest.tsx b/client/src/pages/ApiTest.tsx index b3e8a96..3c108df 100644 --- a/client/src/pages/ApiTest.tsx +++ b/client/src/pages/ApiTest.tsx @@ -1,9 +1,9 @@ -import { Box } from "@mui/material" import { useCities } from "../hooks/swrHooks" import { useEffect, useState } from "react" import { DataGrid, GridColDef } from "@mui/x-data-grid" import axiosInstance from "../http/axiosInstance" import { BASE_URL } from "../constants" +import { Flex } from "@mantine/core" export default function ApiTest() { @@ -36,7 +36,7 @@ export default function ApiTest() { ] return ( - + - + ) } \ No newline at end of file diff --git a/client/src/pages/ComponentTest.tsx b/client/src/pages/ComponentTest.tsx new file mode 100644 index 0000000..6ea762a --- /dev/null +++ b/client/src/pages/ComponentTest.tsx @@ -0,0 +1,12 @@ +import { Flex } from '@mantine/core' +import ServerHardware from '../components/ServerHardware' + +const ComponentTest = () => { + return ( + + + + ) +} + +export default ComponentTest \ No newline at end of file diff --git a/client/src/pages/MonitorPage.tsx b/client/src/pages/MonitorPage.tsx index c249bce..32f2cc9 100644 --- a/client/src/pages/MonitorPage.tsx +++ b/client/src/pages/MonitorPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Card, Stack } from '@mui/material'; +import { Card, Flex } from '@mantine/core'; function CardComponent({ url, @@ -7,10 +7,10 @@ function CardComponent({ }: { url: any, is_alive: any }) { return ( - +

{url}

{JSON.stringify(is_alive)}

-
+
) } @@ -38,11 +38,11 @@ export default function MonitorPage() { return (
- + {servers.length > 0 && servers.map((server: any) => ( ))} - +
) } \ No newline at end of file diff --git a/client/src/pages/auth/PasswordReset.tsx b/client/src/pages/auth/PasswordReset.tsx index b78e7aa..cd10866 100644 --- a/client/src/pages/auth/PasswordReset.tsx +++ b/client/src/pages/auth/PasswordReset.tsx @@ -1,9 +1,8 @@ -import { CircularProgress, Fade, Grow } from '@mui/material' import { useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form'; import AuthService from '../../services/AuthService'; import { CheckCircle } from '@mui/icons-material'; -import { Button, Flex, Paper, Text, TextInput } from '@mantine/core'; +import { Button, Flex, Loader, Paper, Text, TextInput, Transition } from '@mantine/core'; interface PasswordResetProps { email: string; @@ -39,47 +38,54 @@ function PasswordReset() { - {!success && - - - Введите адрес электронной почты, на который будут отправлены новые данные для авторизации: - - - - - - - - - - - - } - {success && - - - - + {!success && + + {(styles) => + - На указанный адрес было отправлено письмо с новыми данными для авторизации. + Введите адрес электронной почты, на который будут отправлены новые данные для авторизации: + + + + + + + + + - - + } + + + } + {success && + + {(styles) => + + + + + На указанный адрес было отправлено письмо с новыми данными для авторизации. + + + + + - - + } + }
diff --git a/client/src/pages/auth/SignUp.tsx b/client/src/pages/auth/SignUp.tsx index 8813dc5..c6e45ba 100644 --- a/client/src/pages/auth/SignUp.tsx +++ b/client/src/pages/auth/SignUp.tsx @@ -1,10 +1,10 @@ import { useForm, SubmitHandler } from 'react-hook-form'; -import { TextField, Button, Container, Typography, Box } from '@mui/material'; import UserService from '../../services/UserService'; import { IUser } from '../../interfaces/user'; +import { Button, Flex, Loader, Paper, Text, TextInput } from '@mantine/core'; const SignUp = () => { - const { register, handleSubmit, formState: { errors } } = useForm({ + const { register, handleSubmit, formState: { errors, isValid, isSubmitting } } = useForm({ defaultValues: { email: '', login: '', @@ -26,77 +26,66 @@ const SignUp = () => { }; return ( - - - + + + Регистрация - +
- + + - + - + - + - + - + - + + + + -
-
+ + ); }; diff --git a/ems/src/api/general/index.ts b/ems/src/api/general/index.ts new file mode 100644 index 0000000..bb91559 --- /dev/null +++ b/ems/src/api/general/index.ts @@ -0,0 +1,154 @@ +import express, { Request, Response } from 'express'; +import { tediousQuery } from '../../utils/tedious'; +const router = express.Router() + +router.get('/cities/all', async (req: Request, res: Response) => { + try { + const { offset, limit, search, id } = req.query + + const result = await tediousQuery( + ` + SELECT * FROM nGeneral..Cities + ${id ? `WHERE id = '${id}'` : ''} + ${search ? `WHERE name LIKE '%${search || ''}%'` : ''} + ORDER BY id + OFFSET ${Number(offset) || 0} ROWS + FETCH NEXT ${Number(limit) || 10} ROWS ONLY; + ` + ) + res.status(200).json(result) + } catch (err) { + res.status(500) + } +}) + +router.get('/types/all', async (req: Request, res: Response) => { + try { + const result = await tediousQuery( + ` + SELECT * FROM nGeneral..tTypes + ORDER BY id + ` + ) + res.status(200).json(result) + } catch (err) { + res.status(500) + } +}) + +router.get('/objects/all', async (req: Request, res: Response) => { + try { + const { offset, limit, city_id } = req.query + + const result = await tediousQuery( + ` + SELECT * FROM nGeneral..vObjects + ${city_id ? `WHERE id_city = ${city_id}` : ''} + 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) + } +}) + +router.get('/objects/list', async (req: Request, res: Response) => { + try { + const { city_id, year, planning } = 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) + } catch (err) { + res.status(500) + } +}) + +router.get('/objects/:id([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})', async (req: Request, res: Response) => { + try { + const { id } = req.params; + + const result = await tediousQuery( + ` + SELECT * FROM nGeneral..vObjects + ${id ? `WHERE object_id = '${id}'` : ''} + ` + ) + if (Array.isArray(result) && result.length > 0) { + res.status(200).json(result[0]) + } + } catch (err) { + res.status(500) + } +}) + +router.get('/values/all', async (req: Request, res: Response) => { + try { + const { object_id } = req.query + + if (!object_id) { + res.status(500) + } + + const result = await tediousQuery( + ` + SELECT * FROM nGeneral..tValues + WHERE id_object = '${object_id}' + ` + ) + + res.status(200).json(result) + } catch (err) { + res.status(500) + } +}) + +router.get('/params/all', async (req: Request, res: Response) => { + try { + const { param_id } = req.query + + if (!param_id) { + res.status(500) + } + + const result = await tediousQuery( + ` + SELECT * FROM nGeneral..tParameters + WHERE id = '${param_id}' + ` + ) + + res.status(200).json(result) + } catch (err) { + res.status(500) + } +}) + +export default router \ No newline at end of file diff --git a/ems/src/api/gis/index.ts b/ems/src/api/gis/index.ts new file mode 100644 index 0000000..11d4c80 --- /dev/null +++ b/ems/src/api/gis/index.ts @@ -0,0 +1,65 @@ +import express, { Request, Response } from 'express'; +import { tediousQuery } from '../../utils/tedious'; +const router = express.Router() + +router.get('/images/all', async (req: Request, res: Response) => { + try { + const { offset, limit, city_id } = req.query + + const result = await tediousQuery( + ` + SELECT * FROM New_Gis..images + ${city_id ? `WHERE city_id = ${city_id}` : ''} + ORDER BY city_id + OFFSET ${Number(offset) || 0} ROWS + FETCH NEXT ${Number(limit) || 10} ROWS ONLY; + ` + ) + res.status(200).json(result) + } catch (err) { + res.status(500) + } +}) + + +// Get figures by year and city id +router.get('/figures/all', async (req: Request, res: Response) => { + try { + const { offset, limit, object_id, year, city_id } = req.query + + const result = await tediousQuery( + ` + SELECT * FROM New_Gis..figures f + JOIN nGeneral..vObjects o ON f.object_id = o.object_id WHERE o.id_city = ${city_id} AND f.year = ${year} + ORDER BY f.year + OFFSET ${Number(offset) || 0} ROWS + FETCH NEXT ${Number(limit) || 10} ROWS ONLY; + ` + ) + res.status(200).json(result) + } catch (err) { + res.status(500) + } +}) + +// Get lines by year and city id +router.get('/lines/all', async (req: Request, res: Response) => { + try { + const { offset, limit, object_id, year, city_id } = req.query + + const result = await tediousQuery( + ` + SELECT * FROM New_Gis..lines l + JOIN nGeneral..vObjects o ON l.object_id = o.object_id WHERE o.id_city = ${city_id} AND l.year = ${year} + ORDER BY l.year + 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 diff --git a/ems/src/api/nodes/index.ts b/ems/src/api/nodes/index.ts new file mode 100644 index 0000000..73f796c --- /dev/null +++ b/ems/src/api/nodes/index.ts @@ -0,0 +1,72 @@ +import express, { Request, Response } from 'express'; +import { query, validationResult } from 'express-validator'; +import { PrismaClient } from '@prisma/client'; +const router = express.Router() + +const prisma = new PrismaClient() + +router.get('/all', async (req: Request, res: Response) => { + try { + const nodes = await prisma.nodes.findMany() + + res.json(nodes) + } catch (error) { + console.error('Error getting node:', error); + res.status(500).json({ error: 'Failed to get node' }); + } +}) + +router.get('/', query('id').isString().isUUID(), async (req: Request, res: Response) => { + try { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.send({ errors: result.array() }) + } + + const { id } = req.params + + const node = await prisma.nodes.findFirst({ + where: { + id: id + } + }) + + res.json(node) + + } catch (error) { + console.error('Error getting node:', error); + res.status(500).json({ error: 'Failed to get node' }); + } +}) + +router.post('/', async (req: Request, res: Response) => { + try { + const { coordinates, object_id, type } = req.body; + + // Convert the incoming array of coordinates into the shape structure + const shape = coordinates.map((point: number[]) => ({ + object_id: object_id || null, + x: point[0], + y: point[1] + })); + + console.log(shape) + + // Create a new node in the database + const node = await prisma.nodes.create({ + data: { + object_id: object_id || null, // Nullable if object_id is not provided + shape_type: type, // You can adjust this dynamically + shape: shape, // Store the shape array as Json[] + label: 'Default' + } + }); + + res.status(201).json(node); + } catch (error) { + console.error('Error creating node:', error); + res.status(500).json({ error: 'Failed to create node' }); + } +}) + +export default router \ No newline at end of file diff --git a/ems/src/api/tiles/index.ts b/ems/src/api/tiles/index.ts new file mode 100644 index 0000000..5eff191 --- /dev/null +++ b/ems/src/api/tiles/index.ts @@ -0,0 +1,83 @@ +import express, { Request, Response } from 'express'; +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import { Coordinate } from '../../interfaces/map'; +import { generateTilesForZoomLevel } from '../../utils/tiles'; +import axios from 'axios'; + +const router = express.Router() + +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, path.join(__dirname, '..', 'public', 'temp')) + }, + filename: function (req, file, cb) { + cb(null, Date.now() + path.extname(file.originalname)) + } +}) + +const upload = multer({ storage: storage }) + +const tileFolder = path.join(__dirname, '..', '..', '..', 'public', 'tile_data') +const uploadDir = path.join(__dirname, '..', '..', '..', 'public', 'temp') + +router.post('/upload', upload.single('file'), async (req: Request, res: Response) => { + const { extentMinX, extentMinY, extentMaxX, extentMaxY, blX, blY, tlX, tlY, trX, trY, brX, brY } = req.body + + const bottomLeft: Coordinate = { x: blX, y: blY } + const topLeft: Coordinate = { x: tlX, y: tlY } + const topRight: Coordinate = { x: trX, y: trY } + const bottomRight: Coordinate = { x: brX, y: brY } + + if (req.file) { + for (let z = 0; z <= 21; z++) { + await generateTilesForZoomLevel(uploadDir, tileFolder, req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z) + } + } + + return res.status(200) +}) + +router.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => { + const { provider, z, x, y } = req.params + + if (!['google', 'yandex', 'custom'].includes(provider)) { + return res.status(400).send('Invalid provider') + } + + const tilePath = provider === 'custom' ? path.join(tileFolder, provider, z, x, `${y}.png`) : path.join(tileFolder, provider, z, x, `${y}.jpg`) + + if (fs.existsSync(tilePath)) { + return res.sendFile(tilePath) + } else { + if (provider !== 'custom') { + try { + const tileData = await fetchTileFromAPI(provider, z, x, y) + + fs.mkdirSync(path.dirname(tilePath), { recursive: true }) + + fs.writeFileSync(tilePath, tileData) + + res.contentType('image/jpeg') + res.send(tileData) + } catch (error) { + console.error('Error fetching tile from API:', error) + res.status(500).send('Error fetching tile from API') + } + } else { + res.status(404).send('Tile is not generated or not provided') + } + } +}) + +const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise => { + const url = provider === 'google' + ? `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}` + : `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU` + + const response = await axios.get(url, { responseType: 'arraybuffer' }) + return response.data +} + +export default router \ No newline at end of file diff --git a/ems/src/index.ts b/ems/src/index.ts index 912d007..f8d7101 100644 --- a/ems/src/index.ts +++ b/ems/src/index.ts @@ -1,401 +1,23 @@ -import express, { Request, Response } from 'express' -import { PrismaClient } from '@prisma/client' -import fs from 'fs' -import path from 'path' -import axios from 'axios' -import multer from 'multer' +import express from 'express' import bodyParser from 'body-parser' import cors from 'cors' -import { Coordinate } from './interfaces/map' -import { generateTilesForZoomLevel } from './utils/tiles' -import { query, validationResult } from 'express-validator' -import { Connection, ConnectionConfiguration, Request as TediousRequest } from 'tedious' +import generalRouter from './api/general' +import gisRouter from './api/gis' +import nodesRouter from './api/nodes' +import tilesRouter from './api/tiles' -const tediousConfig: ConnectionConfiguration = { - server: 'localhost', - options: { - trustServerCertificate: true, - port: 1433, - database: 'nGeneral' - }, - authentication: { - type: 'default', - options: { - userName: 'SA', - password: 'oMhthmsvbYHc' - } - } -} - -const prisma = new PrismaClient() const app = express() const PORT = process.env.EMS_PORT || 5000 -const tileFolder = path.join(__dirname, '..', 'public', 'tile_data') -const uploadDir = path.join(__dirname, '..', 'public', 'temp') - app.use(cors()) app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) -const storage = multer.diskStorage({ - destination: function (req, file, cb) { - cb(null, path.join(__dirname, '..', 'public', 'temp')) - }, - filename: function (req, file, cb) { - cb(null, Date.now() + path.extname(file.originalname)) - } -}) - -const upload = multer({ storage: storage }) - -app.get('/nodes/all', async (req: Request, res: Response) => { - try { - const nodes = await prisma.nodes.findMany() - - res.json(nodes) - } catch (error) { - console.error('Error getting node:', error); - res.status(500).json({ error: 'Failed to get node' }); - } -}) - -app.get('/nodes', query('id').isString().isUUID(), async (req: Request, res: Response) => { - try { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.send({ errors: result.array() }) - } - - const { id } = req.params - - const node = await prisma.nodes.findFirst({ - where: { - id: id - } - }) - - res.json(node) - - } catch (error) { - console.error('Error getting node:', error); - res.status(500).json({ error: 'Failed to get node' }); - } -}) - -app.post('/nodes', async (req: Request, res: Response) => { - try { - const { coordinates, object_id, type } = req.body; - - // Convert the incoming array of coordinates into the shape structure - const shape = coordinates.map((point: number[]) => ({ - object_id: object_id || null, - x: point[0], - y: point[1] - })); - - console.log(shape) - - // Create a new node in the database - const node = await prisma.nodes.create({ - data: { - object_id: object_id || null, // Nullable if object_id is not provided - shape_type: type, // You can adjust this dynamically - shape: shape, // Store the shape array as Json[] - label: 'Default' - } - }); - - res.status(201).json(node); - } catch (error) { - console.error('Error creating node:', error); - res.status(500).json({ error: 'Failed to create node' }); - } -}) - -app.post('/upload', upload.single('file'), async (req: Request, res: Response) => { - const { extentMinX, extentMinY, extentMaxX, extentMaxY, blX, blY, tlX, tlY, trX, trY, brX, brY } = req.body - - const bottomLeft: Coordinate = { x: blX, y: blY } - const topLeft: Coordinate = { x: tlX, y: tlY } - const topRight: Coordinate = { x: trX, y: trY } - const bottomRight: Coordinate = { x: brX, y: brY } - - if (req.file) { - for (let z = 0; z <= 21; z++) { - await generateTilesForZoomLevel(uploadDir, tileFolder, req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z) - } - } - - return res.status(200) -}) - -const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise => { - const url = provider === 'google' - ? `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}` - : `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU` - - const response = await axios.get(url, { responseType: 'arraybuffer' }) - return response.data -} - -app.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => { - const { provider, z, x, y } = req.params - - if (!['google', 'yandex', 'custom'].includes(provider)) { - return res.status(400).send('Invalid provider') - } - - const tilePath = provider === 'custom' ? path.join(tileFolder, provider, z, x, `${y}.png`) : path.join(tileFolder, provider, z, x, `${y}.jpg`) - - if (fs.existsSync(tilePath)) { - return res.sendFile(tilePath) - } else { - if (provider !== 'custom') { - try { - const tileData = await fetchTileFromAPI(provider, z, x, y) - - fs.mkdirSync(path.dirname(tilePath), { recursive: true }) - - fs.writeFileSync(tilePath, tileData) - - res.contentType('image/jpeg') - res.send(tileData) - } catch (error) { - console.error('Error fetching tile from API:', error) - res.status(500).send('Error fetching tile from API') - } - } else { - res.status(404).send('Tile is not generated or not provided') - } - } -}) - -function tediousQuery(query: string) { - // Read all rows from table - - - return new Promise((resolve, reject) => { - const connection = new Connection(tediousConfig) - - connection.on('connect', (err) => { - if (err) { - reject(err) - return - } - - const result: any = []; - - const request = new TediousRequest( - query, - (err, rowCount) => { - if (err) { - console.log(`Executing ${query}, ${rowCount} rows.`); - console.error(err.message); - } else { - console.log(`Executing ${query}, ${rowCount} rows.`); - } - } - ) - - request.on("row", (columns) => { - const entry: any = {}; - columns.forEach((column: any) => { - entry[column.metadata.colName] = column.value; - }); - result.push(entry); - }); - - request.on('error', error => reject(error));// some error happened, reject the promise - request.on('requestCompleted', () => { - connection.close(); - resolve(result) - }); // resolve the promise with the result rows. - - connection.execSql(request) - }) - - connection.on('error', (err) => { - reject(err) - }) - - connection.connect() - }); -} - -app.get('/general/cities/all', async (req: Request, res: Response) => { - try { - const { offset, limit, search, id } = req.query - - const result = await tediousQuery( - ` - SELECT * FROM nGeneral..Cities - ${id ? `WHERE id = '${id}'` : ''} - ${search ? `WHERE name LIKE '%${search || ''}%'` : ''} - ORDER BY id - OFFSET ${Number(offset) || 0} ROWS - FETCH NEXT ${Number(limit) || 10} ROWS ONLY; - ` - ) - res.status(200).json(result) - } catch (err) { - res.status(500) - } -}) - -app.get('/general/types/all', async (req: Request, res: Response) => { - try { - const result = await tediousQuery( - ` - SELECT * FROM nGeneral..tTypes - ORDER BY id - ` - ) - res.status(200).json(result) - } catch (err) { - res.status(500) - } -}) - -app.get('/general/objects/all', async (req: Request, res: Response) => { - try { - const { offset, limit, city_id } = req.query - - const result = await tediousQuery( - ` - SELECT * FROM nGeneral..vObjects - ${city_id ? `WHERE id_city = ${city_id}` : ''} - 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) - } -}) - -app.get('/general/objects/:id([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})', async (req: Request, res: Response) => { - try { - const { id } = req.params; - - const result = await tediousQuery( - ` - SELECT * FROM nGeneral..vObjects - ${id ? `WHERE object_id = '${id}'` : ''} - ` - ) - if (Array.isArray(result) && result.length > 0) { - res.status(200).json(result[0]) - } - } catch (err) { - res.status(500) - } -}) - -app.get('/general/values/all', async (req: Request, res: Response) => { - try { - const { object_id } = req.query - - if (!object_id) { - res.status(500) - } - - const result = await tediousQuery( - ` - SELECT * FROM nGeneral..tValues - WHERE id_object = '${object_id}' - ` - ) - - res.status(200).json(result) - } catch (err) { - res.status(500) - } -}) - -app.get('/general/params/all', async (req: Request, res: Response) => { - try { - const { param_id } = req.query - - if (!param_id) { - res.status(500) - } - - const result = await tediousQuery( - ` - SELECT * FROM nGeneral..tParameters - WHERE id = '${param_id}' - ` - ) - - res.status(200).json(result) - } catch (err) { - res.status(500) - } -}) - -app.get('/gis/images/all', async (req: Request, res: Response) => { - try { - const { offset, limit, city_id } = req.query - - const result = await tediousQuery( - ` - SELECT * FROM New_Gis..images - ${city_id ? `WHERE city_id = ${city_id}` : ''} - ORDER BY city_id - OFFSET ${Number(offset) || 0} ROWS - FETCH NEXT ${Number(limit) || 10} ROWS ONLY; - ` - ) - res.status(200).json(result) - } catch (err) { - res.status(500) - } -}) - - -// Get figures by year and city id -app.get('/gis/figures/all', async (req: Request, res: Response) => { - try { - const { offset, limit, object_id, year, city_id } = req.query - - const result = await tediousQuery( - ` - SELECT * FROM New_Gis..figures f - JOIN nGeneral..tObjects o ON f.object_id = o.id WHERE o.id_city = ${city_id} AND f.year = ${year} - ORDER BY f.year - OFFSET ${Number(offset) || 0} ROWS - FETCH NEXT ${Number(limit) || 10} ROWS ONLY; - ` - ) - res.status(200).json(result) - } catch (err) { - res.status(500) - } -}) - -// Get lines by year and city id -app.get('/gis/lines/all', async (req: Request, res: Response) => { - try { - const { offset, limit, object_id, year, city_id } = req.query - - const result = await tediousQuery( - ` - SELECT * FROM New_Gis..lines l - JOIN nGeneral..tObjects o ON l.object_id = o.id WHERE o.id_city = ${city_id} AND l.year = ${year} - ORDER BY l.year - OFFSET ${Number(offset) || 0} ROWS - FETCH NEXT ${Number(limit) || 10} ROWS ONLY; - ` - ) - res.status(200).json(result) - } catch (err) { - res.status(500) - } -}) +app.use('/general', generalRouter) +app.use('/gis', gisRouter) +app.use('/nodes', nodesRouter) +app.use('/tiles', tilesRouter) app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); \ No newline at end of file diff --git a/ems/src/utils/tedious.ts b/ems/src/utils/tedious.ts new file mode 100644 index 0000000..6dc2704 --- /dev/null +++ b/ems/src/utils/tedious.ts @@ -0,0 +1,69 @@ +import { Connection, ConnectionConfiguration, Request } from "tedious"; + +const tediousConfig: ConnectionConfiguration = { + server: 'localhost', + options: { + trustServerCertificate: true, + port: 1433, + database: 'nGeneral' + }, + authentication: { + type: 'default', + options: { + userName: 'SA', + password: 'oMhthmsvbYHc' + } + } +} + +export function tediousQuery(query: string) { + // Read all rows from table + + + return new Promise((resolve, reject) => { + const connection = new Connection(tediousConfig) + + connection.on('connect', (err) => { + if (err) { + reject(err) + return + } + + const result: any = []; + + const request = new Request( + query, + (err, rowCount) => { + if (err) { + console.log(`Executing ${query}, ${rowCount} rows.`); + console.error(err.message); + } else { + console.log(`Executing ${query}, ${rowCount} rows.`); + } + } + ) + + request.on("row", (columns) => { + const entry: any = {}; + columns.forEach((column: any) => { + entry[column.metadata.colName] = column.value; + }); + result.push(entry); + }); + + request.on('error', error => reject(error));// some error happened, reject the promise + request.on('requestCompleted', () => { + connection.close(); + resolve(result) + }); // resolve the promise with the result rows. + + connection.execSql(request) + }) + + connection.on('error', (err) => { + reject(err) + }) + + connection.connect() + }); +} \ No newline at end of file