2026-06-03 15:03:31 +08:00
|
|
|
|
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';
|
2026-06-03 15:03:31 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 11:36:17 +08:00
|
|
|
|
// 备注:获取当前屏幕可视区域内的所有点要素(仅包含图层可见且图例可见的要素)。
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:03:31 +08:00
|
|
|
|
// 备注:统一返回当前点图层注册表,供外层做遍历或清理。
|
|
|
|
|
|
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) {
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
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;
|
|
|
|
|
|
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;
|
|
|
|
|
|
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);
|
|
|
|
|
|
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
|
|
|
|
|
|
]);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:03:31 +08:00
|
|
|
|
// 备注:判断指定图层 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);
|
2026-06-16 11:36:17 +08:00
|
|
|
|
vectorLayer.changed();
|
2026-06-03 15:03:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 备注:按图层 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,
|
|
|
|
|
|
fieldKey: string = '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);
|
2026-06-03 15:03:31 +08:00
|
|
|
|
feature.set('_legendVisible', 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;
|
|
|
|
|
|
const geometry = feature.getGeometry();
|
|
|
|
|
|
if (
|
|
|
|
|
|
!baseCoordinates ||
|
|
|
|
|
|
baseCoordinates.length < 2 ||
|
|
|
|
|
|
!geometry ||
|
|
|
|
|
|
geometry.getType() !== 'Point'
|
|
|
|
|
|
) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
(geometry as Point).setCoordinates([baseCoordinates[0], baseCoordinates[1]]);
|
|
|
|
|
|
}
|
2026-06-03 15:03:31 +08:00
|
|
|
|
}
|