392 lines
12 KiB
TypeScript
392 lines
12 KiB
TypeScript
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';
|
||
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());
|
||
}
|
||
|
||
// 备注:按当前缩放级别统一刷新近邻点展开坐标,保证抽吸后的点击和 popup 与视觉位置一致。
|
||
updateNearbyFeatureLayout(currentZoom?: number): void {
|
||
if (!this.map) return;
|
||
|
||
if (currentZoom === undefined || !Number.isFinite(currentZoom)) {
|
||
return;
|
||
}
|
||
|
||
this.forEachFeature(feature => {
|
||
this.resetFeatureToBaseCoordinate(feature);
|
||
});
|
||
}
|
||
|
||
// 备注:判断指定图层 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,
|
||
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
|
||
});
|
||
const rawFeatureId =
|
||
item.stcd || item._id || `${lon.toFixed(6)},${lat.toFixed(6)}`;
|
||
feature.setId(`${targetLayerKey}:${rawFeatureId}`);
|
||
feature.set('_iconUrl', iconUrl);
|
||
feature.set(
|
||
'_labelText',
|
||
item.sttpMap === 'ylfb' ? item.ftp || '' : titleName || stnm || ennm || ''
|
||
);
|
||
feature.set('_baseCoordinates', coord);
|
||
feature.set('_legendVisible', true);
|
||
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)) || [];
|
||
}
|
||
|
||
// 备注:先恢复要素原始坐标,再按缩放阶段决定是否做展开偏移,避免多次缩放后偏移累计。
|
||
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]
|
||
]);
|
||
}
|
||
}
|