WholeProcessPlatform/frontend/src/components/gis/ol/region-mask-manager.ts

342 lines
9.8 KiB
TypeScript

import OlMap from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import GeoJSON from 'ol/format/GeoJSON';
import VectorSource from 'ol/source/Vector';
import { fromLonLat } from 'ol/proj';
import { PointLayerManager } from './point-layer-manager';
type RegionMaskManagerOptions = {
map: OlMap | null;
view: View | null;
pointLayerManager: PointLayerManager;
defaultCenter: [number, number];
defaultZoom: number;
};
export class RegionMaskManager {
private map: OlMap | null;
private view: View | null;
private pointLayerManager: PointLayerManager;
private defaultCenter: [number, number];
private defaultZoom: number;
private maskBindings: Array<{
layer: TileLayer;
prerender: (event: any) => void;
postrender: (event: any) => void;
}> = [];
constructor(options: RegionMaskManagerOptions) {
this.map = options.map;
this.view = options.view;
this.pointLayerManager = options.pointLayerManager;
this.defaultCenter = options.defaultCenter;
this.defaultZoom = options.defaultZoom;
}
// 备注:同步地图和视图上下文,供地图初始化、销毁和重建后复用区域过滤能力。
setContext(map: OlMap | null, view: View | null) {
this.map = map;
this.view = view;
}
// 备注:统一隐藏所有点位的区域显示状态,供基地裁切前的预处理复用。
hideAllPoints(): void {
this.pointLayerManager.forEachFeature(feature => {
feature.set('_regionVisible', false);
feature.changed();
});
}
// 备注:统一恢复所有点位的区域显示状态,供取消裁切或异常兜底时复用。
showAllPoints(): void {
this.pointLayerManager.forEachFeature(feature => {
feature.set('_regionVisible', true);
feature.changed();
});
}
// 备注:根据 GeoJSON 边界批量更新点位的区域显隐状态。
filterPointsByRegion(geoJson: any): void {
const regionCoords = this.extractPolygonCoords(geoJson);
if (!regionCoords.length) {
console.warn('无法解析区域边界,显示所有锚点');
this.showAllPoints();
return;
}
this.pointLayerManager.forEachFeature(feature => {
const props = feature.getProperties();
const lon = Number(props.lgtd);
const lat = Number(props.lttd);
if (!isFinite(lon) || !isFinite(lat)) {
feature.set('_regionVisible', false);
feature.changed();
return;
}
feature.set('_regionVisible', this.isPointInPolygon(lon, lat, regionCoords));
feature.changed();
});
}
// 备注:根据 GeoJSON 范围拟合地图视野,供基地切换后快速聚焦到目标区域。
fitViewToGeoJson(geoJson: any): void {
if (!this.map || !this.view) return;
try {
const features = new GeoJSON().readFeatures(geoJson, {
dataProjection: 'EPSG:4326',
featureProjection: 'EPSG:3857'
});
if (!features.length) return;
const source = new VectorSource({
features
});
const extent = source.getExtent();
if (extent[0] === Infinity || extent[0] === -Infinity) {
console.warn('无法计算有效的地图包围盒');
return;
}
this.view.fit(extent, {
padding: [100, 200, 50, 50],
duration: 1000,
maxZoom: 13
});
} catch (error) {
console.error('调整地图视野失败:', error);
}
}
// 备注:把 GeoJSON 边界绑定到底图 prerender/postrender 事件,实现区域裁切遮罩。
applyMapMask(rasterLayer: TileLayer, clipGeoJson: any): void {
this.applyMapMaskToLayers(rasterLayer ? [rasterLayer] : [], clipGeoJson);
}
// 备注:把 GeoJSON 边界同时绑定到多个底图图层,保证多底图叠加场景下裁切边界保持一致。
applyMapMaskToLayers(rasterLayers: TileLayer[] = [], clipGeoJson: any): void {
if (!clipGeoJson) return;
const targetLayers = rasterLayers.filter(Boolean);
if (targetLayers.length === 0) return;
const features = new GeoJSON().readFeatures(clipGeoJson, {
dataProjection: 'EPSG:4326',
featureProjection: 'EPSG:3857'
});
if (!features.length) {
console.warn('裁切数据为空,无法应用遮罩');
return;
}
this.clearMaskEvents();
targetLayers.forEach(rasterLayer => {
const maskPrerender = (event: any) => {
const context = event.context;
const frameState = event.frameState;
if (!context || !frameState || !this.map) return;
context.save();
context.setTransform(1, 0, 0, 1, 0, 0);
context.beginPath();
let hasValidPath = false;
for (const feature of features) {
const geometry = feature.getGeometry();
if (!geometry) continue;
if (geometry.getType() === 'Polygon') {
const rings = (geometry as any).getCoordinates() as number[][][];
for (const ring of rings) {
if (this.drawRingToContext(context, ring, frameState)) {
hasValidPath = true;
}
}
} else if (geometry.getType() === 'MultiPolygon') {
const polygons = (geometry as any).getCoordinates() as number[][][][];
for (const polygonRings of polygons) {
for (const ring of polygonRings) {
if (this.drawRingToContext(context, ring, frameState)) {
hasValidPath = true;
}
}
}
}
}
if (!hasValidPath) {
console.warn('未能生成有效的裁切路径');
return;
}
context.save();
context.lineWidth = 6;
context.strokeStyle = '#6D64DF';
context.stroke();
context.clip();
context.strokeStyle = '#CCC9F4';
context.lineWidth = 5;
context.stroke();
context.clip();
};
const maskPostrender = (event: any) => {
if (event.context) {
event.context.restore();
event.context.restore();
}
};
this.maskBindings.push({
layer: rasterLayer,
prerender: maskPrerender,
postrender: maskPostrender
});
rasterLayer.on('prerender', maskPrerender);
rasterLayer.on('postrender', maskPostrender);
rasterLayer.changed();
});
}
// 备注:清除当前底图遮罩并按需恢复默认视野,供切换基地和异常兜底复用。
clearMapMask(resetView: boolean = true): void {
const maskedLayers = this.maskBindings.map(binding => binding.layer);
this.clearMaskEvents();
maskedLayers.forEach(layer => layer.changed());
if (resetView && this.view) {
this.view.animate({
center: fromLonLat(this.defaultCenter),
zoom: this.defaultZoom,
duration: 1000
});
}
}
// 备注:统一销毁区域过滤相关引用和事件,供地图销毁阶段复用。
destroy(): void {
this.clearMapMask(false);
this.map = null;
this.view = null;
}
// 备注:从 GeoJSON 中提取可用于点在面判断的多边形环坐标。
private extractPolygonCoords(geoJson: any): number[][][] {
if (!geoJson?.features?.length) {
return [];
}
const coords: number[][][] = [];
geoJson.features.forEach((feature: any) => {
const geometry = feature.geometry;
if (!geometry) return;
if (geometry.type === 'Polygon') {
coords.push(...geometry.coordinates);
} else if (geometry.type === 'MultiPolygon') {
geometry.coordinates.forEach((polygon: number[][][]) => {
coords.push(...polygon);
});
}
});
return coords;
}
// 备注:使用射线法判断点是否落在任意一个区域环内。
private isPointInPolygon(
lon: number,
lat: number,
polygons: number[][][]
): boolean {
for (const polygon of polygons) {
if (this.isPointInRing(lon, lat, polygon)) {
return true;
}
}
return false;
}
// 备注:使用射线法判断点是否落在单个闭合环内。
private isPointInRing(lon: number, lat: number, ring: number[][]): boolean {
let inside = false;
const ringLength = ring.length;
for (let i = 0, j = ringLength - 1; i < ringLength; j = i++) {
const xi = ring[i][0];
const yi = ring[i][1];
const xj = ring[j][0];
const yj = ring[j][1];
const intersect =
yi > lat !== yj > lat &&
lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
if (intersect) {
inside = !inside;
}
}
return inside;
}
// 备注:把单个面环转换为当前帧 Canvas 可裁切的路径。
private drawRingToContext(
context: CanvasRenderingContext2D,
ring: number[][],
frameState: any
): boolean {
if (!ring || ring.length < 3) return false;
const pixelRatio = frameState.pixelRatio || window.devicePixelRatio || 1;
let moved = false;
for (let i = 0; i < ring.length; i++) {
const coord = ring[i];
if (!coord || coord.length < 2 || typeof coord[0] !== 'number') {
continue;
}
const cssPixel = this.map?.getPixelFromCoordinate(coord as [number, number]);
if (!cssPixel) continue;
const canvasX = cssPixel[0] * pixelRatio;
const canvasY = cssPixel[1] * pixelRatio;
if (!moved) {
context.moveTo(canvasX, canvasY);
moved = true;
} else {
context.lineTo(canvasX, canvasY);
}
}
if (moved) {
context.closePath();
return true;
}
return false;
}
// 备注:解除旧底图上的遮罩事件,避免重复绑定或残留旧裁切效果。
private clearMaskEvents(): void {
this.maskBindings.forEach(binding => {
binding.layer.un('prerender', binding.prerender);
binding.layer.un('postrender', binding.postrender);
});
this.maskBindings = [];
}
}