tg-map-echarts 4.3.0
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/README.md +43 -0
- package/dist/src/extension/TgMapCoordSys.d.ts +46 -0
- package/dist/src/extension/TgMapModel.d.ts +51 -0
- package/dist/src/extension/TgMapView.d.ts +16 -0
- package/dist/src/extension/TgMapViewportState.d.ts +21 -0
- package/dist/src/extension/__tests__/TgMapCoordSys.test.d.ts +1 -0
- package/dist/src/extension/install.d.ts +4 -0
- package/dist/src/extension/types.d.ts +38 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/tg-map-echarts.cjs +409 -0
- package/dist/tg-map-echarts.mjs +407 -0
- package/docs/migration-from-bmap.md +508 -0
- package/package.json +55 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { ComponentModel, graphic, matrix, ComponentView } from 'echarts/core';
|
|
2
|
+
import { exhaustiveCheck, lateinit } from 'tg-commons';
|
|
3
|
+
import { COORD_TYPE_DEFAULT, MapZoom, TgMapType, LatLng, TgMapFactory, ElementOverlay, MapPane } from 'tg-map-core';
|
|
4
|
+
|
|
5
|
+
const DYNAMIC_UI_OPTION_KEYS = [
|
|
6
|
+
'gestureHandling',
|
|
7
|
+
'minZoom',
|
|
8
|
+
'maxZoom',
|
|
9
|
+
'mapStyle',
|
|
10
|
+
'hideLogo',
|
|
11
|
+
'fractionalZoom',
|
|
12
|
+
];
|
|
13
|
+
class TgMapModel extends ComponentModel {
|
|
14
|
+
static type = 'tgMap';
|
|
15
|
+
type = TgMapModel.type;
|
|
16
|
+
static defaultOption = {
|
|
17
|
+
engine: TgMapType.google,
|
|
18
|
+
mapOptions: {
|
|
19
|
+
center: { lat: 0, lng: 0, coord: COORD_TYPE_DEFAULT },
|
|
20
|
+
zoom: MapZoom.STATE,
|
|
21
|
+
},
|
|
22
|
+
dataCoordType: COORD_TYPE_DEFAULT,
|
|
23
|
+
};
|
|
24
|
+
coordinateSystem;
|
|
25
|
+
runtime;
|
|
26
|
+
getEngine() {
|
|
27
|
+
return this.option.engine;
|
|
28
|
+
}
|
|
29
|
+
getDataCoordType() {
|
|
30
|
+
return this.option.dataCoordType;
|
|
31
|
+
}
|
|
32
|
+
getMapOptions() {
|
|
33
|
+
return this.option.mapOptions;
|
|
34
|
+
}
|
|
35
|
+
getMap() {
|
|
36
|
+
if (!this.runtime) {
|
|
37
|
+
throw new Error('tgMap尚未创建');
|
|
38
|
+
}
|
|
39
|
+
return this.runtime.map;
|
|
40
|
+
}
|
|
41
|
+
getRuntime() {
|
|
42
|
+
return this.runtime;
|
|
43
|
+
}
|
|
44
|
+
setRuntime(runtime) {
|
|
45
|
+
this.runtime = runtime;
|
|
46
|
+
}
|
|
47
|
+
setCenterAndZoom(center, zoom) {
|
|
48
|
+
this.option.mapOptions.center = {
|
|
49
|
+
lat: center.lat,
|
|
50
|
+
lng: center.lng,
|
|
51
|
+
coord: center.coord,
|
|
52
|
+
};
|
|
53
|
+
this.option.mapOptions.zoom = zoom;
|
|
54
|
+
}
|
|
55
|
+
/** 应用坐标相关选项到地图 */
|
|
56
|
+
applyCoordinateOptions(runtime) {
|
|
57
|
+
const options = this.getMapOptions();
|
|
58
|
+
runtime.applyingOptions = true;
|
|
59
|
+
try {
|
|
60
|
+
if (runtime.dynamicOptionSnapshots.get('buildInMapTypeId') !== options.buildInMapTypeId && options.buildInMapTypeId) {
|
|
61
|
+
runtime.map.setBuildInMapTypeId(options.buildInMapTypeId);
|
|
62
|
+
runtime.dynamicOptionSnapshots.set('buildInMapTypeId', options.buildInMapTypeId);
|
|
63
|
+
}
|
|
64
|
+
if (runtime.map.getZoom() !== options.zoom) {
|
|
65
|
+
runtime.map.setZoom(options.zoom);
|
|
66
|
+
}
|
|
67
|
+
const center = LatLng.fromLiteral(options.center);
|
|
68
|
+
runtime.map.setCenterIfChanged(center);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
runtime.applyingOptions = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** 应用UI相关选项到地图 */
|
|
75
|
+
applyUIOptions(runtime) {
|
|
76
|
+
const options = this.getMapOptions();
|
|
77
|
+
runtime.applyingOptions = true;
|
|
78
|
+
try {
|
|
79
|
+
DYNAMIC_UI_OPTION_KEYS.forEach(key => {
|
|
80
|
+
const value = options[key];
|
|
81
|
+
const snapshot = typeof value !== 'object'
|
|
82
|
+
? value // 非对象, 可以直接缓存值
|
|
83
|
+
: JSON.stringify(value);
|
|
84
|
+
if (runtime.dynamicOptionSnapshots.get(key) === snapshot) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
runtime.dynamicOptionSnapshots.set(key, snapshot);
|
|
88
|
+
switch (key) {
|
|
89
|
+
case 'gestureHandling':
|
|
90
|
+
options.gestureHandling != null && runtime.map.setGestureHandling(options.gestureHandling);
|
|
91
|
+
break;
|
|
92
|
+
case 'minZoom':
|
|
93
|
+
options.minZoom != null && runtime.map.setMinZoom(options.minZoom);
|
|
94
|
+
break;
|
|
95
|
+
case 'maxZoom':
|
|
96
|
+
options.maxZoom != null && runtime.map.setMaxZoom(options.maxZoom);
|
|
97
|
+
break;
|
|
98
|
+
case 'mapStyle':
|
|
99
|
+
runtime.map.setMapStyle(options.mapStyle ? JSON.parse(options.mapStyle) : undefined);
|
|
100
|
+
break;
|
|
101
|
+
case 'hideLogo':
|
|
102
|
+
runtime.map.setHideLogo(options.hideLogo ?? false);
|
|
103
|
+
break;
|
|
104
|
+
case 'fractionalZoom':
|
|
105
|
+
runtime.map.setFractionalZoom(options.fractionalZoom ?? false);
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
exhaustiveCheck(key);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
runtime.applyingOptions = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class TgMapViewportOverlay extends ElementOverlay {
|
|
120
|
+
viewportRoot;
|
|
121
|
+
onOverlayDraw;
|
|
122
|
+
constructor(map, viewportRoot, onOverlayDraw) {
|
|
123
|
+
super(map, { mapPane: MapPane.overlayMouseTarget });
|
|
124
|
+
this.viewportRoot = viewportRoot;
|
|
125
|
+
this.onOverlayDraw = onOverlayDraw;
|
|
126
|
+
}
|
|
127
|
+
onCreate() {
|
|
128
|
+
return this.viewportRoot;
|
|
129
|
+
}
|
|
130
|
+
onDraw(_projection) {
|
|
131
|
+
this.onOverlayDraw();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
class TgMapCoordSys {
|
|
135
|
+
map;
|
|
136
|
+
api;
|
|
137
|
+
dataCoordType;
|
|
138
|
+
static dimensions = ['lng', 'lat'];
|
|
139
|
+
static create = (ecModel, api) => {
|
|
140
|
+
let coordinateSystem;
|
|
141
|
+
ecModel.eachComponent('tgMap', (model) => {
|
|
142
|
+
if (!(model instanceof TgMapModel))
|
|
143
|
+
return;
|
|
144
|
+
if (coordinateSystem) {
|
|
145
|
+
throw new Error('一个ECharts实例只能配置一个tgMap组件');
|
|
146
|
+
}
|
|
147
|
+
let runtime = model.getRuntime();
|
|
148
|
+
if (runtime) {
|
|
149
|
+
model.applyCoordinateOptions(runtime);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
runtime = TgMapCoordSys.createMapRuntime(model, api);
|
|
153
|
+
}
|
|
154
|
+
console.log('TgMapCoordSys.create', runtime);
|
|
155
|
+
coordinateSystem = new TgMapCoordSys(runtime.map, api, model.getDataCoordType());
|
|
156
|
+
model.coordinateSystem = coordinateSystem;
|
|
157
|
+
});
|
|
158
|
+
ecModel.eachSeries((seriesModel) => {
|
|
159
|
+
if (seriesModel.get('coordinateSystem') === 'tgMap') {
|
|
160
|
+
if (!coordinateSystem) {
|
|
161
|
+
throw new Error('series使用了tgMap坐标系,但option中没有配置tgMap组件');
|
|
162
|
+
}
|
|
163
|
+
seriesModel.coordinateSystem = coordinateSystem;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
return coordinateSystem ? [coordinateSystem] : [];
|
|
167
|
+
};
|
|
168
|
+
type = 'tgMap';
|
|
169
|
+
dimensions = TgMapCoordSys.dimensions;
|
|
170
|
+
constructor(map, api, dataCoordType) {
|
|
171
|
+
this.map = map;
|
|
172
|
+
this.api = api;
|
|
173
|
+
this.dataCoordType = dataCoordType;
|
|
174
|
+
}
|
|
175
|
+
getMap() {
|
|
176
|
+
return this.map;
|
|
177
|
+
}
|
|
178
|
+
dataToPoint(data, _clamp, out = []) {
|
|
179
|
+
// bmap的实现中使用的是`overlay.getProjection()`表示的相对Pane的坐标, 再减去pane在平移时的偏移, 最终得到相对地图容器的坐标
|
|
180
|
+
// 这里直接使用containerPoint, 省得偏移来偏移去
|
|
181
|
+
const point = this.map.fromLatLngToContainerPoint(LatLng.fromLngLat(data[0], data[1], this.dataCoordType));
|
|
182
|
+
out[0] = point.x;
|
|
183
|
+
out[1] = point.y;
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
pointToData(point) {
|
|
187
|
+
const latLng = this.map.fromContainerPointToLatLng({ x: point[0], y: point[1] }).to(this.dataCoordType);
|
|
188
|
+
return [latLng.lng, latLng.lat];
|
|
189
|
+
}
|
|
190
|
+
containPoint(point) {
|
|
191
|
+
return this.getViewRect().contain(point[0], point[1]);
|
|
192
|
+
}
|
|
193
|
+
getViewRect() {
|
|
194
|
+
return new graphic.BoundingRect(0, 0, this.api.getWidth(), this.api.getHeight());
|
|
195
|
+
}
|
|
196
|
+
getRoamTransform() {
|
|
197
|
+
return matrix.create();
|
|
198
|
+
}
|
|
199
|
+
prepareCustoms() {
|
|
200
|
+
const rect = this.getViewRect();
|
|
201
|
+
return {
|
|
202
|
+
coordSys: {
|
|
203
|
+
type: 'tgMap',
|
|
204
|
+
x: rect.x,
|
|
205
|
+
y: rect.y,
|
|
206
|
+
width: rect.width,
|
|
207
|
+
height: rect.height,
|
|
208
|
+
},
|
|
209
|
+
api: {
|
|
210
|
+
coord: this.dataToPoint.bind(this),
|
|
211
|
+
size: this.dataToCoordSize.bind(this),
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
dataToCoordSize(dataSize, dataItem = [0, 0]) {
|
|
216
|
+
return [0, 1].map(dimIndex => {
|
|
217
|
+
const value = dataItem[dimIndex];
|
|
218
|
+
const halfSize = dataSize[dimIndex] / 2;
|
|
219
|
+
const first = [dataItem[0], dataItem[1]];
|
|
220
|
+
const second = [...first];
|
|
221
|
+
first[dimIndex] = value - halfSize;
|
|
222
|
+
second[dimIndex] = value + halfSize;
|
|
223
|
+
return Math.abs(this.dataToPoint(first)[dimIndex] - this.dataToPoint(second)[dimIndex]);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* 创建的DOM层级:
|
|
228
|
+
* ```
|
|
229
|
+
* root/viewportParent echarts的元素, viewportRoot原本的parent
|
|
230
|
+
* |--mapRoot class=ec-extension-tg-map
|
|
231
|
+
* |--pane
|
|
232
|
+
* |--viewportRoot painter的viewportRoot
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
static createMapRuntime(model, api) {
|
|
236
|
+
const root = api.getDom();
|
|
237
|
+
const painter = api.getZr().painter;
|
|
238
|
+
const viewportRoot = painter.getViewportRoot();
|
|
239
|
+
const viewportParent = viewportRoot.parentNode;
|
|
240
|
+
if (!viewportParent) {
|
|
241
|
+
throw new Error('无法获取ECharts viewportRoot的父节点');
|
|
242
|
+
}
|
|
243
|
+
const mapRoot = document.createElement('div');
|
|
244
|
+
mapRoot.className = 'ec-extension-tg-map';
|
|
245
|
+
mapRoot.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%';
|
|
246
|
+
root.insertBefore(mapRoot, root.firstChild);
|
|
247
|
+
const mapOptions = model.getMapOptions();
|
|
248
|
+
const map = TgMapFactory.createMap(model.getEngine(), mapRoot, {
|
|
249
|
+
...mapOptions,
|
|
250
|
+
center: LatLng.fromLiteral(mapOptions.center),
|
|
251
|
+
mapStyle: mapOptions.mapStyle ? JSON.parse(mapOptions.mapStyle) : undefined,
|
|
252
|
+
});
|
|
253
|
+
const runtime = {
|
|
254
|
+
map,
|
|
255
|
+
mapRoot,
|
|
256
|
+
overlay: lateinit(),
|
|
257
|
+
viewportRoot,
|
|
258
|
+
viewportBackup: {
|
|
259
|
+
parent: viewportParent,
|
|
260
|
+
nextSibling: viewportRoot.nextSibling,
|
|
261
|
+
className: viewportRoot.className,
|
|
262
|
+
position: viewportRoot.style.position,
|
|
263
|
+
left: viewportRoot.style.left,
|
|
264
|
+
top: viewportRoot.style.top,
|
|
265
|
+
getViewportRootOffset: painter.getViewportRootOffset,
|
|
266
|
+
},
|
|
267
|
+
syncViewportPosition: lateinit(),
|
|
268
|
+
applyingOptions: false,
|
|
269
|
+
dynamicOptionSnapshots: new Map(),
|
|
270
|
+
};
|
|
271
|
+
runtime.syncViewportPosition = () => {
|
|
272
|
+
const pane = viewportRoot.parentElement;
|
|
273
|
+
if (!pane) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// 地图平移后, pane的左上角可能变化, 这里补偿变化的值, 保证viewportRoot始终和mapRoot的左上角对齐
|
|
277
|
+
const mapRect = mapRoot.getBoundingClientRect();
|
|
278
|
+
const paneRect = pane.getBoundingClientRect();
|
|
279
|
+
viewportRoot.style.left = `${mapRect.left - paneRect.left}px`;
|
|
280
|
+
viewportRoot.style.top = `${mapRect.top - paneRect.top}px`;
|
|
281
|
+
};
|
|
282
|
+
runtime.overlay = new TgMapViewportOverlay(map, viewportRoot, runtime.syncViewportPosition);
|
|
283
|
+
viewportRoot.style.position = 'absolute';
|
|
284
|
+
viewportRoot.style.left = '0';
|
|
285
|
+
viewportRoot.style.top = '0';
|
|
286
|
+
map.addElementOverlay(runtime.overlay);
|
|
287
|
+
runtime.syncViewportPosition();
|
|
288
|
+
painter.getViewportRootOffset = () => ({ offsetLeft: 0, offsetTop: 0 });
|
|
289
|
+
model.setRuntime(runtime);
|
|
290
|
+
return runtime;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
class TgMapView extends ComponentView {
|
|
295
|
+
static type = 'tgMap';
|
|
296
|
+
type = TgMapView.type;
|
|
297
|
+
model;
|
|
298
|
+
map;
|
|
299
|
+
rendering = false;
|
|
300
|
+
changeHandler;
|
|
301
|
+
render(model, _ecModel, api) {
|
|
302
|
+
this.rendering = true;
|
|
303
|
+
try {
|
|
304
|
+
this.model = model;
|
|
305
|
+
const runtime = model.getRuntime();
|
|
306
|
+
if (!runtime) {
|
|
307
|
+
throw new Error('tgMap尚未创建');
|
|
308
|
+
}
|
|
309
|
+
if (this.map !== runtime.map) {
|
|
310
|
+
this.unbindMapEvents();
|
|
311
|
+
this.map = runtime.map;
|
|
312
|
+
this.bindMapEvents(runtime, api);
|
|
313
|
+
}
|
|
314
|
+
model.applyUIOptions(runtime);
|
|
315
|
+
console.log('TgMapView.render', runtime);
|
|
316
|
+
runtime.syncViewportPosition();
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
this.rendering = false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
dispose(_ecModel, api) {
|
|
323
|
+
this.unbindMapEvents();
|
|
324
|
+
const model = this.model;
|
|
325
|
+
const runtime = model?.getRuntime();
|
|
326
|
+
if (!model || !runtime) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (runtime.animationFrameId !== undefined) {
|
|
330
|
+
cancelAnimationFrame(runtime.animationFrameId);
|
|
331
|
+
}
|
|
332
|
+
runtime.map.removeElementOverlay(runtime.overlay);
|
|
333
|
+
const backup = runtime.viewportBackup;
|
|
334
|
+
if (backup.nextSibling?.parentNode === backup.parent) {
|
|
335
|
+
backup.parent.insertBefore(runtime.viewportRoot, backup.nextSibling);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
backup.parent.appendChild(runtime.viewportRoot);
|
|
339
|
+
}
|
|
340
|
+
runtime.viewportRoot.className = backup.className;
|
|
341
|
+
runtime.viewportRoot.style.position = backup.position;
|
|
342
|
+
runtime.viewportRoot.style.left = backup.left;
|
|
343
|
+
runtime.viewportRoot.style.top = backup.top;
|
|
344
|
+
runtime.mapRoot.parentNode?.removeChild(runtime.mapRoot);
|
|
345
|
+
const painter = api.getZr().painter;
|
|
346
|
+
painter.getViewportRootOffset = backup.getViewportRootOffset;
|
|
347
|
+
model.setRuntime(undefined);
|
|
348
|
+
this.model = undefined;
|
|
349
|
+
}
|
|
350
|
+
bindMapEvents(runtime, api) {
|
|
351
|
+
this.changeHandler = () => {
|
|
352
|
+
if (this.rendering || runtime.applyingOptions || runtime.animationFrameId !== undefined) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
runtime.animationFrameId = requestAnimationFrame(() => {
|
|
356
|
+
runtime.animationFrameId = undefined;
|
|
357
|
+
runtime.syncViewportPosition();
|
|
358
|
+
api.dispatchAction({
|
|
359
|
+
type: 'tgMapRoam',
|
|
360
|
+
animation: { duration: 0 },
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
runtime.map.addEventListener('center-changed', this.changeHandler);
|
|
365
|
+
runtime.map.addEventListener('zoom-changed', this.changeHandler);
|
|
366
|
+
runtime.map.addEventListener('tilesloaded', this.changeHandler);
|
|
367
|
+
}
|
|
368
|
+
unbindMapEvents() {
|
|
369
|
+
if (!this.map || !this.changeHandler) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
this.map.removeEventListener('center-changed', this.changeHandler);
|
|
373
|
+
this.map.removeEventListener('zoom-changed', this.changeHandler);
|
|
374
|
+
this.map.removeEventListener('tilesloaded', this.changeHandler);
|
|
375
|
+
this.changeHandler = undefined;
|
|
376
|
+
this.map = undefined;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const TgMapExtension = {
|
|
381
|
+
install(registers) {
|
|
382
|
+
registers.registerComponentModel(TgMapModel);
|
|
383
|
+
registers.registerComponentView(TgMapView);
|
|
384
|
+
registers.registerCoordinateSystem('tgMap', TgMapCoordSys);
|
|
385
|
+
// 注册地图漫游Action:dispatchAction后先将地图实际的center/zoom回写到Model,
|
|
386
|
+
// 再通过updateLayout触发完整更新,重新创建坐标系并渲染图表;event用于更新完成后的对外通知。
|
|
387
|
+
registers.registerAction({ type: 'tgMapRoam', event: 'tgMapRoam', update: 'updateLayout' }, (_payload, ecModel) => {
|
|
388
|
+
ecModel.eachComponent('tgMap', (model) => {
|
|
389
|
+
if (!(model instanceof TgMapModel))
|
|
390
|
+
return;
|
|
391
|
+
const runtime = model.getRuntime();
|
|
392
|
+
if (runtime) {
|
|
393
|
+
model.setCenterAndZoom(runtime.map.getCenter(), runtime.map.getZoom());
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
},
|
|
398
|
+
// {@template echarts_on_demand_import}
|
|
399
|
+
// 导入echarts, 会自动安装LineChart等常用组件, 当前包是个echarts扩展, 不需要导入那些东西, 故只导入了echarts/core;
|
|
400
|
+
//
|
|
401
|
+
// 而echarts和echarts/core的类型声明是当前是两个独立的d.ts文件, 相互没有关系, 虽然他们的类型声明类似,
|
|
402
|
+
// 但是依然TS依然会报不兼容错误, 故对于需要导出给外部使用的类型, 需要抹除详细的类型信息
|
|
403
|
+
//
|
|
404
|
+
// {@endtemplate}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
export { TgMapExtension };
|