@zwishing/emap 0.2.0 → 0.3.1
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/CHANGELOG.md +226 -1
- package/FEATURES.md +455 -0
- package/README.md +414 -209
- package/dist/core/event-map.d.ts +67 -0
- package/dist/core/feature-event-dispatcher.d.ts +70 -0
- package/dist/core/handler-manager.d.ts +49 -0
- package/dist/core/handler.d.ts +48 -0
- package/dist/core/handlers/box-select.d.ts +53 -0
- package/dist/core/handlers/click-select.d.ts +31 -0
- package/dist/core/handlers/drag-pan.d.ts +35 -0
- package/dist/core/handlers/draw-feature.d.ts +76 -0
- package/dist/core/handlers/lasso-select.d.ts +57 -0
- package/dist/core/handlers/scroll-zoom.d.ts +24 -0
- package/dist/core/handlers/select-geometry.d.ts +24 -0
- package/dist/core/handlers/select-mode.d.ts +14 -0
- package/dist/core/handlers/transform-feature.d.ts +47 -0
- package/dist/core/handlers/vertex-edit.d.ts +98 -0
- package/dist/core/pointer-event-dispatcher.d.ts +40 -0
- package/dist/core/tween.d.ts +1 -0
- package/dist/emap.css +42 -8
- package/dist/emap.js +2 -2
- package/dist/emap.mjs +1 -1
- package/dist/geo/camera.d.ts +100 -0
- package/dist/geo/projection.d.ts +8 -1
- package/dist/geo/viewport.d.ts +18 -0
- package/dist/index.d.ts +26 -2
- package/dist/map/edit-state-store.d.ts +1 -1
- package/dist/map/map.d.ts +89 -2
- package/dist/map/selection.d.ts +5 -2
- package/dist/renderer/edit-overlay-renderer.d.ts +2 -1
- package/dist/source/source.d.ts +2 -2
- package/dist/source/topology-source.d.ts +7 -2
- package/dist/ui/box-select-control.d.ts +19 -37
- package/dist/ui/draw-feature-control.d.ts +6 -71
- package/dist/ui/lasso-select-control.d.ts +14 -61
- package/dist/ui/status-control.d.ts +2 -2
- package/dist/ui/vertex-edit-control.d.ts +5 -100
- package/package.json +5 -1
- package/dist/core/drag-pan-handler.d.ts +0 -28
package/README.md
CHANGED
|
@@ -1,47 +1,121 @@
|
|
|
1
1
|
# emap
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
`emap` is a browser-only TypeScript map rendering and editing engine built on
|
|
4
|
+
Mapshaper topology data structures and HTML5 Canvas 2D. It is designed for
|
|
5
|
+
framework-agnostic GIS applications that need client-side loading, rendering,
|
|
6
|
+
selection, topology-aware editing, data operations, undo/redo, and custom UI
|
|
7
|
+
integration without a backend service.
|
|
8
|
+
|
|
9
|
+
## Project Status
|
|
10
|
+
|
|
11
|
+
`emap` is an AI-driven development project and is still evolving quickly. The
|
|
12
|
+
API, behavior, and implementation details may change without the compatibility
|
|
13
|
+
guarantees expected from a mature GIS engine. It is not recommended for
|
|
14
|
+
production environments yet; evaluate it in prototypes, demos, research tools,
|
|
15
|
+
or controlled internal workflows first.
|
|
16
|
+
|
|
17
|
+
## Highlights
|
|
18
|
+
|
|
19
|
+
- Topology-first rendering for Mapshaper datasets, with shared-arc reuse and LOD.
|
|
20
|
+
- GeoJSON, TopoJSON, ZIP Shapefile, and Mapshaper snapshot loading.
|
|
21
|
+
- MapLibre-style layer specs: `fill`, `line`, and `circle`.
|
|
22
|
+
- Named interaction handlers: `dragPan`, `scrollZoom`, `clickSelect`,
|
|
23
|
+
`boxSelect`, `lassoSelect`, `vertexEdit`, `drawFeature`, and
|
|
24
|
+
`transformFeature`.
|
|
25
|
+
- Handler-driven customization: use built-in interactions without the bundled UI
|
|
26
|
+
controls, or build your own toolbar around the same handlers.
|
|
27
|
+
- Delegated feature events: `feature:click`, `feature:hover`, `feature:enter`,
|
|
28
|
+
and `feature:leave`.
|
|
29
|
+
- MapLibre-parity camera methods: `jumpTo`, `easeTo`, `flyTo`, `fitBounds`,
|
|
30
|
+
`panBy`, `panTo`, `zoomTo`, and `stop`.
|
|
31
|
+
- 30+ `map.ops.*` data operations backed by Mapshaper commands.
|
|
32
|
+
- Undo/redo, transactions, validators, feature accessors, highlighting, and
|
|
33
|
+
full-session snapshots.
|
|
34
|
+
- Optional Web Worker offloading for expensive dataset-replace operations.
|
|
35
|
+
- CSS design tokens for theming built-in UI surfaces.
|
|
36
|
+
|
|
37
|
+
For the full capability matrix, see [FEATURES.md](FEATURES.md).
|
|
17
38
|
|
|
18
39
|
## Install
|
|
19
40
|
|
|
20
41
|
```bash
|
|
21
|
-
pnpm add @zwishing/emap
|
|
22
|
-
# or
|
|
23
42
|
npm install @zwishing/emap
|
|
43
|
+
# or
|
|
44
|
+
pnpm add @zwishing/emap
|
|
24
45
|
```
|
|
25
46
|
|
|
26
|
-
##
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
### Bundlers
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import {
|
|
53
|
+
Emap,
|
|
54
|
+
TopologySource,
|
|
55
|
+
NavigationControl,
|
|
56
|
+
HistoryControl,
|
|
57
|
+
} from '@zwishing/emap';
|
|
58
|
+
import '@zwishing/emap/style.css';
|
|
59
|
+
|
|
60
|
+
const map = new Emap({ container: 'map' });
|
|
61
|
+
map.addControl(new NavigationControl(), 'top-left');
|
|
62
|
+
map.addControl(new HistoryControl(), 'top-left');
|
|
63
|
+
|
|
64
|
+
const source = await TopologySource.fromUrl(
|
|
65
|
+
'china',
|
|
66
|
+
'https://geojson.cn/api/china/1.6.3/china.topo.json',
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
map.addSource('china', source);
|
|
70
|
+
map.setExtent(source.getExtent());
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The ESM build is browser-ready. Mapshaper is shipped as a sibling ES module
|
|
74
|
+
resolved from `node_modules/@zwishing/emap/dist/`, so Vite, webpack, esbuild,
|
|
75
|
+
and Bun consumers should not need a `Buffer` polyfill or a custom alias.
|
|
76
|
+
|
|
77
|
+
### Static Assets
|
|
78
|
+
|
|
79
|
+
Some runtime files are loaded by URL instead of being inlined into your
|
|
80
|
+
application bundle:
|
|
81
|
+
|
|
82
|
+
| Asset | When it is needed |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `dist/emap.css` | Built-in controls, context menus, and default UI styling |
|
|
85
|
+
| `dist/mapshaper-vendor.js` | IIFE script usage and worker bootstrapping |
|
|
86
|
+
| `dist/emap-worker.js` | `useWorker: true` or `useWorker: 'auto'` |
|
|
87
|
+
|
|
88
|
+
For bundlers, prefer:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import '@zwishing/emap/style.css';
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For script-tag or static-host deployments, copy the files from
|
|
95
|
+
`node_modules/@zwishing/emap/dist/` into your public assets directory. When
|
|
96
|
+
using the worker, keep `emap-worker.js` and `mapshaper-vendor.js` in the same
|
|
97
|
+
directory unless you build your own worker bundle; the worker imports the vendor
|
|
98
|
+
file with a relative `importScripts('mapshaper-vendor.js')` call.
|
|
99
|
+
|
|
100
|
+
### IIFE Script
|
|
27
101
|
|
|
28
102
|
```html
|
|
29
|
-
<!
|
|
103
|
+
<!doctype html>
|
|
30
104
|
<html>
|
|
31
105
|
<head>
|
|
32
106
|
<link rel="stylesheet" href="node_modules/@zwishing/emap/dist/emap.css" />
|
|
33
|
-
<style
|
|
107
|
+
<style>
|
|
108
|
+
html, body, #map { width: 100%; height: 100%; margin: 0; }
|
|
109
|
+
</style>
|
|
34
110
|
</head>
|
|
35
111
|
<body>
|
|
36
112
|
<div id="map"></div>
|
|
37
113
|
<script src="node_modules/@zwishing/emap/dist/mapshaper-vendor.js"></script>
|
|
38
114
|
<script src="node_modules/@zwishing/emap/dist/emap.js"></script>
|
|
39
115
|
<script>
|
|
40
|
-
const { Emap, TopologySource, NavigationControl
|
|
41
|
-
|
|
116
|
+
const { Emap, TopologySource, NavigationControl } = emap;
|
|
42
117
|
const map = new Emap({ container: 'map' });
|
|
43
118
|
map.addControl(new NavigationControl(), 'top-left');
|
|
44
|
-
map.addControl(new EditToolbar(), 'top-left');
|
|
45
119
|
|
|
46
120
|
(async () => {
|
|
47
121
|
const source = await TopologySource.fromUrl(
|
|
@@ -56,303 +130,434 @@ npm install @zwishing/emap
|
|
|
56
130
|
</html>
|
|
57
131
|
```
|
|
58
132
|
|
|
59
|
-
|
|
133
|
+
## Loading Data
|
|
60
134
|
|
|
61
135
|
```ts
|
|
62
|
-
|
|
63
|
-
|
|
136
|
+
const source = await TopologySource.fromUrl('roads', '/data/roads.geojson');
|
|
137
|
+
map.addSource('roads', source);
|
|
138
|
+
map.setExtent(source.getExtent());
|
|
64
139
|
|
|
65
|
-
const
|
|
66
|
-
|
|
140
|
+
const local = new TopologySource('local');
|
|
141
|
+
await local.setData({ filename: 'upload.zip', content: bytes });
|
|
142
|
+
map.addSource('local', local);
|
|
67
143
|
```
|
|
68
144
|
|
|
69
|
-
|
|
70
|
-
|
|
145
|
+
`TopologySource.setData()` is awaitable. It resolves after the dataset is
|
|
146
|
+
populated and the `data` event has fired. `map.addSource()` is order-independent:
|
|
147
|
+
you can add a loaded source or add the source first and load it later.
|
|
71
148
|
|
|
72
|
-
|
|
73
|
-
|
|
149
|
+
Supported inputs:
|
|
150
|
+
|
|
151
|
+
- GeoJSON and TopoJSON.
|
|
152
|
+
- ZIP Shapefile archives containing `.shp`, `.dbf`, `.shx`, and optional `.prj`.
|
|
153
|
+
- Mapshaper `.msx` snapshots via `map.loadSnapshot()`.
|
|
154
|
+
|
|
155
|
+
## Layers and Rendering
|
|
74
156
|
|
|
75
157
|
```ts
|
|
76
|
-
|
|
158
|
+
map.addLayer({
|
|
159
|
+
id: 'roads-line',
|
|
160
|
+
source: 'roads',
|
|
161
|
+
type: 'line',
|
|
162
|
+
paint: {
|
|
163
|
+
'line-color': '#4a5568',
|
|
164
|
+
'line-width': 1,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
map.addLayer({
|
|
169
|
+
id: 'district-fill',
|
|
170
|
+
source: 'districts',
|
|
171
|
+
type: 'fill',
|
|
172
|
+
paint: {
|
|
173
|
+
'fill-color': 'rgba(66, 153, 225, 0.25)',
|
|
174
|
+
'line-width': 0,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
77
177
|
```
|
|
78
178
|
|
|
79
|
-
|
|
179
|
+
When no user layers are added, `emap` creates default display layers from the
|
|
180
|
+
source geometry type. Polygon defaults are outline-only for large-data browsing;
|
|
181
|
+
add an explicit `fill` layer when you want area fills.
|
|
80
182
|
|
|
81
|
-
##
|
|
183
|
+
## Interactions Without Built-In Controls
|
|
82
184
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
path; your bundler resolves it automatically from
|
|
87
|
-
`node_modules/@zwishing/emap/dist/`.
|
|
185
|
+
Most editing and selection tools are exposed as named handlers. You can use
|
|
186
|
+
them directly from your own UI instead of adding `BoxSelectControl`,
|
|
187
|
+
`DrawFeatureControl`, `VertexEditControl`, or `EditToolbar`.
|
|
88
188
|
|
|
89
189
|
```ts
|
|
90
|
-
|
|
91
|
-
|
|
190
|
+
map.boxSelect.setOptions({
|
|
191
|
+
layers: ['district-fill'],
|
|
192
|
+
dragThreshold: 4,
|
|
193
|
+
});
|
|
194
|
+
map.boxSelect.enable();
|
|
195
|
+
|
|
196
|
+
map.lassoSelect.setOptions({ layers: ['district-fill'] });
|
|
197
|
+
map.lassoSelect.enable();
|
|
198
|
+
|
|
199
|
+
map.drawFeature.setOptions({
|
|
200
|
+
source: 'editing',
|
|
201
|
+
sourceLayer: 'areas',
|
|
202
|
+
type: 'polygon',
|
|
203
|
+
snapSources: ['editing', 'reference'],
|
|
204
|
+
});
|
|
205
|
+
map.drawFeature.enable();
|
|
206
|
+
|
|
207
|
+
map.vertexEdit.enable();
|
|
208
|
+
|
|
209
|
+
map.clickSelect.enable();
|
|
210
|
+
map.transformFeature.setOptions({ mode: 'translate' });
|
|
211
|
+
map.transformFeature.enable();
|
|
92
212
|
```
|
|
93
213
|
|
|
94
|
-
|
|
95
|
-
deploy `dist/emap-worker.js` and `dist/mapshaper-vendor.js` as same-directory
|
|
96
|
-
static assets then.
|
|
214
|
+
All handlers expose the same basic surface:
|
|
97
215
|
|
|
98
|
-
|
|
216
|
+
```ts
|
|
217
|
+
handler.enable();
|
|
218
|
+
handler.disable();
|
|
219
|
+
handler.isEnabled();
|
|
220
|
+
handler.setOptions({...});
|
|
221
|
+
handler.getOptions();
|
|
222
|
+
```
|
|
99
223
|
|
|
100
|
-
|
|
224
|
+
Available named handlers:
|
|
225
|
+
|
|
226
|
+
| Handler | Purpose |
|
|
227
|
+
|---|---|
|
|
228
|
+
| `map.dragPan` | Pointer drag map panning |
|
|
229
|
+
| `map.scrollZoom` | Wheel zoom |
|
|
230
|
+
| `map.clickSelect` | Click selection |
|
|
231
|
+
| `map.boxSelect` | Active-tool rectangular selection |
|
|
232
|
+
| `map.lassoSelect` | Free-form lasso selection |
|
|
233
|
+
| `map.vertexEdit` | Topology-aware vertex editing |
|
|
234
|
+
| `map.drawFeature` | Point, polyline, and polygon drawing |
|
|
235
|
+
| `map.transformFeature` | Translate, rotate, or scale selected features |
|
|
236
|
+
|
|
237
|
+
## Building Your Own UI
|
|
238
|
+
|
|
239
|
+
Built-in controls are optional. A custom toolbar can call the public map API and
|
|
240
|
+
handlers directly:
|
|
101
241
|
|
|
102
242
|
```ts
|
|
103
|
-
|
|
243
|
+
toolbar.querySelector('[data-tool="box"]').onclick = () => {
|
|
244
|
+
map.lassoSelect.disable();
|
|
245
|
+
map.boxSelect.enable();
|
|
246
|
+
};
|
|
104
247
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
map.setExtent(source.getExtent());
|
|
248
|
+
toolbar.querySelector('[data-tool="undo"]').onclick = () => map.undo();
|
|
249
|
+
toolbar.querySelector('[data-tool="redo"]').onclick = () => map.redo();
|
|
108
250
|
|
|
109
|
-
map.
|
|
110
|
-
|
|
251
|
+
map.on('historychange', (e) => {
|
|
252
|
+
undoButton.disabled = !e.canUndo;
|
|
253
|
+
redoButton.disabled = !e.canRedo;
|
|
254
|
+
});
|
|
111
255
|
```
|
|
112
256
|
|
|
113
|
-
|
|
257
|
+
For a custom control that still plugs into the corner layout, implement:
|
|
114
258
|
|
|
115
259
|
```ts
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
source.getLayers();
|
|
260
|
+
const control = {
|
|
261
|
+
onAdd(map) {
|
|
262
|
+
const el = document.createElement('div');
|
|
263
|
+
el.textContent = 'My tool';
|
|
264
|
+
return el;
|
|
265
|
+
},
|
|
266
|
+
onRemove() {},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
map.addControl(control, 'top-right');
|
|
127
270
|
```
|
|
128
271
|
|
|
129
|
-
###
|
|
272
|
+
### Custom Box Selection
|
|
130
273
|
|
|
131
|
-
|
|
274
|
+
If you want your own rectangular selection UI instead of `BoxSelectControl`, use
|
|
275
|
+
the built-in handler as the gesture engine and keep your buttons outside
|
|
276
|
+
`emap`:
|
|
132
277
|
|
|
133
278
|
```ts
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Layer management
|
|
142
|
-
await map.ops.renameLayer({ source: 's', target: 'old', name: 'new' });
|
|
143
|
-
await map.ops.mergeLayers({ source: 's', targets: ['a', 'b'], name: 'combined' });
|
|
144
|
-
await map.ops.splitLayer({ source: 's', target: 'us', expression: 'STATE' });
|
|
145
|
-
await map.ops.dropLayer({ source: 's', target: 'tmp' });
|
|
146
|
-
|
|
147
|
-
// Attribute / data
|
|
148
|
-
await map.ops.applyExpression({ source: 's', target: 'l', expression: 'this.area = $.area' });
|
|
149
|
-
await map.ops.filterFeatures({ source: 's', target: 'l', expression: 'this.pop > 1e6' });
|
|
150
|
-
await map.ops.joinTable({
|
|
151
|
-
source: 's', target: 'states',
|
|
152
|
-
data: { csv: csvBytes }, keys: ['STATE_FIPS', 'fips'],
|
|
279
|
+
const boxButton = document.querySelector<HTMLButtonElement>('[data-tool="box"]')!;
|
|
280
|
+
const clearButton = document.querySelector<HTMLButtonElement>('[data-tool="clear"]')!;
|
|
281
|
+
|
|
282
|
+
map.boxSelect.setOptions({
|
|
283
|
+
layers: ['district-fill'],
|
|
284
|
+
dragThreshold: 4,
|
|
153
285
|
});
|
|
154
286
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
await map.ops.snapLayer({ source: 's', target: 'l', interval: 0.001 });
|
|
158
|
-
const report = await map.ops.checkGeometry({ source: 's' });
|
|
159
|
-
// → { ok: true, value: { ok, intersections: [...], intersectionCount } }
|
|
287
|
+
// Plain drag replaces the current selection. Shift+drag adds to it;
|
|
288
|
+
// Shift+Alt+drag toggles matching features.
|
|
160
289
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
290
|
+
function activateBoxSelect() {
|
|
291
|
+
map.lassoSelect.disable();
|
|
292
|
+
map.vertexEdit.disable();
|
|
293
|
+
map.drawFeature.disable();
|
|
294
|
+
map.transformFeature.disable();
|
|
295
|
+
map.boxSelect.enable();
|
|
296
|
+
boxButton.dataset.active = 'true';
|
|
297
|
+
}
|
|
164
298
|
|
|
165
|
-
|
|
299
|
+
function deactivateBoxSelect() {
|
|
300
|
+
map.boxSelect.disable();
|
|
301
|
+
delete boxButton.dataset.active;
|
|
302
|
+
}
|
|
166
303
|
|
|
167
|
-
|
|
304
|
+
boxButton.onclick = () => {
|
|
305
|
+
if (map.boxSelect.isEnabled()) deactivateBoxSelect();
|
|
306
|
+
else activateBoxSelect();
|
|
307
|
+
};
|
|
168
308
|
|
|
169
|
-
|
|
309
|
+
clearButton.onclick = () => map.clearSelection();
|
|
170
310
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (!r1.ok) { tx.rollback(); return; }
|
|
175
|
-
const r2 = await map.ops.dissolveLayer({ source, target, field: 'STATE' });
|
|
176
|
-
if (!r2.ok) { tx.rollback(); return; }
|
|
177
|
-
await tx.commit('Process boundaries');
|
|
311
|
+
map.on('selectionchange', (e) => {
|
|
312
|
+
selectionCount.textContent = String(e.selected.length);
|
|
313
|
+
});
|
|
178
314
|
```
|
|
179
315
|
|
|
180
|
-
|
|
316
|
+
The handler owns pointer capture, hit testing, selection mode resolution, and
|
|
317
|
+
its overlay drawing. Your application owns toolbar state, mode switching, and
|
|
318
|
+
whether selection is allowed for the current workflow.
|
|
181
319
|
|
|
182
|
-
|
|
320
|
+
## Feature Events and Highlighting
|
|
183
321
|
|
|
184
322
|
```ts
|
|
185
|
-
map.
|
|
186
|
-
|
|
187
|
-
map.
|
|
323
|
+
map.on('feature:click', { layers: ['district-fill'] }, (e) => {
|
|
324
|
+
console.log(e.ref, e.feature.properties);
|
|
325
|
+
map.select([e.ref]);
|
|
326
|
+
});
|
|
188
327
|
|
|
189
|
-
map.on('
|
|
190
|
-
|
|
328
|
+
map.on('feature:enter', { layers: ['district-fill'] }, (e) => {
|
|
329
|
+
map.setHighlightedFeatures([e.ref], { color: '#1677ff', width: 2, fill: true });
|
|
191
330
|
});
|
|
192
|
-
```
|
|
193
331
|
|
|
194
|
-
|
|
332
|
+
map.on('feature:leave', () => {
|
|
333
|
+
map.clearHighlightedFeatures();
|
|
334
|
+
});
|
|
195
335
|
|
|
196
|
-
|
|
336
|
+
map.on('mousemove', (e) => {
|
|
337
|
+
status.textContent = `${e.mapCoord[0].toFixed(4)}, ${e.mapCoord[1].toFixed(4)}`;
|
|
338
|
+
});
|
|
339
|
+
```
|
|
197
340
|
|
|
198
|
-
|
|
341
|
+
Feature and pointer events are delegated through the same pointer arbiter as the
|
|
342
|
+
handlers. The pointer sink is installed lazily, so there is no hit-test overhead
|
|
343
|
+
when no listener is registered.
|
|
199
344
|
|
|
200
|
-
|
|
201
|
-
import { topologyValidator } from '@zwishing/emap';
|
|
345
|
+
## Data Operations
|
|
202
346
|
|
|
203
|
-
|
|
204
|
-
sources: ['china'],
|
|
205
|
-
}));
|
|
347
|
+
All operations return `Promise<OpResult<T>>`:
|
|
206
348
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
349
|
+
```ts
|
|
350
|
+
const result = await map.ops.bufferLayer({
|
|
351
|
+
source: 'roads',
|
|
352
|
+
target: 'roads',
|
|
353
|
+
radius: 50,
|
|
210
354
|
});
|
|
355
|
+
|
|
356
|
+
if (!result.ok) {
|
|
357
|
+
console.error(result.error.kind, result.error.message);
|
|
358
|
+
}
|
|
211
359
|
```
|
|
212
360
|
|
|
213
|
-
|
|
361
|
+
Common operation groups:
|
|
214
362
|
|
|
215
|
-
|
|
363
|
+
- Geometry operations: `clipLayer`, `eraseLayer`, `dissolveLayer`,
|
|
364
|
+
`bufferLayer`, `simplifyLayer`, `projectLayer`, `cleanLayer`, `snapLayer`.
|
|
365
|
+
- Conversion operations: `pointsLayer`, `linesLayer`, `polygonsLayer`,
|
|
366
|
+
`innerlinesLayer`, `explodeLayer`.
|
|
367
|
+
- Layer operations: `renameLayer`, `mergeLayers`, `splitLayer`, `dropLayer`.
|
|
368
|
+
- Attribute operations: `applyExpression`, `filterFeatures`, `joinTable`,
|
|
369
|
+
`sortFeatures`, `uniqueFeatures`, `filterFields`, `renameFields`,
|
|
370
|
+
`dataFill`.
|
|
371
|
+
- Inspection and repair: `checkGeometry`, `rebuildTopology`,
|
|
372
|
+
`intersectionPointsLayer`.
|
|
373
|
+
- Selection-driven operation: `mergeSelected`.
|
|
216
374
|
|
|
217
|
-
|
|
375
|
+
Expression-bearing APIs are disabled by default for safety:
|
|
218
376
|
|
|
219
377
|
```ts
|
|
220
|
-
const
|
|
221
|
-
if (f) console.log(f.properties.NAME, f.geometry);
|
|
222
|
-
|
|
223
|
-
for (const f of map.features.iter('us', 'states')) {
|
|
224
|
-
/* … */
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
map.features.count('us', 'states'); // O(1)
|
|
378
|
+
const map = new Emap({ container: 'map', expressionPolicy: 'trusted' });
|
|
228
379
|
```
|
|
229
380
|
|
|
230
|
-
|
|
381
|
+
Use `'trusted'` only for application-owned expressions and trusted data.
|
|
231
382
|
|
|
232
|
-
|
|
383
|
+
## Transactions, Undo, and Validation
|
|
233
384
|
|
|
234
385
|
```ts
|
|
235
|
-
map.
|
|
236
|
-
|
|
237
|
-
map.
|
|
386
|
+
const tx = map.beginTransaction();
|
|
387
|
+
|
|
388
|
+
const clipped = await map.ops.clipLayer({ source: 's', target: 'parcels', mask: 'clip' });
|
|
389
|
+
if (!clipped.ok) {
|
|
390
|
+
tx.rollback();
|
|
391
|
+
} else {
|
|
392
|
+
await tx.commit('Clip parcels');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
map.undo();
|
|
396
|
+
map.redo();
|
|
238
397
|
|
|
239
398
|
map.on('selectionchange', (e) => {
|
|
240
|
-
console.log(e.
|
|
399
|
+
console.log(e.selected, e.added, e.removed);
|
|
241
400
|
});
|
|
242
401
|
```
|
|
243
402
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
### Worker offloading
|
|
403
|
+
Register validators for post-commit checks:
|
|
247
404
|
|
|
248
405
|
```ts
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
406
|
+
import { topologyValidator } from '@zwishing/emap';
|
|
407
|
+
|
|
408
|
+
const unregister = map.validators.register(topologyValidator({
|
|
409
|
+
sources: ['editing'],
|
|
410
|
+
}));
|
|
411
|
+
|
|
412
|
+
map.on('validationfailed', (e) => {
|
|
413
|
+
console.warn(e.results);
|
|
255
414
|
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The engine reports validation failures; it does not auto-undo. The application
|
|
418
|
+
decides whether to warn, block save, or call `map.undo()`.
|
|
419
|
+
|
|
420
|
+
## Camera
|
|
256
421
|
|
|
257
|
-
|
|
258
|
-
map.
|
|
422
|
+
```ts
|
|
423
|
+
map.jumpTo({ center: [120, 30], zoom: 6 });
|
|
424
|
+
map.easeTo({ center: [120, 30], zoom: 8, duration: 600 });
|
|
425
|
+
map.flyTo({ center: [120, 30], zoom: 10, duration: 1500 });
|
|
426
|
+
map.fitBounds(source.getExtent(), { padding: 40, duration: 600 });
|
|
427
|
+
map.panBy([120, 0]);
|
|
428
|
+
map.stop();
|
|
259
429
|
```
|
|
260
430
|
|
|
261
|
-
|
|
262
|
-
`
|
|
431
|
+
Camera methods are chainable and fire `movestart`, `move`, `moveend`,
|
|
432
|
+
`zoomstart`, `zoom`, and `zoomend` events.
|
|
263
433
|
|
|
264
|
-
|
|
265
|
-
`mapshaper-vendor.js` from the same directory. The worker loads the vendor file
|
|
266
|
-
with `importScripts('mapshaper-vendor.js')`.
|
|
434
|
+
## Worker Offloading
|
|
267
435
|
|
|
268
436
|
```ts
|
|
269
437
|
const map = new Emap({
|
|
270
438
|
container: 'map',
|
|
271
439
|
useWorker: 'auto',
|
|
440
|
+
workerThreshold: 200_000,
|
|
272
441
|
workerUrl: '/vendor/emap/emap-worker.js',
|
|
442
|
+
workerPoolSize: 2,
|
|
273
443
|
});
|
|
274
444
|
```
|
|
275
445
|
|
|
276
|
-
|
|
277
|
-
your app build:
|
|
446
|
+
When workers are enabled, serve these files from the same directory:
|
|
278
447
|
|
|
279
448
|
```text
|
|
280
449
|
node_modules/@zwishing/emap/dist/emap-worker.js
|
|
281
450
|
node_modules/@zwishing/emap/dist/mapshaper-vendor.js
|
|
282
451
|
```
|
|
283
452
|
|
|
284
|
-
|
|
453
|
+
`workerRouting` can override the built-in cheap/expensive operation routing.
|
|
454
|
+
|
|
455
|
+
Workers only cover dataset-replace style operations such as clip, dissolve,
|
|
456
|
+
union, simplify, project, and similar `map.ops.*` commands. Pointer
|
|
457
|
+
interactions, selection transforms, vertex edits, and drawing sessions stay on
|
|
458
|
+
the main thread because their per-frame state is UI-bound and usually cheaper
|
|
459
|
+
than worker round trips.
|
|
460
|
+
|
|
461
|
+
## Theming
|
|
462
|
+
|
|
463
|
+
Import the default CSS once:
|
|
285
464
|
|
|
286
465
|
```ts
|
|
287
|
-
|
|
288
|
-
await map.loadSnapshot(blob); // restore datasets + history
|
|
466
|
+
import '@zwishing/emap/style.css';
|
|
289
467
|
```
|
|
290
468
|
|
|
291
|
-
|
|
469
|
+
Override CSS tokens at `:root` or an app-specific container:
|
|
470
|
+
|
|
471
|
+
```css
|
|
472
|
+
:root {
|
|
473
|
+
--emap-accent: #1677ff;
|
|
474
|
+
--emap-accent-soft: rgba(22, 119, 255, 0.12);
|
|
475
|
+
--emap-surface-bg: #fff;
|
|
476
|
+
--emap-surface-border: #d9d9d9;
|
|
477
|
+
--emap-surface-shadow: 0 4px 12px rgba(0, 0, 0, 0.16);
|
|
478
|
+
--emap-radius: 4px;
|
|
479
|
+
--emap-hover-bg: #f5f5f5;
|
|
480
|
+
--emap-danger: #d32f2f;
|
|
481
|
+
--emap-font: 13px/1.5 system-ui, sans-serif;
|
|
482
|
+
}
|
|
483
|
+
```
|
|
292
484
|
|
|
293
|
-
|
|
485
|
+
## Built-In Controls
|
|
294
486
|
|
|
295
487
|
| Control | Purpose |
|
|
296
488
|
|---|---|
|
|
297
|
-
| `NavigationControl` | Zoom in
|
|
298
|
-
| `StatusControl` |
|
|
489
|
+
| `NavigationControl` | Zoom in, zoom out, reset |
|
|
490
|
+
| `StatusControl` | Pointer coordinates and CRS |
|
|
299
491
|
| `BasemapControl` | MapLibre raster basemap toggle |
|
|
300
|
-
| `
|
|
301
|
-
| `
|
|
302
|
-
| `
|
|
303
|
-
| `
|
|
304
|
-
| `BoxSelectControl` |
|
|
305
|
-
| `LassoSelectControl` |
|
|
492
|
+
| `HistoryControl` | Undo/redo buttons |
|
|
493
|
+
| `EditToolbar` | Combined edit toolbar |
|
|
494
|
+
| `VertexEditControl` | Button shell over `map.vertexEdit` |
|
|
495
|
+
| `DrawFeatureControl` | Button shell over `map.drawFeature` |
|
|
496
|
+
| `BoxSelectControl` | Thin shell over `map.boxSelect` |
|
|
497
|
+
| `LassoSelectControl` | Thin shell over `map.lassoSelect` |
|
|
498
|
+
| `SimplifyControl` | Simplification UI |
|
|
499
|
+
|
|
500
|
+
## Known Limitations
|
|
501
|
+
|
|
502
|
+
- The project is AI-driven and still moving quickly; avoid depending on
|
|
503
|
+
undocumented internals in production applications.
|
|
504
|
+
- `fill` rendering is heavier than `line` rendering for large polygon datasets
|
|
505
|
+
because area fills require full polygon path materialization and fill rules.
|
|
506
|
+
For large-data browsing, keep polygon layers outline-first and enable fills
|
|
507
|
+
only at suitable scales or for filtered subsets.
|
|
508
|
+
- Rendering is Canvas 2D only. There is no WebGL renderer, symbol placement
|
|
509
|
+
engine, tiled vector source, or server-side tile pipeline.
|
|
510
|
+
- CRS support follows the bundled Mapshaper/projection path and common browser
|
|
511
|
+
data workflows. Validate project-specific CRS definitions before building
|
|
512
|
+
user-facing editing workflows around them.
|
|
513
|
+
- Expression-bearing operations are disabled by default. Set
|
|
514
|
+
`expressionPolicy: 'trusted'` only for first-party expressions and trusted
|
|
515
|
+
data.
|
|
516
|
+
- Worker offloading requires the host app to deploy worker/vendor assets
|
|
517
|
+
correctly and does not make every interaction asynchronous.
|
|
306
518
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
## Architecture
|
|
519
|
+
## Development
|
|
310
520
|
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
├── source/ # TopologySource, DisplayArcs (LOD)
|
|
317
|
-
├── core/ # EventDispatcher, MapshaperWorkerPool, drag/wheel
|
|
318
|
-
├── geo/ # Viewport, Projection, Bounds, AffineTransform, CRS resolver
|
|
319
|
-
├── renderer/ # CanvasPainter (HTML5 Canvas 2D)
|
|
320
|
-
├── edit/ # EditHistory + per-command memento classes
|
|
321
|
-
├── ui/ # Control implementations + controls.css
|
|
322
|
-
├── worker/ # off-thread mapshaper entry
|
|
323
|
-
└── types/ # central mapshaper type definitions
|
|
521
|
+
```bash
|
|
522
|
+
pnpm install
|
|
523
|
+
cmd /c npm run typecheck
|
|
524
|
+
cmd /c npm run test:run
|
|
525
|
+
cmd /c npm run build
|
|
324
526
|
```
|
|
325
527
|
|
|
326
|
-
|
|
528
|
+
Build outputs:
|
|
327
529
|
|
|
328
|
-
-
|
|
329
|
-
-
|
|
330
|
-
-
|
|
331
|
-
-
|
|
332
|
-
-
|
|
333
|
-
-
|
|
530
|
+
- `dist/emap.js` - IIFE bundle.
|
|
531
|
+
- `dist/emap.mjs` - ES module.
|
|
532
|
+
- `dist/emap-worker.js` - worker bundle.
|
|
533
|
+
- `dist/mapshaper-vendor.js` and `dist/mapshaper-vendor.mjs` - Mapshaper vendor bundles.
|
|
534
|
+
- `dist/emap.css` - control styles.
|
|
535
|
+
- `dist/index.d.ts` - TypeScript declarations.
|
|
334
536
|
|
|
335
|
-
|
|
537
|
+
Manual examples live in `test/examples/*.html`.
|
|
538
|
+
|
|
539
|
+
## Release Checklist
|
|
540
|
+
|
|
541
|
+
Before publishing a version:
|
|
336
542
|
|
|
337
543
|
```bash
|
|
338
|
-
|
|
339
|
-
npm run
|
|
340
|
-
npm run
|
|
341
|
-
npm run build
|
|
342
|
-
|
|
343
|
-
npm run typecheck # strict tsc
|
|
544
|
+
cmd /c npm ci
|
|
545
|
+
cmd /c npm run typecheck
|
|
546
|
+
cmd /c npm run test:run
|
|
547
|
+
cmd /c npm run build
|
|
548
|
+
cmd /c npm pack --dry-run
|
|
344
549
|
```
|
|
345
550
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
- `emap.js` — production IIFE bundle
|
|
349
|
-
- `emap.mjs` — ES module
|
|
350
|
-
- `emap-dev.js` — dev bundle with sourcemaps
|
|
351
|
-
- `emap-worker.js` — worker entry
|
|
352
|
-
- `mapshaper-vendor.js` — mapshaper vendor bundle (load before `emap.js` in IIFE mode)
|
|
353
|
-
- `index.d.ts` — TypeScript declarations
|
|
551
|
+
Then check:
|
|
354
552
|
|
|
355
|
-
|
|
553
|
+
- `package.json` version matches the release section in `CHANGELOG.md`.
|
|
554
|
+
- `README.md`, `FEATURES.md`, `SECURITY.md`, and `CHANGELOG.md` describe the
|
|
555
|
+
same public API.
|
|
556
|
+
- The dry-run package contains only current `dist` files, docs, license, and
|
|
557
|
+
package metadata.
|
|
558
|
+
- A fresh consumer project can import `@zwishing/emap`, import
|
|
559
|
+
`@zwishing/emap/style.css`, and resolve `dist/emap-worker.js` if worker
|
|
560
|
+
mode is documented for the release.
|
|
356
561
|
|
|
357
562
|
## License
|
|
358
563
|
|