raintee-maputils 1.0.1 → 1.0.2
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.
- package/lib/CustomOptionsControl.js +168 -0
- package/lib/CustomToggleControl.js +125 -0
- package/lib/RainteeConstants.js +820 -0
- package/lib/RainteeGISUtil.js +149 -0
- package/lib/RainteeSourceMapTool.js +104 -0
- package/lib/RasterLayerController.js +261 -0
- package/lib/TerrainToggleControl.js +127 -0
- package/lib/useDrawCache.js +58 -0
- package/package.json +3 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as turf from '@turf/turf';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 传入一个 features 数组(至少包含两个要素),
|
|
6
|
+
* 计算第二个要素(features[1])中,不在第一个要素(features[0])内部的部分,
|
|
7
|
+
* 即:features[1] 减去 features[0],返回这一部分(GeoJSON Feature)。
|
|
8
|
+
*
|
|
9
|
+
* @param {Array<turf.Feature>} features - GeoJSON Feature 数组,至少包含两个要素
|
|
10
|
+
* @returns {turf.Feature | null} - 返回 features[1] 减去 features[0] 的部分,如果出错或无效返回 null
|
|
11
|
+
*/
|
|
12
|
+
export function getFeatureOutsidePart(features) {
|
|
13
|
+
if (!Array.isArray(features) || features.length < 2) {
|
|
14
|
+
console.error('Input must be an array of at least two GeoJSON Features.');
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 我们要计算的是:featureB - featureA,即 featureB 的外部部分(不在 featureA 中的部分)
|
|
19
|
+
const differenceResult = turf.difference(turf.featureCollection(features));
|
|
20
|
+
|
|
21
|
+
if (!differenceResult) {
|
|
22
|
+
console.warn('Difference result is empty or invalid. The second feature may be fully inside the first one.');
|
|
23
|
+
return null; // 两者无差异,或者输入几何有问题
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return differenceResult; // 返回的就是 features[1] 在 features[0] 外部的部分
|
|
27
|
+
}
|
|
28
|
+
export const RT_FitBoundsMapbox = (map, featureCollection) => {
|
|
29
|
+
try {
|
|
30
|
+
if (!map || !featureCollection) {
|
|
31
|
+
console.error('RT_FitBoundsMapbox: map 或 featureCollection 参数不能为空');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
const boundsArray = turf.bbox(featureCollection); // [minLng, minLat, maxLng, maxLat]
|
|
37
|
+
|
|
38
|
+
if (!boundsArray || boundsArray.length !== 4) {
|
|
39
|
+
console.error('RT_FitBoundsMapbox: 无法计算有效的包围盒');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const [minLng, minLat, maxLng, maxLat] = boundsArray;
|
|
44
|
+
|
|
45
|
+
const bounds = [
|
|
46
|
+
[minLng, minLat], // 左下角
|
|
47
|
+
[maxLng, maxLat] // 右上角
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
map.fitBounds(bounds, {
|
|
51
|
+
padding: 20, // 可选:边距(单位像素)
|
|
52
|
+
maxZoom: 15, // 可选:最大缩放级别,避免太近
|
|
53
|
+
duration: 1000, // 可选:动画时长(毫秒)
|
|
54
|
+
easing: (t) => {
|
|
55
|
+
return t;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('RT_FitBoundsMapbox: 拟合地图边界时出错', error);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
};
|
|
63
|
+
export const RT_FitBoundsMapboxNative = (map, featureCollection) => {
|
|
64
|
+
try {
|
|
65
|
+
if (!map || !featureCollection) {
|
|
66
|
+
console.error('RT_FitBoundsMapbox: map 或 featureCollection 参数不能为空');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ✅ 手写函数:计算 FeatureCollection 的 Bounding Box [minLng, minLat, maxLng, maxLat]
|
|
71
|
+
const calculateBoundingBox = (featureCollection) => {
|
|
72
|
+
let minLng = Infinity;
|
|
73
|
+
let minLat = Infinity;
|
|
74
|
+
let maxLng = -Infinity;
|
|
75
|
+
let maxLat = -Infinity;
|
|
76
|
+
|
|
77
|
+
// 遍历每个 feature
|
|
78
|
+
for (const feature of featureCollection.features) {
|
|
79
|
+
const coords = feature.geometry.coordinates;
|
|
80
|
+
|
|
81
|
+
// 递归遍历坐标(支持 Point、LineString、Polygon、Multi 等)
|
|
82
|
+
const traverseCoordinates = (coords) => {
|
|
83
|
+
if (!Array.isArray(coords)) return;
|
|
84
|
+
|
|
85
|
+
// 判断坐标类型:
|
|
86
|
+
if (typeof coords[0] === 'number') {
|
|
87
|
+
// 🟢 这是一个点 [lng, lat]
|
|
88
|
+
const [lng, lat] = coords;
|
|
89
|
+
if (lng < minLng) minLng = lng;
|
|
90
|
+
if (lng > maxLng) maxLng = lng;
|
|
91
|
+
if (lat < minLat) minLat = lat;
|
|
92
|
+
if (lat > maxLat) maxLat = lat;
|
|
93
|
+
} else if (Array.isArray(coords[0])) {
|
|
94
|
+
// 🔵 可能是 LineString、Polygon、MultiPoint 等(嵌套数组)
|
|
95
|
+
for (const subCoords of coords) {
|
|
96
|
+
traverseCoordinates(subCoords);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// 注意:更复杂的 GeometryCollection 或 MultiPolygon 也按类似方式递归,但此处已覆盖大部分情况
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
traverseCoordinates(coords);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 如果没有有效的点(比如 featureCollection 为空或没有坐标)
|
|
106
|
+
if (
|
|
107
|
+
minLng === Infinity ||
|
|
108
|
+
minLat === Infinity ||
|
|
109
|
+
maxLng === -Infinity ||
|
|
110
|
+
maxLat === -Infinity
|
|
111
|
+
) {
|
|
112
|
+
console.error('RT_FitBoundsMapbox: 未找到有效的坐标点');
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [minLng, minLat, maxLng, maxLat];
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ✅ 调用手写函数,计算 bounding box
|
|
120
|
+
const boundsArray = calculateBoundingBox(featureCollection);
|
|
121
|
+
|
|
122
|
+
if (!boundsArray) {
|
|
123
|
+
console.error('RT_FitBoundsMapbox: 无法计算有效的包围盒');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const [minLng, minLat, maxLng, maxLat] = boundsArray;
|
|
128
|
+
|
|
129
|
+
const bounds = [
|
|
130
|
+
[minLng, minLat], // 左下角
|
|
131
|
+
[maxLng, maxLat] // 右上角
|
|
132
|
+
];
|
|
133
|
+
console.log(`output->bounds`, bounds)
|
|
134
|
+
bounds.forEach(item => {
|
|
135
|
+
item.forEach((i, index) => {
|
|
136
|
+
item[index] = Number((i).toFixed(6))
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
// ✅ 调用 Mapbox 的 fitBounds
|
|
140
|
+
map.fitBounds(bounds, {
|
|
141
|
+
padding: 50, // 可选:边距(单位像素)
|
|
142
|
+
maxZoom: 15, // 可选:最大缩放级别,避免太近
|
|
143
|
+
duration: 1000, // 可选:动画时长(毫秒)
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error('RT_FitBoundsMapbox: 拟合地图边界时出错', error);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const treeDataAdapter = (data) => {
|
|
2
|
+
let i = 0
|
|
3
|
+
let treeData = []
|
|
4
|
+
let checkedKeys = []
|
|
5
|
+
data.forEach((item) => {
|
|
6
|
+
let kvs = item.id.split(';').map(kvstr => kvstr.split('='))
|
|
7
|
+
let groups = kvs.find(item => item[0] === 'Main')[1].split('/')
|
|
8
|
+
let layerName = kvs.find(item => item[0] === 'CN')[1]
|
|
9
|
+
let currentNode = treeData;
|
|
10
|
+
groups.forEach((group, index) => {
|
|
11
|
+
currentNode.find(item => item.label === group) || i++
|
|
12
|
+
currentNode.find(item => item.label === group) || currentNode.push({
|
|
13
|
+
id: i,
|
|
14
|
+
label: group,
|
|
15
|
+
children: []
|
|
16
|
+
})
|
|
17
|
+
currentNode = currentNode.find(item => item.label === group).children
|
|
18
|
+
})
|
|
19
|
+
i++
|
|
20
|
+
currentNode.push({
|
|
21
|
+
id: i,
|
|
22
|
+
label: layerName,
|
|
23
|
+
fullLayerName: groups.length === 0 ? layerName : groups.join('-') + '-' + layerName,
|
|
24
|
+
layerId: item.id,
|
|
25
|
+
opacity: '1.0',
|
|
26
|
+
})
|
|
27
|
+
if (item.layout && item.layout.visibility == 'visible') {
|
|
28
|
+
checkedKeys.push(i)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
return { treeData, checkedKeys }
|
|
32
|
+
}
|
|
33
|
+
export const treeDataAdapterNext = (data) => {
|
|
34
|
+
let i = 0
|
|
35
|
+
let treeData = []
|
|
36
|
+
let checkedKeys = []
|
|
37
|
+
data.forEach((item) => {
|
|
38
|
+
let kvs = item.id.split(';').map(kvstr => kvstr.split('='))
|
|
39
|
+
let groups = kvs.find(item => item[0] === 'Main')[1].split('/')
|
|
40
|
+
let layerName = kvs.find(item => item[0] === 'CN')[1]
|
|
41
|
+
let currentNode = treeData;
|
|
42
|
+
let fullLayerName = groups.length === 0 ? layerName : groups.join('-') + '-' + layerName
|
|
43
|
+
groups.forEach((group, index) => {
|
|
44
|
+
currentNode.find(item => item.label === group) || i++
|
|
45
|
+
currentNode.find(item => item.label === group) || currentNode.push({
|
|
46
|
+
id: `TreeId=${i};`,
|
|
47
|
+
label: group,
|
|
48
|
+
children: []
|
|
49
|
+
})
|
|
50
|
+
currentNode = currentNode.find(item => item.label === group).children
|
|
51
|
+
})
|
|
52
|
+
i++
|
|
53
|
+
currentNode.push({
|
|
54
|
+
id: item.id,
|
|
55
|
+
label: layerName,
|
|
56
|
+
fullLayerName: getLayerIdFidld(item.id, 'Main') + '-' + getLayerIdFidld(item.id, 'CN'),
|
|
57
|
+
layerId: item.id,
|
|
58
|
+
opacity: '1.0',
|
|
59
|
+
})
|
|
60
|
+
if (item.layout && item.layout.visibility == 'visible') {
|
|
61
|
+
checkedKeys.push(i)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
return { treeData, checkedKeys }
|
|
65
|
+
}
|
|
66
|
+
export const getLayerIdFidld = (str, field) => {
|
|
67
|
+
let kvs = str.split(';').map(kvstr => kvstr.split('='))
|
|
68
|
+
let layerId = kvs.find(item => item[0] === field)[1]
|
|
69
|
+
return layerId
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 生成唯一ID字符串
|
|
73
|
+
* @param {string} projectId 项目ID
|
|
74
|
+
* @param {string} objectTypeId 对象种类ID
|
|
75
|
+
* @param {string} groupId 对象分组ID
|
|
76
|
+
* @param {string} objectId 对象ID
|
|
77
|
+
* @returns {string} 唯一ID字符串
|
|
78
|
+
*/
|
|
79
|
+
export const GenerateUniqueId = (
|
|
80
|
+
projectId,
|
|
81
|
+
objectTypeId,
|
|
82
|
+
groupId,
|
|
83
|
+
objectId
|
|
84
|
+
) => {
|
|
85
|
+
// 将参数组合成一个字符串
|
|
86
|
+
const combinedString = `${projectId}|${objectTypeId}|${groupId}|${objectId}`;
|
|
87
|
+
|
|
88
|
+
// 使用简单的哈希函数生成固定长度的字符串
|
|
89
|
+
let hash = 0;
|
|
90
|
+
for (let i = 0; i < combinedString.length; i++) {
|
|
91
|
+
const char = combinedString.charCodeAt(i);
|
|
92
|
+
hash = (hash << 5) - hash + char;
|
|
93
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 转换为16进制字符串并补零到8位
|
|
97
|
+
const hexHash = (hash >>> 0).toString(16).padStart(8, "0");
|
|
98
|
+
|
|
99
|
+
// 添加前缀和分隔符增强可读性
|
|
100
|
+
return `UID-${projectId.slice(0, 2)}-${objectTypeId.slice(
|
|
101
|
+
0,
|
|
102
|
+
2
|
|
103
|
+
)}-${groupId.slice(0, 2)}-${hexHash}`;
|
|
104
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// RasterLayerController.js
|
|
2
|
+
|
|
3
|
+
export default class RasterLayerController {
|
|
4
|
+
constructor() {
|
|
5
|
+
this._panel = null;
|
|
6
|
+
this._isOpen = false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
onAdd(map) {
|
|
10
|
+
this._map = map;
|
|
11
|
+
|
|
12
|
+
this._container = document.createElement('div');
|
|
13
|
+
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-raster-control';
|
|
14
|
+
|
|
15
|
+
this._button = document.createElement('button');
|
|
16
|
+
this._button.type = 'button';
|
|
17
|
+
this._button.innerHTML = '底图:初始化中';
|
|
18
|
+
this._button.style.cssText = `
|
|
19
|
+
width: 100%;
|
|
20
|
+
padding: 6px 10px;
|
|
21
|
+
margin: 0;
|
|
22
|
+
font-size: 14px;
|
|
23
|
+
font-family: Arial, sans-serif;
|
|
24
|
+
background: #f8f9fa;
|
|
25
|
+
color: #333;
|
|
26
|
+
border: 1px solid #dee2e6;
|
|
27
|
+
border-radius: 4px;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
text-align: left;
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
line-height: 1.4;
|
|
33
|
+
`;
|
|
34
|
+
this._button.addEventListener('mouseenter', () => {
|
|
35
|
+
this._button.style.background = '#e9ecef';
|
|
36
|
+
});
|
|
37
|
+
this._button.addEventListener('mouseleave', () => {
|
|
38
|
+
this._button.style.background = '#f8f9fa';
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this._container.appendChild(this._button);
|
|
42
|
+
|
|
43
|
+
this._button.addEventListener('click', () => {
|
|
44
|
+
this._togglePanel();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this._map.on('idle', () => {
|
|
48
|
+
this._updateRasterLayers();
|
|
49
|
+
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
this._map.on('styledata', () => {
|
|
53
|
+
this._updateRasterLayers();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this._map.on('sourcedata', () => {
|
|
57
|
+
this._updateRasterLayers();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this._updateRasterLayers();
|
|
61
|
+
|
|
62
|
+
return this._container;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onRemove() {
|
|
66
|
+
if (this._panel && this._panel.parentNode) {
|
|
67
|
+
this._panel.parentNode.removeChild(this._panel);
|
|
68
|
+
}
|
|
69
|
+
if (this._container && this._container.parentNode) {
|
|
70
|
+
this._container.parentNode.removeChild(this._container);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_togglePanel() {
|
|
75
|
+
if (this._isOpen) {
|
|
76
|
+
this._closePanel();
|
|
77
|
+
} else {
|
|
78
|
+
this._openPanel();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_openPanel() {
|
|
83
|
+
this._createPanelIfNeeded();
|
|
84
|
+
this._updateRasterLayers();
|
|
85
|
+
this._panel.style.display = 'block';
|
|
86
|
+
this._isOpen = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
_closePanel() {
|
|
90
|
+
if (this._panel) {
|
|
91
|
+
this._panel.style.display = 'none';
|
|
92
|
+
}
|
|
93
|
+
this._isOpen = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_createPanelIfNeeded() {
|
|
97
|
+
if (!this._panel) {
|
|
98
|
+
// 创建整体的弹窗容器(包含遮罩 + 弹窗内容)
|
|
99
|
+
this._panel = document.createElement('div');
|
|
100
|
+
this._panel.style.cssText = `
|
|
101
|
+
position: fixed;
|
|
102
|
+
top: 0;
|
|
103
|
+
left: 0;
|
|
104
|
+
width: 100vw;
|
|
105
|
+
height: 100vh;
|
|
106
|
+
background: rgba(0, 0, 0, 0.5);
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
z-index: 10000;
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
// 创建实际的对话框(白色背景,居中内容区域)
|
|
114
|
+
this._dialog = document.createElement('div');
|
|
115
|
+
this._dialog.style.cssText = `
|
|
116
|
+
position: relative;
|
|
117
|
+
top: 50%;
|
|
118
|
+
left: 50%;
|
|
119
|
+
transform: translate(-50%, -50%);
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
background: white;
|
|
125
|
+
padding: 20px;
|
|
126
|
+
border-radius: 8px;
|
|
127
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
128
|
+
min-width: 300px;
|
|
129
|
+
max-width: 500px;
|
|
130
|
+
max-height: 80vh;
|
|
131
|
+
overflow-y: auto;
|
|
132
|
+
font-family: Arial, sans-serif;
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
// 标题
|
|
136
|
+
const title = document.createElement('div');
|
|
137
|
+
title.textContent = '🗂 点击切换底图影像图层';
|
|
138
|
+
title.style.fontWeight = 'bold';
|
|
139
|
+
title.style.marginBottom = '15px';
|
|
140
|
+
title.style.fontSize = '16px';
|
|
141
|
+
this._dialog.appendChild(title);
|
|
142
|
+
|
|
143
|
+
// 图层列表容器(原 _layerListContainer)
|
|
144
|
+
this._layerListContainer = document.createElement('div');
|
|
145
|
+
this._layerListContainer.style.cssText = `
|
|
146
|
+
width: 100%;
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-direction: column;
|
|
149
|
+
gap: 4px;
|
|
150
|
+
`;
|
|
151
|
+
this._dialog.appendChild(this._layerListContainer);
|
|
152
|
+
|
|
153
|
+
// 关闭按钮(可选增强)
|
|
154
|
+
const closeButton = document.createElement('button');
|
|
155
|
+
closeButton.textContent = '✅ 关闭';
|
|
156
|
+
closeButton.style.cssText = `
|
|
157
|
+
margin-left: auto;
|
|
158
|
+
margin-right: auto;
|
|
159
|
+
margin-top: 15px;
|
|
160
|
+
padding: 6px 12px;
|
|
161
|
+
background: #6c757d;
|
|
162
|
+
color: white;
|
|
163
|
+
border: none;
|
|
164
|
+
border-radius: 4px;
|
|
165
|
+
cursor: pointer;
|
|
166
|
+
`;
|
|
167
|
+
closeButton.addEventListener('click', () => {
|
|
168
|
+
this._closePanel();
|
|
169
|
+
});
|
|
170
|
+
this._dialog.appendChild(closeButton);
|
|
171
|
+
|
|
172
|
+
this._panel.appendChild(this._dialog);
|
|
173
|
+
document.body.appendChild(this._panel);
|
|
174
|
+
|
|
175
|
+
// 点击背景遮罩关闭弹窗(增强用户体验,可选)
|
|
176
|
+
this._panel.addEventListener('click', (e) => {
|
|
177
|
+
// 只有点击背景遮罩部分才关闭,点击对话框内容不关闭
|
|
178
|
+
if (e.target === this._panel) {
|
|
179
|
+
this._closePanel();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_updateRasterLayers() {
|
|
186
|
+
|
|
187
|
+
const layers = this._map.getStyle().layers || [];
|
|
188
|
+
const rasterLayers = layers.filter(layer => layer.type === 'raster');
|
|
189
|
+
|
|
190
|
+
let buttonText = '底图:无';
|
|
191
|
+
let firstVisibleLayerId = null;
|
|
192
|
+
|
|
193
|
+
// 查找当前显示的图层(visibility === 'visible')
|
|
194
|
+
for (const layer of rasterLayers) {
|
|
195
|
+
const visibility = this._map.getLayoutProperty(layer.id, 'visibility');
|
|
196
|
+
if (visibility === 'visible') {
|
|
197
|
+
firstVisibleLayerId = layer.id;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (firstVisibleLayerId) {
|
|
203
|
+
buttonText = `底图:${firstVisibleLayerId}`;
|
|
204
|
+
} else if (rasterLayers.length > 0) {
|
|
205
|
+
buttonText = '底图:无';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this._button.innerHTML = buttonText;
|
|
209
|
+
|
|
210
|
+
// 清空旧的图层列表
|
|
211
|
+
if (this._layerListContainer) { this._layerListContainer.innerHTML = '' };
|
|
212
|
+
|
|
213
|
+
rasterLayers.forEach(layer => {
|
|
214
|
+
const layerId = layer.id;
|
|
215
|
+
const row = document.createElement('div');
|
|
216
|
+
row.style.cssText = `
|
|
217
|
+
padding: 6px 8px;
|
|
218
|
+
margin-bottom: 2px;
|
|
219
|
+
background: #f8f9fa;
|
|
220
|
+
border-radius: 4px;
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
transition: background 0.2s;
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
row.addEventListener('click', () => {
|
|
226
|
+
// 设置当前图层为 visible,其他为 none
|
|
227
|
+
rasterLayers.forEach(l => {
|
|
228
|
+
const vis = l.id === layerId ? 'visible' : 'none';
|
|
229
|
+
this._map.setLayoutProperty(l.id, 'visibility', vis);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// 更新按钮文字
|
|
233
|
+
if (this._button) {
|
|
234
|
+
this._button.innerHTML = `底图:${layerId}`;
|
|
235
|
+
}
|
|
236
|
+
this._togglePanel();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// 鼠标 hover 效果
|
|
240
|
+
row.addEventListener('mouseenter', () => {
|
|
241
|
+
row.style.background = '#e9ecef';
|
|
242
|
+
});
|
|
243
|
+
row.addEventListener('mouseleave', () => {
|
|
244
|
+
row.style.background = '#f8f9fa';
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 显示图层 ID(可加样式或 icon)
|
|
248
|
+
const label = document.createElement('span');
|
|
249
|
+
label.textContent = layerId;
|
|
250
|
+
label.style.fontFamily = 'Arial, sans-serif';
|
|
251
|
+
label.style.fontSize = '13px';
|
|
252
|
+
label.style.color = '#333';
|
|
253
|
+
|
|
254
|
+
row.appendChild(label);
|
|
255
|
+
this._layerListContainer && this._layerListContainer.appendChild(row);
|
|
256
|
+
});
|
|
257
|
+
if (rasterLayers.length === 0) {
|
|
258
|
+
this._layerListContainer && this._layerListContainer.appendChild(document.createTextNode('无可选内容'));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// terrain-toggle-control.js
|
|
2
|
+
|
|
3
|
+
// 自定义控件类
|
|
4
|
+
export default class TerrainToggleControl {
|
|
5
|
+
constructor() {
|
|
6
|
+
// 创建外层容器
|
|
7
|
+
this._container = document.createElement('div');
|
|
8
|
+
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group terrain-toggle-ctrl';
|
|
9
|
+
|
|
10
|
+
// 默认状态为关闭
|
|
11
|
+
this._terrainEnabled = false;
|
|
12
|
+
|
|
13
|
+
// 创建按钮
|
|
14
|
+
this._toggleButton = document.createElement('button');
|
|
15
|
+
this._toggleButton.type = 'button';
|
|
16
|
+
this._toggleButton.className = 'terrain-toggle-button';
|
|
17
|
+
|
|
18
|
+
// 创建一个包裹容器,用于放置 SVG 图标
|
|
19
|
+
this._iconWrapper = document.createElement('span');
|
|
20
|
+
this._iconWrapper.className = 'terrain-toggle-icon';
|
|
21
|
+
|
|
22
|
+
// === 重点:插入你自己的 SVG 图标到这里 ===
|
|
23
|
+
// 我们使用 currentColor 来控制颜色,你可以替换下面的 SVG 为你自己的
|
|
24
|
+
this._iconWrapper.innerHTML = `
|
|
25
|
+
<svg t="1761534052011" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16743" width="16" height="16"><path d="M1012.736 833.44a12910.144 12910.144 0 0 0-104.096-221.248 15708.992 15708.992 0 0 1-60.48-126.56c-20.384-43.072-22.592-86.208-42.176-126.272-45.056-94.496-57.6-342.528-158.4-287.808-100.864 54.624-272.512 581.472-272.512 581.472S302.272 313.088 236.672 340.96c-65.6 27.872-76.224 113.376-103.2 171.104-11.584 24.16-15.04 53.92-27.392 80.64l-24.16 77.664c-12.256 26.912-24.48 53.056-36.544 79.104-12.032 25.984-23.04 50.048-33.28 72.768a137.088 137.088 0 0 0-11.968 49.472c-0.704 16.32 1.856 30.88 7.552 43.904 5.888 12.96 15.04 23.68 27.84 32.064 12.736 8.288 28.96 12.544 48.48 12.544H931.84a109.472 109.472 0 0 0 47.968-9.92 80.096 80.096 0 0 0 32.064-26.816c7.584-11.296 11.68-24.736 12.096-40.128a110.176 110.176 0 0 0-11.2-49.92h-0.064z m0 0" p-id="16744" fill="#000000"></path></svg>
|
|
26
|
+
`;
|
|
27
|
+
// 如果你有自己的 SVG 文件,也可以通过 <img src="..." /> 引入,但无法直接改颜色,推荐用内联 SVG + currentColor
|
|
28
|
+
|
|
29
|
+
// 将图标插入按钮
|
|
30
|
+
this._toggleButton.appendChild(this._iconWrapper);
|
|
31
|
+
|
|
32
|
+
// 绑定点击事件
|
|
33
|
+
this._toggleButton.addEventListener('click', () => {
|
|
34
|
+
this._toggle();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 将按钮加入控件容器
|
|
38
|
+
this._container.appendChild(this._toggleButton);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 切换地形状态
|
|
42
|
+
_toggle() {
|
|
43
|
+
const map = this._map; // 获取当前地图实例
|
|
44
|
+
if (!map) return;
|
|
45
|
+
|
|
46
|
+
if (this._terrainEnabled) {
|
|
47
|
+
// 当前是开启的,要关闭
|
|
48
|
+
map.setTerrain(null);
|
|
49
|
+
// setPitch with an animation of 2 seconds.
|
|
50
|
+
map.setPitch(0, { duration: 2000 });
|
|
51
|
+
|
|
52
|
+
this._terrainEnabled = false;
|
|
53
|
+
} else {
|
|
54
|
+
// 当前是关闭的,要开启
|
|
55
|
+
this._load3DTerrain(map);
|
|
56
|
+
map.setPitch(45, { duration: 2000 });
|
|
57
|
+
|
|
58
|
+
// setPitch with an animation of 2 seconds.
|
|
59
|
+
this._terrainEnabled = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 更新 UI 显示
|
|
63
|
+
this._updateToggleTextAndStyle();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 加载并开启 3D 地形(复用你提供的逻辑)
|
|
67
|
+
_load3DTerrain(map) {
|
|
68
|
+
if (!map) return;
|
|
69
|
+
if (map.getTerrain()) return;
|
|
70
|
+
if (!map.getSource('三维地形')) {
|
|
71
|
+
map.addSource('三维地形', this._default3DTerrainProperties);
|
|
72
|
+
}
|
|
73
|
+
map.setTerrain({ source: '三维地形', exaggeration: 1.5 });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 更新按钮文字和样式
|
|
77
|
+
_updateToggleTextAndStyle() {
|
|
78
|
+
const btn = this._toggleButton;
|
|
79
|
+
const isOn = this._terrainEnabled;
|
|
80
|
+
|
|
81
|
+
if (isOn) {
|
|
82
|
+
// 开启状态:亮蓝色
|
|
83
|
+
btn.style.backgroundColor = '#007cbf'; // 亮蓝色背景
|
|
84
|
+
btn.style.color = 'white'; // 文字颜色(如果有)
|
|
85
|
+
this._iconWrapper.style.color = '#007cbf'; // SVG 图标颜色
|
|
86
|
+
} else {
|
|
87
|
+
// 关闭状态:灰色
|
|
88
|
+
btn.style.backgroundColor = '#cccccc'; // 灰色背景
|
|
89
|
+
btn.style.color = 'black'; // 文字颜色
|
|
90
|
+
this._iconWrapper.style.color = '#666666'; // SVG 图标颜色(推荐用深灰)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 按钮基础样式
|
|
94
|
+
btn.style.border = 'none';
|
|
95
|
+
btn.style.borderRadius = '4px';
|
|
96
|
+
btn.style.padding = '8px 12px';
|
|
97
|
+
btn.style.cursor = 'pointer';
|
|
98
|
+
btn.style.fontWeight = 'bold';
|
|
99
|
+
btn.style.display = 'flex';
|
|
100
|
+
btn.style.alignItems = 'center';
|
|
101
|
+
btn.style.justifyContent = 'center';
|
|
102
|
+
}
|
|
103
|
+
_onStyleData = () => {
|
|
104
|
+
let isTerrainOn = map.getTerrain() !== null;
|
|
105
|
+
if (this._terrainEnabled == isTerrainOn) { return } else {
|
|
106
|
+
this._terrainEnabled = isTerrainOn;
|
|
107
|
+
this._updateToggleTextAndStyle();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
// Mapbox 要求的接口:当控件被添加到地图时调用
|
|
111
|
+
onAdd(map) {
|
|
112
|
+
this._map = map; // 保存地图实例的引用
|
|
113
|
+
this._default3DTerrainProperties = map._default3DTerrainProperties
|
|
114
|
+
const isTerrainEnabled = map.getTerrain() !== null;
|
|
115
|
+
this._terrainEnabled = isTerrainEnabled;
|
|
116
|
+
this._map.on('styledata', this._onStyleData);
|
|
117
|
+
return this._container; // 返回控件的 DOM 元素
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Mapbox 要求的接口:当控件从地图移除时调用
|
|
121
|
+
onRemove() {
|
|
122
|
+
this._container.remove();
|
|
123
|
+
this._map.off('styledata', _onStyleData);
|
|
124
|
+
this._map = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const taskQueue = [];
|
|
2
|
+
const addTask = (task) => {
|
|
3
|
+
taskQueue.push(task)
|
|
4
|
+
}
|
|
5
|
+
const runTask = () => {
|
|
6
|
+
|
|
7
|
+
if (taskQueue.length > 0) {
|
|
8
|
+
const task = taskQueue.shift()
|
|
9
|
+
let drawCacheIndex = localStorage.getItem('drawCacheIndex') != null ? JSON.parse(localStorage.getItem('drawCacheIndex')) : []
|
|
10
|
+
if (task.action === 'add') {
|
|
11
|
+
drawCacheIndex.push(String(task.data.id))
|
|
12
|
+
localStorage.setItem(String(task.data.id), JSON.stringify(task.data))
|
|
13
|
+
} else if (task.action === 'remove') {
|
|
14
|
+
let index = drawCacheIndex.indexOf(String(task.data.id))
|
|
15
|
+
if (index !== -1) {
|
|
16
|
+
drawCacheIndex.splice(index, 1)
|
|
17
|
+
localStorage.removeItem(String(task.data.id))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
localStorage.setItem('drawCacheIndex', JSON.stringify(drawCacheIndex))
|
|
21
|
+
runTask()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const addFeature = (feature) => {
|
|
25
|
+
addTask({
|
|
26
|
+
action: 'add',
|
|
27
|
+
data: feature
|
|
28
|
+
})
|
|
29
|
+
runTask()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const removeFeature = (feature) => {
|
|
33
|
+
addTask({
|
|
34
|
+
action: 'remove',
|
|
35
|
+
data: feature
|
|
36
|
+
})
|
|
37
|
+
runTask()
|
|
38
|
+
}
|
|
39
|
+
const getDrawCache = () => {
|
|
40
|
+
let drawCacheIndex = localStorage.getItem('drawCacheIndex') != null ? JSON.parse(localStorage.getItem('drawCacheIndex')) : []
|
|
41
|
+
let drawCache = drawCacheIndex.map(item => {
|
|
42
|
+
return JSON.parse(localStorage.getItem(item))
|
|
43
|
+
})
|
|
44
|
+
return drawCache
|
|
45
|
+
}
|
|
46
|
+
const clearDrawCache = () => {
|
|
47
|
+
let drawCacheIndex = localStorage.getItem('drawCacheIndex') != null ? JSON.parse(localStorage.getItem('drawCacheIndex')) : []
|
|
48
|
+
drawCacheIndex.forEach(item => {
|
|
49
|
+
localStorage.removeItem(item)
|
|
50
|
+
})
|
|
51
|
+
localStorage.removeItem('drawCacheIndex')
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
addFeature,
|
|
55
|
+
removeFeature,
|
|
56
|
+
getDrawCache,
|
|
57
|
+
clearDrawCache
|
|
58
|
+
}
|