This commit is contained in:
cracklesparkle
2025-01-30 12:36:39 +09:00
parent e6b3dc05d3
commit 0788a401ca
43 changed files with 3710 additions and 1724 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
import { Checkbox, Flex, NavLink, Slider, Stack } from '@mantine/core'
import BaseLayer from 'ol/layer/Base'
import Map from 'ol/Map'
import React, { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
interface MapLayersProps {
map: React.MutableRefObject<Map | null>
map: Map | null
}
const MapLayers = ({
@ -12,7 +12,7 @@ const MapLayers = ({
}: MapLayersProps) => {
return (
<Stack gap='0'>
{map.current?.getLayers().getArray() && map.current?.getLayers().getArray().map((layer, index) => (
{map?.getLayers().getArray() && map?.getLayers().getArray().map((layer, index) => (
<LayerSetting key={index} index={index} layer={layer} />
))}
</Stack>

View File

@ -0,0 +1,90 @@
import { Accordion, ColorSwatch, Flex, ScrollAreaAutosize, Stack, Text, useMantineColorScheme } from '@mantine/core'
import useSWR from 'swr';
import { fetcher } from '../../../http/axiosInstance';
import { BASE_URL } from '../../../constants';
const MapLegend = ({
selectedDistrict,
selectedYear
}: {
selectedDistrict: number | null,
selectedYear: number | null
}) => {
const { colorScheme } = useMantineColorScheme();
const { data: existingObjectsList } = useSWR(
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: planningObjectsList } = useSWR(
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
return (
<ScrollAreaAutosize offsetScrollbars maw='300px' w='100%' fz='xs' mt='auto' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
<Stack gap='sm' p='sm'>
<Text fz='xs'>
Легенда
</Text>
<Accordion defaultValue={['existing', 'planning']} multiple>
<Accordion.Item value='existing' key='existing'>
<Accordion.Control>Существующие</Accordion.Control>
<Accordion.Panel>
{existingObjectsList && <LegendGroup objectsList={existingObjectsList} border='solid' />}
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value='planning' key='planning'>
<Accordion.Control>Планируемые</Accordion.Control>
<Accordion.Panel>
{planningObjectsList && <LegendGroup objectsList={planningObjectsList} border='dotted' />}
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Stack>
</ScrollAreaAutosize>
)
}
const LegendGroup = ({
objectsList,
border
}: {
objectsList: { id: number, name: string, count: number, r: number | null, g: number | null, b: number | null }[],
border: 'solid' | 'dotted'
}) => {
const borderStyle = () => {
switch (border) {
case 'solid':
return '2px solid black'
case 'dotted':
return '2px dotted black'
default:
return 'none'
}
}
return (
<Stack gap={4}>
{objectsList.map(object => (
<Flex gap='xs' align='center' key={object.id}>
<ColorSwatch style={{ border: borderStyle() }} radius={0} size={16} color={`rgb(${object.r},${object.g},${object.b})`} />
-
<Text fz='xs'>{object.name}</Text>
</Flex>
))}
</Stack>
)
}
export default MapLegend

View File

@ -0,0 +1,32 @@
import { Center, SegmentedControl } from '@mantine/core'
import { getMode, Mode, setMode } from '../../store/map'
import { IconEdit, IconEye } from '@tabler/icons-react'
const MapMode = ({
map_id
}: { map_id: string }) => {
return (
<SegmentedControl value={getMode(map_id)} onChange={(value) => setMode(map_id, value as Mode)} color="blue" w='auto' data={[
{
value: 'view',
label: (
<Center style={{ gap: 10 }}>
<IconEye size={16} />
<span>Просмотр</span>
</Center>
),
},
{
value: 'edit',
label: (
<Center style={{ gap: 10 }}>
<IconEdit size={16} />
<span>Редактирование</span>
</Center>
),
},
]} />
)
}
export default MapMode

View File

@ -26,7 +26,7 @@ const customMapSource = new XYZ({
})
const regionsLayerSource = new VectorSource({
url: 'sakha_republic.geojson',
url: 'http://localhost:5000/gis/bounds/region',
format: new GeoJSON(),
})

View File

@ -4,39 +4,41 @@ import { useMapStore } from '../../../store/map';
interface IMapStatusbarProps {
mapControlsStyle: CSSProperties;
map_id: string;
}
const MapStatusbar = ({
mapControlsStyle,
map_id
}: IMapStatusbarProps) => {
const mapState = useMapStore()
const { currentCoordinate, currentX, currentY, currentZ, statusText } = useMapStore().id[map_id]
return (
<Flex gap='sm' p={'4px'} miw={'100%'} fz={'xs'} pos='absolute' bottom='0px' left='0px' style={{ ...mapControlsStyle, borderRadius: 0 }}>
<Flex gap='sm' p={'4px'} w={'100%'} fz={'xs'} style={{ ...mapControlsStyle, borderRadius: 0 }}>
<Text fz='xs' w={rem(130)}>
x: {mapState.currentCoordinate?.[0]}
x: {currentCoordinate?.[0]}
</Text>
<Text fz='xs' w={rem(130)}>
y: {mapState.currentCoordinate?.[1]}
y: {currentCoordinate?.[1]}
</Text>
<Divider orientation='vertical' />
<Text fz='xs'>
Z={mapState.currentZ}
Z={currentZ}
</Text>
<Text fz='xs'>
X={mapState.currentX}
X={currentX}
</Text>
<Text fz='xs'>
Y={mapState.currentY}
Y={currentY}
</Text>
<Text fz='xs' ml='auto'>
{mapState.statusText}
{statusText}
</Text>
</Flex>
)

View File

@ -95,7 +95,7 @@ const figureStyle = new Style({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: '#3399CC',
color: 'black',
width: 1.25
}),
text: new Text({
@ -108,12 +108,9 @@ const figureStyle = new Style({
})
const lineStyle = new Style({
fill: new Fill({
color: 'rgba(255,255,255,0.4)'
}),
stroke: new Stroke({
color: '#3399CC',
width: 1.25
width: 1
}),
text: new Text({
font: '12px Calibri,sans-serif',

View File

@ -1,93 +1,90 @@
import { ActionIcon, useMantineColorScheme } from '@mantine/core'
import { ActionIcon, Flex, useMantineColorScheme } from '@mantine/core'
import { IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPoint, IconPolygon, IconRuler, IconTransformPoint } from '@tabler/icons-react'
import { setCurrentTool, useMapStore } from '../../../store/map';
interface IToolbarProps {
onSave: () => void;
onRemove: () => void;
}
import { getDraw, setCurrentTool, useMapStore } from '../../../store/map';
import { saveFeatures } from '../mapUtils';
const MapToolbar = ({
onSave,
onRemove,
}: IToolbarProps) => {
const mapState = useMapStore()
map_id
}: { map_id: string }) => {
const { currentTool } = useMapStore().id[map_id]
const { colorScheme } = useMantineColorScheme();
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={onSave}>
<IconExclamationCircle />
</ActionIcon>
<Flex>
<ActionIcon.Group orientation='vertical' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
<ActionIcon size='lg' variant='transparent' onClick={() => saveFeatures(map_id)}>
<IconExclamationCircle />
</ActionIcon>
<ActionIcon size='lg' variant='transparent' onClick={onRemove}>
<IconArrowBackUp />
</ActionIcon>
<ActionIcon size='lg' variant='transparent' onClick={() => getDraw(map_id)?.removeLastPoint()}>
<IconArrowBackUp />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Edit' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Edit')
}}>
<IconTransformPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Edit' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool(map_id, 'Edit')
}}>
<IconTransformPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Point' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Point')
}}>
<IconPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Point' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool(map_id, 'Point')
}}>
<IconPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'LineString' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('LineString')
}}>
<IconLine />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'LineString' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool(map_id, 'LineString')
}}>
<IconLine />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Polygon' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Polygon')
}}>
<IconPolygon />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Polygon' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool(map_id, 'Polygon')
}}>
<IconPolygon />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Circle' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Circle')
}}>
<IconCircle />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Circle' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool(map_id, 'Circle')
}}>
<IconCircle />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Mover' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Mover')
}}
>
<IconArrowsMove />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Mover' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool(map_id, 'Mover')
}}
>
<IconArrowsMove />
</ActionIcon>
<ActionIcon
size='lg'
variant={mapState.currentTool === 'Measure' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool('Measure')
}}>
<IconRuler />
</ActionIcon>
</ActionIcon.Group>
<ActionIcon
size='lg'
variant={currentTool === 'Measure' ? 'filled' : 'transparent'}
onClick={() => {
setCurrentTool(map_id, 'Measure')
}}>
<IconRuler />
</ActionIcon>
</ActionIcon.Group>
</Flex>
)
}

View File

@ -138,6 +138,7 @@ const formatArea = function (polygon: Geometry) {
};
export function measureStyleFunction(
map_id: string,
feature: FeatureLike,
drawType?: Type,
tip?: string,
@ -149,7 +150,7 @@ export function measureStyleFunction(
const type = geometry?.getType();
const segmentStyles = [segmentStyle];
const segments = getMeasureShowSegments()
const segments = getMeasureShowSegments(map_id)
if (!geometry) return

View File

@ -6,12 +6,14 @@ import TCBParameter from './TCBParameter'
import TableValue from './TableValue'
interface ObjectParameterProps {
showLabel?: boolean,
param: IObjectParam,
showLabel?: boolean;
param: IObjectParam;
map_id: string;
}
const ObjectParameter = ({
param
param,
map_id
}: ObjectParameterProps) => {
const { data: paramData } = useSWR(
`/general/params/all?param_id=${param.id_param}`,
@ -26,44 +28,44 @@ const ObjectParameter = ({
switch (type) {
case 'bit':
return (
<TableValue value={value} name={name} type='boolean' />
<TableValue map_id={map_id} value={value} name={name} type='boolean' />
)
case 'bigint':
return (
<TableValue value={value} name={name} type='number' />
<TableValue map_id={map_id} value={value} name={name} type='number' />
)
case 'tinyint':
return (
<TableValue value={value} name={name} type='number' />
<TableValue map_id={map_id} value={value} name={name} type='number' />
)
// TODO: Calculate from calc procedures
case 'calculate':
return (
<TableValue value={value} name={name} type='value' />
<TableValue map_id={map_id} value={value} name={name} type='value' />
)
case 'GTCB':
return (
<TCBParameter value={value as string} vtable={vtable} name={name} />
<TCBParameter map_id={map_id} value={value as string} vtable={vtable} name={name} />
)
case 'TCB':
return (
<TCBParameter value={value as string} vtable={vtable} name={name} />
<TCBParameter map_id={map_id} value={value as string} vtable={vtable} name={name} />
)
case type.match(/varchar\((\d+)\)/)?.input:
return (
<TableValue value={value} name={name} type='string' />
<TableValue map_id={map_id} value={value} name={name} type='string' />
)
case type.match(/numeric\((\d+),(\d+)\)/)?.input:
return (
<TableValue value={value} name={name} type='number' unit={unit} />
<TableValue map_id={map_id} value={value} name={name} type='number' unit={unit} />
)
case 'year':
return (
<TableValue value={value} name={name} type='number' />
<TableValue map_id={map_id} value={value} name={name} type='number' />
)
case 'uniqueidentifier':
return (
<TableValue value={value} name={name} type='value'/>
<TableValue map_id={map_id} value={value} name={name} type='value'/>
)
default:
return (

View File

@ -6,8 +6,13 @@ import { BASE_URL } from '../../../constants';
import { fetcher } from '../../../http/axiosInstance';
import { useObjectsStore } from '../../../store/objects';
const ObjectParameters = () => {
const { currentObjectId } = useObjectsStore()
const ObjectParameters = ({
map_id
}: {
map_id: string
}) => {
const { currentObjectId } = useObjectsStore().id[map_id]
const { data: valuesData, isValidating: valuesValidating } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null,
@ -42,13 +47,13 @@ const ObjectParameters = () => {
sortedParams.map((param: IObjectParam) => {
if (param.date_po == null) {
return (
<ObjectParameter key={id_param} param={param} showLabel={false} />
<ObjectParameter map_id={map_id} key={id_param} param={param} showLabel={false} />
)
}
}
)
) : (
<ObjectParameter key={id_param} param={sortedParams[0]} />
<ObjectParameter map_id={map_id} key={id_param} param={sortedParams[0]} />
);
})
}

View File

@ -9,12 +9,14 @@ interface ITCBParameterProps {
vtable: string;
inactive?: boolean;
name: string;
map_id: string;
}
const TCBParameter = ({
value,
vtable,
name
name,
map_id
}: ITCBParameterProps) => {
//Get value
@ -80,7 +82,7 @@ const TCBParameter = ({
const TCBValue = (vtable: string) => {
if (tables.includes(vtable)) {
return (
<TableValue value={tcbValue?.id} name={name} type='select' vtable={vtable} />
<TableValue map_id={map_id} value={tcbValue?.id} name={name} type='select' vtable={vtable} />
)
} else {
return (

View File

@ -10,6 +10,7 @@ interface TableValueProps {
type: 'value' | 'boolean' | 'number' | 'select' | 'string';
unit?: string | null | undefined;
vtable?: string;
map_id: string;
}
const TableValue = ({
@ -17,10 +18,10 @@ const TableValue = ({
value,
type,
unit,
vtable
vtable,
map_id
}: TableValueProps) => {
const { selectedDistrict } = useObjectsStore()
const { selectedDistrict } = useObjectsStore().id[map_id]
//Get available values
const { data: tcbAll, isValidating } = useSWR(
type === 'select' && selectedDistrict ? `/general/params/tcb?vtable=${vtable}&id_city=${selectedDistrict}` : null,
@ -56,10 +57,10 @@ const TableValue = ({
/>
:
type === 'select' && !isValidating && tcbAll ?
<Select size='xs' data={tcbAll} defaultValue={JSON.stringify(value)} />
<Select size='xs' data={tcbAll} value={JSON.stringify(value)} />
:
type === 'string' ?
<Textarea size='xs' defaultValue={value as string} autosize minRows={1} />
<Textarea size='xs' value={value as string} autosize minRows={1} />
:
<Text size='xs'>{value as string}</Text>
}

View File

@ -31,9 +31,9 @@ const TabsPane = ({
</ScrollAreaAutosize>
<ScrollAreaAutosize h='100%' offsetScrollbars p='xs'>
<ScrollAreaAutosize h='100%' offsetScrollbars>
{tabs.map(tab => (
<Tabs.Panel key={tab.value} value={tab.value}>
<Tabs.Panel p='xs' key={tab.value} value={tab.value}>
{tab.view}
</Tabs.Panel>
))}

View File

@ -1,28 +1,22 @@
import { Coordinate, distance, rotate } from "ol/coordinate";
import { containsExtent, Extent, getCenter, getHeight, getWidth } from "ol/extent";
import { Extent, getCenter, getHeight, getWidth } from "ol/extent";
import Feature from "ol/Feature";
import GeoJSON from "ol/format/GeoJSON";
import { Circle, Geometry, LineString, Polygon, SimpleGeometry } from "ol/geom";
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 { selectStyle } from "./MapStyles";
import { Type } from "ol/geom/Geometry";
import { Draw, Modify, Snap, Translate } from "ol/interaction";
import { noModifierKeys } from "ol/events/condition";
import { IGeometryType, IRectCoords } from "../../interfaces/map";
import { never, noModifierKeys, platformModifierKeyOnly, primaryAction } from "ol/events/condition";
import { IGeometryType } 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, fromExtent } from "ol/geom/Polygon";
import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles";
import { getCurrentTool, getMeasureClearPrevious, getMeasureType, getTipPoint, setStatusText } from "../../store/map";
import { MutableRefObject } from "react";
import { getSelectedCity, getSelectedYear, setSelectedRegion } from "../../store/objects";
import { getCurrentTool, getDraw, getDrawingLayerSource, getImageLayer, getMap, getMeasureClearPrevious, getMeasureDraw, getMeasureModify, getMeasureSource, getMeasureType, getOverlayLayerSource, getSnap, getTipPoint, getTranslate, setDraw, setFile, setMeasureDraw, setPolygonExtent, setRectCoords, setSnap, setTranslate } from "../../store/map";
import Collection from "ol/Collection";
const calculateAngle = (coords: [number, number][]) => {
const [start, end] = coords;
@ -34,8 +28,7 @@ const calculateAngle = (coords: [number, number][]) => {
export function processLine(
line: ILine,
scaling: number,
mapCenter: Coordinate,
linesLayer: MutableRefObject<VectorLayer<VectorSource>>
mapCenter: Coordinate
) {
const x1 = line.x1 * scaling
const y1 = line.y1 * scaling
@ -49,22 +42,25 @@ export function processLine(
[center[0] + x2, center[1] - y2],
]
const feature = new Feature(new LineString(testCoords))
const geometry = new LineString(testCoords)
feature.set('type', line.type)
feature.set('geometry_type', 'line')
feature.set('planning', line.planning)
feature.set('object_id', line.object_id)
feature.set('rotation', calculateAngle(testCoords))
linesLayer.current?.getSource()?.addFeature(feature)
return {
type: "Feature",
geometry: new GeoJSON().writeGeometryObject(geometry),
properties: {
type: line.type,
geometry_type: 'line',
object_id: line.object_id,
planning: line.planning,
rotation: calculateAngle(testCoords)
}
}
}
export function processFigure(
figure: IFigure,
scaling: number,
mapCenter: Coordinate,
figuresLayer: MutableRefObject<VectorLayer<VectorSource>>
) {
if (figure.figure_type_id == 1) {
const width = figure.width * scaling
@ -82,12 +78,15 @@ export function processFigure(
const ellipseGeom = fromCircle(circleGeom, 64)
ellipseGeom.scale(1, height / width)
const feature = new Feature(ellipseGeom)
feature.set('type', figure.type)
feature.set('object_id', figure.object_id)
feature.set('planning', figure.planning)
figuresLayer.current?.getSource()?.addFeature(feature)
return {
type: "Feature",
geometry: new GeoJSON().writeGeometryObject(ellipseGeom),
properties: {
type: figure.type,
object_id: figure.object_id,
planning: figure.planning
}
}
}
if (figure.figure_type_id == 3) {
@ -107,14 +106,15 @@ export function processFigure(
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)
figuresLayer.current?.getSource()?.addFeature(feature)
return {
type: "Feature",
geometry: new GeoJSON().writeGeometryObject(polygon),
properties: {
type: figure.type,
object_id: figure.object_id,
planning: figure.planning
}
}
}
}
@ -140,22 +140,179 @@ export function processFigure(
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)
figuresLayer.current?.getSource()?.addFeature(feature1)
return {
type: "Feature",
geometry: new GeoJSON().writeGeometryObject(geometry1),
properties: {
type: figure.type,
object_id: figure.object_id,
planning: figure.planning,
angle: figure.angle
}
}
}
}
export const handleImageDrop = (
event: React.DragEvent<HTMLDivElement>,
map_id: string,
) => {
event.preventDefault()
event.stopPropagation()
if (!event.dataTransfer?.files) return
const files = event.dataTransfer.files
if (files.length > 0) {
const file = files[0]
setFile(map_id, file)
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = () => {
const imageUrl = reader.result as string;
const img = new Image();
img.src = imageUrl;
img.onload = () => {
const map = getMap(map_id)
if (map) {
const center = map.getView().getCenter() || [0, 0];
const width = img.naturalWidth;
const height = img.naturalHeight;
const resolution = map.getView().getResolution() || 0;
const extent = [
center[0] - (width * resolution) / 20,
center[1] - (height * resolution) / 20,
center[0] + (width * resolution) / 20,
center[1] + (height * resolution) / 20,
];
// Create a polygon feature with the same extent as the image
const polygonFeature = new Feature({ geometry: fromExtent(extent), });
const overlayLayerSource = getOverlayLayerSource(map_id)
// Add the polygon feature to the drawing layer source
overlayLayerSource.addFeature(polygonFeature);
const imageLayer = getImageLayer(map_id)
// Set up the initial image layer with the extent
imageLayer.setSource(new ImageStatic({ url: imageUrl, imageExtent: extent, }));
updateImageSource(map_id, imageUrl, polygonFeature)
//map.current.addLayer(imageLayer.current);
// Add interactions for translation and scaling
setTranslate(map_id, new Translate({
layers: [imageLayer],
features: new Collection([polygonFeature]),
}))
const defaultStyle = new Modify({ source: overlayLayerSource })
.getOverlay()
.getStyleFunction();
const modify = new Modify({
insertVertexCondition: never,
source: overlayLayerSource,
condition: function (event) {
return primaryAction(event) && !platformModifierKeyOnly(event);
},
deleteCondition: never,
features: new Collection([polygonFeature]),
style: function (feature) {
feature.get('features').forEach(function (modifyFeature: Feature) {
const modifyGeometry = modifyFeature.get('modifyGeometry')
if (modifyGeometry) {
const point = (feature.getGeometry() as Point).getCoordinates()
let modifyPoint = modifyGeometry.point
if (!modifyPoint) {
// save the initial geometry and vertex position
modifyPoint = point;
modifyGeometry.point = modifyPoint;
modifyGeometry.geometry0 = modifyGeometry.geometry;
// get anchor and minimum radius of vertices to be used
const result = calculateCenter(modifyGeometry.geometry0);
modifyGeometry.center = result.center;
modifyGeometry.minRadius = result.minRadius;
}
const center = modifyGeometry.center;
const minRadius = modifyGeometry.minRadius;
let dx, dy;
dx = modifyPoint[0] - center[0];
dy = modifyPoint[1] - center[1];
const initialRadius = Math.sqrt(dx * dx + dy * dy);
if (initialRadius > minRadius) {
const initialAngle = Math.atan2(dy, dx);
dx = point[0] - center[0];
dy = point[1] - center[1];
const currentRadius = Math.sqrt(dx * dx + dy * dy);
if (currentRadius > 0) {
const currentAngle = Math.atan2(dy, dx);
const geometry = modifyGeometry.geometry0.clone();
geometry.scale(currentRadius / initialRadius, undefined, center);
geometry.rotate(currentAngle - initialAngle, center);
modifyGeometry.geometry = geometry;
}
}
}
})
const res = map?.getView()?.getResolution()
if (typeof res === 'number' && feature && defaultStyle) {
return defaultStyle(feature, res)
}
}
});
const translate = getTranslate(map_id)
if (translate) {
translate.on('translateend', () => updateImageSource(map_id, imageUrl, polygonFeature));
map.addInteraction(translate);
}
//modify.on('modifyend', updateImageSource);
modify.on('modifystart', function (event) {
event.features.forEach(function (feature) {
feature.set(
'modifyGeometry',
{ geometry: feature.getGeometry()?.clone() },
true,
);
});
});
modify.on('modifyend', function (event) {
event.features.forEach(function (feature) {
const modifyGeometry = feature.get('modifyGeometry');
if (modifyGeometry) {
feature.setGeometry(modifyGeometry.geometry);
feature.unset('modifyGeometry', true);
}
})
updateImageSource(map_id, imageUrl, polygonFeature)
})
map.addInteraction(modify);
}
};
};
reader.readAsDataURL(file);
}
}
}
// Function to update the image layer with a new source when extent changes
export const updateImageSource = (
map_id: string,
imageUrl: string,
imageLayer: React.MutableRefObject<ImageLayer<ImageStatic>>,
polygonFeature: Feature<Polygon>,
setPolygonExtent: (value: React.SetStateAction<Extent | undefined>) => void,
setRectCoords: React.Dispatch<React.SetStateAction<IRectCoords | undefined>>
polygonFeature: Feature<Polygon>
) => {
const newExtent = polygonFeature.getGeometry()?.getExtent();
@ -164,14 +321,14 @@ export const updateImageSource = (
const topRight = polygonFeature.getGeometry()?.getCoordinates()[0][2]
const bottomRight = polygonFeature.getGeometry()?.getCoordinates()[0][3]
setRectCoords({
setRectCoords(map_id, {
bl: bottomLeft,
tl: topLeft,
tr: topRight,
br: bottomRight
})
setPolygonExtent(newExtent)
setPolygonExtent(map_id, newExtent)
if (newExtent && bottomLeft && bottomRight && topRight && topLeft) {
const originalExtent = calculateExtent(bottomLeft, topLeft, topRight, bottomRight)
@ -179,55 +336,58 @@ export const updateImageSource = (
url: imageUrl,
imageExtent: originalExtent,
projection: rotateProjection('EPSG:3857', calculateRotationAngle(bottomLeft, bottomRight), originalExtent)
});
imageLayer.current.setSource(newImageSource);
})
getImageLayer(map_id).setSource(newImageSource)
}
};
export const addInteractions = (
drawingLayerSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>,
translate: React.MutableRefObject<Translate | null>,
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>,
map_id: string
) => {
const currentTool = getCurrentTool()
const clearPrevious = getMeasureClearPrevious()
const measureType = getMeasureType()
const tipPoint = getTipPoint()
const currentTool = getCurrentTool(map_id)
const clearPrevious = getMeasureClearPrevious(map_id)
const measureType = getMeasureType(map_id)
const tipPoint = getTipPoint(map_id)
const map = getMap(map_id)
const measureModify = getMeasureModify(map_id)
if (currentTool !== 'Measure' && currentTool !== 'Mover' && currentTool !== 'Edit') {
draw.current = new Draw({
source: drawingLayerSource.current,
setDraw(map_id, new Draw({
source: getDrawingLayerSource(map_id),
type: currentTool as Type,
condition: noModifierKeys
})
}))
draw.current.on('drawend', function (s) {
console.log(s.feature.getGeometry()?.getType())
let type: IGeometryType = 'POLYGON'
const draw = getDraw(map_id)
switch (s.feature.getGeometry()?.getType()) {
case 'LineString':
type = 'LINE'
break
case 'Polygon':
type = 'POLYGON'
break
default:
type = 'POLYGON'
break
if (draw) {
draw.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?.addInteraction(draw)
setSnap(map_id, new Snap({ source: getDrawingLayerSource(map_id) }))
const snap = getSnap(map_id)
if (snap) {
map?.addInteraction(snap)
}
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') {
@ -238,35 +398,45 @@ export const addInteractions = (
const idleTip = 'Кликните, чтобы начать измерение';
let tip = idleTip;
measureDraw.current = new Draw({
source: measureSource.current,
setMeasureDraw(map_id, new Draw({
source: getMeasureSource(map_id),
type: drawType,
style: function (feature) {
return measureStyleFunction(feature, drawType, tip);
return measureStyleFunction(map_id, 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('');
}))
const measureDraw = getMeasureDraw(map_id)
if (measureDraw) {
measureDraw.on('drawstart', function () {
if (clearPrevious) {
getMeasureSource(map_id).clear();
}
measureModify.setActive(false);
tip = activeTip;
});
tip = idleTip;
});
measureModify.current.setActive(true);
map.current?.addInteraction(measureDraw.current);
measureDraw.on('drawend', function () {
modifyStyle.setGeometry(tipPoint as Geometry);
measureModify.setActive(true);
map?.once('pointermove', function () {
modifyStyle.setGeometry('');
});
tip = idleTip;
});
measureModify.setActive(true)
map?.addInteraction(measureDraw)
}
}
if (currentTool == 'Mover') {
translate.current = new Translate()
map?.current?.addInteraction(translate.current)
setTranslate(map_id, new Translate())
const translate = getTranslate(map_id)
if (translate) {
map?.addInteraction(translate)
}
}
if (currentTool == 'Edit') {
@ -275,90 +445,12 @@ export const addInteractions = (
}
}
export function regionsInit(
map: React.MutableRefObject<Map | null>,
selectedRegion: React.MutableRefObject<Feature<Geometry> | null>,
regionsLayer: React.MutableRefObject<VectorImageLayer<Feature<Geometry>, VectorSource<Feature<Geometry>>>>,
) {
regionsLayer.current.once('change', function () {
if (getSelectedCity() || getSelectedYear()) return
const extent = regionsLayer.current.getSource()?.getExtent()
if (extent && !extent?.every(val => Math.abs(val) === Infinity)) {
map.current?.getView().fit(fromExtent(extent) as SimpleGeometry, { duration: 500, maxZoom: 18, padding: [60, 60, 60, 60] })
}
})
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)
if (feature.get('id')) {
setSelectedRegion(feature.get('id'))
}
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) => {
export const zoomToFeature = (map_id: string, feature: Feature) => {
const geometry = feature.getGeometry()
const extent = geometry?.getExtent()
if (map.current && extent) {
map.current.getView().fit(extent, {
if (getMap(map_id) && extent) {
getMap(map_id)?.getView().fit(extent, {
duration: 300,
maxZoom: 19,
})
@ -366,8 +458,8 @@ const zoomToFeature = (map: React.MutableRefObject<Map | null>, feature: Feature
}
// Function to save features to localStorage
export const saveFeatures = (layerRef: MutableRefObject<VectorLayer<VectorSource> | null>) => {
const features = layerRef.current?.getSource()?.getFeatures()
export const saveFeatures = (map_id: string) => {
const features = getDrawingLayerSource(map_id).getFeatures()
if (features && features.length > 0) {
const geoJSON = new GeoJSON()
const featuresJSON = geoJSON.writeFeatures(features)
@ -376,14 +468,14 @@ export const saveFeatures = (layerRef: MutableRefObject<VectorLayer<VectorSource
}
// Function to load features from localStorage
export const loadFeatures = (layerSource: React.MutableRefObject<VectorSource<Feature<Geometry>>>) => {
export const loadFeatures = (map_id: string) => {
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
getDrawingLayerSource(map_id).addFeatures(features)
//drawingLayer.current?.getSource()?.changed()
}
}
@ -534,6 +626,25 @@ function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number)
return { tileX, tileY };
}
function calculateTransformations(alignPoints: Coordinate[]) {
const [P1, P2, P3, P4] = alignPoints;
// Translation vector (move P1 to P3)
const translation = [P3[0] - P1[0], P3[1] - P1[1]];
// Scaling factor (distance between P3 and P4 divided by P1 and P2)
const distanceLayer = Math.sqrt((P2[0] - P1[0]) ** 2 + (P2[1] - P1[1]) ** 2);
const distanceMap = Math.sqrt((P4[0] - P3[0]) ** 2 + (P4[1] - P3[1]) ** 2);
const scale = distanceMap / distanceLayer;
// Rotation angle (difference in angles between the two lines)
const angleLayer = Math.atan2(P2[1] - P1[1], P2[0] - P1[0]);
const angleMap = Math.atan2(P4[1] - P3[1], P4[0] - P3[0]);
const rotation = angleMap - angleLayer;
return { translation, scale, rotation };
}
function calculateCenter(geometry: SimpleGeometry) {
let center, coordinates, minRadius;
const type = geometry.getType();
@ -577,8 +688,41 @@ function calculateCenter(geometry: SimpleGeometry) {
};
}
function applyTransformations(
layer: VectorLayer,
transformations: {
translation: number[];
scale: number;
rotation: number;
},
origin: Coordinate
) {
const { translation, scale, rotation } = transformations;
const source = layer.getSource();
if (!source) return;
source.getFeatures().forEach((feature) => {
const geometry = feature.getGeometry();
if (geometry) {
// Translate
geometry.translate(translation[0], translation[1]);
// Scale (around the origin)
geometry.scale(scale, scale, origin);
// Rotate (around the origin)
geometry.rotate(rotation, origin);
}
});
console.log("Transformations applied to figuresLayer");
}
export {
rotateProjection,
calculateTransformations,
calculateRotationAngle,
calculateExtent,
calculateCentroid,
@ -586,5 +730,6 @@ export {
normalize,
getTileIndex,
getGridCellPosition,
calculateCenter
calculateCenter,
applyTransformations
}