新版3d修改提交,差碰撞,popup

This commit is contained in:
扈兆增 2026-07-14 16:09:01 +08:00
parent ba64f8a223
commit 1fc41ef1f6
14 changed files with 1185 additions and 1313 deletions

View File

@ -7,14 +7,14 @@ VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
VITE_APP_PORT = 3000 VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/dev-api' VITE_APP_BASE_API = '/dev-api'
# 本地环境 # 本地环境
# VITE_APP_BASE_URL = 'http://10.84.121.127:8093' VITE_APP_BASE_URL = 'http://localhost:8093'
# 测试环境 # 测试环境
# VITE_APP_BASE_URL = 'http://172.16.21.142:8093' # VITE_APP_BASE_URL = 'http://172.16.21.142:8093'
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096' # VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
# 汤伟 # 汤伟
# VITE_APP_BASE_URL = 'http://10.84.111.235:8093' VITE_APP_BASE_URL = 'http://10.84.111.235:8093'
# 李林 # 李林
VITE_APP_BASE_URL = 'http://10.84.111.25:8093' # VITE_APP_BASE_URL = 'http://10.84.111.25:8093'
# 测试环境线上10.84.121.122: # 测试环境线上10.84.121.122:
VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122' VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'

View File

@ -132,7 +132,7 @@
## 5. 分步骤实施计划 ## 5. 分步骤实施计划
### Step 1修正 3D 初始化并默认定位中国 ### Step 1修正 3D 初始化并默认定位中国 ✅️
目标: 目标:
@ -157,7 +157,7 @@
- 首屏有加载过渡。 - 首屏有加载过渡。
- 进入后稳定落在中国。 - 进入后稳定落在中国。
### Step 2展示 3D 基础底图 ### Step 2展示 3D 基础底图 ✅️
目标: 目标:
@ -171,6 +171,10 @@
- 实现 `addBaseDataLayer(layer, isShow)` - 实现 `addBaseDataLayer(layer, isShow)`
- 对 `customBaseLayer` 使用 `url_3d` - 对 `customBaseLayer` 使用 `url_3d`
- 对 `BASEMAP-img`、`BASEMAP-white` 预留兼容能力 - 对 `BASEMAP-img`、`BASEMAP-white` 预留兼容能力
- **必须处理 `wmts` 类型图层**2D 通过 `WMTS` source 加载3D 使用 `Cesium.WebMapTileServiceImageryProvider`,从 URL 参数中提取 LAYER/TILEMATRIXSET
- **必须提供 `tileMatrixLabels`**Cesium 默认用纯数字作为 tileMatrix 参数值,但本系统 WMTS 服务器要求 `TileMatrixSet:zoom` 格式(如 `EPSG:3857_hbb_zrbhq_l13:4`)。需构建 `["TileMatrixSet:0", "TileMatrixSet:1", ...]` 传入 `tileMatrixLabels`
- 其他类型:`raster-dem`→XYZ、MapServer→ArcGIS、其余 →WMS
- **注意**:还存在一个 `key = "powerBaseStation"` 的基地底图图层,该图层**初始化时不加载**(已在 `IGNORED_BASE_LAYER_KEYS` 中忽略),但后续可通过图层树手动勾选显示。
- 实现 `controlBaseLayerTreeShowAndHidden()` - 实现 `controlBaseLayerTreeShowAndHidden()`
- 让图层树勾选能控制 3D 底图显隐 - 让图层树勾选能控制 3D 底图显隐
- 根据你的要求UI 层先不开放 `BaseLayerSwitcher` 的 3D 切换行为,只保留默认基础底图加载。 - 根据你的要求UI 层先不开放 `BaseLayerSwitcher` 的 3D 切换行为,只保留默认基础底图加载。
@ -180,7 +184,7 @@
- 切到 3D 后可稳定看到基础底图。 - 切到 3D 后可稳定看到基础底图。
- 图层初始化时 `addBaseDataLayer(this.baseLayerConfig, checked, true)` 的思路在 3D 可落地。 - 图层初始化时 `addBaseDataLayer(this.baseLayerConfig, checked, true)` 的思路在 3D 可落地。
### Step 3打通 3D 锚点展示 ### Step 3打通 3D 锚点展示 ✅️
目标: 目标:
@ -189,13 +193,14 @@
计划: 计划:
- 在 `MapCesium` 内建立点位图层注册结构,建议`layerKey -> entity[]` 或 `CustomDataSource` 管理。 - 在 `MapCesium` 内建立点位图层注册结构,按 `layerKey -> entity[]` 管理。
- 实现 `addInitDataLayer(pointData, layerType)` - 实现 `addInitDataLayer(pointData, layerType)`
- 复用当前点位数据结构 - 复用当前点位数据结构
- 解析 `lgtd/lttd/iconCode/titleName/stnm/anchoPointState/popupHtml` - 解析 `lgtd/lttd/iconCode/titleName/stnm/anchoPointState/popupHtml`
- 复用现有图标资源路径 - 复用现有图标资源路径
- Entity ID 使用 `${layerKey}:${item._id || `${iconType || 'point'}_${stcd}`}`,与 2D 对齐,确保同 stcd 不同 sttpMap 的点不被覆盖
- 确保 `eng_point` 仍按现有 `mapStore` 过滤后的数据展示,中型站点只在 `7.5` 以上层级出现 - 确保 `eng_point` 仍按现有 `mapStore` 过滤后的数据展示,中型站点只在 `7.5` 以上层级出现
- 为后续缩放驱动的增量显示预留运行态字段,不把所有点简单一次性硬渲染 - 为后续缩放驱动的增量显示预留运行态字段`_legendVisible`、`_regionVisible`、`_densityVisible`,不把所有点简单一次性硬渲染
- 实现 `mdLayerTreeShowOrHidden(layerType, checked)` - 实现 `mdLayerTreeShowOrHidden(layerType, checked)`
- 图层树控制整个点图层显隐 - 图层树控制整个点图层显隐
- 实现 `removePointLayer()` / `hasLayer()` - 实现 `removePointLayer()` / `hasLayer()`
@ -208,7 +213,34 @@
- 取消勾选图层后,对应锚点消失。 - 取消勾选图层后,对应锚点消失。
- `eng_point` 在低于 `7.5` 时不显示中型水电站,高于 `7.5` 时正常显示。 - `eng_point` 在低于 `7.5` 时不显示中型水电站,高于 `7.5` 时正常显示。
### Step 4打通 3D popup 与 hover ### Step 4打通 3D 基地裁切jdPanelControlShowAndHidden ✅️
目标:
- 3D 点击基地面板后,与 2D 行为一致。
- 加载基地边界 GeoJSON只显示区域内锚点隐藏区域外锚点。
- 取消基地时恢复显示所有锚点。
计划:
- 在 `MapCesium` 中捕获 `hydropBase` 配置,与 2D 一样在 `addBaseDataLayer` 中记录。
- 实现 `jdPanelControlShowAndHidden(baseid, isAll)`
- `isAll=false`:清除 3D 球面遮罩实体、恢复所有锚点区域可见性、清除裁切状态。
- `isAll=true`:先隐藏所有锚点,异步加载基地边界 GeoJSON执行点在面判断后仅显示区域内锚点同时在球面绘制裁切边界多边形`applyRegionMask`),最后飞行到区域视野。
- 在 `createEntityPropertiesBag` 中新增 `_regionVisible` 属性,初始为 `true`
- 在 `applyPointVisibilityRefresh` 中跳过 `_regionVisible !== false` 的实体,确保裁切后碰撞检测不会把区域外锚点重新显示出来。
- 复用 `extractPolygonCoords` / `isPointInRing` 射线法判断逻辑(与 `region-mask-manager.ts` 一致)。
- 支持 `AbortController` 取消前一次未完成的请求,避免响应顺序错乱。
- `applyRegionMask` 将 GeoJSON 多边形转换为 Cesium `PolygonHierarchy`,在球面绘制半透明填充 + 可见边界的区域多边形,模拟 2D 底图 Canvas 裁切遮罩的视觉效果。
验收:
- 点击基地后3D 只显示该基地区域内的锚点,区域外锚点隐藏。
- 取消基地后,所有锚点恢复正常显示。
- 快速切换基地不会出现旧数据覆盖新数据的问题。
- 多次切换 2D/3D 后基地裁切行为正常。
### Step 5打通 3D popup 与 hover
目标: 目标:
@ -233,7 +265,7 @@
- 样式和 2D 保持一致。 - 样式和 2D 保持一致。
- 移出后 popup 正常消失。 - 移出后 popup 正常消失。
### Step 5:补齐缩放驱动显示与碰撞检测 ### Step 6:补齐缩放驱动显示与碰撞检测
目标: 目标:
@ -263,7 +295,7 @@
- 放大到具体层级后会看到更多锚点逐步出现。 - 放大到具体层级后会看到更多锚点逐步出现。
- 标签和锚点不会大面积重叠到不可读。 - 标签和锚点不会大面积重叠到不可读。
### Step 6打通 3D 锚点点击详情 ### Step 7打通 3D 锚点点击详情 ✅️
目标: 目标:
@ -287,7 +319,7 @@
- `ylfb` 和普通锚点分支行为一致。 - `ylfb` 和普通锚点分支行为一致。
- 点击空白区域不会误触发弹框。 - 点击空白区域不会误触发弹框。
### Step 7打通图例联动 ### Step 8打通图例联动 ✅️
目标: 目标:
@ -305,7 +337,7 @@
- 图例单项勾选/取消时3D 对应类别锚点同步显隐。 - 图例单项勾选/取消时3D 对应类别锚点同步显隐。
- 图例分组勾选/取消时3D 行为与 2D 保持一致。 - 图例分组勾选/取消时3D 行为与 2D 保持一致。
### Step 8:补足收口与稳定性 ### Step 9:补足收口与稳定性
目标: 目标:
@ -329,7 +361,7 @@
1. 默认定位到中国 + 启动动画 1. 默认定位到中国 + 启动动画
2. 展示基础底图 2. 展示基础底图
3. 展示锚点 3. 展示锚点 3.5. 补基地裁切jdPanelControlShowAndHidden
4. 补 hover popup 4. 补 hover popup
5. 补缩放驱动显示与碰撞检测 5. 补缩放驱动显示与碰撞检测
6. 补锚点点击详情 6. 补锚点点击详情
@ -349,6 +381,10 @@
- `stinfo/stinfo_video_point``stinfo_ai_video_point` 互斥 - `stinfo/stinfo_video_point``stinfo_ai_video_point` 互斥
- `facilities``facilities_built` 两个分支互斥 - `facilities``facilities_built` 两个分支互斥
- 点图层取消勾选时只隐藏,不删除缓存;重新勾选时优先从缓存恢复。 - 点图层取消勾选时只隐藏,不删除缓存;重新勾选时优先从缓存恢复。
- **图层树勾选时按 `type` 字段走不同分支**(见 `updateLayerData`
- `type === 'pointMap'` → 走 `handlePointMapLayer` → 调用 `mdLayerTreeShowOrHidden`(控制锚点显隐)
- `type === 'GISMap'` → 走 `controlBaseLayerTreeShowAndHidden`(控制底图/叠加影像图层显隐)
- 3D Cesium 必须同时实现这两个方法才能与图层树完整联动。
### 9.2 图例与点内显隐 ### 9.2 图例与点内显隐
@ -360,11 +396,16 @@
- `eng_point` 的中型水电站只在 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上显示。 - `eng_point` 的中型水电站只在 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上显示。
- 水电开发页缩放跨过 `12` 级时,会自动补开一批动态图层;缩回去时再自动收口。 - 水电开发页缩放跨过 `12` 级时,会自动补开一批动态图层;缩回去时再自动收口。
- `2D` 存在“缩放越大显示越多”的行为,不是所有锚点在所有层级都等量显示。 - `2D` 存在"缩放越大显示越多"的行为,不是所有锚点在所有层级都等量显示。
- `2D` 还存在 hover、批量 popup、密度控制等缩放阈值 - `2D` 还存在 hover、批量 popup、密度控制等缩放阈值
- hover 低于一定缩放不触发 - hover 低于一定缩放不触发
- 高缩放会进入批量 popup 模式 - 高缩放会进入批量 popup 模式
- 点位会随着缩放提升逐步放开显示限制 - 点位会随着缩放提升逐步放开显示限制
- **图标动态缩放**`scale = 0.7 + (zoom - 4.5) * 0.08`,钳位到 `[0.5, 3.0]`
- **字体动态大小**`fontSize = clamp(12 * scale, 10, 24)`,即 10px~24px 之间随缩放变化。
- **文字在图标上方**2D 中 `text.offsetY = -22 * scale`,负值表示文字偏移到图标上方。
- **注意**:上述 2D scale 公式是针对 OpenLayers 2D 投影校准的Cesium 3D 球面视角需要更高的基准值(建议 1.0 起步),且 `scaleByDistance` 在 9,000km 中国高度处不应过度压缩(建议 near/far 范围设为 1km→2x, 20,000km→0.6x)。
- **Cesium 中必须使用 `CallbackProperty` 实现动态缩放**3D 中 `billboard.scale` 不能是静态值,否则放大后图标不会跟随变大。使用 `CallbackProperty` 每帧重新计算当前 zoom 对应的 scale与 2D `getDynamicPointScale` 行为对齐。
### 9.4 锚点碰撞与控量 ### 9.4 锚点碰撞与控量
@ -395,6 +436,28 @@
- 鱼类分布模式会接管 `ylfb_point` 的数据、缓存、图例与显隐。 - 鱼类分布模式会接管 `ylfb_point` 的数据、缓存、图例与显隐。
- 时间筛选当前只重载指定时间相关图层,不是全量重载。 - 时间筛选当前只重载指定时间相关图层,不是全量重载。
### 9.8 数据进入 addInitDataLayer 前的预处理
- 所有点位数据在进入 `addInitDataLayer` 前会经过 `normalizePointLayerItems()` 过滤:
- 没有 `iconCode``code !== 'colorLayer'` 的项会被抛弃
- `baseId == 'all'``anchoPointState` 结尾为 `_nbuilt` 的项会被抛弃
- `_id` 默认值生成规则:如果项没有 `_id`,会生成 `${sttpMap || iconType || 'point'}_${stcd}`
- **stcd+经纬度重复锚点处理**:同一个 layerKey 下,相同 stcd 和经纬度但不同 sttpMap/anchoPointState 的锚点需要全部保留,不能因为 Feature/Entity ID 冲突而被覆盖。2D 使用 `${layerKey}:${_id}` 作为 ID其中 `_id` 默认为 `${sttpMap}_${stcd}`,天然区分了同 stcd 不同业务类型的点。
### 9.9 图标渐进加载机制
- 2D 中图标不是一次性全加载完。有 `loading → loaded → error` 三种状态跟踪。
- 只有 `loaded` 状态的图标才参与样式渲染loader 失败的不渲染。
- 如果在图标加载过程中就触发了样式刷新,需要通过 `requestAnimationFrame` 回刷。
- 3D Cesium 中如果要实际渲染图标纹理,同样需要这种渐进加载机制,或在实现时先以简单 billboard 色块兜底。
### 9.10 密度分档与近邻点元数据
- 每个锚点的 `distance` 字段表示密度分档值(不是物理距离),值越大表示越稀疏。
- 不同 density 值对应不同的 `minZoom` 显示门槛(见 `nearby-point-rules.ts``densityDisplayRules`)。
- 全量数据加载后store 会调用 `attachNearbyPointRuntimeMeta()` 给所有点打上 `_nearbyRule`、`_nearbyGroupId`、`_nearbyPriority` 等元数据。
- **孤立点例外规则**:如果一个点在当前视口内 ~56px 范围内没有同 layerKey 的其他可见点,它可以绕过密度门槛直接显示。
## 7. 预计修改文件 ## 7. 预计修改文件
- `src/components/gis/map.cesium.ts` - `src/components/gis/map.cesium.ts`

View File

@ -42,9 +42,6 @@ export const legendData2Obj = (data: any[]) => {
if (item?.childrenList && item.childrenList?.length > 0) { if (item?.childrenList && item.childrenList?.length > 0) {
f(item.childrenList); f(item.childrenList);
} else { } else {
if (item.nameEn.includes('_测试')) {
item.nameEn = item.nameEn.split('_测试')[0];
}
_tempData[item.nameEn] = item; _tempData[item.nameEn] = item;
} }
}); });

File diff suppressed because it is too large Load Diff

View File

@ -381,7 +381,7 @@ export class MapOl implements MapInterface {
legendVisible !== false && legendVisible !== false &&
regionVisible !== false && regionVisible !== false &&
densityVisible && densityVisible &&
this.shouldRenderNearbyFeature(feature, currentZoom); this.shouldRenderNearbyFeature();
const dynamicScale = this.getDynamicPointScale(currentZoom); const dynamicScale = this.getDynamicPointScale(currentZoom);
const fontSize = this.getPointFontSize(dynamicScale); const fontSize = this.getPointFontSize(dynamicScale);
@ -706,7 +706,7 @@ export class MapOl implements MapInterface {
legendVisible: feature.get('_legendVisible') !== false, legendVisible: feature.get('_legendVisible') !== false,
regionVisible: feature.get('_regionVisible') !== false, regionVisible: feature.get('_regionVisible') !== false,
densityPriority: this.getFeatureDensityPriority(feature), densityPriority: this.getFeatureDensityPriority(feature),
nearbyVisible: this.shouldRenderNearbyFeature(feature, currentZoom), nearbyVisible: this.shouldRenderNearbyFeature(),
labelText: this.formatPointLabelText( labelText: this.formatPointLabelText(
feature.get('_labelText') as string feature.get('_labelText') as string
), ),
@ -1396,7 +1396,27 @@ export class MapOl implements MapInterface {
); );
} }
private isFeatureEngAlarm(feature: Feature): boolean {
const layerKey = String(
feature.get('_layerKey') || feature.get('type') || ''
).toLowerCase();
const legendState = String(feature.get('anchoPointState') || '')
.trim()
.toLowerCase();
return (
layerKey.includes('eng_alarm_point') ||
layerKey.includes('alarm_range') ||
legendState.startsWith('alarm_range_') ||
legendState.startsWith('large_eng_built_alarm_range_') ||
legendState.startsWith('mid_eng_built_alarm_range_')
);
}
private getFeatureEngRenderPriority(feature: Feature): number { private getFeatureEngRenderPriority(feature: Feature): number {
if (this.isFeatureEngAlarm(feature)) {
return 300;
}
return this.isFeatureEng(feature) ? 200 : 100; return this.isFeatureEng(feature) ? 200 : 100;
} }
@ -1472,52 +1492,10 @@ export class MapOl implements MapInterface {
} }
/** /**
* *
*/ */
private shouldRenderNearbyFeature( private shouldRenderNearbyFeature(): boolean {
feature: Feature, return true;
currentZoom: number
): boolean {
const nearbyGroupId = feature.get('_nearbyGroupId');
const groupSize = Number(feature.get('_nearbyGroupSize'));
const priority = Number(feature.get('_nearbyPriority'));
const showZoomMin = feature.get('_nearbyShowZoomMin');
const showZoomMax = feature.get('_nearbyShowZoomMax');
const expandAtZoom = feature.get('_nearbyExpandAtZoom');
if (!nearbyGroupId || !Number.isFinite(groupSize) || groupSize < 2) {
return true;
}
if (!Number.isFinite(priority) || priority <= 0) {
return true;
}
if (
showZoomMin !== null &&
showZoomMin !== undefined &&
currentZoom < Number(showZoomMin)
) {
return false;
}
if (
showZoomMax !== null &&
showZoomMax !== undefined &&
currentZoom > Number(showZoomMax)
) {
return false;
}
if (
expandAtZoom !== null &&
expandAtZoom !== undefined &&
currentZoom >= Number(expandAtZoom)
) {
return true;
}
return priority === 1;
} }
/** /**
* *

View File

@ -5,8 +5,6 @@ import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector'; import VectorSource from 'ol/source/Vector';
import { fromLonLat } from 'ol/proj'; import { fromLonLat } from 'ol/proj';
import { getIconPath } from '@/utils/index'; import { getIconPath } from '@/utils/index';
import { getNearbyPointConfig } from '@/modules/map/domain/nearby-point-rules';
type PointLayerStyleFactory = (feature: Feature) => any; type PointLayerStyleFactory = (feature: Feature) => any;
type PointLayerManagerOptions = { type PointLayerManagerOptions = {
@ -80,103 +78,12 @@ export class PointLayerManager {
updateNearbyFeatureLayout(currentZoom?: number): void { updateNearbyFeatureLayout(currentZoom?: number): void {
if (!this.map) return; if (!this.map) return;
const resolution = this.map.getView().getResolution(); if (currentZoom === undefined || !Number.isFinite(currentZoom)) {
if (
currentZoom === undefined ||
!Number.isFinite(currentZoom) ||
!resolution
) {
return; return;
} }
const nearbyPointConfig = getNearbyPointConfig();
const { autoRule, layoutRule } = nearbyPointConfig;
const groupMap = new Map<string, Feature[]>();
this.forEachFeature(feature => { this.forEachFeature(feature => {
this.resetFeatureToBaseCoordinate(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
]);
});
}); });
} }
@ -392,7 +299,6 @@ export class PointLayerManager {
if (!isFinite(coord[0]) || !isFinite(coord[1])) { if (!isFinite(coord[0]) || !isFinite(coord[1])) {
return null; return null;
} }
let iconUrl = iconCode ? getIconPath(iconCode) : ''; let iconUrl = iconCode ? getIconPath(iconCode) : '';
if (!iconUrl) { if (!iconUrl) {
iconUrl = getIconPath('default') || ''; iconUrl = getIconPath('default') || '';
@ -403,7 +309,9 @@ export class PointLayerManager {
...item, ...item,
_layerKey: targetLayerKey _layerKey: targetLayerKey
}); });
feature.setId(item.stcd || item._id); const rawFeatureId =
item.stcd || item._id || `${lon.toFixed(6)},${lat.toFixed(6)}`;
feature.setId(`${targetLayerKey}:${rawFeatureId}`);
feature.set('_iconUrl', iconUrl); feature.set('_iconUrl', iconUrl);
feature.set( feature.set(
'_labelText', '_labelText',
@ -457,7 +365,6 @@ export class PointLayerManager {
const valueMap = fieldIndexMap.get(fieldKey); const valueMap = fieldIndexMap.get(fieldKey);
if (!valueMap) return []; if (!valueMap) return [];
return valueMap.get(String(matchValue)) || []; return valueMap.get(String(matchValue)) || [];
} }

View File

@ -1,8 +1,5 @@
<template> <template>
<div <div class="map-filter-container">
class="map-filter-container"
:style="{ left: uiStore.mapType == '2D' ? '220px' : '20px' }"
>
<div class="toolbar"> <div class="toolbar">
<a-form :model="formModel"> <a-form :model="formModel">
<a-row :gutter="10"> <a-row :gutter="10">

View File

@ -88,9 +88,7 @@ const hasEnvFac = computed(() =>
const legendItemClick = (item: any) => { const legendItemClick = (item: any) => {
if (item.parentName == '环保设施') return; if (item.parentName == '环保设施') return;
if (item.canBeChecked == 0) { if (item.canBeChecked == 0) return;
return;
}
if (item.nameEn) { if (item.nameEn) {
mapOrchestrator.toggleLegend(item.nameEn, item.checked == 0 ? 1 : 0); mapOrchestrator.toggleLegend(item.nameEn, item.checked == 0 ? 1 : 0);
} }

View File

@ -1,11 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useUiStore } from '@/store/modules/ui';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent'; import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
const loading = ref(false);
const isOpen = ref(true); const isOpen = ref(true);
const JidiSelectEventStore = useJidiSelectEventStore(); const JidiSelectEventStore = useJidiSelectEventStore();
const uiStore = useUiStore();
const jidiDataNum = ref(9); const jidiDataNum = ref(9);
const itemClick = (index: any) => { const itemClick = (index: any) => {
@ -15,7 +12,7 @@ onMounted(() => {});
</script> </script>
<template> <template>
<div class="jidiSelectorMod" v-if="uiStore.mapType == '2D'"> <div class="jidiSelectorMod">
<a-spin :spinning="JidiSelectEventStore.loading"> <a-spin :spinning="JidiSelectEventStore.loading">
<div <div
class="qgc-dropdown-select" class="qgc-dropdown-select"

View File

@ -302,13 +302,11 @@ export const useMapOrchestrator = () => {
refreshEngPoint: true refreshEngPoint: true
}); });
if (currentMapType === '2D') { const selectedBaseId = mapViewStore.selectedBaseId;
const selectedBaseId = mapViewStore.selectedBaseId; mapClass.jdPanelControlShowAndHidden(
mapClass.jdPanelControlShowAndHidden( selectedBaseId || 'all',
selectedBaseId || 'all', !!selectedBaseId
!!selectedBaseId );
);
}
bindZoomListener(options.getIsHydroMenu); bindZoomListener(options.getIsHydroMenu);
}; };
@ -660,12 +658,15 @@ export const useMapOrchestrator = () => {
// 备注:统一处理单个图例切换命令,供图例组件复用。 // 备注:统一处理单个图例切换命令,供图例组件复用。
const toggleLegend = (nameEn: string, checked?: number) => { const toggleLegend = (nameEn: string, checked?: number) => {
if (!nameEn) return; if (!nameEn) return;
console.log('toggleLegend', nameEn, checked);
console.log(mapViewStore.getLegendChecked(nameEn));
const nextChecked = const nextChecked =
checked === undefined checked === undefined
? mapViewStore.getLegendChecked(nameEn) === 1 ? mapViewStore.getLegendChecked(nameEn) === 1
? 0 ? 0
: 1 : 1
: checked; : checked;
console.log(nextChecked);
mapStore.updateLegendChecked(nameEn, nextChecked); mapStore.updateLegendChecked(nameEn, nextChecked);
}; };
@ -680,12 +681,10 @@ export const useMapOrchestrator = () => {
const changeBaseId = (baseId: string) => { const changeBaseId = (baseId: string) => {
const nextBaseId = !baseId || baseId === 'all' ? '' : baseId; const nextBaseId = !baseId || baseId === 'all' ? '' : baseId;
mapViewStore.setSelectedBaseId(nextBaseId); mapViewStore.setSelectedBaseId(nextBaseId);
if (currentMapType === '2D') { mapClass.jdPanelControlShowAndHidden(
mapClass.jdPanelControlShowAndHidden( baseId || 'all',
baseId || 'all', baseId !== 'all' && !!baseId
baseId !== 'all' && !!baseId );
);
}
}; };
// 备注:统一处理时间范围变更,并触发相关图层数据重载。 // 备注:统一处理时间范围变更,并触发相关图层数据重载。

View File

@ -74,19 +74,19 @@ export const DEFAULT_NEARBY_POINT_DENSITY_DISPLAY_RULES: NearbyPointDensityDispl
}, },
{ {
minDensityValue: 800000, minDensityValue: 800000,
minZoom: 6.1 minZoom: 0
}, },
{ {
minDensityValue: 400000, minDensityValue: 400000,
minZoom: 8.1 minZoom: 0
}, },
{ {
minDensityValue: 50000, minDensityValue: 50000,
minZoom: 10.6 minZoom: 2.0
}, },
{ {
minDensityValue: 0, minDensityValue: 0,
minZoom: 12.2 minZoom: 4.0
} }
]; ];
@ -121,6 +121,37 @@ type GroupCandidate = {
layerKey: string; layerKey: string;
}; };
const isAlarmRangeLegendState = (legendState: string): boolean => {
return (
legendState.startsWith('alarm_range_') ||
legendState.startsWith('large_eng_built_alarm_range_') ||
legendState.startsWith('mid_eng_built_alarm_range_')
);
};
const getPointRenderPriority = (point: Record<string, any>): number => {
const layerKey = String(point.layerKey || point.type || '').toLowerCase();
const legendState = String(point.anchoPointState || '').trim().toLowerCase();
if (
layerKey.includes('eng_alarm_point') ||
layerKey.includes('alarm_range') ||
isAlarmRangeLegendState(legendState)
) {
return 300;
}
if (
layerKey.includes('eng_point') ||
legendState.startsWith('large_eng_') ||
legendState.startsWith('mid_eng_')
) {
return 200;
}
return 100;
};
const normalizeText = (value: unknown): string => { const normalizeText = (value: unknown): string => {
return String(value ?? '') return String(value ?? '')
.trim() .trim()
@ -207,6 +238,12 @@ const compareGroupCandidates = (
left: GroupCandidate, left: GroupCandidate,
right: GroupCandidate right: GroupCandidate
): number => { ): number => {
const leftRenderPriority = getPointRenderPriority(left.point);
const rightRenderPriority = getPointRenderPriority(right.point);
if (leftRenderPriority !== rightRenderPriority) {
return rightRenderPriority - leftRenderPriority;
}
const leftDensityValue = toFiniteNumber(left.point.distance); const leftDensityValue = toFiniteNumber(left.point.distance);
const rightDensityValue = toFiniteNumber(right.point.distance); const rightDensityValue = toFiniteNumber(right.point.distance);
@ -232,21 +269,18 @@ const buildRuntimeMeta = (
displayName: string, displayName: string,
rule: NearbyPointAutoRule rule: NearbyPointAutoRule
): NearbyPointRuntimeMeta => { ): NearbyPointRuntimeMeta => {
const displayMode: NearbyPointDisplayMode =
priority === 1 ? 'default' : priority === 2 ? 'replace' : 'expand';
return { return {
groupId, groupId,
displayName, displayName,
groupSize, groupSize,
priority, priority,
showZoomMin: priority === 1 ? null : rule.replaceAtZoom, showZoomMin: null,
showZoomMax: null, showZoomMax: null,
replaceAtZoom: rule.replaceAtZoom, replaceAtZoom: rule.replaceAtZoom,
expandAtZoom: rule.expandAtZoom, expandAtZoom: rule.expandAtZoom,
expandOffsetPx: rule.expandOffsetPx, expandOffsetPx: rule.expandOffsetPx,
expandAngleDeg: priority === 1 ? 0 : (priority - 2) * rule.angleStepDeg, expandAngleDeg: 0,
displayMode displayMode: 'default'
}; };
}; };
@ -279,61 +313,49 @@ const buildGroupCandidates = (
return candidates; return candidates;
}; };
const buildLayerScopedNearbyGroups = ( const buildNearbyGroups = (
candidates: GroupCandidate[], candidates: GroupCandidate[],
rule: NearbyPointAutoRule rule: NearbyPointAutoRule
): GroupCandidate[][] => { ): GroupCandidate[][] => {
const layerScopedCandidates = new Map<string, GroupCandidate[]>();
candidates.forEach(candidate => {
const layerKey = candidate.layerKey || '__empty_layer__';
const layerItems = layerScopedCandidates.get(layerKey) || [];
layerItems.push(candidate);
layerScopedCandidates.set(layerKey, layerItems);
});
const result: GroupCandidate[][] = []; const result: GroupCandidate[][] = [];
const sortedCandidates = [...candidates].sort(compareGroupCandidates);
const consumedIndexes = new Set<number>();
layerScopedCandidates.forEach(layerItems => { sortedCandidates.forEach((seedCandidate, seedIndex) => {
const sortedCandidates = [...layerItems].sort(compareGroupCandidates); if (consumedIndexes.has(seedIndex)) {
const consumedIndexes = new Set<number>(); return;
}
sortedCandidates.forEach((seedCandidate, seedIndex) => { const groupItems: GroupCandidate[] = [seedCandidate];
if (consumedIndexes.has(seedIndex)) { const groupIndexes = [seedIndex];
return;
for (
let candidateIndex = seedIndex + 1;
candidateIndex < sortedCandidates.length;
candidateIndex += 1
) {
if (consumedIndexes.has(candidateIndex)) {
continue;
} }
const groupItems: GroupCandidate[] = [seedCandidate]; const currentCandidate = sortedCandidates[candidateIndex];
const groupIndexes = [seedIndex]; const distanceMeters = haversineDistanceMeters(
seedCandidate.lon,
seedCandidate.lat,
currentCandidate.lon,
currentCandidate.lat
);
for ( if (distanceMeters <= rule.distanceThresholdMeters) {
let candidateIndex = seedIndex + 1; groupItems.push(currentCandidate);
candidateIndex < sortedCandidates.length; groupIndexes.push(candidateIndex);
candidateIndex += 1
) {
if (consumedIndexes.has(candidateIndex)) {
continue;
}
const currentCandidate = sortedCandidates[candidateIndex];
const distanceMeters = haversineDistanceMeters(
seedCandidate.lon,
seedCandidate.lat,
currentCandidate.lon,
currentCandidate.lat
);
if (distanceMeters <= rule.distanceThresholdMeters) {
groupItems.push(currentCandidate);
groupIndexes.push(candidateIndex);
}
} }
}
if (groupItems.length >= rule.minGroupSize) { if (groupItems.length >= rule.minGroupSize) {
groupIndexes.forEach(index => consumedIndexes.add(index)); groupIndexes.forEach(index => consumedIndexes.add(index));
result.push(groupItems); result.push(groupItems);
} }
});
}); });
return result; return result;
@ -354,7 +376,7 @@ export const attachNearbyPointRuntimeMeta = (
if (candidates.length < rule.minGroupSize) { if (candidates.length < rule.minGroupSize) {
return points; return points;
} }
const groupedCandidates = buildLayerScopedNearbyGroups(candidates, rule); const groupedCandidates = buildNearbyGroups(candidates, rule);
let groupSeed = 0; let groupSeed = 0;
groupedCandidates.forEach(groupItems => { groupedCandidates.forEach(groupItems => {

View File

@ -28,10 +28,9 @@ export const useMapConfigStore = defineStore('map-config', () => {
const legendLoading = ref(false); const legendLoading = ref(false);
const lastLoadOptions = ref<MapConfigLoadOptions | null>(null); const lastLoadOptions = ref<MapConfigLoadOptions | null>(null);
// 备注:统一规范图例 nameEn避免 `_测试` 后缀影响索引命中。
const normalizeLegendNameEn = (nameEn?: string): string => { const normalizeLegendNameEn = (nameEn?: string): string => {
if (!nameEn) return ''; if (!nameEn) return '';
return nameEn.includes('_测试') ? nameEn.split('_测试')[0] : nameEn; return nameEn;
}; };
// 备注:重建图层配置索引,后续按 key 读取图层配置时不再全量递归。 // 备注:重建图层配置索引,后续按 key 读取图层配置时不再全量递归。

View File

@ -16,6 +16,7 @@ import { useMapViewStore } from '@/modules/map/stores/map-view.store';
import request from '@/utils/request'; import request from '@/utils/request';
const mapClass = MapClass.getInstance(); const mapClass = MapClass.getInstance();
const ENG_POINT_LAYER_KEY = 'eng_point'; const ENG_POINT_LAYER_KEY = 'eng_point';
const ENG_ALARM_POINT_LAYER_KEY = 'eng_alarm_point';
const YLFB_POINT_LAYER_KEY = 'ylfb_point'; const YLFB_POINT_LAYER_KEY = 'ylfb_point';
const ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5; const ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5;
const ENG_POINT_LARGE_STATES = [ const ENG_POINT_LARGE_STATES = [
@ -29,6 +30,22 @@ const ENG_POINT_MEDIUM_STATES = [
'mid_eng_nbuilt' 'mid_eng_nbuilt'
]; ];
const isEngPointRuntimeItem = (item: any): boolean => {
const runtimeLayerKey = String(
item?.layerKey || item?.type || item?._layerKey || ''
).toLowerCase();
const sttpMap = String(
item?._sttpMap || item?.sttpMap || item?.popupSttpMap || ''
).toUpperCase();
const sttpCode = String(item?.sttpCode || item?.sttp || '').toUpperCase();
if (runtimeLayerKey) {
return runtimeLayerKey.includes(ENG_POINT_LAYER_KEY);
}
return sttpMap === 'ENG' || sttpMap === 'ENG2' || sttpCode === 'ENG';
};
const normalizeCachePayload = (value: any): any => { const normalizeCachePayload = (value: any): any => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map(item => normalizeCachePayload(item)); return value.map(item => normalizeCachePayload(item));
@ -183,6 +200,10 @@ export const useMapStore = defineStore('map', () => {
const allowMedium = currentZoom > ENG_POINT_MEDIUM_VISIBLE_ZOOM; const allowMedium = currentZoom > ENG_POINT_MEDIUM_VISIBLE_ZOOM;
return sourceData.filter((item: any) => { return sourceData.filter((item: any) => {
if (!isEngPointRuntimeItem(item)) {
return true;
}
const state = String(item?.anchoPointState || ''); const state = String(item?.anchoPointState || '');
if (ENG_POINT_LARGE_STATES.includes(state)) { if (ENG_POINT_LARGE_STATES.includes(state)) {
return true; return true;
@ -985,14 +1006,21 @@ export const useMapStore = defineStore('map', () => {
layerKey: string, layerKey: string,
nameEn: string nameEn: string
): string[] => { ): string[] => {
void layerKey;
const normalizedNameEn = normalizeLegendNameEn(nameEn); const normalizedNameEn = normalizeLegendNameEn(nameEn);
if (!normalizedNameEn) { if (!normalizedNameEn) {
return []; return [];
} }
if (normalizedNameEn.includes('alarm_range_')) { if (normalizedNameEn.includes('alarm_range_')) {
return [`large_eng_built_${normalizedNameEn}`]; const candidates: string[] = [];
if (layerKey === ENG_ALARM_POINT_LAYER_KEY) {
candidates.push(`mid_eng_built_${normalizedNameEn}`);
candidates.push(`large_eng_built_${normalizedNameEn}`);
} else {
candidates.push(`large_eng_built_${normalizedNameEn}`);
candidates.push(`mid_eng_built_${normalizedNameEn}`);
}
return candidates;
} }
return [normalizedNameEn]; return [normalizedNameEn];

View File

@ -1,4 +1,4 @@
<!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\components\LegendData\index.vue --> <!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\componentsData\index.vue -->
<template> <template>
<div class="content"> <div class="content">
<LegendDataSearch <LegendDataSearch