Better map

This commit is contained in:
cracklesparkle
2024-10-29 15:08:23 +09:00
parent 115c6ec417
commit f51835584d
20 changed files with 685 additions and 444 deletions

View File

@ -17,9 +17,8 @@ import Settings from "./pages/Settings"
import PasswordReset from "./pages/auth/PasswordReset"
import MapTest from "./pages/MapTest"
import MonitorPage from "./pages/MonitorPage"
import ChunkedUpload from "./components/map/ChunkedUpload"
import DashboardLayout from "./layouts/DashboardLayout"
import { IconApi, IconBuildingFactory2, IconDeviceDesktopAnalytics, IconFiles, IconFlag2, IconHome, IconLogin, IconLogin2, IconMap, IconPassword, IconReport, IconServer, IconSettings, IconShield, IconTable, IconUsers } from "@tabler/icons-react"
import { IconApi, IconBuildingFactory2, 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"
@ -142,15 +141,6 @@ export const pages = [
dashboard: true,
enabled: true,
},
{
label: "Chunk test",
path: "/chunk-test",
icon: <IconFlag2 />,
component: <ChunkedUpload />,
drawer: true,
dashboard: true,
enabled: false,
},
{
label: "Монитор",
path: "/monitor",

View File

@ -1,4 +1,4 @@
import { Button, Flex, Input, Table } from '@mantine/core';
import { Input, Table } from '@mantine/core';
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { useMemo, useState } from 'react';
import styles from './CustomTable.module.scss'

View File

@ -6,7 +6,7 @@ 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, Table, Text } from '@mantine/core'
import { ActionIcon, Anchor, Breadcrumbs, Button, FileButton, Flex, Loader, RingProgress, ScrollAreaAutosize, Table, Text } from '@mantine/core'
import { IconCancel, IconDownload, IconFile, IconFilePlus, IconFileUpload, IconX } from '@tabler/icons-react'
interface FolderProps {
@ -176,12 +176,7 @@ export default function FolderViewer() {
}
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
p: '16px'
}}>
<ScrollAreaAutosize w={'100%'} h={'100%'} p={'sm'}>
<FileViewer
open={fileViewerModal}
setOpen={setFileViewerModal}
@ -313,6 +308,6 @@ export default function FolderViewer() {
)}
</Table.Tbody>
</Table>
</Box>
</ScrollAreaAutosize>
)
}

View File

@ -1,9 +1,8 @@
import { AppBar, Box, CircularProgress, Dialog, Grid, IconButton, TextField, Toolbar } from '@mui/material'
import { Fragment, useState } from 'react'
import { AppBar, Box, Dialog, Grid, IconButton, Toolbar } from '@mui/material'
import { useState } from 'react'
import { IRegion } from '../interfaces/fuel'
import { useRegions, useServers, useServersInfo } from '../hooks/swrHooks'
import ServerService from '../services/ServersService'
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'
import { GridColDef } from '@mui/x-data-grid'
import { Close, Cloud, CloudOff } from '@mui/icons-material'
import ServerData from './ServerData'
import { IServersInfo } from '../interfaces/servers'
@ -20,14 +19,14 @@ export default function ServersView() {
const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { regions, isLoading } = useRegions(10, 1, debouncedSearch)
const { regions } = useRegions(10, 1, debouncedSearch)
const { serversInfo } = useServersInfo(selectedOption)
const [serverDataOpen, setServerDataOpen] = useState(false)
const [currentServerData, setCurrentServerData] = useState<any | null>(null)
const { servers, isLoading: serversLoading } = useServers(selectedOption, 0, 10)
const { servers } = useServers(selectedOption, 0, 10)
const serversColumns: GridColDef[] = [
//{ field: 'id', headerName: 'ID', type: "number" },

View File

@ -1,58 +0,0 @@
import React, { useState } from 'react';
import axios from 'axios';
const ChunkedUpload = () => {
const [file, setFile] = useState<File | null>(null);
const [uploadProgress, setUploadProgress] = useState<number>(0);
// Handle file selection
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
setFile(event.target.files[0]);
}
};
// Upload the file in chunks
const uploadFile = async () => {
if (!file) return;
const chunkSize = 1024 * 1024; // 1MB per chunk
const totalChunks = Math.ceil(file.size / chunkSize);
const fileId = `${file.name}-${Date.now()}`; // Unique file identifier
let uploadedChunks = 0;
for (let start = 0; start < file.size; start += chunkSize) {
const chunk = file.slice(start, start + chunkSize);
const chunkNumber = Math.ceil(start / chunkSize) + 1;
try {
await axios.post(`${import.meta.env.VITE_API_EMS_URL}/upload`, chunk, {
headers: {
'Content-Type': 'application/octet-stream',
'X-Chunk-Number': chunkNumber.toString(),
'X-Total-Chunks': totalChunks.toString(),
'X-File-Id': fileId,
},
});
uploadedChunks++;
setUploadProgress((uploadedChunks / totalChunks) * 100);
} catch (error) {
console.error('Chunk upload failed', error);
// Implement retry logic if needed
break;
}
}
};
return (
<div>
<input type="file" onChange={handleFileChange} />
<button onClick={uploadFile} disabled={!file}>
Upload File
</button>
<div>Upload Progress: {uploadProgress.toFixed(2)}%</div>
</div>
);
};
export default ChunkedUpload;

View File

@ -6,7 +6,6 @@ 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 { Stack, Typography } from '@mui/material'
import { Type } from 'ol/geom/Geometry'
import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
import Feature from 'ol/Feature'
@ -24,27 +23,30 @@ import { Coordinate } from 'ol/coordinate'
import { Stroke, Fill, Circle as CircleStyle, Style, Text } from 'ol/style'
import { calculateCenter, calculateExtent, calculateRotationAngle, rotateProjection } from './mapUtils'
import MapBrowserEvent from 'ol/MapBrowserEvent'
import { fromLonLat, get, transform } from 'ol/proj'
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, Box, Button, Flex, Select as MantineSelect, MantineStyleProp, rem, Slider, useMantineColorScheme } from '@mantine/core'
import { IconApi, IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPlus, IconPoint, IconPolygon, IconRuler, IconTable, IconUpload } from '@tabler/icons-react'
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 { getGridCellPosition } from './mapUtils'
import { IFigure, ILine } from '../../interfaces/gis'
import { Height } from '@mui/icons-material'
import axios from 'axios'
import ObjectParameter from './ObjectParameter'
import { IObjectData, IObjectParam } from '../../interfaces/objects'
import ObjectData from './ObjectData'
const MapComponent = () => {
const { cities } = useCities(100, 1)
//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])
// 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<Coordinate | null>(null)
const [currentZ, setCurrentZ] = useState<number | undefined>(undefined)
@ -104,6 +106,10 @@ const MapComponent = () => {
source: new VectorSource()
}))
const linesLayer = useRef<VectorLayer>(new VectorLayer({
source: new VectorSource()
}))
const regionsLayer = useRef<VectorImageLayer>(new VectorImageLayer({
source: regionsLayerSource,
style: regionsLayerStyle
@ -576,7 +582,7 @@ const MapComponent = () => {
map.current = new Map({
controls: [],
layers: [baseLayer.current, satLayer.current, regionsLayer.current, citiesLayer.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],
target: mapElement.current as HTMLDivElement,
view: new View({
center: transform([129.7659541, 62.009504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]),
@ -596,6 +602,14 @@ const MapComponent = () => {
setCurrentY(tileY)
})
map.current.on('click', function (e: MapBrowserEvent<any>) {
const pixel = map.current?.getEventPixel(e.originalEvent)
map.current?.forEachFeatureAtPixel(pixel, function (feature) {
setCurrentObjectId(feature.get('object_id'))
})
})
const modify = new Modify({ source: drawingLayerSource.current })
map.current.addInteraction(modify)
@ -842,291 +856,407 @@ const MapComponent = () => {
];
}
const [currentObjectId, setCurrentObjectId] = useState<string | null>(null)
const [selectedCity, setSelectedCity] = useState<number | null>(null)
const [selectedYear, setSelectedYear] = useState<number | null>(2023)
const [citiesPage, setCitiesPage] = useState<number>(0)
const [searchCity, setSearchCity] = useState<string | undefined>("")
const { data: currentObjectData } = useSWR(
currentObjectId ? `/general/objects/${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: valuesData } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: citiesData } = useSWR(
`/general/cities/all?limit=${10}&offset=${citiesPage || 0}${searchCity ? `&search=${searchCity}` : ''}`,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: figuresData, isValidating: figuresValidating } = useSWR(
selectedCity && selectedYear ? `/gis/figures/all?city_id=${selectedCity}&year=${selectedYear}&offset=0&limit=${10000}` : null,
(url) => axios.get(url, {
baseURL: BASE_URL.ems
}).then((res) => res.data),
{
revalidateOnFocus: false
}
)
const { data: linesData } = useSWR(
!figuresValidating && selectedCity && selectedYear ? `/gis/lines/all?city_id=${selectedCity}&year=${selectedYear}&offset=0&limit=${10000}` : null,
(url) => axios.get(url, {
baseURL: BASE_URL.ems
}).then((res) => {
return res.data
}),
{
revalidateOnFocus: false
}
)
useEffect(() => {
if (Array.isArray(figuresData)) {
figuresLayer.current.getSource()?.clear()
if (figuresData.length > 0) {
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
}
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('object_id', figure.object_id)
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.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('angle', figure.angle)
feature1.setStyle(fourthStyleFunction(feature1))
figuresLayer.current?.getSource()?.addFeature(feature1)
}
})
}
}
if (Array.isArray(linesData)) {
linesLayer.current.getSource()?.clear()
if (linesData.length > 0) {
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
}
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('object_id', line.object_id)
linesLayer.current?.getSource()?.addFeature(feature)
})
}
}
}, [figuresData, linesData, selectedCity, selectedYear])
return (
<Box w={'100%'} h={'calc(100% - 64px)'} mah={'100%'} flex={'1'} pos={'relative'}>
<ActionIcon.Group orientation='vertical' pos='absolute' top='8px' right='8px' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
<ActionIcon size='lg' variant='transparent' onClick={() => {
fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res))
}}>
<IconApi />
</ActionIcon>
<Box w={'100%'} h='100%' pos={'relative'}>
<Portal target='#header-portal'>
<Flex gap={'sm'} direction={'row'}>
<form>
<Autocomplete
placeholder="Район"
flex={'1'}
data={citiesData ? citiesData.map((item: any) => ({ label: item.name, value: item.id.toString() })) : []}
//onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearchCity(value)}
onOptionSubmit={(value) => setSelectedCity(Number(value))}
rightSection={
searchCity !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearchCity('')
setSelectedCity(null)
}}
aria-label="Clear value"
/>
)
}
value={searchCity}
/>
</form>
<ActionIcon size='lg' variant='transparent' onClick={() => {
saveFeatures()
}}>
<IconExclamationCircle />
</ActionIcon>
<MantineSelect
data={[
{ label: '2018', value: '2018' },
{ label: '2019', value: '2019' },
{ label: '2020', value: '2020' },
{ label: '2021', value: '2021' },
{ label: '2022', value: '2022' },
{ label: '2023', value: '2023' },
{ label: '2024', value: '2024' },
]}
onChange={(e) => {
setSelectedYear(Number(e))
}}
defaultValue={selectedYear?.toString()}
//variant="unstyled"
allowDeselect={false}
/>
<ActionIcon size='lg' variant='transparent' onClick={() => {
draw.current?.removeLastPoint()
}}>
<IconArrowBackUp />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Point' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Point')
}}>
<IconPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'LineString' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('LineString')
}}>
<IconLine />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Polygon' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Polygon')
}}>
<IconPolygon />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Circle' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Circle')
}}>
<IconCircle />
</ActionIcon>
<ActionIcon
size='lg'
variant='transparent'
onClick={() => map?.current?.addInteraction(new Translate())}
>
<IconArrowsMove />
</ActionIcon>
<ActionIcon
size='lg'
variant='transparent'
>
<IconRuler />
</ActionIcon>
</ActionIcon.Group>
<Flex direction='column' style={{
...mapControlsStyle,
maxWidth: '300px',
width: '100%',
top: '8px',
left: '8px'
}}>
<Flex direction='row'>
<ActionIcon
size='lg'
variant='transparent'
onClick={() => submitOverlay()}
title='Настройки ИКС'
>
<IconUpload style={{ width: rem(20), height: rem(20) }} />
<IconSettings style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
</Flex>
</Portal>
<Flex w={'100%'} h={'100%'} pos={'absolute'}>
<ActionIcon.Group orientation='vertical' pos='absolute' top='8px' right='8px' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
<ActionIcon size='lg' variant='transparent' onClick={() => {
fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res))
}}>
<IconApi />
</ActionIcon>
<ActionIcon size='lg' variant='transparent' onClick={() => {
saveFeatures()
}}>
<IconExclamationCircle />
</ActionIcon>
<ActionIcon size='lg' variant='transparent' onClick={() => {
draw.current?.removeLastPoint()
}}>
<IconArrowBackUp />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Point' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Point')
}}>
<IconPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'LineString' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('LineString')
}}>
<IconLine />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Polygon' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Polygon')
}}>
<IconPolygon />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Circle' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Circle')
}}>
<IconCircle />
</ActionIcon>
<ActionIcon
size='lg'
variant='transparent'
title='Добавить подложку'
onClick={() => map?.current?.addInteraction(new Translate())}
>
<IconPlus style={{ width: rem(20), height: rem(20) }} />
<IconArrowsMove />
</ActionIcon>
</Flex>
<Flex align='center' direction='row' p='sm' gap='sm'>
<Slider w='100%' min={0} max={1} step={0.001} value={satelliteOpacity} defaultValue={satelliteOpacity} onChange={(value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
<ActionIcon
size='lg'
variant='transparent'
>
<IconRuler />
</ActionIcon>
</ActionIcon.Group>
<MantineSelect variant='filled' value={satMapsProvider} data={[{ label: 'Google', value: 'google' }, { label: 'Yandex', value: 'yandex' }, { label: 'Custom', value: 'custom' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} />
</Flex>
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
<Accordion.Item key={'s'} value={'Объекты'}>
<Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
<Accordion.Panel>
<Button
onClick={async () => {
//open()
const city_id = 145
const year = 2023
const figuresLimit = 10000
const linesLimit = 10000
figuresLayer.current.getSource()?.clear()
try {
// const response = await fetch(`${import.meta.env.VITE_API_EMS_URL}/gis/images/all?city_id=${city_id}`, {
// method: 'GET',
// headers: {
// 'Content-Type': 'application/json',
// },
// })
//const responseData = await response.json()
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
}
const figuresResponse = await fetch(`${import.meta.env.VITE_API_EMS_URL}/gis/figures/all?city_id=${city_id}&year=${year}&offset=0&limit=${figuresLimit}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const linesResponse = await fetch(`${import.meta.env.VITE_API_EMS_URL}/gis/lines/all?city_id=${city_id}&year=${year}&offset=0&limit=${linesLimit}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
await linesResponse.json().then(linesData => {
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('object_id', line.object_id)
figuresLayer.current?.getSource()?.addFeature(feature)
})
})
await figuresResponse.json().then(figuresData => {
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('object_id', figure.object_id)
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.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('angle', figure.angle)
feature1.setStyle(fourthStyleFunction(feature1))
figuresLayer.current?.getSource()?.addFeature(feature1)
}
})
})
} catch (err) {
console.error('No data')
}
}}
<Flex direction='column' mah={'86%'} pl='sm' style={{
...mapControlsStyle,
maxWidth: '300px',
width: '100%',
top: '8px',
left: '8px',
}}>
<ScrollAreaAutosize offsetScrollbars>
<Flex direction='row'>
<ActionIcon
size='lg'
variant='transparent'
onClick={() => submitOverlay()}
>
Test
</Button>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Flex>
<IconUpload style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
<Flex direction='row' pos='absolute' bottom='8px' left='8px' style={{ ...mapControlsStyle }}>
<Stack>
<Typography>
<ActionIcon
size='lg'
variant='transparent'
title='Добавить подложку'
>
<IconPlus style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
</Flex>
<Flex align='center' direction='row' p='sm' gap='sm'>
<Slider w='100%' min={0} max={1} step={0.001} value={satelliteOpacity} defaultValue={satelliteOpacity} onChange={(value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
<MantineSelect variant='filled' value={satMapsProvider} data={[{ label: 'Google', value: 'google' }, { label: 'Yandex', value: 'yandex' }, { label: 'Custom', value: 'custom' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} />
</Flex>
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
<Accordion.Item key={'objects'} value={'Объекты'}>
<Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
<Accordion.Panel>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item key={'current_object'} value='current_object'>
<Accordion.Control icon={<IconTable />}>
{'Текущий объект'}
</Accordion.Control>
<Accordion.Panel>
<ObjectData {...currentObjectData as IObjectData} />
</Accordion.Panel>
</Accordion.Item>
{valuesData && <Accordion.Item key={'parameters'} value={'Параметры объекта'}>
<Accordion.Control icon={<IconTable />}>{'Параметры объекта'}</Accordion.Control>
<Accordion.Panel>
<Flex gap={'sm'} direction={'column'}>
{Array.isArray(valuesData) && valuesData.map((param: IObjectParam) => {
return (
<ObjectParameter key={param.id_param} value={param.value} id_param={param.id_param} />
)
})}
</Flex>
</Accordion.Panel>
</Accordion.Item>}
</Accordion>
</ScrollAreaAutosize>
</Flex>
<Flex gap='sm' p={'4px'} miw={'100%'} fz={'xs'} pos='absolute' bottom='0px' left='0px' style={{ ...mapControlsStyle, borderRadius: 0 }}>
<MantineText fz='xs' w={rem(130)}>
x: {currentCoordinate?.[0]}
</Typography>
<Typography>
</MantineText>
<MantineText fz='xs' w={rem(130)}>
y: {currentCoordinate?.[1]}
</Typography>
</Stack>
</MantineText>
<Typography>
Z={currentZ}
X={currentX}
Y={currentY}
</Typography>
<Divider orientation='vertical' />
<MantineText fz='xs'>
Z={currentZ}
</MantineText>
<MantineText fz='xs'>
X={currentX}
</MantineText>
<MantineText fz='xs'>
Y={currentY}
</MantineText>
<MantineText fz='xs' ml='auto'>
{statusText}
</MantineText>
</Flex>
</Flex>
<Flex direction='row' style={{ ...mapControlsStyle, bottom: '8px', right: '8px' }}>
{statusText}
</Flex>
<div
id="map-container"

View File

@ -0,0 +1,25 @@
import { Flex, Table } from '@mantine/core'
import { IObjectData, IObjectType } from '../../interfaces/objects'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
const ObjectData = (object_data: IObjectData) => {
const { data: typeData } = useSWR(
object_data.type ? `/general/types/all` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
return (
<Flex gap='sm' direction='column'>
{Array.isArray(typeData) && (typeData.find(type => Number(type.id) === Number(object_data.type)) as IObjectType).name}
</Flex>
)
}
export default ObjectData

View File

@ -0,0 +1,51 @@
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 { IObjectParam, IParam } from '../../interfaces/objects'
const ObjectParameter = ({
id_param,
value
}: IObjectParam) => {
const { data: paramData } = useSWR(
`/general/params/all?param_id=${id_param}`,
(url) => fetcher(url, BASE_URL.ems).then(res => res[0] as IParam),
{
revalidateOnFocus: false
}
)
const Parameter = (type: string, name: string, value: unknown) => {
switch (type) {
case 'bit':
return (
<Grid align='center' gutter='xl'>
<Grid.Col span={1}>
<Checkbox defaultChecked={value as boolean} />
</Grid.Col>
<Grid.Col span={'auto'}>
<p>{name}</p>
</Grid.Col>
</Grid>
)
default:
return (
<div>
Неподдерживаемый параметр
</div>
)
}
}
return (
<div>
{paramData &&
Parameter(paramData.format, paramData.name, value)
}
</div>
)
}
export default ObjectParameter

View File

@ -0,0 +1,38 @@
export interface IObjectData {
object_id: string,
id_city: number,
year: number,
id_parent: number | null,
type: number,
planning: boolean,
activity: boolean,
kvr: string | null,
jur: string | null,
fuel: string | null,
boiler_id: string | null
}
export interface IObjectParam {
id_object: string,
id_param: number,
value: string,
date_s: string | null,
date_po: string | null,
id_user: number
}
export interface IParam {
id: number,
id_group: number | null,
name: string,
format: string,
vtable: string,
unit: string | null,
exact_format: string | null,
inHistory: string | null
}
export interface IObjectType {
id: number,
name: string
}

View File

@ -49,10 +49,14 @@ function DashboardLayout() {
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
</Group>
<Group w='100%'>
<Group w='auto'>
{getPageTitle()}
</Group>
<Group id='header-portal' w='auto' ml='auto'>
</Group>
<Group style={{ flexShrink: 0 }}>
<Menu
width={260}
@ -111,12 +115,17 @@ function DashboardLayout() {
label={item.label}
leftSection={item.icon}
active={location.pathname === item.path}
style={{textWrap: 'nowrap'}}
style={{ textWrap: 'nowrap' }}
/>
))}
</AppShell.Navbar>
<AppShell.Main>
<Outlet />
<Flex w={{
sm: desktopOpened ? 'calc(100% - 200px)' : 'calc(100% - 50px)',
base: '100%'
}} h={'calc(100% - 60px)'} style={{ transition: "width 0.2s ease" }} pos={'fixed'}>
<Outlet />
</Flex>
</AppShell.Main>
</AppShell>
)

View File

@ -1,7 +1,7 @@
import { GridColDef } from '@mui/x-data-grid'
import { useEffect, useState } from 'react'
import { useBoilers } from '../hooks/swrHooks'
import { Badge, Flex, Table, Text } from '@mantine/core'
import { Badge, ScrollAreaAutosize, Table, Text } from '@mantine/core'
function Boilers() {
const [boilersPage, setBoilersPage] = useState(1)
@ -33,7 +33,7 @@ function Boilers() {
]
return (
<Flex direction='column' gap='sm' p='sm'>
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Text size="xl" fw={600}>
Котельные
</Text>
@ -78,7 +78,7 @@ function Boilers() {
</Table.Tbody>
</Table>
}
</Flex>
</ScrollAreaAutosize>
)
}

View File

@ -34,7 +34,7 @@ export default function Main() {
}
return (
<Flex direction='column' gap='sm' p='sm'>
<Flex w={'100%'} h={'100%'} direction='column' gap='sm' p='sm'>
<Text size="xl" fw={700}>
Главная
</Text>

View File

@ -3,14 +3,13 @@ import { IconError404 } from "@tabler/icons-react";
export default function NotFound() {
return (
<Flex p='sm' gap='sm' align='center' justify='center'>
<Flex w={'100%'} h={'100%'} p='sm' gap='sm' align='center' justify='center'>
<Flex direction='column' gap='sm' align='center'>
<IconError404 size={100} />
<Text size="xl" fw={500} ta='center'>
Запрашиваемая страница не найдена.
</Text>
</Flex>
</Flex>
)
}

View File

@ -3,7 +3,7 @@ import { useCities, useReport, useReportExport } from "../hooks/swrHooks"
import { useDebounce } from "@uidotdev/usehooks"
import { ICity } from "../interfaces/fuel"
import { mutate } from "swr"
import { ActionIcon, Autocomplete, Badge, Button, CloseButton, Flex, Table } from "@mantine/core"
import { ActionIcon, Autocomplete, Badge, Button, CloseButton, Flex, ScrollAreaAutosize, Table } from "@mantine/core"
import { IconRefresh } from "@tabler/icons-react"
export default function Reports() {
@ -40,7 +40,7 @@ export default function Reports() {
}
return (
<Flex direction='column' gap='sm' p='sm'>
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Flex component="form" gap={'sm'}>
{/* <SearchableSelect /> */}
<Autocomplete
@ -201,6 +201,6 @@ export default function Reports() {
onProcessRowUpdateError={() => {
}}
/> */}
</Flex>
</ScrollAreaAutosize>
)
}

View File

@ -3,7 +3,7 @@ import { useRoles } from '../hooks/swrHooks'
import { CreateField } from '../interfaces/create'
import RoleService from '../services/RoleService'
import FormFields from '../components/FormFields'
import { Button, Flex, Loader, Modal, Table } from '@mantine/core'
import { Button, Loader, Modal, ScrollAreaAutosize, Table } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
export default function Roles() {
@ -26,7 +26,7 @@ export default function Roles() {
if (isLoading) return <Loader />
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Button onClick={open}>
Добавить роль
</Button>
@ -78,6 +78,6 @@ export default function Roles() {
onProcessRowUpdateError={() => {
}}
/> */}
</Flex>
</ScrollAreaAutosize>
)
}

View File

@ -3,13 +3,13 @@ import ServersView from "../components/ServersView"
import ServerIpsView from "../components/ServerIpsView"
import ServerHardware from "../components/ServerHardware"
import ServerStorage from "../components/ServerStorages"
import { Flex, Tabs } from "@mantine/core"
import { Flex, ScrollAreaAutosize, Tabs } from "@mantine/core"
export default function Servers() {
const [currentTab, setCurrentTab] = useState<string | null>('0')
return (
<Flex gap='sm' p='sm' direction='column'>
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Flex gap='sm' direction='column'>
<Tabs value={currentTab} onChange={setCurrentTab}>
<Tabs.List>
@ -43,6 +43,6 @@ export default function Servers() {
width={500}
height={300}
/> */}
</Flex>
</ScrollAreaAutosize>
)
}

View File

@ -5,7 +5,7 @@ import { CreateField } from "../interfaces/create"
import { IUser } from "../interfaces/user"
import FormFields from "../components/FormFields"
import AuthService from "../services/AuthService"
import { Flex } from "@mantine/core"
import { Flex, ScrollAreaAutosize } from "@mantine/core"
export default function Settings() {
const { token } = useAuthStore()
@ -39,10 +39,9 @@ export default function Settings() {
]
return (
<Flex
direction='column'
align='flex-start'
gap='sm'
<ScrollAreaAutosize
w={'100%'}
h={'100%'}
p='sm'
>
{currentUser &&
@ -64,6 +63,6 @@ export default function Settings() {
/>
</Flex>
}
</Flex>
</ScrollAreaAutosize>
)
}

View File

@ -1,4 +1,3 @@
import React from 'react'
import { Flex } from '@mantine/core';
import CustomTable from '../components/CustomTable';
@ -6,24 +5,6 @@ function TableTest() {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
{/* <Table
columns={[
{ key: 'column1', title: 'Column 1', dataType: DataType.String },
{ key: 'column2', title: 'Column 2', dataType: DataType.String },
{ key: 'column3', title: 'Column 3', dataType: DataType.String },
{ key: 'column4', title: 'Column 4', dataType: DataType.String },
]}
data={Array(100).fill(undefined).map(
(_, index) => ({
column1: `column:1 row:${index}`,
column2: `column:2 row:${index}`,
column3: `column:3 row:${index}`,
column4: `column:4 row:${index}`,
id: index,
}),
)}
rowKeyField={'id'}
/> */}
<CustomTable />
</Flex>
)

View File

@ -5,7 +5,7 @@ import { useEffect, useState } from "react"
import { CreateField } from "../interfaces/create"
import UserService from "../services/UserService"
import FormFields from "../components/FormFields"
import { Badge, Button, Flex, Loader, Modal, Pagination, Select, Table } from "@mantine/core"
import { Badge, Button, Flex, Loader, Modal, Pagination, ScrollAreaAutosize, Select, Table } from "@mantine/core"
import { useDisclosure } from "@mantine/hooks"
export default function Users() {
@ -65,7 +65,7 @@ export default function Users() {
}
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
<Button onClick={open}>
Добавить пользователя
</Button>
@ -157,6 +157,6 @@ export default function Users() {
onProcessRowUpdateError={() => {
}}
/> */}
</Flex>
</ScrollAreaAutosize>
)
}

View File

@ -9,9 +9,9 @@ import cors from 'cors'
import { Coordinate } from './interfaces/map'
import { generateTilesForZoomLevel } from './utils/tiles'
import { query, validationResult } from 'express-validator'
import { Connection, Request as TediousRequest } from 'tedious'
import { Connection, ConnectionConfiguration, Request as TediousRequest } from 'tedious'
const tedious = new Connection({
const tediousConfig: ConnectionConfiguration = {
server: 'localhost',
options: {
trustServerCertificate: true,
@ -25,17 +25,7 @@ const tedious = new Connection({
password: 'oMhthmsvbYHc'
}
}
})
tedious.on('connect', function (err) {
if (err) {
console.log('Error: ', err)
}
console.log('Connected to General')
})
tedious.connect()
}
const prisma = new PrismaClient()
const app = express()
@ -185,33 +175,53 @@ app.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => {
function tediousQuery(query: string) {
// Read all rows from table
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.`);
}
}
);
return new Promise((resolve, reject) => {
const result: any = [];
const connection = new Connection(tediousConfig)
request.on("row", (columns) => {
const entry: any = {};
columns.forEach((column: any) => {
entry[column.metadata.colName] = column.value;
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);
});
result.push(entry);
});
request.on('error', error => reject(error));// some error happened, reject the promise
request.on('requestCompleted', () => resolve(result)); // resolve the promise with the result rows.
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.
tedious.execSql(request);
connection.execSql(request)
})
connection.on('error', (err) => {
reject(err)
})
connection.connect()
});
}
@ -235,16 +245,29 @@ app.get('/general/cities/all', async (req: Request, res: Response) => {
}
})
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, id } = req.query
const { offset, limit, city_id } = req.query
const result = await tediousQuery(
`
SELECT * FROM nGeneral..tObjects
SELECT * FROM nGeneral..vObjects
${city_id ? `WHERE id_city = ${city_id}` : ''}
${id ? `WHERE id = '${id}'` : ''}
ORDER BY id
ORDER BY object_id
OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
`
@ -255,6 +278,66 @@ app.get('/general/objects/all', async (req: Request, res: Response) => {
}
})
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