Object data

This commit is contained in:
cracklesparkle
2024-11-26 18:00:18 +09:00
parent a4513e7e7a
commit bd0a317e76
17 changed files with 1517 additions and 719 deletions

View File

@ -55,6 +55,7 @@
"react-router-dom": "^6.23.1",
"recharts": "^2.12.7",
"swr": "^2.2.5",
"uuid": "^11.0.3",
"zustand": "^4.5.2"
},
"devDependencies": {
@ -12121,6 +12122,18 @@
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",

View File

@ -58,6 +58,7 @@
"react-router-dom": "^6.23.1",
"recharts": "^2.12.7",
"swr": "^2.2.5",
"uuid": "^11.0.3",
"zustand": "^4.5.2"
},
"devDependencies": {

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,190 @@
import Feature, { FeatureLike } from "ol/Feature";
import { Text } from "ol/style";
import Fill from "ol/style/Fill";
import { FlatStyleLike } from "ol/style/flat";
import Stroke from "ol/style/Stroke";
import Style from "ol/style/Style";
import { calculateCenter } from "./mapUtils";
import CircleStyle from "ol/style/Circle";
import { MultiPoint, Point } from "ol/geom";
export const highlightStyleYellow = new Style({
stroke: new Stroke({
color: 'yellow',
width: 3,
}),
fill: new Fill({
color: 'rgba(255, 255, 0, 0.3)',
}),
});
export const highlightStyleRed = new Style({
stroke: new Stroke({
color: 'red',
width: 3,
}),
fill: new Fill({
color: 'rgba(255, 255, 0, 0.3)',
}),
});
export function overlayStyle(feature: FeatureLike) {
const styles = [new Style({
geometry: function (feature) {
const modifyGeometry = feature.get('modifyGeometry');
return modifyGeometry ? modifyGeometry.geometry : feature.getGeometry();
},
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)',
}),
stroke: new Stroke({
color: '#ffcc33',
width: 2,
}),
image: new CircleStyle({
radius: 7,
fill: new Fill({
color: '#ffcc33',
}),
}),
})]
const modifyGeometry = feature.get('modifyGeometry')
const geometry = modifyGeometry ? modifyGeometry.geometry : feature.getGeometry()
const result = calculateCenter(geometry)
const center = result.center
if (center) {
styles.push(
new Style({
geometry: new Point(center),
image: new CircleStyle({
radius: 4,
fill: new Fill({
color: '#ff3333'
})
})
})
)
const coordinates = result.coordinates
if (coordinates) {
const minRadius = result.minRadius
const sqDistances = result.sqDistances
const rsq = minRadius * minRadius
if (Array.isArray(sqDistances)) {
const points = coordinates.filter(function (_coordinate, index) {
return sqDistances[index] > rsq
})
styles.push(
new Style({
geometry: new MultiPoint(points),
image: new CircleStyle({
radius: 4,
fill: new Fill({
color: '#33cc33'
})
})
})
)
}
}
}
return styles
}
export function styleFunction(feature: Feature) {
return [
new Style({
fill: new Fill({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: '#3399CC',
width: 1.25
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
}),
// get the text from the feature - `this` is ol.Feature
// and show only under certain resolution
text: feature.get('object_id')
})
})
];
}
export function firstStyleFunction(feature: Feature) {
return [
new Style({
fill: new Fill({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: 'red',
width: 1.25
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
}),
// get the text from the feature - `this` is ol.Feature
// and show only under certain resolution
text: feature.get('object_id')
})
})
];
}
export function thirdStyleFunction(feature: Feature) {
return [
new Style({
fill: new Fill({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: '#33ccb3',
width: 1.25
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
}),
// get the text from the feature - `this` is ol.Feature
// and show only under certain resolution
text: feature.get('object_id')
})
})
];
}
export function fourthStyleFunction(feature: Feature) {
return [
new Style({
fill: new Fill({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: '#3399CC',
width: 1.25
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
}),
// get the text from the feature - `this` is ol.Feature
// and show only under certain resolution
text: `${feature.get('object_id')}\n ${feature.get('angle')}`
})
})
];
}
const drawingLayerStyle: FlatStyleLike = {
'fill-color': 'rgba(255, 255, 255, 0.2)',

View File

@ -1,25 +1,22 @@
import { ActionIcon, MantineColorScheme } from '@mantine/core'
import { IconApi, IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPoint, IconPolygon, IconRuler } from '@tabler/icons-react'
import { Type } from 'ol/geom/Geometry'
import React from 'react'
import { setCurrentTool, useMapStore } from '../../../store/map';
interface IToolbarProps {
currentTool: Type | null;
onSave: () => void;
onRemove: () => void;
handleToolSelect: (tool: Type) => void;
onMover: () => void;
colorScheme: MantineColorScheme;
}
const MapToolbar = ({
currentTool,
onSave,
onRemove,
handleToolSelect,
onMover,
colorScheme
}: IToolbarProps) => {
const mapState = useMapStore()
return (
<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={() => {
@ -38,36 +35,36 @@ const MapToolbar = ({
<ActionIcon
size='lg'
variant={currentTool === 'Point' ? 'filled' : 'transparent'}
variant={mapState.currentTool === 'Point' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Point')
setCurrentTool('Point')
}}>
<IconPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'LineString' ? 'filled' : 'transparent'}
variant={mapState.currentTool === 'LineString' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('LineString')
setCurrentTool('LineString')
}}>
<IconLine />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Polygon' ? 'filled' : 'transparent'}
variant={mapState.currentTool === 'Polygon' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Polygon')
setCurrentTool('Polygon')
}}>
<IconPolygon />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Circle' ? 'filled' : 'transparent'}
variant={mapState.currentTool === 'Circle' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Circle')
setCurrentTool('Circle')
}}>
<IconCircle />
</ActionIcon>
@ -82,8 +79,10 @@ const MapToolbar = ({
<ActionIcon
size='lg'
variant='transparent'
>
variant={mapState.currentTool === 'Measure' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Measure')
}}>
<IconRuler />
</ActionIcon>
</ActionIcon.Group>

View File

@ -0,0 +1,39 @@
import { Checkbox, Group, RenderTreeNodePayload } from "@mantine/core";
import { IconChevronDown } from "@tabler/icons-react";
import { useEffect } from "react";
export const MapTreeCheckbox = ({
node,
expanded,
hasChildren,
elementProps,
tree,
}: RenderTreeNodePayload) => {
const checked = tree.isNodeChecked(node.value);
const indeterminate = tree.isNodeIndeterminate(node.value);
useEffect(() => {
console.log(node.value)
}, [checked])
return (
<Group gap="xs" {...elementProps}>
<Checkbox.Indicator
checked={checked}
indeterminate={indeterminate}
onClick={() => (!checked ? tree.checkNode(node.value) : tree.uncheckNode(node.value))}
/>
<Group gap={5} onClick={() => tree.toggleExpanded(node.value)}>
<span>{node.label}</span>
{hasChildren && (
<IconChevronDown
size={14}
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
)}
</Group>
</Group>
);
};

View File

@ -0,0 +1,199 @@
import { FeatureLike } from "ol/Feature";
import { LineString, Point, Polygon } from "ol/geom";
import Geometry, { Type } from "ol/geom/Geometry";
import { Fill, RegularShape, Stroke, Style, Text } from "ol/style";
import CircleStyle from "ol/style/Circle";
import { getArea, getLength } from 'ol/sphere'
import { Modify } from "ol/interaction";
import { getMeasureShowSegments } from "../../../store/map";
export const style = new Style({
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)',
}),
stroke: new Stroke({
color: 'rgba(0, 0, 0, 0.5)',
lineDash: [10, 10],
width: 2,
}),
image: new CircleStyle({
radius: 5,
stroke: new Stroke({
color: 'rgba(0, 0, 0, 0.7)',
}),
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)',
}),
}),
});
export const labelStyle = new Style({
text: new Text({
font: '14px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.7)',
}),
padding: [3, 3, 3, 3],
textBaseline: 'bottom',
offsetY: -15,
}),
image: new RegularShape({
radius: 8,
points: 3,
angle: Math.PI,
displacement: [0, 10],
fill: new Fill({
color: 'rgba(0, 0, 0, 0.7)',
}),
}),
});
export const tipStyle = new Style({
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
padding: [2, 2, 2, 2],
textAlign: 'left',
offsetX: 15,
}),
});
export const modifyStyle = new Style({
image: new CircleStyle({
radius: 5,
stroke: new Stroke({
color: 'rgba(0, 0, 0, 0.7)',
}),
fill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
}),
text: new Text({
text: 'Drag to modify',
font: '12px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.7)',
}),
padding: [2, 2, 2, 2],
textAlign: 'left',
offsetX: 15,
}),
});
export const segmentStyle = new Style({
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
padding: [2, 2, 2, 2],
textBaseline: 'bottom',
offsetY: -12,
}),
image: new RegularShape({
radius: 6,
points: 3,
angle: Math.PI,
displacement: [0, 8],
fill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
}),
});
const formatLength = function (line: Geometry) {
const length = getLength(line);
let output;
if (length > 100) {
output = Math.round((length / 1000) * 100) / 100 + ' km';
} else {
output = Math.round(length * 100) / 100 + ' m';
}
return output;
};
const formatArea = function (polygon: Geometry) {
const area = getArea(polygon);
let output;
if (area > 10000) {
output = Math.round((area / 1000000) * 100) / 100 + ' km\xB2';
} else {
output = Math.round(area * 100) / 100 + ' m\xB2';
}
return output;
};
export function measureStyleFunction(
feature: FeatureLike,
drawType?: Type,
tip?: string,
setTipPoint?: React.Dispatch<React.SetStateAction<Point | null>>,
modify?: React.MutableRefObject<Modify>
) {
const styles = [];
const geometry = feature.getGeometry();
const type = geometry?.getType();
const segmentStyles = [segmentStyle];
const segments = getMeasureShowSegments()
if (!geometry) return
let point, label, line;
if (!drawType || drawType === type || type === 'Point') {
styles.push(style);
if (type === 'Polygon') {
point = (geometry as Polygon).getInteriorPoint();
label = formatArea(geometry as Polygon);
line = new LineString((geometry as Polygon).getCoordinates()[0]);
} else if (type === 'LineString') {
point = new Point((geometry as Polygon).getLastCoordinate());
label = formatLength(geometry as LineString);
line = geometry;
}
}
if (segments && line) {
let count = 0;
(line as LineString).forEachSegment(function (a, b) {
const segment = new LineString([a, b]);
const label = formatLength(segment);
if (segmentStyles.length - 1 < count) {
segmentStyles.push(segmentStyle.clone());
}
const segmentPoint = new Point(segment.getCoordinateAt(0.5));
segmentStyles[count].setGeometry(segmentPoint);
segmentStyles[count].getText()?.setText(label);
styles.push(segmentStyles[count]);
count++;
});
}
if (label) {
labelStyle.setGeometry(point as Geometry);
labelStyle.getText()?.setText(label);
styles.push(labelStyle);
}
if (
tip &&
type === 'Point' &&
!modify?.current.getOverlay()?.getSource()?.getFeatures().length
) {
setTipPoint?.(geometry as Point);
tipStyle.getText()?.setText(tip);
styles.push(tipStyle);
}
return styles;
}

View File

@ -1,49 +1,83 @@
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Checkbox, Grid } from '@mantine/core'
import { Checkbox, Divider, Flex, Grid, Stack, Text } from '@mantine/core'
import { IObjectParam, IParam } from '../../interfaces/objects'
import { decodeDoubleEncodedString } from '../../utils/format'
import TCBParameter from './TCBParameter'
interface ObjectParameterProps {
showLabel?: boolean,
param: IObjectParam,
}
const ObjectParameter = ({
id_param,
value
}: IObjectParam) => {
param,
showLabel = true
}: ObjectParameterProps) => {
const { data: paramData } = useSWR(
`/general/params/all?param_id=${id_param}`,
`/general/params/all?param_id=${param.id_param}`,
(url) => fetcher(url, BASE_URL.ems).then(res => res[0] as IParam),
{
revalidateOnFocus: false
}
)
const Parameter = (type: string, name: string, value: unknown) => {
const Parameter = (type: string, name: string, value: unknown, vtable: string) => {
switch (type) {
case 'bit':
return (
<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>
<Flex direction='row' align='center' gap='sm'>
<Checkbox defaultChecked={value as boolean} />
<Text>{name}</Text>
</Flex>
)
case 'varchar(200)':
return (
<Text>
{decodeDoubleEncodedString(value as string)}
</Text>
)
case 'varchar(5)':
return (
<Text>
{decodeDoubleEncodedString(value as string)}
</Text>
)
case 'bigint':
return (
<Text>
{(value as string)}
</Text>
)
case 'GTCB':
return (
<TCBParameter value={value as string} vtable={vtable} />
)
case 'TCB':
return (
<TCBParameter value={value as string} vtable={vtable} />
)
default:
return (
<div>
Неподдерживаемый параметр
{type}
</div>
)
}
}
return (
<div>
<>
{paramData &&
Parameter(paramData.format, paramData.name, value)
<Stack gap={0}>
{showLabel &&
<Divider my="xs" label={paramData.name} labelPosition="left" />
}
{Parameter(paramData.format, paramData.name, param.value, paramData.vtable)}
</Stack>
}
</div>
</>
)
}

View File

@ -0,0 +1,100 @@
import React from 'react'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Text } from '@mantine/core'
interface ITCBParameterProps {
value: string,
vtable: string,
inactive?: boolean
}
interface vStreet {
id: number,
id_city: number,
name: string,
kv: number
}
interface tType {
id: number,
name: string,
}
const TCBParameter = ({
value,
vtable
}: ITCBParameterProps) => {
//Get value
const { data: tcbValue } = useSWR(
`/general/params/tcb?id=${value}&vtable=${vtable}`,
(url) => fetcher(url, BASE_URL.ems).then(res => res[0]),
{
revalidateOnFocus: false
}
)
//Get available values
const { data: tcbAll } = useSWR(
`/general/params/tcb?vtable=${vtable}`,
(url) => fetcher(url, BASE_URL.ems).then(res => res),
{
revalidateOnFocus: false
}
)
const TCBValue = (vtable: string) => {
switch (vtable) {
case 'vStreets':
return (
<Text>
{JSON.stringify(tcbValue)}
</Text>
)
case 'tTypes':
return (
<Text>
{(tcbValue as tType)?.name}
</Text>
)
case 'vPipesGround':
return (
<Text>
{(tcbValue)?.name}
</Text>
)
case 'vRepairEvent':
return (
<Text>
{(tcbValue)?.name}
</Text>
)
case 'vPipesMaterial':
return (
<Text>
{(tcbValue)?.name}
</Text>
)
case 'vBoilers':
return (
<Text>
{(tcbValue)?.name}
</Text>
)
default:
return (
<Text>
{JSON.stringify(tcbValue)}
</Text>
)
}
}
return (
TCBValue(vtable)
)
}
export default TCBParameter

View File

@ -1,12 +1,368 @@
import { Coordinate, distance, rotate } from "ol/coordinate";
import { Extent, getCenter, getHeight, getWidth } from "ol/extent";
import { LineString, Polygon, SimpleGeometry } from "ol/geom";
import { containsExtent, Extent, getCenter, getHeight, getWidth } from "ol/extent";
import Feature from "ol/Feature";
import GeoJSON from "ol/format/GeoJSON";
import { Circle, Geometry, LineString, Point, Polygon, SimpleGeometry } from "ol/geom";
import VectorLayer from "ol/layer/Vector";
import VectorImageLayer from "ol/layer/VectorImage";
import Map from "ol/Map";
import { addCoordinateTransforms, addProjection, get, getTransform, Projection, ProjectionLike, transform } from "ol/proj";
import VectorSource from "ol/source/Vector";
import proj4 from "proj4";
import { firstStyleFunction, fourthStyleFunction, selectStyle, styleFunction, thirdStyleFunction } from "./MapStyles";
import { Type } from "ol/geom/Geometry";
import { Draw, Modify, Snap } from "ol/interaction";
import { noModifierKeys } from "ol/events/condition";
import { IGeometryType, IRectCoords } from "../../interfaces/map";
import { uploadCoordinates } from "../../actions/map";
import { ImageStatic } from "ol/source";
import ImageLayer from "ol/layer/Image";
import { IFigure, ILine } from "../../interfaces/gis";
import { fromCircle } from "ol/geom/Polygon";
import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles";
import { getCurrentTool, getMeasureClearPrevious, getMeasureShowSegments, getMeasureType, getTipPoint } from "../../store/map";
export function processLine(
line: ILine,
scaling: { w: number, h: number },
mapCenter: Coordinate,
linesLayer: React.MutableRefObject<VectorLayer<VectorSource<any>, any>>
) {
const x1 = line.x1 * scaling.w
const y1 = line.y1 * scaling.h
const x2 = line.x2 * scaling.w
const y2 = line.y2 * scaling.h
const center = [mapCenter[0], mapCenter[1]]
const testCoords = [
[center[0] + x1, center[1] - y1],
[center[0] + x2, center[1] - y2],
]
const feature = new Feature(new LineString(testCoords))
feature.setStyle(styleFunction(feature))
feature.set('type', line.type)
feature.set('planning', line.planning)
feature.set('object_id', line.object_id)
linesLayer.current?.getSource()?.addFeature(feature)
}
export function processFigure(
figure: IFigure,
scaling: { w: number, h: number },
mapCenter: Coordinate,
figuresLayer: React.MutableRefObject<VectorLayer<VectorSource<any>, any>>
) {
if (figure.figure_type_id == 1) {
const width = figure.width * scaling.w
const height = figure.height * scaling.h
const left = figure.left * scaling.w
const top = figure.top * scaling.h
const centerX = mapCenter[0] + left + (width / 2)
const centerY = mapCenter[1] - top - (height / 2)
const radius = width / 2;
const circleGeom = new Circle([centerX, centerY], radius)
const ellipseGeom = fromCircle(circleGeom, 64)
ellipseGeom.scale(1, height / width)
const feature = new Feature(ellipseGeom)
feature.setStyle(firstStyleFunction(feature))
feature.set('type', figure.type)
feature.set('object_id', figure.object_id)
feature.set('planning', figure.planning)
figuresLayer.current?.getSource()?.addFeature(feature)
}
if (figure.figure_type_id == 3) {
const x = figure.left * scaling.w
const y = figure.top * scaling.h
const center = [mapCenter[0] + x, mapCenter[1] - y]
const coords = figure.points?.split(' ').map(pair => {
const [x, y] = pair.split(';').map(Number)
return [
center[0] + (x * scaling.w),
center[1] - (y * scaling.h)
]
})
if (coords) {
const polygon = new Polygon([coords])
const feature = new Feature({
geometry: polygon
})
feature.set('object_id', figure.object_id)
feature.set('planning', figure.planning)
feature.set('type', figure.type)
feature.setStyle(thirdStyleFunction(feature))
figuresLayer.current?.getSource()?.addFeature(feature)
}
}
if (figure.figure_type_id == 4) {
const width = figure.width * scaling.w
const height = figure.height * scaling.h
const left = figure.left * scaling.w
const top = figure.top * scaling.h
const halfWidth = width / 2
const halfHeight = height / 2
const center = [mapCenter[0] + left + halfWidth, mapCenter[1] - top - halfHeight]
const testCoords = [
[center[0] - halfWidth, center[1] - halfHeight],
[center[0] - halfWidth, center[1] + halfHeight],
[center[0] + halfWidth, center[1] + halfHeight],
[center[0] + halfWidth, center[1] - halfHeight],
[center[0] - halfWidth, center[1] - halfHeight]
]
const geometry1 = new Polygon([testCoords])
const anchor1 = center
geometry1.rotate(-figure.angle * Math.PI / 180, anchor1)
const feature1 = new Feature(geometry1)
feature1.set('object_id', figure.object_id)
feature1.set('planning', figure.planning)
feature1.set('type', figure.type)
feature1.set('angle', figure.angle)
feature1.setStyle(fourthStyleFunction(feature1))
figuresLayer.current?.getSource()?.addFeature(feature1)
}
}
// Function to update the image layer with a new source when extent changes
export const updateImageSource = (
imageUrl: string,
imageLayer: React.MutableRefObject<ImageLayer<ImageStatic>>,
polygonFeature: Feature<Polygon>,
setPolygonExtent: (value: React.SetStateAction<Extent | undefined>) => void,
setRectCoords: React.Dispatch<React.SetStateAction<IRectCoords | undefined>>
) => {
const newExtent = polygonFeature.getGeometry()?.getExtent();
const bottomLeft = polygonFeature.getGeometry()?.getCoordinates()[0][0]
const topLeft = polygonFeature.getGeometry()?.getCoordinates()[0][1]
const topRight = polygonFeature.getGeometry()?.getCoordinates()[0][2]
const bottomRight = polygonFeature.getGeometry()?.getCoordinates()[0][3]
setRectCoords({
bl: bottomLeft,
tl: topLeft,
tr: topRight,
br: bottomRight
})
setPolygonExtent(newExtent)
if (newExtent && bottomLeft && bottomRight && topRight && topLeft) {
const originalExtent = calculateExtent(bottomLeft, topLeft, topRight, bottomRight)
const newImageSource = new ImageStatic({
url: imageUrl,
imageExtent: originalExtent,
projection: rotateProjection('EPSG:3857', calculateRotationAngle(bottomLeft, bottomRight), originalExtent)
});
imageLayer.current.setSource(newImageSource);
}
};
export const addInteractions = (
drawingLayerSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>,
draw: React.MutableRefObject<Draw | null>,
map: React.MutableRefObject<Map | null>,
snap: React.MutableRefObject<Snap | null>,
measureDraw: React.MutableRefObject<Draw | null>,
measureSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>,
measureModify: React.MutableRefObject<Modify>,
) => {
const currentTool = getCurrentTool()
const showSegments = getMeasureShowSegments()
const clearPrevious = getMeasureClearPrevious()
const measureType = getMeasureType()
const tipPoint = getTipPoint()
if (currentTool !== 'Measure') {
draw.current = new Draw({
source: drawingLayerSource.current,
type: currentTool as Type,
condition: noModifierKeys
})
draw.current.on('drawend', function (s) {
console.log(s.feature.getGeometry()?.getType())
let type: IGeometryType = 'POLYGON'
switch (s.feature.getGeometry()?.getType()) {
case 'LineString':
type = 'LINE'
break
case 'Polygon':
type = 'POLYGON'
break
default:
type = 'POLYGON'
break
}
const coordinates = (s.feature.getGeometry() as SimpleGeometry).getCoordinates() as Coordinate[]
uploadCoordinates(coordinates, type)
})
map?.current?.addInteraction(draw.current)
snap.current = new Snap({ source: drawingLayerSource.current })
map?.current?.addInteraction(snap.current)
}
if (currentTool == 'Measure') {
const drawType = measureType;
const activeTip =
'Кликните, чтобы продолжить рисовать ' +
(drawType === 'Polygon' ? 'многоугольник' : 'линию');
const idleTip = 'Кликните, чтобы начать измерение';
let tip = idleTip;
measureDraw.current = new Draw({
source: measureSource.current,
type: drawType,
style: function (feature) {
return measureStyleFunction(feature, drawType, tip);
},
});
measureDraw.current.on('drawstart', function () {
if (clearPrevious) {
measureSource.current.clear();
}
measureModify.current.setActive(false);
tip = activeTip;
});
measureDraw.current.on('drawend', function () {
modifyStyle.setGeometry(tipPoint as Geometry);
measureModify.current.setActive(true);
map.current?.once('pointermove', function () {
modifyStyle.setGeometry('');
});
tip = idleTip;
});
measureModify.current.setActive(true);
map.current?.addInteraction(measureDraw.current);
}
}
export function regionsInit(
map: React.MutableRefObject<Map | null>,
selectedRegion: React.MutableRefObject<Feature<Geometry> | null>,
regionsLayer: React.MutableRefObject<VectorImageLayer<Feature<Geometry>, VectorSource<Feature<Geometry>>>>,
setStatusText: (value: React.SetStateAction<string>) => void,
) {
map.current?.on('click', function (e) {
if (selectedRegion.current !== null) {
selectedRegion.current = null
}
if (map.current) {
map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) {
if (layer === regionsLayer.current) {
selectedRegion.current = feature as Feature
// Zoom to the selected feature
zoomToFeature(map, selectedRegion.current)
return true
} else return false
});
}
})
// Show current selected region
map.current?.on('pointermove', function (e) {
if (selectedRegion.current !== null) {
selectedRegion.current.setStyle(undefined)
selectedRegion.current = null
}
if (map.current) {
map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) {
if (layer === regionsLayer.current) {
selectedRegion.current = feature as Feature
selectedRegion.current.setStyle(selectStyle)
if (feature.get('district')) {
setStatusText(feature.get('district'))
}
return true
} else return false
})
}
})
// Hide regions layer when fully visible
map.current?.on('moveend', function () {
const viewExtent = map.current?.getView().calculateExtent(map.current.getSize())
const features = regionsLayer.current.getSource()?.getFeatures()
let isViewCovered = false
features?.forEach((feature: Feature) => {
const featureExtent = feature?.getGeometry()?.getExtent()
if (viewExtent && featureExtent) {
if (containsExtent(featureExtent, viewExtent)) {
isViewCovered = true
}
}
})
regionsLayer.current.setVisible(!isViewCovered)
})
}
const zoomToFeature = (map: React.MutableRefObject<Map | null>, feature: Feature) => {
const geometry = feature.getGeometry()
const extent = geometry?.getExtent()
if (map.current && extent) {
map.current.getView().fit(extent, {
duration: 300,
maxZoom: 19,
})
}
}
// Function to save features to localStorage
export const saveFeatures = (layerRef: React.MutableRefObject<VectorLayer<VectorSource<any>, any> | null>) => {
const features = layerRef.current?.getSource()?.getFeatures()
if (features && features.length > 0) {
const geoJSON = new GeoJSON()
const featuresJSON = geoJSON.writeFeatures(features)
localStorage.setItem('savedFeatures', featuresJSON)
}
}
// Function to load features from localStorage
export const loadFeatures = (layerSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>) => {
const savedFeatures = localStorage.getItem('savedFeatures')
if (savedFeatures) {
const geoJSON = new GeoJSON()
const features = geoJSON.readFeatures(savedFeatures, {
featureProjection: 'EPSG:4326', // Ensure the projection is correct
})
layerSource.current?.addFeatures(features) // Add features to the vector source
//drawingLayer.current?.getSource()?.changed()
}
}
function rotateProjection(projection: ProjectionLike, angle: number, extent: Extent) {
function rotateCoordinate(coordinate: Coordinate, angle: number, anchor: Coordinate) {
var coord = rotate(
const coord = rotate(
[coordinate[0] - anchor[0], coordinate[1] - anchor[1]],
angle
);
@ -21,10 +377,10 @@ function rotateProjection(projection: ProjectionLike, angle: number, extent: Ext
return rotateCoordinate(coordinate, -angle, getCenter(extent));
}
var normalProjection = get(projection);
const normalProjection = get(projection);
if (normalProjection) {
var rotatedProjection = new Projection({
const rotatedProjection = new Projection({
code: normalProjection.getCode() + ":" + angle.toString() + ":" + extent.toString(),
units: normalProjection.getUnits(),
extent: extent
@ -55,9 +411,9 @@ function rotateProjection(projection: ProjectionLike, angle: number, extent: Ext
// also set up transforms with any projections defined using proj4
if (typeof proj4 !== "undefined") {
var projCodes = Object.keys(proj4.defs);
const projCodes = Object.keys(proj4.defs);
projCodes.forEach(function (code) {
var proj4Projection = get(code) as Projection;
const proj4Projection = get(code) as Projection;
if (proj4Projection) {
if (!getTransform(proj4Projection, rotatedProjection)) {
addCoordinateTransforms(
@ -177,7 +533,7 @@ function calculateCenter(geometry: SimpleGeometry) {
const dy = coordinate[1] - center[1];
return dx * dx + dy * dy;
});
minRadius = Math.sqrt(Math.max.apply(Math, sqDistances)) / 3;
minRadius = Math.sqrt(Math.max(...sqDistances)) / 3;
} else {
minRadius =
Math.max(

View File

@ -1,3 +1,5 @@
import { Coordinate } from "ol/coordinate";
export interface SatelliteMapsProviders {
google: 'google';
yandex: 'yandex';
@ -11,3 +13,10 @@ export interface IGeometryTypes {
}
export type IGeometryType = IGeometryTypes[keyof IGeometryTypes]
export interface IRectCoords {
bl: Coordinate | undefined,
tl: Coordinate | undefined,
tr: Coordinate | undefined,
br: Coordinate | undefined
}

View File

@ -12,4 +12,4 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<App />
</MantineProvider>
</React.StrictMode>,
)
)

85
client/src/store/map.ts Normal file
View File

@ -0,0 +1,85 @@
import { create } from 'zustand';
import { ToolType } from '../types/tools';
import { Point } from 'ol/geom';
import Map from 'ol/Map';
interface MapState {
currentTool: ToolType,
measureType: "LineString" | "Polygon",
measureShowSegments: boolean,
measureClearPrevious: boolean,
tipPoint: Point | null,
map: Map | null
}
export const useMapStore = create<MapState>(() => ({
currentTool: null,
measureType: "LineString",
measureShowSegments: true,
measureClearPrevious: true,
tipPoint: null,
map: null
}));
const getMap = () => {
return useMapStore.getState().map
}
const setMap = (map: Map | null) => {
useMapStore.setState(() => ({ map: map }))
}
const setTipPoint = (tipPoint: Point | null) => {
useMapStore.setState(() => ({ tipPoint: tipPoint }))
}
const getTipPoint = () => {
return useMapStore.getState().tipPoint
}
const setMeasureType = (tool: "LineString" | "Polygon") => {
useMapStore.setState(() => ({ measureType: tool }))
}
const getMeasureType = () => {
return useMapStore.getState().measureType
}
const setCurrentTool = (tool: ToolType) => {
tool === useMapStore.getState().currentTool
? useMapStore.setState(() => ({ currentTool: null }))
: useMapStore.setState(() => ({ currentTool: tool }))
}
const getCurrentTool = () => {
return useMapStore.getState().currentTool
}
const getMeasureShowSegments = () => {
return useMapStore.getState().measureShowSegments
}
const getMeasureClearPrevious = () => {
return useMapStore.getState().measureClearPrevious
}
const setMeasureShowSegments = (bool: boolean) => {
useMapStore.setState(() => ({ measureShowSegments: bool }))
}
const setMeasureClearPrevious = (bool: boolean) => {
useMapStore.setState(() => ({ measureClearPrevious: bool }))
}
export {
setCurrentTool,
getCurrentTool,
setMeasureShowSegments,
setMeasureClearPrevious,
getMeasureShowSegments,
getMeasureClearPrevious,
setMeasureType,
getMeasureType,
getTipPoint,
setTipPoint
}

12
client/src/types/tools.ts Normal file
View File

@ -0,0 +1,12 @@
export type ToolType =
"Point" |
"LineString" |
"LinearRing" |
"Polygon" |
"MultiPoint" |
"MultiLineString" |
"MultiPolygon" |
"GeometryCollection" |
"Circle" |
"Measure" |
null

View File

@ -0,0 +1,44 @@
// CP437 Character Map
const CP437_MAP = [
'\0', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '◙', '♂', '♀', '♪', '♫', '☼', '►',
'◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '→', '←', '∟', '↔', '▲', '▼', ' ', '!', '"',
'#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4',
'5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F',
'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|',
'}', '~', '⌂', 'Ç', 'ü', 'é', 'â', 'ä', 'à', 'å', 'ç', 'ê', 'ë', 'è', 'ï', 'î', 'ì', 'Ä',
'Å', 'É', 'æ', 'Æ', 'ô', 'ö', 'ò', 'û', 'ù', 'ÿ', 'Ö', 'Ü', '¢', '£', '¥', '₧', 'ƒ', 'á',
'í', 'ó', 'ú', 'ñ', 'Ñ', 'ª', 'º', '¿', '⌐', '¬', '½', '¼', '¡', '«', '»', '░', '▒', '▓',
'│', '┤', '╡', '╢', '╖', '╕', '╣', '║', '╗', '╝', '╜', '╛', '┐', '└', '┴', '┬', '├', '─',
'┼', '╞', '╟', '╚', '╔', '╩', '╦', '╠', '═', '╬', '╧', '╨', '╤', '╥', '╙', '╘', '╒', '╓',
'╫', '╪', '┘', '┌', '█', '▄', '▌', '▐', '▀', 'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ', 'Φ',
'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩', '≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·',
'√', 'ⁿ', '²', '■', ' '
];
function decodeCP437ToBytes(garbledString: string) {
const bytes = [];
for (const char of garbledString) {
const byte = CP437_MAP.indexOf(char);
if (byte === -1) {
//console.warn(`Character '${char}' not found in CP437 map`);
bytes.push(63); // '?' as a placeholder
}
bytes.push(byte);
}
return Uint8Array.from(bytes);
}
function decodeWindows1251FromBytes(byteArray: any) {
const decoder = new TextDecoder('windows-1251');
return decoder.decode(byteArray);
}
export function decodeDoubleEncodedString(garbledString: string) {
// Step 1: Decode from CP437 to bytes
const bytes = decodeCP437ToBytes(garbledString);
// Step 2: Decode bytes as WINDOWS-1251
return decodeWindows1251FromBytes(bytes);
}

View File

@ -6592,6 +6592,11 @@ utrie@^1.0.2:
dependencies:
base64-arraybuffer "^1.0.2"
uuid@^11.0.3:
version "11.0.3"
resolved "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz"
integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==
varint@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz"