huweili-cesium 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,242 @@
1
+ /**
2
+ * 无人机规划航迹模块
3
+ *
4
+ * 根据前端传入的经纬度+高度数组,在空中绘制航迹连线及航点标注。
5
+ * 无人机实际位置仍由 WebSocket 数据通过 moveDronePoint 驱动。
6
+ */
7
+ import * as Cesium from 'cesium'
8
+ import { useMapStore } from './stores/mapStore.js'
9
+
10
+ const PLANNED_ROUTE_STORE_SUFFIX = '_planned_route'
11
+ const DEFAULT_LINE_COLOR = 'rgba(0, 191, 255, 0.85)'
12
+ const DEFAULT_LINE_WIDTH = 3
13
+
14
+ /**
15
+ * 将前端航点数组规范为 { lng, lat, height }
16
+ * 支持字段:lng/lon/longitude, lat/latitude, height/alt/altitude
17
+ */
18
+ function normalizeWaypoints(waypoints) {
19
+ if (!Array.isArray(waypoints)) return []
20
+
21
+ return waypoints
22
+ .map((wp, index) => {
23
+ const lng = Number(wp?.lng ?? wp?.lon ?? wp?.longitude ?? wp?.LONGITUDE)
24
+ const lat = Number(wp?.lat ?? wp?.latitude ?? wp?.LATITUDE)
25
+ const height = Number(wp?.height ?? wp?.alt ?? wp?.altitude ?? wp?.HEIGHT ?? 0)
26
+ if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null
27
+ return {
28
+ lng,
29
+ lat,
30
+ height: Number.isFinite(height) ? height : 0,
31
+ index,
32
+ }
33
+ })
34
+ .filter(Boolean)
35
+ }
36
+
37
+ function getRouteStoreKey(routeId) {
38
+ return `${routeId}${PLANNED_ROUTE_STORE_SUFFIX}`
39
+ }
40
+
41
+ function removeRouteEntities(map, routeData) {
42
+ if (!map || !routeData) return
43
+
44
+ routeData.entities?.forEach((entity) => {
45
+ if (entity) {
46
+ map.entities.remove(entity)
47
+ }
48
+ })
49
+ routeData.entities = []
50
+ routeData.polylineEntity = null
51
+ }
52
+
53
+ export function dronePlannedRouteConfig() {
54
+ const mapStore = useMapStore()
55
+
56
+ /**
57
+ * 绘制无人机规划航迹(空中折线 + 航点标注)
58
+ * @param {Object} options
59
+ * @param {string} options.routeId 航迹唯一标识,通常与无人机 pointId/uavId 一致
60
+ * @param {string} [options.pointId] 同 routeId,二选一
61
+ * @param {string} options.mapId 地图实例 ID
62
+ * @param {Array<{lng, lat, height}|{longitude, latitude, height}>} options.waypoints 航点数组
63
+ * @param {string} [options.lineColor] 航迹线颜色,默认青色
64
+ * @param {number} [options.lineWidth] 航迹线宽度
65
+ * @param {boolean} [options.showWaypointLabels=true] 是否显示「航点1、航点2…」标签
66
+ * @param {number} [options.waypointPixelSize=10] 航点圆点大小
67
+ * @param {boolean} [options.flyToRoute=false] 绘制后是否飞到航迹范围
68
+ * @returns {Object|null} 航迹数据对象
69
+ */
70
+ const setDronePlannedRoute = (options = {}) => {
71
+ const routeId = options.routeId || options.pointId
72
+ const { mapId, flyToRoute = false } = options
73
+
74
+ if (!routeId) {
75
+ console.warn('setDronePlannedRoute: 缺少 routeId / pointId')
76
+ return null
77
+ }
78
+
79
+ const normalized = normalizeWaypoints(options.waypoints)
80
+ if (normalized.length < 1) {
81
+ console.warn('setDronePlannedRoute: 航点数组为空或无效')
82
+ return null
83
+ }
84
+
85
+ const map = mapStore.getMap(mapId)
86
+ if (!map) {
87
+ console.error('地图实例不存在')
88
+ return null
89
+ }
90
+
91
+ // 已存在则先清除再重建,保证航迹与航点一致
92
+ clearDronePlannedRoute({ routeId, mapId })
93
+
94
+ const lineColor = options.lineColor || DEFAULT_LINE_COLOR
95
+ const lineWidth = Number(options.lineWidth) || DEFAULT_LINE_WIDTH
96
+ const showWaypointLabels = options.showWaypointLabels !== false
97
+ const waypointPixelSize = Number(options.waypointPixelSize) || 10
98
+ const color = Cesium.Color.fromCssColorString(lineColor)
99
+
100
+ const positions = normalized.map((wp) =>
101
+ Cesium.Cartesian3.fromDegrees(wp.lng, wp.lat, wp.height)
102
+ )
103
+
104
+ const routeData = {
105
+ routeId,
106
+ mapId,
107
+ waypoints: normalized,
108
+ entities: [],
109
+ polylineEntity: null,
110
+ isVisible: true,
111
+ }
112
+
113
+ if (normalized.length >= 2) {
114
+ routeData.polylineEntity = map.entities.add({
115
+ id: `${routeId}_planned_route_line`,
116
+ name: `规划航迹:${routeId}`,
117
+ polyline: {
118
+ positions,
119
+ width: lineWidth,
120
+ material: color,
121
+ arcType: Cesium.ArcType.NONE,
122
+ clampToGround: false,
123
+ },
124
+ })
125
+ routeData.entities.push(routeData.polylineEntity)
126
+ }
127
+
128
+ normalized.forEach((wp, i) => {
129
+ const position = Cesium.Cartesian3.fromDegrees(wp.lng, wp.lat, wp.height)
130
+ const waypointEntity = map.entities.add({
131
+ id: `${routeId}_planned_route_wp_${i}`,
132
+ name: `规划航点${i + 1}:${routeId}`,
133
+ position,
134
+ point: {
135
+ pixelSize: waypointPixelSize,
136
+ color,
137
+ outlineColor: Cesium.Color.WHITE,
138
+ outlineWidth: 2,
139
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
140
+ },
141
+ label: showWaypointLabels
142
+ ? {
143
+ text: `航点${i + 1}`,
144
+ font: '14px Microsoft YaHei, sans-serif',
145
+ fillColor: Cesium.Color.WHITE,
146
+ outlineColor: Cesium.Color.BLACK,
147
+ outlineWidth: 2,
148
+ style: Cesium.LabelStyle.FILL_AND_OUTLINE,
149
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
150
+ pixelOffset: new Cesium.Cartesian2(0, -12),
151
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
152
+ }
153
+ : undefined,
154
+ })
155
+ routeData.entities.push(waypointEntity)
156
+ })
157
+
158
+ routeData.destroy = () => {
159
+ removeRouteEntities(map, routeData)
160
+ mapStore.removeGraphicMap(getRouteStoreKey(routeId), mapId)
161
+ }
162
+
163
+ mapStore.setGraphicMap(getRouteStoreKey(routeId), routeData, mapId)
164
+
165
+ if (flyToRoute && positions.length > 0) {
166
+ const boundingSphere = Cesium.BoundingSphere.fromPoints(positions)
167
+ map.camera.flyToBoundingSphere(boundingSphere, { duration: 1.2 })
168
+ }
169
+
170
+ return routeData
171
+ }
172
+
173
+ /**
174
+ * 更新规划航迹(等价于清除后重新绘制)
175
+ */
176
+ const updateDronePlannedRoute = (options) => setDronePlannedRoute(options)
177
+
178
+ /**
179
+ * 清除规划航迹及航点标注
180
+ * @param {Object} options
181
+ * @param {string} options.routeId 航迹 ID(或 pointId)
182
+ * @param {string} options.mapId 地图实例 ID
183
+ */
184
+ const clearDronePlannedRoute = (options = {}) => {
185
+ const routeId = options.routeId || options.pointId
186
+ const { mapId } = options
187
+
188
+ if (!routeId) {
189
+ console.warn('clearDronePlannedRoute: 缺少 routeId / pointId')
190
+ return false
191
+ }
192
+
193
+ const storeKey = getRouteStoreKey(routeId)
194
+ const routeData = mapStore.getGraphicMap(storeKey, mapId)
195
+ if (!routeData) return false
196
+
197
+ const map = mapStore.getMap(mapId)
198
+ removeRouteEntities(map, routeData)
199
+ mapStore.removeGraphicMap(storeKey, mapId)
200
+ return true
201
+ }
202
+
203
+ /**
204
+ * 显示/隐藏规划航迹
205
+ */
206
+ const toggleDronePlannedRouteVisibility = (options = {}) => {
207
+ const routeId = options.routeId || options.pointId
208
+ const { mapId, visible } = options
209
+
210
+ if (!routeId || visible === undefined) return false
211
+
212
+ const routeData = mapStore.getGraphicMap(getRouteStoreKey(routeId), mapId)
213
+ if (!routeData) {
214
+ console.warn(`规划航迹不存在,ID: ${routeId}`)
215
+ return false
216
+ }
217
+
218
+ routeData.isVisible = visible
219
+ routeData.entities?.forEach((entity) => {
220
+ if (entity) entity.show = visible
221
+ })
222
+ return true
223
+ }
224
+
225
+ /**
226
+ * 获取已绘制的规划航迹数据
227
+ */
228
+ const getDronePlannedRoute = (options = {}) => {
229
+ const routeId = options.routeId || options.pointId
230
+ const { mapId } = options
231
+ if (!routeId) return null
232
+ return mapStore.getGraphicMap(getRouteStoreKey(routeId), mapId) || null
233
+ }
234
+
235
+ return {
236
+ setDronePlannedRoute,
237
+ updateDronePlannedRoute,
238
+ clearDronePlannedRoute,
239
+ toggleDronePlannedRouteVisibility,
240
+ getDronePlannedRoute,
241
+ }
242
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * movePoint 扩展模块(不修改原 movePoint.js)
3
+ *
4
+ * 在原有 movePointConfig 能力基础上,组合规划航迹模块,
5
+ * 并提供同时销毁无人机与规划航迹的方法。
6
+ */
7
+ import { movePointConfig } from './movePoint.js'
8
+ import { dronePlannedRouteConfig } from './dronePlannedRoute.js'
9
+
10
+ export function movePointPlannedRouteConfig(baseUrl) {
11
+ const movePoint = movePointConfig(baseUrl)
12
+ const plannedRoute = dronePlannedRouteConfig()
13
+
14
+ /**
15
+ * 销毁无人机模型、实时轨迹,并清除对应规划航迹
16
+ */
17
+ const destroyDroneTrailAndPlannedRoute = (options) => {
18
+ const result = movePoint.destroyDroneTrail(options)
19
+ plannedRoute.clearDronePlannedRoute({
20
+ routeId: options?.pointId,
21
+ mapId: options?.mapId,
22
+ })
23
+ return result
24
+ }
25
+
26
+ return {
27
+ ...movePoint,
28
+ ...plannedRoute,
29
+ destroyDroneTrailAndPlannedRoute,
30
+ }
31
+ }
package/package.json CHANGED
@@ -1,101 +1,109 @@
1
- {
2
- "name": "huweili-cesium",
3
- "version": "1.0.13",
4
- "description": "基于 Cesium 的地图工具库(无人机态势、轨迹、围栏、工具栏等)",
5
- "type": "module",
6
- "main": "./index.js",
7
- "module": "./index.js",
8
- "exports": {
9
- ".": "./index.js",
10
- "./basis": "./basis.js",
11
- "./basis.js": "./basis.js",
12
- "./captureFenceScreenshot": "./captureFenceScreenshot.js",
13
- "./captureFenceScreenshot.js": "./captureFenceScreenshot.js",
14
- "./cardPool": "./cardPool.js",
15
- "./cardPool.js": "./cardPool.js",
16
- "./clickHandler": "./clickHandler.js",
17
- "./clickHandler.js": "./clickHandler.js",
18
- "./customToolbarButtons": "./customToolbarButtons.js",
19
- "./customToolbarButtons.js": "./customToolbarButtons.js",
20
- "./drawFence": "./drawFence.js",
21
- "./drawFence.js": "./drawFence.js",
22
- "./drawFenceNew": "./drawFenceNew.js",
23
- "./drawFenceNew.js": "./drawFenceNew.js",
24
- "./droneRipple": "./droneRipple.js",
25
- "./droneRipple.js": "./droneRipple.js",
26
- "./geometry": "./geometry.js",
27
- "./geometry.js": "./geometry.js",
28
- "./groundLink": "./groundLink.js",
29
- "./groundLink.js": "./groundLink.js",
30
- "./hemisphere": "./hemisphere.js",
31
- "./hemisphere.js": "./hemisphere.js",
32
- "./labelDiv": "./labelDiv.js",
33
- "./labelDiv.js": "./labelDiv.js",
34
- "./movePath": "./movePath.js",
35
- "./movePath.js": "./movePath.js",
36
- "./movePoint": "./movePoint.js",
37
- "./movePoint.js": "./movePoint.js",
38
- "./qrCodeGenerator": "./qrCodeGenerator.js",
39
- "./qrCodeGenerator.js": "./qrCodeGenerator.js",
40
- "./setPath": "./setPath.js",
41
- "./setPath.js": "./setPath.js",
42
- "./setPoint": "./setPoint.js",
43
- "./setPoint.js": "./setPoint.js",
44
- "./tileProviders": "./tileProviders.js",
45
- "./tileProviders.js": "./tileProviders.js",
46
- "./toolbar/basemapSwitcher": "./toolbar/basemapSwitcher.js",
47
- "./toolbar/basemapSwitcher.js": "./toolbar/basemapSwitcher.js",
48
- "./toolbar/compass": "./toolbar/compass.js",
49
- "./toolbar/compass.js": "./toolbar/compass.js",
50
- "./toolbar/fullscreenController": "./toolbar/fullscreenController.js",
51
- "./toolbar/fullscreenController.js": "./toolbar/fullscreenController.js",
52
- "./toolbar/zoomController": "./toolbar/zoomController.js",
53
- "./toolbar/zoomController.js": "./toolbar/zoomController.js",
54
- "./stores/mapStore": "./stores/mapStore.js",
55
- "./stores/mapStore.js": "./stores/mapStore.js",
56
- "./utils/eventBus": "./utils/eventBus.js",
57
- "./utils/useEventBus": "./utils/useEventBus.js",
58
- "./utils/getMapCenterPosition": "./utils/getMapCenterPosition.js",
59
- "./utils/droneSelection": "./utils/droneSelection.js",
60
- "./config": "./config/index.js",
61
- "./config/index": "./config/index.js",
62
- "./config/hooks": "./config/hooks.js",
63
- "./api/gaode": "./api/gaode.js"
64
- },
65
- "files": [
66
- "*.js",
67
- "toolbar/**/*.js",
68
- "stores/**/*.js",
69
- "utils/**/*.js",
70
- "config/**/*.js",
71
- "api/**/*.js"
72
- ],
73
- "scripts": {
74
- "sync": "node scripts/sync-from-source.mjs",
75
- "prepublishOnly": "node scripts/verify-files.mjs"
76
- },
77
- "keywords": [
78
- "cesium",
79
- "map",
80
- "drone",
81
- "3d",
82
- "gis"
83
- ],
84
- "author": "huweili <czxyhuweili@163.com>",
85
- "license": "MIT",
86
- "repository": {
87
- "type": "git",
88
- "url": ""
89
- },
90
- "peerDependencies": {
91
- "cesium": ">=1.100.0",
92
- "mitt": "^3.0.0",
93
- "pinia": "^2.0.0 || ^3.0.0",
94
- "qrcode": "^1.5.0",
95
- "vue": "^3.3.0"
96
- },
97
- "dependencies": {},
98
- "engines": {
99
- "node": ">=18"
100
- }
101
- }
1
+ {
2
+ "name": "huweili-cesium",
3
+ "version": "1.0.15",
4
+ "description": "基于 Cesium 的地图工具库(无人机态势、轨迹、围栏、工具栏等)",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "module": "./index.js",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./basis": "./basis.js",
11
+ "./basis.js": "./basis.js",
12
+ "./captureFenceScreenshot": "./captureFenceScreenshot.js",
13
+ "./captureFenceScreenshot.js": "./captureFenceScreenshot.js",
14
+ "./cardPool": "./cardPool.js",
15
+ "./cardPool.js": "./cardPool.js",
16
+ "./clickHandler": "./clickHandler.js",
17
+ "./clickHandler.js": "./clickHandler.js",
18
+ "./customToolbarButtons": "./customToolbarButtons.js",
19
+ "./customToolbarButtons.js": "./customToolbarButtons.js",
20
+ "./drawFence": "./drawFence.js",
21
+ "./drawFence.js": "./drawFence.js",
22
+ "./drawFenceNew": "./drawFenceNew.js",
23
+ "./drawFenceNew.js": "./drawFenceNew.js",
24
+ "./dronePlannedRoute": "./dronePlannedRoute.js",
25
+ "./dronePlannedRoute.js": "./dronePlannedRoute.js",
26
+ "./droneRipple": "./droneRipple.js",
27
+ "./droneRipple.js": "./droneRipple.js",
28
+ "./geometry": "./geometry.js",
29
+ "./geometry.js": "./geometry.js",
30
+ "./groundLink": "./groundLink.js",
31
+ "./groundLink.js": "./groundLink.js",
32
+ "./hemisphere": "./hemisphere.js",
33
+ "./hemisphere.js": "./hemisphere.js",
34
+ "./labelDiv": "./labelDiv.js",
35
+ "./labelDiv.js": "./labelDiv.js",
36
+ "./movePath": "./movePath.js",
37
+ "./movePath.js": "./movePath.js",
38
+ "./movePoint": "./movePoint.js",
39
+ "./movePoint.js": "./movePoint.js",
40
+ "./movePointPlannedRoute": "./movePointPlannedRoute.js",
41
+ "./movePointPlannedRoute.js": "./movePointPlannedRoute.js",
42
+ "./qrCodeGenerator": "./qrCodeGenerator.js",
43
+ "./qrCodeGenerator.js": "./qrCodeGenerator.js",
44
+ "./setPath": "./setPath.js",
45
+ "./setPath.js": "./setPath.js",
46
+ "./setPoint": "./setPoint.js",
47
+ "./setPoint.js": "./setPoint.js",
48
+ "./tileProviders": "./tileProviders.js",
49
+ "./tileProviders.js": "./tileProviders.js",
50
+ "./toolbar/basemapSwitcher": "./toolbar/basemapSwitcher.js",
51
+ "./toolbar/basemapSwitcher.js": "./toolbar/basemapSwitcher.js",
52
+ "./toolbar/compass": "./toolbar/compass.js",
53
+ "./toolbar/compass.js": "./toolbar/compass.js",
54
+ "./toolbar/fullscreenController": "./toolbar/fullscreenController.js",
55
+ "./toolbar/fullscreenController.js": "./toolbar/fullscreenController.js",
56
+ "./toolbar/zoomController": "./toolbar/zoomController.js",
57
+ "./toolbar/zoomController.js": "./toolbar/zoomController.js",
58
+ "./stores/mapStore": "./stores/mapStore.js",
59
+ "./stores/mapStore.js": "./stores/mapStore.js",
60
+ "./utils/basemapSwitcher": "./utils/basemapSwitcher.js",
61
+ "./utils/basemapSwitcher.js": "./utils/basemapSwitcher.js",
62
+ "./utils/mapImagery": "./utils/mapImagery.js",
63
+ "./utils/mapImagery.js": "./utils/mapImagery.js",
64
+ "./utils/eventBus": "./utils/eventBus.js",
65
+ "./utils/useEventBus": "./utils/useEventBus.js",
66
+ "./utils/getMapCenterPosition": "./utils/getMapCenterPosition.js",
67
+ "./utils/droneSelection": "./utils/droneSelection.js",
68
+ "./config": "./config/index.js",
69
+ "./config/index": "./config/index.js",
70
+ "./config/hooks": "./config/hooks.js",
71
+ "./api/gaode": "./api/gaode.js"
72
+ },
73
+ "files": [
74
+ "*.js",
75
+ "toolbar/**/*.js",
76
+ "stores/**/*.js",
77
+ "utils/**/*.js",
78
+ "config/**/*.js",
79
+ "api/**/*.js"
80
+ ],
81
+ "scripts": {
82
+ "sync": "node scripts/sync-from-source.mjs",
83
+ "prepublishOnly": "node scripts/verify-files.mjs"
84
+ },
85
+ "keywords": [
86
+ "cesium",
87
+ "map",
88
+ "drone",
89
+ "3d",
90
+ "gis"
91
+ ],
92
+ "author": "huweili <czxyhuweili@163.com>",
93
+ "license": "MIT",
94
+ "repository": {
95
+ "type": "git",
96
+ "url": ""
97
+ },
98
+ "peerDependencies": {
99
+ "cesium": ">=1.100.0",
100
+ "mitt": "^3.0.0",
101
+ "pinia": "^2.0.0 || ^3.0.0",
102
+ "qrcode": "^1.5.0",
103
+ "vue": "^3.3.0"
104
+ },
105
+ "dependencies": {},
106
+ "engines": {
107
+ "node": ">=18"
108
+ }
109
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * 底图切换器(支持 coexistMap 离线/在线共存)
3
+ */
4
+ import { applyImageryLayers } from './mapImagery.js'
5
+
6
+ export const BasemapIds = window.MapConfig?.basemaps?.reduce((acc, item) => {
7
+ const key = item.id.toUpperCase().replace(/-/g, '_')
8
+ acc[key] = item.id
9
+ return acc
10
+ }, {}) || {}
11
+
12
+ export function createBasemapSwitcher() {
13
+ let currentBasemapId = null
14
+
15
+ const getBasemaps = () => window.MapConfig?.basemaps || []
16
+ const getBasemapById = (id) => getBasemaps().find((b) => b.id === id)
17
+
18
+ const loadBasemap = (viewer, basemapConfig) => {
19
+ if (!viewer || !basemapConfig) return
20
+ applyImageryLayers(viewer, window.MapConfig || {}, basemapConfig)
21
+ currentBasemapId = basemapConfig.id
22
+ window._currentBasemapId = basemapConfig.id
23
+ }
24
+
25
+ const switchById = (viewer, id) => {
26
+ const basemap = getBasemapById(id)
27
+ if (!basemap) {
28
+ console.error(`Basemap not found with id: ${id}`)
29
+ return
30
+ }
31
+ loadBasemap(viewer, basemap)
32
+ }
33
+
34
+ const toggleSatelliteBasemap = (viewer) => {
35
+ if (!viewer || !window.MapConfig) return
36
+
37
+ if (currentBasemapId === BasemapIds.AMAP_SATELLITE) {
38
+ const defaultBasemap = getBasemaps().find((b) => b.show)
39
+ if (defaultBasemap) {
40
+ loadBasemap(viewer, defaultBasemap)
41
+ }
42
+ } else {
43
+ switchById(viewer, BasemapIds.AMAP_SATELLITE)
44
+ }
45
+ }
46
+
47
+ const getCurrentBasemapId = () => currentBasemapId
48
+ const setCurrentBasemapId = (id) => {
49
+ currentBasemapId = id
50
+ }
51
+
52
+ const toggleBasemapPickerPanel = (viewer, triggerButton) => {
53
+ if (!viewer?.container) return
54
+
55
+ const PANEL_ID = 'cesium-basemap-picker-panel'
56
+ let panel = document.getElementById(PANEL_ID)
57
+
58
+ const syncPanelViewport = () => {
59
+ if (!panel || panel.style.display === 'none') return
60
+ const buttonRect = triggerButton?.getBoundingClientRect()
61
+ if (!buttonRect) return
62
+
63
+ const margin = 35
64
+ const panelRect = panel.getBoundingClientRect()
65
+ const toolbar = document.querySelector('.cesium-viewer-toolbar')
66
+ const toolbarRect = toolbar?.getBoundingClientRect()
67
+
68
+ let left = (toolbarRect?.left || 0) + (toolbarRect?.width || 0)
69
+ let top = buttonRect.bottom - panelRect.height
70
+
71
+ if (top < 0) top = margin
72
+ if (top + panelRect.height > window.innerHeight) {
73
+ top = window.innerHeight - panelRect.height - margin
74
+ }
75
+
76
+ panel.style.position = 'fixed'
77
+ panel.style.right = 'auto'
78
+ panel.style.bottom = 'auto'
79
+ panel.style.top = `${Math.round(top)}px`
80
+ panel.style.left = `${Math.round(left)}px`
81
+ }
82
+
83
+ const detachOutside = () => {
84
+ if (panel?._removeOutsideListener) {
85
+ panel._removeOutsideListener()
86
+ panel._removeOutsideListener = null
87
+ }
88
+ if (panel?._onReposition) {
89
+ window.removeEventListener('resize', panel._onReposition)
90
+ window.removeEventListener('scroll', panel._onReposition, true)
91
+ panel._onReposition = null
92
+ }
93
+ }
94
+
95
+ const hidePanel = () => {
96
+ detachOutside()
97
+ if (panel) panel.style.display = 'none'
98
+ }
99
+
100
+ const isPanelVisible = () => panel && panel.style.display !== 'none'
101
+
102
+ if (isPanelVisible()) {
103
+ hidePanel()
104
+ return
105
+ }
106
+
107
+ if (!panel) {
108
+ panel = document.createElement('div')
109
+ panel.id = PANEL_ID
110
+ panel.style.cssText = `
111
+ position: fixed;
112
+ z-index: 2147483647;
113
+ min-width: 280px;
114
+ max-width: 320px;
115
+ max-height: min(70vh, 500px);
116
+ overflow-y: auto;
117
+ overflow-x: hidden;
118
+ background: rgba(28, 32, 40, 0.94);
119
+ border: 1px solid rgba(255, 255, 255, 0.12);
120
+ border-radius: 8px;
121
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
122
+ display: none;
123
+ flex-direction: column;
124
+ padding: 8px;
125
+ scrollbar-width: thin;
126
+ scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
127
+ `
128
+ const list = document.createElement('div')
129
+ list.setAttribute('data-role', 'basemap-list')
130
+ list.style.cssText = 'display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px;'
131
+ panel.appendChild(list)
132
+ document.body.appendChild(panel)
133
+
134
+ const style = document.createElement('style')
135
+ style.textContent = `
136
+ #${PANEL_ID}::-webkit-scrollbar { width: 6px; }
137
+ #${PANEL_ID}::-webkit-scrollbar-track { background: transparent; }
138
+ #${PANEL_ID}::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 3px; }
139
+ #${PANEL_ID}::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.35); }
140
+ `
141
+ document.head.appendChild(style)
142
+ }
143
+
144
+ const listEl = panel.querySelector('[data-role="basemap-list"]')
145
+ listEl.innerHTML = ''
146
+
147
+ const currentId = getCurrentBasemapId()
148
+ getBasemaps().forEach((bm) => {
149
+ const row = document.createElement('button')
150
+ row.type = 'button'
151
+ const active = bm.id === currentId
152
+ row.style.cssText = `
153
+ display: flex;
154
+ flex-direction: column;
155
+ width: 100%;
156
+ padding: 8px;
157
+ border: none;
158
+ background: ${active ? 'rgba(255, 255, 255, 0.1)' : 'transparent'};
159
+ cursor: pointer;
160
+ gap: 6px;
161
+ align-items: center;
162
+ justify-content: center;
163
+ `
164
+
165
+ const imgContainer = document.createElement('div')
166
+ imgContainer.style.cssText = `
167
+ width: 100%;
168
+ height: 60px;
169
+ border-radius: 4px;
170
+ overflow: hidden;
171
+ border: 2px solid ${active ? '#3B82F6' : 'transparent'};
172
+ position: relative;
173
+ `
174
+ if (bm.customMapColorStyle?.enabled) {
175
+ imgContainer.style.backgroundColor = bm.customMapColorStyle.MapBaseColor
176
+ }
177
+
178
+ const img = document.createElement('img')
179
+ img.src = import.meta.env.BASE_URL + bm.thumbnail || ''
180
+ img.style.cssText = `
181
+ width: 100%;
182
+ height: 100%;
183
+ object-fit: cover;
184
+ opacity: ${bm.customMapColorStyle?.enabled ? 0.7 : 1};
185
+ `
186
+ img.onerror = function () {
187
+ this.style.display = 'none'
188
+ }
189
+ imgContainer.appendChild(img)
190
+
191
+ const nameSpan = document.createElement('span')
192
+ nameSpan.textContent = bm.name
193
+ nameSpan.style.cssText = `
194
+ font-size: 12px;
195
+ color: #e8eaed;
196
+ font-weight: ${active ? '600' : '400'};
197
+ text-align: center;
198
+ `
199
+
200
+ row.appendChild(imgContainer)
201
+ row.appendChild(nameSpan)
202
+ row.onmouseenter = () => {
203
+ row.style.background = 'rgba(255, 255, 255, 0.08)'
204
+ }
205
+ row.onmouseleave = () => {
206
+ row.style.background = active ? 'rgba(255, 255, 255, 0.1)' : 'transparent'
207
+ }
208
+ row.onclick = (e) => {
209
+ e.stopPropagation()
210
+ switchById(viewer, bm.id)
211
+ hidePanel()
212
+ }
213
+ listEl.appendChild(row)
214
+ })
215
+
216
+ panel.style.display = 'flex'
217
+ requestAnimationFrame(() => {
218
+ requestAnimationFrame(() => syncPanelViewport())
219
+ })
220
+
221
+ detachOutside()
222
+ const onReposition = () => syncPanelViewport()
223
+ panel._onReposition = onReposition
224
+ window.addEventListener('resize', onReposition)
225
+ window.addEventListener('scroll', onReposition, true)
226
+
227
+ const onOutsidePointer = (e) => {
228
+ if (!panel || panel.style.display === 'none') return
229
+ if (panel.contains(e.target)) return
230
+ if (triggerButton && triggerButton.contains(e.target)) return
231
+ hidePanel()
232
+ }
233
+
234
+ requestAnimationFrame(() => {
235
+ requestAnimationFrame(() => {
236
+ document.addEventListener('pointerdown', onOutsidePointer, true)
237
+ })
238
+ })
239
+ panel._removeOutsideListener = () =>
240
+ document.removeEventListener('pointerdown', onOutsidePointer, true)
241
+ }
242
+
243
+ return {
244
+ switchById,
245
+ toggleSatelliteBasemap,
246
+ getBasemaps,
247
+ getBasemapById,
248
+ getCurrentBasemapId,
249
+ setCurrentBasemapId,
250
+ toggleBasemapPickerPanel,
251
+ }
252
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * 底图与离线共存图层加载
3
+ */
4
+ import * as Cesium from 'cesium'
5
+ import { customColorTileProvider, hexToRgb } from '../tileProviders.js'
6
+
7
+ /**
8
+ * 解析 coexistMap 自定义配色(支持 hex 或已转换的 rgb)
9
+ * @param {Object} coexistMap
10
+ * @returns {Object | null}
11
+ */
12
+ function resolveCoexistColorStyle(coexistMap) {
13
+ const style = coexistMap?.customMapColorStyle
14
+ if (!style?.enabled) return null
15
+
16
+ return {
17
+ enabled: true,
18
+ MapBaseColor:
19
+ typeof style.MapBaseColor === 'string'
20
+ ? hexToRgb(style.MapBaseColor)
21
+ : style.MapBaseColor,
22
+ RoadLightColor:
23
+ typeof style.RoadLightColor === 'string'
24
+ ? hexToRgb(style.RoadLightColor)
25
+ : style.RoadLightColor,
26
+ }
27
+ }
28
+
29
+ /**
30
+ * @param {Object} basemapConfig
31
+ * @returns {Cesium.ImageryProvider}
32
+ */
33
+ export function createBasemapProvider(basemapConfig) {
34
+ const tileProviderOptions = {
35
+ url: basemapConfig.url,
36
+ subdomains: ['1', '2', '3', '4'],
37
+ maximumLevel: basemapConfig.maximumLevel ?? 18,
38
+ credit: basemapConfig.name,
39
+ }
40
+
41
+ if (basemapConfig.customMapColorStyle?.enabled) {
42
+ return new customColorTileProvider(tileProviderOptions, basemapConfig.customMapColorStyle)
43
+ }
44
+
45
+ return new Cesium.UrlTemplateImageryProvider(tileProviderOptions)
46
+ }
47
+
48
+ /**
49
+ * 离线瓦片(限定矩形范围,仅在该区域内请求瓦片)
50
+ * @param {Object} coexistMap mapConfig.coexistMap
51
+ * @returns {Cesium.ImageryProvider | null}
52
+ */
53
+ export function createCoexistOfflineProvider(coexistMap) {
54
+ const offline = coexistMap?.map
55
+ if (!offline?.url) return null
56
+
57
+ const options = {
58
+ url: offline.url,
59
+ minimumLevel: offline.minimumLevel ?? 0,
60
+ maximumLevel: offline.maximumLevel ?? 18,
61
+ credit: offline.name || '离线地图',
62
+ }
63
+
64
+ const bounds = offline.bounds
65
+ if (bounds) {
66
+ options.rectangle = Cesium.Rectangle.fromDegrees(
67
+ bounds.west,
68
+ bounds.south,
69
+ bounds.east,
70
+ bounds.north,
71
+ )
72
+ }
73
+
74
+ const colorStyle = resolveCoexistColorStyle(coexistMap)
75
+ if (colorStyle) {
76
+ return new customColorTileProvider(options, colorStyle)
77
+ }
78
+
79
+ return new Cesium.UrlTemplateImageryProvider(options)
80
+ }
81
+
82
+ /**
83
+ * 解析在线底图:coexist 开启且配置了 mapId 时优先用该底图,否则用 show:true 的底图
84
+ * @param {Object} mapOptions
85
+ * @returns {Object | undefined}
86
+ */
87
+ export function resolveOnlineBasemap(mapOptions) {
88
+ const basemaps = mapOptions?.basemaps || []
89
+ const coexist = mapOptions?.coexistMap
90
+
91
+ if (coexist?.enabled && coexist.mapId) {
92
+ const byId = basemaps.find((b) => b.id === coexist.mapId)
93
+ if (byId) return byId
94
+ }
95
+
96
+ return basemaps.find((b) => b.show === true)
97
+ }
98
+
99
+ /**
100
+ * 在线底图(底层,全球)+ 离线底图(顶层,仅 bounds 内加载)
101
+ * @param {import('cesium').Viewer} viewer
102
+ * @param {Object} mapOptions
103
+ * @param {Object} basemapConfig
104
+ * @returns {{ onlineLayer: Cesium.ImageryLayer, offlineLayer: Cesium.ImageryLayer | null }}
105
+ */
106
+ export function applyImageryLayers(viewer, mapOptions, basemapConfig) {
107
+ viewer.imageryLayers.removeAll()
108
+
109
+ const onlineLayer = new Cesium.ImageryLayer(createBasemapProvider(basemapConfig))
110
+ viewer.imageryLayers.add(onlineLayer)
111
+
112
+ let offlineLayer = null
113
+ const coexist = mapOptions?.coexistMap
114
+ if (coexist?.enabled) {
115
+ const offlineProvider = createCoexistOfflineProvider(coexist)
116
+ if (offlineProvider) {
117
+ offlineLayer = new Cesium.ImageryLayer(offlineProvider)
118
+ viewer.imageryLayers.add(offlineLayer)
119
+ console.log(
120
+ `离线共存已启用:${coexist.map?.name},范围外使用在线底图「${basemapConfig.name}」`,
121
+ )
122
+ }
123
+ }
124
+
125
+ return { onlineLayer, offlineLayer }
126
+ }
127
+
128
+ /**
129
+ * 底图切换后重新叠加离线层(仅替换在线层,保留离线层逻辑:先清再全量加载)
130
+ */
131
+ export function switchBasemapWithCoexist(viewer, mapOptions, basemapConfig) {
132
+ const layers = applyImageryLayers(viewer, mapOptions, basemapConfig)
133
+ window._currentBasemapId = basemapConfig.id
134
+ return layers
135
+ }