This commit is contained in:
cracklesparkle
2025-01-10 11:38:00 +09:00
parent 59fded5cab
commit 2bf657e8ed
10 changed files with 855 additions and 210 deletions

View File

@ -8,11 +8,11 @@ import { Tile as TileLayer, VectorImage, Vector as VectorLayer } from 'ol/layer'
import { click, never, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
import Feature from 'ol/Feature'
import { IRectCoords, SatelliteMapsProvider } from '../../interfaces/map'
import { Extent } from 'ol/extent'
import { drawingLayerStyle, highlightStyleRed, highlightStyleYellow, overlayStyle, regionsLayerStyle } from './MapStyles'
import { Extent, getCenter } from 'ol/extent'
import { drawingLayerStyle, figureStyle, highlightStyleRed, highlightStyleYellow, lineStyle, overlayStyle, regionsLayerStyle } from './MapStyles'
import { customMapSource, googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources'
import ImageLayer from 'ol/layer/Image'
import { LineString, Point, SimpleGeometry } from 'ol/geom'
import { Geometry, LineString, Point, SimpleGeometry } from 'ol/geom'
import { fromExtent } from 'ol/geom/Polygon'
import Collection from 'ol/Collection'
import { Coordinate } from 'ol/coordinate'
@ -23,14 +23,14 @@ import useSWR, { SWRConfiguration } from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { ActionIcon, Autocomplete, Box, CloseButton, Flex, Select as MantineSelect, MantineStyleProp, rem, useMantineColorScheme, Portal, Menu, Button, Group, Divider, LoadingOverlay } from '@mantine/core'
import { IconBoxMultiple, IconChevronDown, IconPlus, IconSearch, IconUpload } from '@tabler/icons-react'
import { IconBoxMultiple, IconBoxPadding, IconChevronDown, IconPlus, IconSearch, IconUpload } from '@tabler/icons-react'
import { getGridCellPosition } from './mapUtils'
import { IFigure, ILine } from '../../interfaces/gis'
import axios from 'axios'
import MapToolbar from './MapToolbar/MapToolbar'
import MapStatusbar from './MapStatusbar/MapStatusbar'
import { measureStyleFunction, modifyStyle } from './Measure/MeasureStyles'
import { setCurrentCoordinate, setCurrentX, setCurrentY, setCurrentZ, setSatMapsProvider, useMapStore } from '../../store/map'
import { setCurrentCoordinate, setCurrentX, setCurrentY, setCurrentZ, setAlignMode, setSatMapsProvider, useMapStore, getAlignMode } from '../../store/map'
import { v4 as uuidv4 } from 'uuid'
import { useThrottle } from '@uidotdev/usehooks'
import ObjectTree from '../Tree/ObjectTree'
@ -38,7 +38,9 @@ import { setCurrentObjectId, setSelectedDistrict, setSelectedRegion, setSelected
import MapLayers from './MapLayers/MapLayers'
import ObjectParameters from './ObjectParameters/ObjectParameters'
import TabsPane, { ITabsPane } from './TabsPane/TabsPane'
import Link from 'ol/interaction/Link'
import { useSearchParams } from 'react-router-dom'
import GeoJSON from 'ol/format/GeoJSON'
import { Stroke, Style } from 'ol/style'
const swrOptions: SWRConfiguration = {
revalidateOnFocus: false
@ -98,7 +100,6 @@ const citySettings: ICitySettings[] = [
city_id: 145,
image_width: 8500,
image_height: 12544,
// scale: 10000,
scale: 9000,
offset_x: 14442665.697619518,
offset_y: 8884520.63524492,
@ -109,7 +110,6 @@ const citySettings: ICitySettings[] = [
city_id: 146,
image_width: 8500,
image_height: 12544,
// scale: 10000,
scale: 8000,
offset_x: 14416475.697619518,
offset_y: 8889280.63524492,
@ -124,10 +124,10 @@ function getCitySettings(
const settings = citySettings.find(el => el.city_id === city_id)
if (settings) {
console.log("City settings found")
//console.log("City settings found")
return settings
} else {
console.log("City settings NOT found")
//console.log("City settings NOT found")
return {
city_id: 0,
image_width: 8500,
@ -145,7 +145,7 @@ const MapComponent = () => {
// States
const { selectedYear, currentObjectId, selectedRegion, selectedDistrict } = useObjectsStore()
const { currentTool, satMapsProvider, selectedObjectType } = useMapStore()
const { currentTool, satMapsProvider, selectedObjectType, alignMode } = useMapStore()
///
const [file, setFile] = useState<File | null>(null)
@ -169,6 +169,65 @@ const MapComponent = () => {
}
}))
const alignModeLayer = useRef(new VectorLayer({
source: new VectorSource(),
properties: {
id: uuidv4(),
type: 'align',
name: 'Подгонка'
}
}))
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 applyTransformations(figuresLayer: React.MutableRefObject<VectorLayer>, transformations: {
translation: number[];
scale: number;
rotation: number;
}, origin: Coordinate) {
const { translation, scale, rotation } = transformations;
const source = figuresLayer.current.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");
}
const alignPoints = useRef<Coordinate[]>([])
const measureDraw = useRef<Draw | null>(null)
const mapElement = useRef<HTMLDivElement | null>(null)
@ -192,7 +251,7 @@ const MapComponent = () => {
const selectFeature = useRef<Select>(new Select({
condition: function (mapBrowserEvent) {
return click(mapBrowserEvent) && shiftKeyOnly(mapBrowserEvent);
return click(mapBrowserEvent) && shiftKeyOnly(mapBrowserEvent)
},
}))
@ -220,6 +279,10 @@ const MapComponent = () => {
id: uuidv4(),
type: 'figures',
name: 'Фигуры'
},
style: function (feature) {
figureStyle.getText()?.setText(feature.get('object_id'))
return figureStyle
}
}))
@ -230,6 +293,12 @@ const MapComponent = () => {
id: uuidv4(),
type: 'lines',
name: 'Линии'
},
style: function (feature) {
//lineStyle.getText()?.setText('11,4')//(feature.get('object_id'))
lineStyle.getText()?.setRotation(feature.get('rotation'))
lineStyle.getText()?.setOffsetY(-8)
return lineStyle
}
}))
@ -243,6 +312,33 @@ const MapComponent = () => {
}
}))
const districtBoundLayer = useRef<VectorImage>(new VectorImage({
style: new Style({
stroke: new Stroke({
color: 'red',
width: 2,
}),
})
}))
useEffect(() => {
if (selectedDistrict && selectedYear) {
const bounds = new VectorSource({
url: `bounds/cities/${selectedDistrict}.geojson`,
format: new GeoJSON(),
})
districtBoundLayer.current.setSource(bounds)
bounds.on('featuresloadend', function () {
// map.current?.setView(new View({
// extent: bounds.getExtent()
// }))
console.log(bounds.getFeatures().length)
})
}
}, [selectedDistrict, selectedYear])
const selectedArea = useRef<Feature | null>(null)
const baseLayer = useRef<TileLayer>(new TileLayer({
@ -450,6 +546,7 @@ const MapComponent = () => {
satLayer.current,
staticMapLayer.current,
regionsLayer.current,
districtBoundLayer.current,
citiesLayer.current,
linesLayer.current,
figuresLayer.current,
@ -457,7 +554,8 @@ const MapComponent = () => {
imageLayer.current,
overlayLayer.current,
nodeLayer.current,
measureLayer.current
measureLayer.current,
alignModeLayer.current
],
target: mapElement.current as HTMLDivElement,
view: new View({
@ -481,19 +579,43 @@ const MapComponent = () => {
if (pixel) {
map.current?.forEachFeatureAtPixel(pixel, function (feature) {
if (feature.get('geometry_type') === 'line') {
console.log(feature.getProperties())
//console.log(feature.getProperties())
}
})
}
})
map.current.on('click', function (e: MapBrowserEvent<UIEvent>) {
const pixel = map.current?.getEventPixel(e.originalEvent)
if (getAlignMode()) {
if (alignPoints.current.length < 4) {
alignPoints.current.push(e.coordinate)
alignModeLayer.current.getSource()?.addFeature(new Feature(new Point(e.coordinate)))
if (alignPoints.current.length === 4) {
console.log("collected 4 points, now can align")
console.log(alignPoints.current)
const transformations = calculateTransformations(alignPoints.current);
console.log(transformations)
if (pixel) {
map.current?.forEachFeatureAtPixel(pixel, function (feature) {
setCurrentObjectId(feature.get('object_id'))
})
// Use the first map point (P3) as the origin for scaling and rotation
const origin = alignPoints.current[2];
console.log(origin)
applyTransformations(figuresLayer, transformations, origin);
applyTransformations(linesLayer, transformations, origin);
console.log("Figures layer aligned!")
alignPoints.current = []
alignModeLayer.current.getSource()?.clear()
}
}
} else {
const pixel = map.current?.getEventPixel(e.originalEvent)
if (pixel) {
map.current?.forEachFeatureAtPixel(pixel, function (feature) {
setCurrentObjectId(feature.get('object_id'))
})
}
}
})
@ -706,30 +828,92 @@ const MapComponent = () => {
swrOptions
)
const link = useRef<Link>(new Link())
const { data: districtData } = useSWR(
selectedDistrict ? `/gis/images/all?city_id=${selectedDistrict}` : null,
(url) => axios.get(url, {
baseURL: BASE_URL.ems
}).then((res) => Array.isArray(res.data) ? res.data[0] : null),
swrOptions
)
const [searchParams, setSearchParams] = useSearchParams()
useEffect(() => {
if (link.current) {
if (selectedRegion) {
link.current.update('r', selectedRegion?.toString())
}
if (selectedRegion) {
setSearchParams((params) => {
params.set('r', selectedRegion.toString());
return params
})
}
if (selectedDistrict) {
link.current.update('d', selectedDistrict?.toString())
}
if (selectedDistrict) {
setSearchParams((params) => {
params.set('d', selectedDistrict?.toString());
return params
})
}
if (selectedYear) {
link.current.update('y', selectedYear?.toString())
}
if (selectedYear) {
setSearchParams((params) => {
params.set('y', selectedYear?.toString());
return params
})
}
if (currentObjectId) {
link.current.update('o', currentObjectId?.toString())
if (currentObjectId) {
setSearchParams((params) => {
params.set('o', currentObjectId?.toString());
return params
})
}
}, [selectedRegion, selectedDistrict, selectedYear, currentObjectId, setSearchParams])
useEffect(() => {
if (Array.isArray(regionsData)) {
const region = searchParams.get('r')
if (searchParams.get('r')) {
setSelectedRegion(Number(region))
}
}
}, [link, selectedRegion, selectedDistrict, selectedYear, currentObjectId])
}, [searchParams, regionsData])
useEffect(() => {
if (selectedDistrict) {
if (Array.isArray(regionsData)) {
const district = searchParams.get('d')
if (Array.isArray(districtsData)) {
if (district) {
setSelectedDistrict(Number(district))
}
}
}
}, [searchParams, regionsData, districtsData])
useEffect(() => {
if (Array.isArray(regionsData)) {
const year = searchParams.get('y')
if (year) {
setSelectedYear(Number(year))
}
}
}, [searchParams, regionsData])
useEffect(() => {
const object = searchParams.get('o')
if (figuresData && linesData && object) {
setCurrentObjectId(object)
}
}, [searchParams, figuresData, linesData])
useEffect(() => {
const districtBoundSource: VectorSource<Feature<Geometry>> | null = districtBoundLayer.current.getSource()
if (selectedDistrict && districtBoundSource && districtBoundSource.getFeatures().length > 0) {
const center: Coordinate = getCenter(districtBoundSource.getExtent())
const settings = getCitySettings(selectedDistrict)
if (Array.isArray(figuresData)) {
@ -739,7 +923,7 @@ const MapComponent = () => {
processFigure(
figure,
settings.scale,
[settings.offset_x, settings.offset_y],
[center[0], center[1]],
figuresLayer
)
})
@ -758,16 +942,23 @@ const MapComponent = () => {
linesLayer.current.getSource()?.clear()
if (linesData.length > 0) {
linesData.map((line: ILine) => {
processLine(line, settings.scale, [settings.offset_x, settings.offset_y], linesLayer)
processLine(line, settings.scale, [center[0], center[1]], linesLayer)
})
}
}
}
}, [figuresData, linesData, selectedDistrict, selectedYear])
}, [figuresData, linesData, selectedDistrict, selectedYear, districtBoundLayer])
useEffect(() => {
if (selectedDistrict) {
if (!selectedRegion) {
setSelectedDistrict(null)
setSelectedYear(null)
}
}, [selectedRegion, selectedDistrict])
useEffect(() => {
if (selectedDistrict && districtData) {
const settings = getCitySettings(selectedDistrict)
const imageUrl = `${import.meta.env.VITE_API_EMS_URL}/static/${selectedDistrict}`;
@ -775,14 +966,13 @@ const MapComponent = () => {
img.src = imageUrl;
img.onload = () => {
if (map.current) {
console.log(districtData)
const width = img.naturalWidth
const height = img.naturalHeight
console.log(width, height)
//const k = (width < height ? width / height : height / width)
const k = settings.image_scale
console.log(k)
const wk = width * k
const hk = height * k
@ -806,7 +996,7 @@ const MapComponent = () => {
}
};
}
}, [selectedDistrict])
}, [selectedDistrict, districtData])
useEffect(() => {
if (baseLayer.current) {
@ -869,6 +1059,13 @@ const MapComponent = () => {
data={regionsData ? regionsData.map((item: { name: string, id: number }) => ({ label: item.name, value: item.id.toString() })) : []}
onChange={(value) => setSelectedRegion(Number(value))}
clearable
onClear={() => {
setSelectedRegion(null)
setSearchParams((params) => {
params.delete('r')
return params
})
}}
searchable
value={selectedRegion ? selectedRegion.toString() : null}
/>
@ -879,6 +1076,13 @@ const MapComponent = () => {
data={districtsData ? districtsData.map((item: { name: string, id: number, district_name: string }) => ({ label: [item.name, item.district_name].join(' - '), value: item.id.toString() })) : []}
onChange={(value) => setSelectedDistrict(Number(value))}
clearable
onClear={() => {
setSelectedDistrict(null)
setSearchParams((params) => {
params.delete('d')
return params
})
}}
searchable
value={selectedDistrict ? selectedDistrict.toString() : null}
/>
@ -888,12 +1092,27 @@ const MapComponent = () => {
w='92px'
data={['2018', '2019', '2020', '2021', '2022', '2023', '2024'].map(el => ({ label: el, value: el }))}
onChange={(e) => {
setSelectedYear(Number(e))
if (e) {
setSelectedYear(Number(e))
} else {
setSelectedYear(null)
}
}}
defaultValue={selectedYear?.toString()}
allowDeselect={false}
onClear={() => {
setSelectedYear(null)
setSearchParams((params) => {
params.delete('y')
return params
})
}}
value={selectedYear ? selectedYear?.toString() : null}
clearable
/>
<Button variant={alignMode ? 'filled' : 'transparent'} onClick={() => setAlignMode(!alignMode)}>
<IconBoxPadding style={{ width: rem(20), height: rem(20) }} />
</Button>
<Menu
position="bottom-end"
transitionProps={{ transition: 'pop-top-right' }}

View File

@ -1,4 +1,4 @@
import Feature, { FeatureLike } from "ol/Feature";
import { FeatureLike } from "ol/Feature";
import { Text } from "ol/style";
import Fill from "ol/style/Fill";
import { FlatStyleLike } from "ol/style/flat";
@ -90,101 +90,42 @@ export function overlayStyle(feature: FeatureLike) {
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')
})
const figureStyle = 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
})
];
}
})
})
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 lineStyle = 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
}),
placement: 'line',
overflow: true,
//declutterMode: 'obstacle'
})
})
const drawingLayerStyle: FlatStyleLike = {
'fill-color': 'rgba(255, 255, 255, 0.2)',
@ -218,5 +159,7 @@ const regionsLayerStyle = new Style({
export {
drawingLayerStyle,
selectStyle,
regionsLayerStyle
regionsLayerStyle,
lineStyle,
figureStyle
}

View File

@ -9,7 +9,7 @@ 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 { selectStyle } from "./MapStyles";
import { Type } from "ol/geom/Geometry";
import { Draw, Modify, Snap, Translate } from "ol/interaction";
import { noModifierKeys } from "ol/events/condition";
@ -24,6 +24,13 @@ import { getCurrentTool, getMeasureClearPrevious, getMeasureType, getTipPoint, s
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,
@ -37,18 +44,18 @@ export function processLine(
const center = [mapCenter[0], mapCenter[1]]
const testCoords = [
const testCoords: [number, number][] = [
[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('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)
}
@ -77,7 +84,6 @@ export function processFigure(
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)
@ -108,7 +114,6 @@ export function processFigure(
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)
}
}
@ -140,7 +145,6 @@ export function processFigure(
feature1.set('planning', figure.planning)
feature1.set('type', figure.type)
feature1.set('angle', figure.angle)
feature1.setStyle(fourthStyleFunction(feature1))
figuresLayer.current?.getSource()?.addFeature(feature1)
}
}
@ -277,12 +281,11 @@ export function regionsInit(
regionsLayer: React.MutableRefObject<VectorImageLayer<Feature<Geometry>, VectorSource<Feature<Geometry>>>>,
) {
regionsLayer.current.once('change', function () {
if (getSelectedCity() === null || getSelectedYear() === null) {
const extent = regionsLayer.current.getSource()?.getExtent()
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] })
}
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] })
}
})