Drop @mui, addded ems api

This commit is contained in:
cracklesparkle
2024-11-15 17:00:23 +09:00
parent f51835584d
commit a4513e7e7a
29 changed files with 1026 additions and 721 deletions

View File

@ -18,9 +18,10 @@ import PasswordReset from "./pages/auth/PasswordReset"
import MapTest from "./pages/MapTest" import MapTest from "./pages/MapTest"
import MonitorPage from "./pages/MonitorPage" import MonitorPage from "./pages/MonitorPage"
import DashboardLayout from "./layouts/DashboardLayout" 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 { Box, Loader } from "@mantine/core"
import TableTest from "./pages/TableTest" import TableTest from "./pages/TableTest"
import ComponentTest from "./pages/ComponentTest"
// Определение страниц с путями и компонентом для рендера // Определение страниц с путями и компонентом для рендера
export const pages = [ export const pages = [
@ -148,7 +149,7 @@ export const pages = [
component: <MonitorPage />, component: <MonitorPage />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
enabled: false, enabled: true,
}, },
{ {
label: "Table test", label: "Table test",
@ -159,6 +160,15 @@ export const pages = [
dashboard: true, dashboard: true,
enabled: true, enabled: true,
}, },
{
label: "Component test",
path: "/component-test",
icon: <IconComponents />,
component: <ComponentTest />,
drawer: true,
dashboard: true,
enabled: true,
},
] ]
function App() { function App() {

23
client/src/actions/map.ts Normal file
View File

@ -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);
}
};

View File

@ -1,4 +1,4 @@
import { Divider, Paper, Typography } from '@mui/material' import { Divider, Flex, Text } from '@mantine/core';
import { PropsWithChildren } from 'react' import { PropsWithChildren } from 'react'
interface CardInfoProps extends PropsWithChildren { interface CardInfoProps extends PropsWithChildren {
@ -10,14 +10,14 @@ export default function CardInfo({
label label
}: CardInfoProps) { }: CardInfoProps) {
return ( return (
<Paper sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}> <Flex direction='column' gap='sm' p='sm'>
<Typography fontWeight={600}> <Text fw={600}>
{label} {label}
</Typography> </Text>
<Divider /> <Divider />
{children} {children}
</Paper> </Flex>
) )
} }

View File

@ -1,4 +1,4 @@
import { Chip } from '@mui/material' import { Chip } from '@mantine/core';
import { ReactElement } from 'react' import { ReactElement } from 'react'
interface CardInfoChipProps { interface CardInfoChipProps {
@ -17,9 +17,10 @@ export default function CardInfoChip({
return ( return (
<Chip <Chip
icon={status ? iconOn : iconOff} icon={status ? iconOn : iconOff}
variant="outlined"
label={label}
color={status ? "success" : "error"} color={status ? "success" : "error"}
/> variant='outline'
>
{label}
</Chip>
) )
} }

View File

@ -1,4 +1,4 @@
import { Box, Typography } from '@mui/material' import { Flex, Text } from '@mantine/core';
interface CardInfoLabelProps { interface CardInfoLabelProps {
label: string; label: string;
value: string | number; value: string | number;
@ -9,14 +9,14 @@ export default function CardInfoLabel({
value value
}: CardInfoLabelProps) { }: CardInfoLabelProps) {
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <Flex justify='space-between' align='center'>
<Typography> <Text>
{label} {label}
</Typography> </Text>
<Typography variant="h6" fontWeight={600}> <Text fw={600}>
{value} {value}
</Typography> </Text>
</Box> </Flex>
) )
} }

View File

@ -1,12 +1,11 @@
import { useDocuments, useDownload, useFolders } from '../hooks/swrHooks' import { useDocuments, useDownload, useFolders } from '../hooks/swrHooks'
import { IDocument, IDocumentFolder } from '../interfaces/documents' import { IDocument, IDocumentFolder } from '../interfaces/documents'
import { Box, CircularProgress, Divider, SxProps } from '@mui/material'
import { Folder, InsertDriveFile } from '@mui/icons-material' import { Folder, InsertDriveFile } from '@mui/icons-material'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import DocumentService from '../services/DocumentService' import DocumentService from '../services/DocumentService'
import { mutate } from 'swr' import { mutate } from 'swr'
import FileViewer from './modals/FileViewer' 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' import { IconCancel, IconDownload, IconFile, IconFilePlus, IconFileUpload, IconX } from '@tabler/icons-react'
interface FolderProps { interface FolderProps {
@ -21,7 +20,7 @@ interface DocumentProps {
handleDocumentClick: (index: number) => void; handleDocumentClick: (index: number) => void;
} }
const FileItemStyle: SxProps = { const FileItemStyle: MantineStyleProp = {
cursor: 'pointer', cursor: 'pointer',
display: 'flex', display: 'flex',
width: '100%', width: '100%',
@ -36,13 +35,13 @@ function ItemFolder({ folder, handleFolderClick, ...props }: FolderProps) {
<Flex <Flex
onClick={() => handleFolderClick(folder)} onClick={() => handleFolderClick(folder)}
> >
<Box <Flex
sx={FileItemStyle} style={FileItemStyle}
{...props} {...props}
> >
<Folder /> <Folder />
{folder.name} {folder.name}
</Box> </Flex>
</Flex> </Flex>
) )
} }
@ -72,15 +71,15 @@ function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentPro
return ( return (
<Flex align='center'> <Flex align='center'>
<Box <Flex
sx={FileItemStyle} style={FileItemStyle}
onClick={() => handleDocumentClick(index)} onClick={() => handleDocumentClick(index)}
{...props} {...props}
> >
<InsertDriveFile /> <InsertDriveFile />
{doc.name} {doc.name}
</Box> </Flex>
<Box> <Flex>
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
if (!isLoading) { if (!isLoading) {
@ -94,7 +93,7 @@ function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentPro
<IconDownload /> <IconDownload />
} }
</ActionIcon> </ActionIcon>
</Box> </Flex>
</Flex> </Flex>
) )
} }
@ -171,7 +170,7 @@ export default function FolderViewer() {
if (foldersLoading || documentsLoading) { if (foldersLoading || documentsLoading) {
return ( return (
<CircularProgress /> <Loader />
) )
} }
@ -205,16 +204,12 @@ export default function FolderViewer() {
</Breadcrumbs> </Breadcrumbs>
{currentFolder && {currentFolder &&
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px' }}> <Flex direction='column' gap='sm'>
<Box sx={{ <Flex direction='column' gap='sm' p='sm' style={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
border: filesToUpload.length > 0 ? '1px dashed gray' : 'none', border: filesToUpload.length > 0 ? '1px dashed gray' : 'none',
borderRadius: '8px', borderRadius: '8px',
p: '16px'
}}> }}>
<Box sx={{ display: 'flex', gap: '16px' }}> <Flex gap='sm'>
<FileButton multiple onChange={handleFileInput}> <FileButton multiple onChange={handleFileInput}>
{(props) => <Button variant='filled' leftSection={isUploading ? <Loader /> : <IconFilePlus />} {...props}>Добавить</Button>} {(props) => <Button variant='filled' leftSection={isUploading ? <Loader /> : <IconFilePlus />} {...props}>Добавить</Button>}
</FileButton> </FileButton>
@ -240,7 +235,7 @@ export default function FolderViewer() {
</Button> </Button>
</> </>
} }
</Box> </Flex>
<Divider /> <Divider />
@ -264,8 +259,8 @@ export default function FolderViewer() {
))} ))}
</Flex> </Flex>
} }
</Box> </Flex>
</Box> </Flex>
} }
<Table <Table

View File

@ -1,8 +1,7 @@
import { Box } from '@mui/material'
import { IServer } from '../interfaces/servers' import { IServer } from '../interfaces/servers'
import { useServerIps } from '../hooks/swrHooks' import { useServerIps } from '../hooks/swrHooks'
import { GridColDef } from '@mui/x-data-grid' import { GridColDef } from '@mui/x-data-grid'
import { Table } from '@mantine/core' import { Flex, Table } from '@mantine/core'
function ServerData({ id }: IServer) { function ServerData({ id }: IServer) {
const { serverIps } = useServerIps(id, 0, 10) const { serverIps } = useServerIps(id, 0, 10)
@ -17,7 +16,7 @@ function ServerData({ id }: IServer) {
] ]
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', p: '16px' }}> <Flex direction='column' p='sm'>
{serverIps && {serverIps &&
// <FullFeaturedCrudGrid // <FullFeaturedCrudGrid
// initialRows={serverIps} // initialRows={serverIps}
@ -48,7 +47,7 @@ function ServerData({ id }: IServer) {
</Table.Tbody> </Table.Tbody>
</Table> </Table>
} }
</Box> </Flex>
) )
} }

View File

@ -1,8 +1,7 @@
import { AppBar, CircularProgress, Dialog, IconButton, Toolbar } from '@mui/material' import { AppBar, CircularProgress, Dialog, IconButton, Toolbar } from '@mui/material'
import { Fragment, useState } from 'react' import { useState } from 'react'
import { IRegion } from '../interfaces/fuel' import { IRegion } from '../interfaces/fuel'
import { useHardwares, useServers } from '../hooks/swrHooks' import { useHardwares, useServers } from '../hooks/swrHooks'
import ServerService from '../services/ServersService'
import { GridColDef } from '@mui/x-data-grid' import { GridColDef } from '@mui/x-data-grid'
import { Close } from '@mui/icons-material' import { Close } from '@mui/icons-material'
import ServerData from './ServerData' import ServerData from './ServerData'

View File

@ -1,5 +1,5 @@
import { AppBar, CircularProgress, Dialog, IconButton, TextField, Toolbar } from '@mui/material' import { AppBar, CircularProgress, Dialog, IconButton, Toolbar } from '@mui/material'
import { Fragment, useState } from 'react' import { useState } from 'react'
import { IRegion } from '../interfaces/fuel' import { IRegion } from '../interfaces/fuel'
import { useServerIps, useServers } from '../hooks/swrHooks' import { useServerIps, useServers } from '../hooks/swrHooks'
import ServerService from '../services/ServersService' import ServerService from '../services/ServersService'
@ -92,7 +92,7 @@ export default function ServerIpsView() {
/> />
) )
} }
//value={search} //value={search}
/> />
</form> </form>

View File

@ -9,7 +9,7 @@ import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { Type } from 'ol/geom/Geometry' import { Type } from 'ol/geom/Geometry'
import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition' import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
import Feature from 'ol/Feature' import Feature from 'ol/Feature'
import { SatelliteMapsProvider } from '../../interfaces/map' import { IGeometryType, SatelliteMapsProvider } from '../../interfaces/map'
import { containsExtent, Extent } from 'ol/extent' import { containsExtent, Extent } from 'ol/extent'
import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles' import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles'
import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources' 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 { calculateCenter, calculateExtent, calculateRotationAngle, rotateProjection } from './mapUtils'
import MapBrowserEvent from 'ol/MapBrowserEvent' import MapBrowserEvent from 'ol/MapBrowserEvent'
import { get, transform } from 'ol/proj' import { get, transform } from 'ol/proj'
import { useCities } from '../../hooks/swrHooks'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance' import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants' import { BASE_URL } from '../../constants'
import { Accordion, ActionIcon, Autocomplete, Box, Button, CloseButton, Flex, Grid, Select as MantineSelect, Text as MantineText, MantineStyleProp, rem, ScrollAreaAutosize, Slider, useMantineColorScheme, Divider, Portal } from '@mantine/core' 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, IconCircle, IconExclamationCircle, IconLine, IconPlus, IconPoint, IconPolygon, IconRuler, IconSettings, IconTable, IconUpload } from '@tabler/icons-react' import { IconApi, IconArrowBackUp, IconArrowsMove, IconChevronDown, IconCircle, IconExclamationCircle, IconLine, IconPlus, IconPoint, IconPolygon, IconRuler, IconSettings, IconTable, IconUpload } from '@tabler/icons-react'
import { getGridCellPosition } from './mapUtils' import { getGridCellPosition } from './mapUtils'
import { IFigure, ILine } from '../../interfaces/gis' import { IFigure, ILine } from '../../interfaces/gis'
import axios from 'axios' import axios from 'axios'
import ObjectParameter from './ObjectParameter' import ObjectParameter from './ObjectParameter'
import { IObjectData, IObjectParam } from '../../interfaces/objects' import { IObjectData, IObjectList, IObjectParam } from '../../interfaces/objects'
import ObjectData from './ObjectData' import ObjectData from './ObjectData'
import { uploadCoordinates } from '../../actions/map'
import MapToolbar from './MapToolbar/MapToolbar'
import MapStatusbar from './MapStatusbar/MapStatusbar'
const MapComponent = () => { 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<Coordinate | null>(null) const [currentCoordinate, setCurrentCoordinate] = useState<Coordinate | null>(null)
const [currentZ, setCurrentZ] = useState<number | undefined>(undefined) const [currentZ, setCurrentZ] = useState<number | undefined>(undefined)
const [currentX, setCurrentX] = useState<number | undefined>(undefined) const [currentX, setCurrentX] = useState<number | undefined>(undefined)
const [currentY, setCurrentY] = useState<number | undefined>(undefined) const [currentY, setCurrentY] = useState<number | undefined>(undefined)
const [file, setFile] = useState(null) const [file, setFile] = useState<File | null>(null)
const [polygonExtent, setPolygonExtent] = useState<Extent | undefined>(undefined) const [polygonExtent, setPolygonExtent] = useState<Extent | undefined>(undefined)
const [bottomLeft, setBottomLeft] = useState<Coordinate | undefined>(undefined) const [bottomLeft, setBottomLeft] = useState<Coordinate | undefined>(undefined)
const [topLeft, setTopLeft] = useState<Coordinate | undefined>(undefined) const [topLeft, setTopLeft] = useState<Coordinate | undefined>(undefined)
@ -70,7 +62,7 @@ const MapComponent = () => {
const gMapsSatSource = useRef<XYZ>(googleMapsSatelliteSource) const gMapsSatSource = useRef<XYZ>(googleMapsSatelliteSource)
const customMapSource = useRef<XYZ>(new XYZ({ const customMapSource = useRef<XYZ>(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' attributions: 'Custom map data'
})) }))
@ -133,7 +125,7 @@ const MapComponent = () => {
draw.current.on('drawend', function (s) { draw.current.on('drawend', function (s) {
console.log(s.feature.getGeometry()?.getType()) console.log(s.feature.getGeometry()?.getType())
let type = 'POLYGON' let type: IGeometryType = 'POLYGON'
switch (s.feature.getGeometry()?.getType()) { switch (s.feature.getGeometry()?.getType()) {
case 'LineString': case 'LineString':
@ -146,7 +138,7 @@ const MapComponent = () => {
type = 'POLYGON' type = 'POLYGON'
break break
} }
const coordinates = (s.feature.getGeometry() as SimpleGeometry).getCoordinates() const coordinates = (s.feature.getGeometry() as SimpleGeometry).getCoordinates() as Coordinate[]
uploadCoordinates(coordinates, type) uploadCoordinates(coordinates, type)
}) })
@ -222,10 +214,12 @@ const MapComponent = () => {
}); });
// tile processing // tile processing
const handleImageDrop = useCallback((event: any) => { const handleImageDrop = useCallback((event: DragEvent) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (!event.dataTransfer?.files) return
const files = event.dataTransfer.files; const files = event.dataTransfer.files;
if (files.length > 0) { if (files.length > 0) {
const file = files[0]; const file = files[0];
@ -657,27 +651,6 @@ const MapComponent = () => {
} }
}, [currentTool]) }, [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<number>(1) const [satelliteOpacity, setSatelliteOpacity] = useState<number>(1)
const [statusText, setStatusText] = useState('') const [statusText, setStatusText] = useState('')
@ -721,7 +694,7 @@ const MapComponent = () => {
formData.append('brX', bottomRight[0].toString()) formData.append('brX', bottomRight[0].toString())
formData.append('brY', bottomRight[1].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<string | undefined>("") const [searchCity, setSearchCity] = useState<string | undefined>("")
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<TreeNodeData[] | null>(null)
const [selectedObjectList, setSelectedObjectList] = useState<number | null>(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( const { data: currentObjectData } = useSWR(
currentObjectId ? `/general/objects/${currentObjectId}` : null, currentObjectId ? `/general/objects/${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems), (url) => fetcher(url, BASE_URL.ems),
@ -939,7 +1028,9 @@ const MapComponent = () => {
const feature = new Feature(ellipseGeom) const feature = new Feature(ellipseGeom)
feature.setStyle(firstStyleFunction(feature)) feature.setStyle(firstStyleFunction(feature))
feature.set('type', figure.type)
feature.set('object_id', figure.object_id) feature.set('object_id', figure.object_id)
feature.set('planning', figure.planning)
figuresLayer.current?.getSource()?.addFeature(feature) figuresLayer.current?.getSource()?.addFeature(feature)
} }
@ -965,6 +1056,8 @@ const MapComponent = () => {
}) })
feature.set('object_id', figure.object_id) feature.set('object_id', figure.object_id)
feature.set('planning', figure.planning)
feature.set('type', figure.type)
feature.setStyle(thirdStyleFunction(feature)) feature.setStyle(thirdStyleFunction(feature))
figuresLayer.current?.getSource()?.addFeature(feature) figuresLayer.current?.getSource()?.addFeature(feature)
} }
@ -994,6 +1087,8 @@ const MapComponent = () => {
geometry1.rotate(-figure.angle * Math.PI / 180, anchor1) geometry1.rotate(-figure.angle * Math.PI / 180, anchor1)
const feature1 = new Feature(geometry1) const feature1 = new Feature(geometry1)
feature1.set('object_id', figure.object_id) feature1.set('object_id', figure.object_id)
feature1.set('planning', figure.planning)
feature1.set('type', figure.type)
feature1.set('angle', figure.angle) feature1.set('angle', figure.angle)
feature1.setStyle(fourthStyleFunction(feature1)) feature1.setStyle(fourthStyleFunction(feature1))
figuresLayer.current?.getSource()?.addFeature(feature1) figuresLayer.current?.getSource()?.addFeature(feature1)
@ -1025,6 +1120,8 @@ const MapComponent = () => {
const feature = new Feature(new LineString(testCoords)) const feature = new Feature(new LineString(testCoords))
feature.setStyle(styleFunction(feature)) feature.setStyle(styleFunction(feature))
feature.set('type', line.type)
feature.set('planning', line.planning)
feature.set('object_id', line.object_id) feature.set('object_id', line.object_id)
linesLayer.current?.getSource()?.addFeature(feature) linesLayer.current?.getSource()?.addFeature(feature)
@ -1037,6 +1134,12 @@ const MapComponent = () => {
<Box w={'100%'} h='100%' pos={'relative'}> <Box w={'100%'} h='100%' pos={'relative'}>
<Portal target='#header-portal'> <Portal target='#header-portal'>
<Flex gap={'sm'} direction={'row'}> <Flex gap={'sm'} direction={'row'}>
<Flex align='center' direction='row' 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>
<form> <form>
<Autocomplete <Autocomplete
placeholder="Район" placeholder="Район"
@ -1091,76 +1194,14 @@ const MapComponent = () => {
</Portal> </Portal>
<Flex w={'100%'} h={'100%'} pos={'absolute'}> <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' }}> <MapToolbar
<ActionIcon size='lg' variant='transparent' onClick={() => { currentTool={currentTool}
fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res)) onSave={() => saveFeatures()}
}}> onRemove={() => draw.current?.removeLastPoint()}
<IconApi /> handleToolSelect={handleToolSelect}
</ActionIcon> onMover={() => map?.current?.addInteraction(new Translate())}
colorScheme={colorScheme}
<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'
onClick={() => map?.current?.addInteraction(new Translate())}
>
<IconArrowsMove />
</ActionIcon>
<ActionIcon
size='lg'
variant='transparent'
>
<IconRuler />
</ActionIcon>
</ActionIcon.Group>
<Flex direction='column' mah={'86%'} pl='sm' style={{ <Flex direction='column' mah={'86%'} pl='sm' style={{
...mapControlsStyle, ...mapControlsStyle,
@ -1188,73 +1229,76 @@ const MapComponent = () => {
</ActionIcon> </ActionIcon>
</Flex> </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 variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
<Accordion.Item key={'objects'} value={'Объекты'}> <Accordion.Item key={'objects'} value={'Объекты'}>
<Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control> <Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
{objectsList &&
<Tree
data={objectsList}
selectOnClick
levelOffset={23}
renderNode={({ node, expanded, hasChildren, elementProps }) => (
<Group gap={6} {...elementProps} onClick={(e) => {
elementProps.onClick(e)
if (node.value !== 'existing' && node.value !== 'planning') {
setSelectedObjectList(Number(node.value))
}
}}>
{hasChildren && (
<IconChevronDown
size={18}
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
)}
<MantineText size='sm'>{node.label}</MantineText>
</Group>
)}
/>
}
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
<Accordion.Item key={'current_object'} value='current_object'> {currentObjectId &&
<Accordion.Control icon={<IconTable />}> <Accordion.Item key={'current_object'} value={currentObjectId}>
{'Текущий объект'} <Accordion.Control icon={<IconTable />}>
</Accordion.Control> {'Текущий объект'}
<Accordion.Panel> </Accordion.Control>
<ObjectData {...currentObjectData as IObjectData} /> <Accordion.Panel>
</Accordion.Panel> <ObjectData {...currentObjectData as IObjectData} />
</Accordion.Item> </Accordion.Panel>
</Accordion.Item>
}
{valuesData && <Accordion.Item key={'parameters'} value={'Параметры объекта'}> {valuesData &&
<Accordion.Control icon={<IconTable />}>{'Параметры объекта'}</Accordion.Control> <Accordion.Item key={'parameters'} value={'Параметры объекта'}>
<Accordion.Panel> <Accordion.Control icon={<IconTable />}>{'Параметры объекта'}</Accordion.Control>
<Flex gap={'sm'} direction={'column'}> <Accordion.Panel>
{Array.isArray(valuesData) && valuesData.map((param: IObjectParam) => { <Flex gap={'sm'} direction={'column'}>
return ( {Array.isArray(valuesData) && valuesData.map((param: IObjectParam) => {
<ObjectParameter key={param.id_param} value={param.value} id_param={param.id_param} /> return (
) <ObjectParameter key={param.id_param} value={param.value} id_param={param.id_param} />
})} )
</Flex> })}
</Accordion.Panel> </Flex>
</Accordion.Item>} </Accordion.Panel>
</Accordion.Item>
}
</Accordion> </Accordion>
</ScrollAreaAutosize> </ScrollAreaAutosize>
</Flex> </Flex>
<Flex gap='sm' p={'4px'} miw={'100%'} fz={'xs'} pos='absolute' bottom='0px' left='0px' style={{ ...mapControlsStyle, borderRadius: 0 }}> <MapStatusbar
<MantineText fz='xs' w={rem(130)}> mapControlsStyle={mapControlsStyle}
x: {currentCoordinate?.[0]} currentCoordinate={currentCoordinate}
</MantineText> currentX={currentX}
currentY={currentY}
<MantineText fz='xs' w={rem(130)}> currentZ={currentZ}
y: {currentCoordinate?.[1]} statusText={statusText}
</MantineText> />
<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>

View File

@ -10,12 +10,12 @@ register(proj4);
const yandexProjection = get('EPSG:3395')?.setExtent([-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]) || 'EPSG:3395' const yandexProjection = get('EPSG:3395')?.setExtent([-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]) || 'EPSG:3395'
const googleMapsSatelliteSource = new XYZ({ 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' attributions: 'Map data © Google'
}) })
const yandexMapsSatelliteSource = new XYZ({ 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', attributions: 'Map data © Yandex',
projection: yandexProjection, projection: yandexProjection,
}) })

View File

@ -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 (
<Flex gap='sm' p={'4px'} miw={'100%'} fz={'xs'} pos='absolute' bottom='0px' left='0px' style={{ ...mapControlsStyle, borderRadius: 0 }}>
<Text fz='xs' w={rem(130)}>
x: {currentCoordinate?.[0]}
</Text>
<Text fz='xs' w={rem(130)}>
y: {currentCoordinate?.[1]}
</Text>
<Divider orientation='vertical' />
<Text fz='xs'>
Z={currentZ}
</Text>
<Text fz='xs'>
X={currentX}
</Text>
<Text fz='xs'>
Y={currentY}
</Text>
<Text fz='xs' ml='auto'>
{statusText}
</Text>
</Flex>
)
}
export default MapStatusbar

View File

@ -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 (
<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={onSave}>
<IconExclamationCircle />
</ActionIcon>
<ActionIcon size='lg' variant='transparent' onClick={onRemove}>
<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={onMover}
>
<IconArrowsMove />
</ActionIcon>
<ActionIcon
size='lg'
variant='transparent'
>
<IconRuler />
</ActionIcon>
</ActionIcon.Group>
)
}
export default MapToolbar

View File

@ -1,4 +1,4 @@
import { Flex, Table } from '@mantine/core' import { Flex } from '@mantine/core'
import { IObjectData, IObjectType } from '../../interfaces/objects' import { IObjectData, IObjectType } from '../../interfaces/objects'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance' import { fetcher } from '../../http/axiosInstance'

View File

@ -1,8 +1,7 @@
import React from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance' import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants' import { BASE_URL } from '../../constants'
import { Checkbox, Flex, Grid } from '@mantine/core' import { Checkbox, Grid } from '@mantine/core'
import { IObjectParam, IParam } from '../../interfaces/objects' import { IObjectParam, IParam } from '../../interfaces/objects'
const ObjectParameter = ({ const ObjectParameter = ({

View File

@ -11,7 +11,9 @@ export interface IFigure {
label_top: number | null, label_top: number | null,
label_angle: number | null, label_angle: number | null,
label_size: number | null, label_size: number | null,
year: number year: number,
type: number,
planning: boolean
} }
export interface ILine { export interface ILine {
@ -28,5 +30,7 @@ export interface ILine {
label_sizes: string | null, label_sizes: string | null,
label_angels: string | null, label_angels: string | null,
label_positions: string | null, label_positions: string | null,
year: number year: number,
type: number,
planning: boolean
} }

View File

@ -4,3 +4,10 @@ export interface SatelliteMapsProviders {
custom: 'custom'; custom: 'custom';
} }
export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders] export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders]
export interface IGeometryTypes {
LINE: 'LINE'
POLYGON: 'POLYGON'
}
export type IGeometryType = IGeometryTypes[keyof IGeometryTypes]

View File

@ -1,3 +1,9 @@
export interface IObjectList {
id: number,
name: string,
count: number
}
export interface IObjectData { export interface IObjectData {
object_id: string, object_id: string,
id_city: number, id_city: number,

View File

@ -1,9 +1,9 @@
import { Box } from "@mui/material"
import { useCities } from "../hooks/swrHooks" import { useCities } from "../hooks/swrHooks"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { DataGrid, GridColDef } from "@mui/x-data-grid" import { DataGrid, GridColDef } from "@mui/x-data-grid"
import axiosInstance from "../http/axiosInstance" import axiosInstance from "../http/axiosInstance"
import { BASE_URL } from "../constants" import { BASE_URL } from "../constants"
import { Flex } from "@mantine/core"
export default function ApiTest() { export default function ApiTest() {
@ -36,7 +36,7 @@ export default function ApiTest() {
] ]
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}> <Flex direction='column' gap='sm' h='100%'>
<DataGrid <DataGrid
rows={cities || []} rows={cities || []}
columns={citiesColumns} columns={citiesColumns}
@ -46,6 +46,6 @@ export default function ApiTest() {
paginationModel={paginationModel} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel} onPaginationModelChange={setPaginationModel}
/> />
</Box> </Flex>
) )
} }

View File

@ -0,0 +1,12 @@
import { Flex } from '@mantine/core'
import ServerHardware from '../components/ServerHardware'
const ComponentTest = () => {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<ServerHardware />
</Flex>
)
}
export default ComponentTest

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Card, Stack } from '@mui/material'; import { Card, Flex } from '@mantine/core';
function CardComponent({ function CardComponent({
url, url,
@ -7,10 +7,10 @@ function CardComponent({
}: { url: any, is_alive: any }) { }: { url: any, is_alive: any }) {
return ( return (
<Card> <Card>
<Stack p='24px' direction='column'> <Flex p='sm' direction='column'>
<p>{url}</p> <p>{url}</p>
<p>{JSON.stringify(is_alive)}</p> <p>{JSON.stringify(is_alive)}</p>
</Stack> </Flex>
</Card> </Card>
) )
} }
@ -38,11 +38,11 @@ export default function MonitorPage() {
return ( return (
<div> <div>
<Stack direction='column' spacing={1}> <Flex direction='column' gap='sm'>
{servers.length > 0 && servers.map((server: any) => ( {servers.length > 0 && servers.map((server: any) => (
<CardComponent url={server.name} is_alive={server.status} /> <CardComponent url={server.name} is_alive={server.status} />
))} ))}
</Stack> </Flex>
</div> </div>
) )
} }

View File

@ -1,9 +1,8 @@
import { CircularProgress, Fade, Grow } from '@mui/material'
import { useState } from 'react' import { useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'; import { SubmitHandler, useForm } from 'react-hook-form';
import AuthService from '../../services/AuthService'; import AuthService from '../../services/AuthService';
import { CheckCircle } from '@mui/icons-material'; 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 { interface PasswordResetProps {
email: string; email: string;
@ -39,47 +38,54 @@ function PasswordReset() {
</Text> </Text>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
{!success && <Fade in={!success}> {!success &&
<Flex direction='column' gap={'md'}> <Transition mounted={!success} transition='fade'>
<Text> {(styles) =>
Введите адрес электронной почты, на который будут отправлены новые данные для авторизации: <Flex style={styles} direction='column' gap={'md'}>
</Text>
<TextInput
label='E-mail'
required
{...register('email', { required: 'Введите E-mail' })}
error={errors.email?.message}
/>
<Flex gap='sm'>
<Button flex={1} type="submit" disabled={isSubmitting || watch('email').length == 0} variant='filled'>
{isSubmitting ? <CircularProgress size={16} /> : 'Восстановить пароль'}
</Button>
<Button flex={1} component='a' href="/auth/signin" type="button" variant='light'>
Назад
</Button>
</Flex>
</Flex>
</Fade>}
{success &&
<Grow in={success}>
<Flex direction='column' gap='sm'>
<Flex align='center' gap='sm'>
<CheckCircle color='success' />
<Text> <Text>
На указанный адрес было отправлено письмо с новыми данными для авторизации. Введите адрес электронной почты, на который будут отправлены новые данные для авторизации:
</Text> </Text>
<TextInput
label='E-mail'
required
{...register('email', { required: 'Введите E-mail' })}
error={errors.email?.message}
/>
<Flex gap='sm'>
<Button flex={1} type="submit" disabled={isSubmitting || watch('email').length == 0} variant='filled'>
{isSubmitting ? <Loader size={16} /> : 'Восстановить пароль'}
</Button>
<Button flex={1} component='a' href="/auth/signin" type="button" variant='light'>
Назад
</Button>
</Flex>
</Flex> </Flex>
<Flex gap='sm'> }
<Button component='a' href="/auth/signin" type="button">
Войти </Transition>
</Button> }
{success &&
<Transition mounted={!success} transition='scale'>
{(styles) =>
<Flex style={styles} direction='column' gap='sm'>
<Flex align='center' gap='sm'>
<CheckCircle color='success' />
<Text>
На указанный адрес было отправлено письмо с новыми данными для авторизации.
</Text>
</Flex>
<Flex gap='sm'>
<Button component='a' href="/auth/signin" type="button">
Войти
</Button>
</Flex>
</Flex> </Flex>
</Flex> }
</Grow> </Transition>
} }
</form> </form>
</Flex> </Flex>

View File

@ -1,10 +1,10 @@
import { useForm, SubmitHandler } from 'react-hook-form'; import { useForm, SubmitHandler } from 'react-hook-form';
import { TextField, Button, Container, Typography, Box } from '@mui/material';
import UserService from '../../services/UserService'; import UserService from '../../services/UserService';
import { IUser } from '../../interfaces/user'; import { IUser } from '../../interfaces/user';
import { Button, Flex, Loader, Paper, Text, TextInput } from '@mantine/core';
const SignUp = () => { const SignUp = () => {
const { register, handleSubmit, formState: { errors } } = useForm<IUser>({ const { register, handleSubmit, formState: { errors, isValid, isSubmitting } } = useForm<IUser>({
defaultValues: { defaultValues: {
email: '', email: '',
login: '', login: '',
@ -26,77 +26,66 @@ const SignUp = () => {
}; };
return ( return (
<Container maxWidth="sm"> <Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Box my={4}> <Flex direction='column' gap='sm'>
<Typography variant="h4" component="h1" gutterBottom> <Text size="xl" fw={500}>
Регистрация Регистрация
</Typography> </Text>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<TextField <Flex direction='column' gap='sm'>
fullWidth <TextInput
margin="normal" label='Email'
label="Email" required
required {...register('email', { required: 'Email обязателен' })}
{...register('email', { required: 'Email обязателен' })} error={errors.email?.message}
error={!!errors.email} />
helperText={errors.email?.message}
/>
<TextField <TextInput
fullWidth label='Логин'
margin="normal" required
label="Логин" {...register('login', { required: 'Логин обязателен' })}
required error={errors.login?.message}
{...register('login', { required: 'Логин обязателен' })} />
error={!!errors.login}
helperText={errors.login?.message}
/>
<TextField <TextInput
fullWidth label='Телефон'
margin="normal" required
label="Телефон" {...register('phone')}
{...register('phone')} error={errors.phone?.message}
error={!!errors.phone} />
helperText={errors.phone?.message}
/>
<TextField <TextInput
fullWidth label='Имя'
margin="normal" required
label="Имя" {...register('name')}
{...register('name')} error={errors.name?.message}
error={!!errors.name} />
helperText={errors.name?.message}
/>
<TextField <TextInput
fullWidth label='Фамилия'
margin="normal" required
label="Фамилия" {...register('surname')}
{...register('surname')} error={errors.surname?.message}
error={!!errors.surname} />
helperText={errors.surname?.message}
/>
<TextField <TextInput
fullWidth label='Пароль'
margin="normal" type="password"
type="password" required
label="Пароль" {...register('password', { required: 'Пароль обязателен' })}
required error={errors.password?.message}
{...register('password', { required: 'Пароль обязателен' })} />
error={!!errors.password}
helperText={errors.password?.message}
/>
<Button type="submit" variant="contained" color="primary"> <Flex gap='sm'>
Зарегистрироваться <Button disabled={!isValid} type="submit" flex={1} variant='filled'>
</Button> {isSubmitting ? <Loader size={16} /> : 'Зарегистрироваться'}
</Button>
</Flex>
</Flex>
</form> </form>
</Box> </Flex>
</Container> </Paper>
); );
}; };

View File

@ -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

65
ems/src/api/gis/index.ts Normal file
View File

@ -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

View File

@ -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

View File

@ -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<Buffer> => {
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

View File

@ -1,401 +1,23 @@
import express, { Request, Response } from 'express' import express from 'express'
import { PrismaClient } from '@prisma/client'
import fs from 'fs'
import path from 'path'
import axios from 'axios'
import multer from 'multer'
import bodyParser from 'body-parser' import bodyParser from 'body-parser'
import cors from 'cors' import cors from 'cors'
import { Coordinate } from './interfaces/map' import generalRouter from './api/general'
import { generateTilesForZoomLevel } from './utils/tiles' import gisRouter from './api/gis'
import { query, validationResult } from 'express-validator' import nodesRouter from './api/nodes'
import { Connection, ConnectionConfiguration, Request as TediousRequest } from 'tedious' 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 app = express()
const PORT = process.env.EMS_PORT || 5000 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(cors())
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true })) app.use(bodyParser.urlencoded({ extended: true }))
const storage = multer.diskStorage({ app.use('/general', generalRouter)
destination: function (req, file, cb) { app.use('/gis', gisRouter)
cb(null, path.join(__dirname, '..', 'public', 'temp')) app.use('/nodes', nodesRouter)
}, app.use('/tiles', tilesRouter)
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<Buffer> => {
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.listen(PORT, () => console.log(`Server running on port ${PORT}`)); app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

69
ems/src/utils/tedious.ts Normal file
View File

@ -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()
});
}