WholeProcessPlatform/frontend/src/components/gis/ol/point-layer-manager.ts

485 lines
15 KiB
TypeScript
Raw Normal View History

import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import OlMap from 'ol/Map';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { fromLonLat } from 'ol/proj';
import { getIconPath } from '@/utils/index';
2026-07-01 08:48:39 +08:00
import { getNearbyPointConfig } from '@/modules/map/domain/nearby-point-rules';
type PointLayerStyleFactory = (feature: Feature) => any;
type PointLayerManagerOptions = {
map: OlMap | null;
createStyle: PointLayerStyleFactory;
};
export class PointLayerManager {
private map: OlMap | null;
private createStyle: PointLayerStyleFactory;
private layerRegistry: Map<string, VectorLayer<VectorSource>> = new Map();
private layerFeatureIndexes: Map<
string,
Map<string, Map<string, Feature[]>>
> = new Map();
constructor(options: PointLayerManagerOptions) {
this.map = options.map;
this.createStyle = options.createStyle;
}
// 备注:同步更新地图实例,供地图初始化后把真实 map 注入点图层管理器。
setMap(map: OlMap | null) {
this.map = map;
}
// 备注:获取当前屏幕可视区域内的所有点要素(仅包含图层可见且图例可见的要素)。
getFeaturesInViewport(): Feature[] {
if (!this.map) return [];
const extent = this.map.getView().calculateExtent(this.map.getSize());
if (!extent) return [];
const [minX, minY, maxX, maxY] = extent;
const result: Feature[] = [];
this.forEachFeature((feature, layer) => {
// 跳过图层本身不可见的要素
if (!layer.getVisible()) return;
const geom = feature.getGeometry();
if (!geom || geom.getType() !== 'Point') return;
const coords = (geom as Point).getCoordinates();
const [x, y] = coords;
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
result.push(feature);
}
});
return result;
}
// 备注:统一返回当前点图层注册表,供外层做遍历或清理。
getRegistry() {
return this.layerRegistry;
}
// 备注:按图层 key 获取已注册的点图层实例。
getLayer(layerKey: string) {
return this.layerRegistry.get(layerKey);
}
// 备注:返回当前所有点图层实例,供命中检测和批量刷新使用。
getLayers(): VectorLayer<VectorSource>[] {
return Array.from(this.layerRegistry.values());
}
2026-07-01 08:48:39 +08:00
// 备注:按当前缩放级别统一刷新近邻点展开坐标,保证抽吸后的点击和 popup 与视觉位置一致。
updateNearbyFeatureLayout(currentZoom?: number): void {
if (!this.map) return;
const resolution = this.map.getView().getResolution();
if (
currentZoom === undefined ||
!Number.isFinite(currentZoom) ||
!resolution
) {
2026-07-01 08:48:39 +08:00
return;
}
const nearbyPointConfig = getNearbyPointConfig();
const { autoRule, layoutRule } = nearbyPointConfig;
const groupMap = new Map<string, Feature[]>();
this.forEachFeature(feature => {
this.resetFeatureToBaseCoordinate(feature);
const groupId = feature.get('_nearbyGroupId');
const groupSize = Number(feature.get('_nearbyGroupSize'));
if (!groupId || !Number.isFinite(groupSize) || groupSize < 2) {
return;
}
const groupFeatures = groupMap.get(groupId) || [];
groupFeatures.push(feature);
groupMap.set(groupId, groupFeatures);
});
groupMap.forEach(features => {
const expandAtZoom = Number(features[0]?.get('_nearbyExpandAtZoom'));
if (!Number.isFinite(expandAtZoom) || currentZoom < expandAtZoom) {
return;
}
const sortedFeatures = [...features].sort((left, right) => {
return (
Number(left.get('_nearbyPriority') || 0) -
Number(right.get('_nearbyPriority') || 0)
);
2026-07-01 08:48:39 +08:00
});
const secondaryFeatures = sortedFeatures.filter(feature => {
const priority = Number(feature.get('_nearbyPriority'));
return Number.isFinite(priority) && priority > 1;
});
secondaryFeatures.forEach((feature, index) => {
const baseCoordinates = feature.get('_baseCoordinates') as
| number[]
| undefined;
const geometry = feature.getGeometry();
if (
!baseCoordinates ||
baseCoordinates.length < 2 ||
!geometry ||
geometry.getType() !== 'Point'
) {
return;
}
const groupSecondaryCount = secondaryFeatures.length;
const baseOffsetPx =
Number(feature.get('_nearbyExpandOffsetPx')) ||
autoRule.expandOffsetPx;
2026-07-01 08:48:39 +08:00
const ringSize = Math.max(1, layoutRule.ringSize);
const ringIndex = Math.floor(index / ringSize);
const ringOffsetIndex = index % ringSize;
const currentRingCount = Math.min(
ringSize,
groupSecondaryCount - ringIndex * ringSize
);
const fanAngleDeg =
currentRingCount <= 1
? 0
: currentRingCount <= 2
? layoutRule.fanAngleForTwoDeg
: currentRingCount <= 4
? layoutRule.fanAngleForFourDeg
: layoutRule.fanAngleForManyDeg;
2026-07-01 08:48:39 +08:00
const angleStepDeg =
currentRingCount <= 1 ? 0 : fanAngleDeg / (currentRingCount - 1);
const startAngleDeg = -90 - fanAngleDeg / 2;
const expandAngleDeg = startAngleDeg + ringOffsetIndex * angleStepDeg;
const expandAngleRad = (expandAngleDeg * Math.PI) / 180;
const expandOffsetPx =
baseOffsetPx +
ringIndex *
Math.max(
layoutRule.ringGapPx,
baseOffsetPx * layoutRule.ringGapFactor
);
2026-07-01 08:48:39 +08:00
const offsetX = Math.cos(expandAngleRad) * expandOffsetPx * resolution;
const offsetY = Math.sin(expandAngleRad) * expandOffsetPx * resolution;
feature.set('_nearbyExpandAngleDeg', expandAngleDeg, true);
(geometry as Point).setCoordinates([
baseCoordinates[0] + offsetX,
baseCoordinates[1] - offsetY
]);
});
});
}
// 备注:判断指定图层 key 的点图层是否已存在。
hasLayer(layerKey: string): boolean {
return this.layerRegistry.has(layerKey);
}
// 备注:按需获取或创建点图层实例,并自动完成注册与挂载。
ensureLayer(layerKey: string, layerType?: string) {
if (!this.map || !layerKey) return null;
let vectorLayer = this.layerRegistry.get(layerKey);
if (vectorLayer) {
return vectorLayer;
}
const vectorSource = new VectorSource();
vectorLayer = new VectorLayer({
source: vectorSource,
zIndex: 100,
declutter: true,
style: feature => this.createStyle(feature as Feature)
});
(vectorLayer as any).type = layerType || layerKey;
this.layerRegistry.set(layerKey, vectorLayer);
this.map.addLayer(vectorLayer);
return vectorLayer;
}
// 备注:把后端点位数组转换为 OpenLayers Feature并写入指定点图层。
addDataLayer(pointData: any, layerType: string): void {
if (!this.map) return;
let dataArray: any[] = [];
let targetLayerKey = layerType;
if (Array.isArray(pointData)) {
dataArray = pointData;
} else {
dataArray = pointData?.data || [];
targetLayerKey = pointData?.key || layerType;
}
if (!targetLayerKey) {
console.warn('缺少图层 Key无法加载描点');
return;
}
const vectorLayer = this.ensureLayer(targetLayerKey, layerType);
if (!vectorLayer) return;
const vectorSource = vectorLayer.getSource();
if (!vectorSource) {
return;
}
vectorSource.clear();
this.resetLayerFeatureIndexes(targetLayerKey);
if (dataArray.length === 0) {
return;
}
const features: Feature[] = [];
dataArray.forEach((item: any) => {
const feature = this.createFeature(item, targetLayerKey);
if (feature) {
this.indexFeature(targetLayerKey, feature);
features.push(feature);
}
});
if (features.length > 0) {
vectorSource.addFeatures(features);
}
}
// 备注:统一控制整个点图层显隐,供图层树勾选联动使用。
setLayerVisible(layerKey: string, visible?: boolean): void {
const vectorLayer = this.layerRegistry.get(layerKey);
if (vectorLayer && vectorLayer.getVisible() !== visible) {
vectorLayer.setVisible(visible);
vectorLayer.changed();
}
}
// 备注:按图层 key 删除点图层并清空其 source 数据。
removeLayer(layerKey: string): void {
if (!this.map || !layerKey) {
console.warn('removePointLayer: 无效的图层 key 或地图未初始化');
return;
}
const vectorLayer = this.layerRegistry.get(layerKey);
if (!vectorLayer) {
console.warn(`未找到标识为 [${layerKey}] 的描点图层实例`);
return;
}
const source = vectorLayer.getSource();
if (source) {
source.clear();
}
this.map.removeLayer(vectorLayer);
this.layerRegistry.delete(layerKey);
this.layerFeatureIndexes.delete(layerKey);
}
// 备注:按图层遍历所有点图层实例,供 hover 刷新和区域裁切复用。
forEachLayer(
callback: (layer: VectorLayer<VectorSource>, layerKey: string) => void
): void {
this.layerRegistry.forEach((layer, layerKey) => {
callback(layer, layerKey);
});
}
// 备注:遍历所有点要素,供区域裁切或批量状态更新使用。
forEachFeature(
callback: (
feature: Feature,
layer: VectorLayer<VectorSource>,
layerKey: string
) => void
): void {
this.forEachLayer((layer, layerKey) => {
const source = layer.getSource();
if (!source) return;
source.getFeatures().forEach(feature => {
callback(feature as Feature, layer, layerKey);
});
});
}
// 备注:按图例字段值统一控制要素显隐,供图例和容量筛选联动复用。
setLegendVisibleByField(
layerKey: string,
matchValue: string,
checked: boolean,
2026-07-10 17:56:16 +08:00
fieldKey = 'anchoPointState'
): void {
const vectorLayer = this.layerRegistry.get(layerKey);
if (!vectorLayer) return;
const source = vectorLayer.getSource();
if (!source) return;
if (!matchValue) {
source.getFeatures().forEach(feature => {
if (feature.get('_legendVisible') !== false) {
feature.set('_legendVisible', false);
feature.changed();
}
});
vectorLayer.changed();
return;
}
const targetFeatures = this.getIndexedFeatures(
layerKey,
fieldKey,
matchValue
);
if (targetFeatures.length === 0) return;
targetFeatures.forEach(feature => {
if (feature.get('_legendVisible') !== checked) {
feature.set('_legendVisible', checked);
feature.changed();
}
});
vectorLayer.changed();
}
// 备注:统一销毁所有点图层并清空注册表,供地图销毁阶段复用。
destroy(): void {
this.forEachLayer(layer => {
const source = layer.getSource();
if (source) {
source.clear();
}
if (this.map && this.map.getLayers().getArray().includes(layer)) {
this.map.removeLayer(layer);
}
if ((layer as any).setMap) {
(layer as any).setMap(null);
}
});
this.layerRegistry.clear();
this.layerFeatureIndexes.clear();
}
// 备注:统一把业务点位数据转换为 Feature并补齐样式依赖字段。
private createFeature(item: any, targetLayerKey: string): Feature | null {
const { lgtd, lttd, stnm, iconCode, titleName, ennm } = item;
if (lgtd == null || lttd == null) {
return null;
}
const lon = Number(lgtd);
const lat = Number(lttd);
if (isNaN(lon) || isNaN(lat) || !isFinite(lon) || !isFinite(lat)) {
return null;
}
if (Math.abs(lon) > 180 || Math.abs(lat) > 90) {
return null;
}
const coord = fromLonLat([lon, lat]);
if (!isFinite(coord[0]) || !isFinite(coord[1])) {
return null;
}
let iconUrl = iconCode ? getIconPath(iconCode) : '';
if (!iconUrl) {
iconUrl = getIconPath('default') || '';
}
const feature = new Feature({
geometry: new Point(coord),
...item,
_layerKey: targetLayerKey
});
feature.setId(item.stcd || item._id);
feature.set('_iconUrl', iconUrl);
2026-06-15 09:18:00 +08:00
feature.set(
'_labelText',
item.sttpMap === 'ylfb' ? item.ftp || '' : titleName || stnm || ennm || ''
);
2026-07-01 08:48:39 +08:00
feature.set('_baseCoordinates', coord);
feature.set('_legendVisible', true);
2026-07-03 15:59:05 +08:00
feature.set('_regionVisible', true);
feature.set('_sttpMap', item.sttpMap);
if (item.popupHtml) {
feature.set('popupHtml', item.popupHtml);
}
return feature;
}
// 备注:重置单个图层的字段索引,供重新灌入数据或删除图层时复用。
private resetLayerFeatureIndexes(layerKey: string): void {
this.layerFeatureIndexes.set(layerKey, new Map());
}
// 备注:按字段值给要素建立索引,供图例切换时快速命中目标锚点集合。
private indexFeature(layerKey: string, feature: Feature): void {
const fieldIndexMap =
this.layerFeatureIndexes.get(layerKey) ||
new Map<string, Map<string, Feature[]>>();
const legendFieldKey = 'anchoPointState';
const legendFieldValue = feature.get(legendFieldKey);
if (legendFieldValue != null) {
const valueMap =
fieldIndexMap.get(legendFieldKey) || new Map<string, Feature[]>();
const normalizedValue = String(legendFieldValue);
const targetFeatures = valueMap.get(normalizedValue) || [];
targetFeatures.push(feature);
valueMap.set(normalizedValue, targetFeatures);
fieldIndexMap.set(legendFieldKey, valueMap);
}
this.layerFeatureIndexes.set(layerKey, fieldIndexMap);
}
// 备注:根据图层 key、字段名和值快速返回目标要素集合避免图例切换时整层遍历。
private getIndexedFeatures(
layerKey: string,
fieldKey: string,
matchValue: string
): Feature[] {
const fieldIndexMap = this.layerFeatureIndexes.get(layerKey);
if (!fieldIndexMap) return [];
const valueMap = fieldIndexMap.get(fieldKey);
if (!valueMap) return [];
return valueMap.get(String(matchValue)) || [];
}
2026-07-01 08:48:39 +08:00
// 备注:先恢复要素原始坐标,再按缩放阶段决定是否做展开偏移,避免多次缩放后偏移累计。
private resetFeatureToBaseCoordinate(feature: Feature): void {
const baseCoordinates = feature.get('_baseCoordinates') as
| number[]
| undefined;
2026-07-01 08:48:39 +08:00
const geometry = feature.getGeometry();
if (
!baseCoordinates ||
baseCoordinates.length < 2 ||
!geometry ||
geometry.getType() !== 'Point'
) {
return;
}
(geometry as Point).setCoordinates([
baseCoordinates[0],
baseCoordinates[1]
]);
2026-07-01 08:48:39 +08:00
}
}