590 lines
21 KiB
TypeScript
590 lines
21 KiB
TypeScript
import { Coordinate, distance, rotate } from "ol/coordinate";
|
|
import { containsExtent, 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 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 { 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";
|
|
|
|
const calculateAngle = (coords: [number, number][]) => {
|
|
const [start, end] = coords;
|
|
const dx = end[0] - start[0];
|
|
const dy = end[1] - start[1];
|
|
return Math.atan2(dy, dx); // Angle in radians
|
|
}
|
|
|
|
export function processLine(
|
|
line: ILine,
|
|
scaling: number,
|
|
mapCenter: Coordinate,
|
|
linesLayer: MutableRefObject<VectorLayer<VectorSource>>
|
|
) {
|
|
const x1 = line.x1 * scaling
|
|
const y1 = line.y1 * scaling
|
|
const x2 = line.x2 * scaling
|
|
const y2 = line.y2 * scaling
|
|
|
|
const center = [mapCenter[0], mapCenter[1]]
|
|
|
|
const testCoords: [number, number][] = [
|
|
[center[0] + x1, center[1] - y1],
|
|
[center[0] + x2, center[1] - y2],
|
|
]
|
|
|
|
const feature = new Feature(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)
|
|
}
|
|
|
|
export function processFigure(
|
|
figure: IFigure,
|
|
scaling: number,
|
|
mapCenter: Coordinate,
|
|
figuresLayer: MutableRefObject<VectorLayer<VectorSource>>
|
|
) {
|
|
if (figure.figure_type_id == 1) {
|
|
const width = figure.width * scaling
|
|
const height = figure.height * scaling
|
|
|
|
const left = figure.left * scaling
|
|
const top = figure.top * scaling
|
|
|
|
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.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
|
|
const y = figure.top * scaling
|
|
|
|
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),
|
|
center[1] - (y * scaling)
|
|
]
|
|
})
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
if (figure.figure_type_id == 4) {
|
|
const width = figure.width * scaling
|
|
const height = figure.height * scaling
|
|
const left = figure.left * scaling
|
|
const top = figure.top * scaling
|
|
|
|
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)
|
|
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>>>,
|
|
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>,
|
|
) => {
|
|
const currentTool = getCurrentTool()
|
|
const clearPrevious = getMeasureClearPrevious()
|
|
const measureType = getMeasureType()
|
|
const tipPoint = getTipPoint()
|
|
|
|
if (currentTool !== 'Measure' && currentTool !== 'Mover' && currentTool !== 'Edit') {
|
|
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);
|
|
}
|
|
|
|
if (currentTool == 'Mover') {
|
|
translate.current = new Translate()
|
|
map?.current?.addInteraction(translate.current)
|
|
}
|
|
|
|
if (currentTool == 'Edit') {
|
|
//const modify = new Modify()
|
|
//map?.current?.addInteraction(translate.current)
|
|
}
|
|
}
|
|
|
|
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) => {
|
|
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: MutableRefObject<VectorLayer<VectorSource> | 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) {
|
|
const coord = rotate(
|
|
[coordinate[0] - anchor[0], coordinate[1] - anchor[1]],
|
|
angle
|
|
);
|
|
return [coord[0] + anchor[0], coord[1] + anchor[1]];
|
|
}
|
|
|
|
function rotateTransform(coordinate: Coordinate) {
|
|
return rotateCoordinate(coordinate, angle, getCenter(extent));
|
|
}
|
|
|
|
function normalTransform(coordinate: Coordinate) {
|
|
return rotateCoordinate(coordinate, -angle, getCenter(extent));
|
|
}
|
|
|
|
const normalProjection = get(projection);
|
|
|
|
if (normalProjection) {
|
|
const rotatedProjection = new Projection({
|
|
code: normalProjection.getCode() + ":" + angle.toString() + ":" + extent.toString(),
|
|
units: normalProjection.getUnits(),
|
|
extent: extent
|
|
});
|
|
addProjection(rotatedProjection);
|
|
|
|
addCoordinateTransforms(
|
|
"EPSG:4326",
|
|
rotatedProjection,
|
|
function (coordinate) {
|
|
return rotateTransform(transform(coordinate, "EPSG:4326", projection));
|
|
},
|
|
function (coordinate) {
|
|
return transform(normalTransform(coordinate), projection, "EPSG:4326");
|
|
}
|
|
);
|
|
|
|
addCoordinateTransforms(
|
|
"EPSG:3857",
|
|
rotatedProjection,
|
|
function (coordinate) {
|
|
return rotateTransform(transform(coordinate, "EPSG:3857", projection));
|
|
},
|
|
function (coordinate) {
|
|
return transform(normalTransform(coordinate), projection, "EPSG:3857");
|
|
}
|
|
);
|
|
|
|
// also set up transforms with any projections defined using proj4
|
|
if (typeof proj4 !== "undefined") {
|
|
const projCodes = Object.keys(proj4.defs);
|
|
projCodes.forEach(function (code) {
|
|
const proj4Projection = get(code) as Projection;
|
|
if (proj4Projection) {
|
|
if (!getTransform(proj4Projection, rotatedProjection)) {
|
|
addCoordinateTransforms(
|
|
proj4Projection,
|
|
rotatedProjection,
|
|
function (coordinate) {
|
|
return rotateTransform(
|
|
transform(coordinate, proj4Projection, projection)
|
|
);
|
|
},
|
|
function (coordinate) {
|
|
return transform(
|
|
normalTransform(coordinate),
|
|
projection,
|
|
proj4Projection
|
|
);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return rotatedProjection;
|
|
}
|
|
}
|
|
|
|
const calculateCentroid = (bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate) => {
|
|
const x = (bottomLeft[0] + topLeft[0] + topRight[0] + bottomRight[0]) / 4;
|
|
const y = (bottomLeft[1] + topLeft[1] + topRight[1] + bottomRight[1]) / 4;
|
|
return [x, y];
|
|
}
|
|
|
|
function calculateRotationAngle(bottomLeft: Coordinate, bottomRight: Coordinate) {
|
|
// Calculate the difference in x and y coordinates between bottom right and bottom left
|
|
const deltaX = bottomRight[0] - bottomLeft[0];
|
|
const deltaY = bottomRight[1] - bottomLeft[1];
|
|
|
|
// Calculate the angle using atan2
|
|
const angle = -Math.atan2(deltaY, deltaX);
|
|
|
|
return angle;
|
|
}
|
|
|
|
function calculateExtent(bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate) {
|
|
const width = distance(bottomLeft, bottomRight);
|
|
const height = distance(bottomLeft, topLeft);
|
|
|
|
// Calculate the centroid of the polygon
|
|
const [centerX, centerY] = calculateCentroid(bottomLeft, topLeft, topRight, bottomRight);
|
|
|
|
// Define the extent based on the center and dimensions
|
|
const extent = [
|
|
centerX - width / 2, // minX
|
|
centerY - height / 2, // minY
|
|
centerX + width / 2, // maxX
|
|
centerY + height / 2 // maxY
|
|
];
|
|
|
|
return extent;
|
|
}
|
|
|
|
function getTilesPerSide(zoom: number) {
|
|
return Math.pow(2, zoom)
|
|
}
|
|
|
|
function normalize(value: number, min: number, max: number) {
|
|
return (value - min) / (max - min)
|
|
}
|
|
|
|
function getTileIndex(normalized: number, tilesPerSide: number) {
|
|
return Math.floor(normalized * tilesPerSide)
|
|
}
|
|
|
|
function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number) {
|
|
const tilesPerSide = getTilesPerSide(zoom);
|
|
const minX = extent[0]
|
|
const minY = extent[1]
|
|
const maxX = extent[2]
|
|
const maxY = extent[3]
|
|
|
|
// Normalize the coordinates
|
|
const xNormalized = normalize(x, minX, maxX);
|
|
const yNormalized = normalize(y, minY, maxY);
|
|
|
|
// Get tile indices
|
|
const tileX = getTileIndex(xNormalized, tilesPerSide);
|
|
const tileY = getTileIndex(1 - yNormalized, tilesPerSide);
|
|
|
|
return { tileX, tileY };
|
|
}
|
|
|
|
function calculateCenter(geometry: SimpleGeometry) {
|
|
let center, coordinates, minRadius;
|
|
const type = geometry.getType();
|
|
if (type === 'Polygon') {
|
|
let x = 0;
|
|
let y = 0;
|
|
let i = 0;
|
|
coordinates = (geometry as Polygon).getCoordinates()[0].slice(1);
|
|
coordinates.forEach(function (coordinate) {
|
|
x += coordinate[0];
|
|
y += coordinate[1];
|
|
i++;
|
|
});
|
|
center = [x / i, y / i];
|
|
} else if (type === 'LineString') {
|
|
center = (geometry as LineString).getCoordinateAt(0.5);
|
|
coordinates = geometry.getCoordinates();
|
|
} else {
|
|
center = getCenter(geometry.getExtent());
|
|
}
|
|
let sqDistances;
|
|
if (coordinates) {
|
|
sqDistances = coordinates.map(function (coordinate: Coordinate) {
|
|
const dx = coordinate[0] - center[0];
|
|
const dy = coordinate[1] - center[1];
|
|
return dx * dx + dy * dy;
|
|
});
|
|
minRadius = Math.sqrt(Math.max(...sqDistances)) / 3;
|
|
} else {
|
|
minRadius =
|
|
Math.max(
|
|
getWidth(geometry.getExtent()),
|
|
getHeight(geometry.getExtent()),
|
|
) / 3;
|
|
}
|
|
return {
|
|
center: center,
|
|
coordinates: coordinates,
|
|
minRadius: minRadius,
|
|
sqDistances: sqDistances,
|
|
};
|
|
}
|
|
|
|
export {
|
|
rotateProjection,
|
|
calculateRotationAngle,
|
|
calculateExtent,
|
|
calculateCentroid,
|
|
getTilesPerSide,
|
|
normalize,
|
|
getTileIndex,
|
|
getGridCellPosition,
|
|
calculateCenter
|
|
} |