@zwishing/emap 0.1.3 → 0.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/CHANGELOG.md +250 -1
- package/FEATURES.md +455 -0
- package/README.md +415 -205
- 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 +54 -0
- package/dist/core/handlers/click-select.d.ts +31 -0
- package/dist/core/handlers/drag-pan.d.ts +28 -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 +41 -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 +33 -4
- package/dist/ui/box-select-control.d.ts +13 -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,298 +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
|
-
|
|
71
|
-
|
|
72
|
-
`BasemapControl` loads MapLibre only when a basemap is first enabled. If you use
|
|
73
|
-
that control, also include MapLibre's stylesheet in your app:
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
77
|
-
```
|
|
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.
|
|
78
148
|
|
|
79
|
-
|
|
149
|
+
Supported inputs:
|
|
80
150
|
|
|
81
|
-
|
|
151
|
+
- GeoJSON and TopoJSON.
|
|
152
|
+
- ZIP Shapefile archives containing `.shp`, `.dbf`, `.shx`, and optional `.prj`.
|
|
153
|
+
- Mapshaper `.msx` snapshots via `map.loadSnapshot()`.
|
|
82
154
|
|
|
83
|
-
|
|
84
|
-
`Buffer` polyfill, no bundler config. mapshaper is shipped as a sibling ES
|
|
85
|
-
module (`dist/mapshaper-vendor.mjs`) that `dist/emap.mjs` imports by relative
|
|
86
|
-
path; your bundler resolves it automatically from
|
|
87
|
-
`node_modules/@zwishing/emap/dist/`.
|
|
155
|
+
## Layers and Rendering
|
|
88
156
|
|
|
89
157
|
```ts
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
});
|
|
92
177
|
```
|
|
93
178
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
97
182
|
|
|
98
|
-
##
|
|
183
|
+
## Interactions Without Built-In Controls
|
|
99
184
|
|
|
100
|
-
|
|
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`.
|
|
101
188
|
|
|
102
189
|
```ts
|
|
103
|
-
|
|
190
|
+
map.boxSelect.setOptions({
|
|
191
|
+
layers: ['district-fill'],
|
|
192
|
+
dragActivator: 'shift',
|
|
193
|
+
dragThreshold: 4,
|
|
194
|
+
});
|
|
195
|
+
map.boxSelect.enable();
|
|
104
196
|
|
|
105
|
-
map.
|
|
106
|
-
map.
|
|
107
|
-
map.setExtent(source.getExtent());
|
|
197
|
+
map.lassoSelect.setOptions({ layers: ['district-fill'] });
|
|
198
|
+
map.lassoSelect.enable();
|
|
108
199
|
|
|
109
|
-
map.
|
|
110
|
-
|
|
200
|
+
map.drawFeature.setOptions({
|
|
201
|
+
source: 'editing',
|
|
202
|
+
sourceLayer: 'areas',
|
|
203
|
+
type: 'polygon',
|
|
204
|
+
snapSources: ['editing', 'reference'],
|
|
205
|
+
});
|
|
206
|
+
map.drawFeature.enable();
|
|
207
|
+
|
|
208
|
+
map.vertexEdit.enable();
|
|
209
|
+
|
|
210
|
+
map.clickSelect.enable();
|
|
211
|
+
map.transformFeature.setOptions({ mode: 'translate' });
|
|
212
|
+
map.transformFeature.enable();
|
|
111
213
|
```
|
|
112
214
|
|
|
113
|
-
|
|
215
|
+
All handlers expose the same basic surface:
|
|
114
216
|
|
|
115
217
|
```ts
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
source.getLayers();
|
|
218
|
+
handler.enable();
|
|
219
|
+
handler.disable();
|
|
220
|
+
handler.isEnabled();
|
|
221
|
+
handler.setOptions({...});
|
|
222
|
+
handler.getOptions();
|
|
122
223
|
```
|
|
123
224
|
|
|
124
|
-
|
|
225
|
+
Available named handlers:
|
|
125
226
|
|
|
126
|
-
|
|
227
|
+
| Handler | Purpose |
|
|
228
|
+
|---|---|
|
|
229
|
+
| `map.dragPan` | Pointer drag map panning |
|
|
230
|
+
| `map.scrollZoom` | Wheel zoom |
|
|
231
|
+
| `map.clickSelect` | Click selection |
|
|
232
|
+
| `map.boxSelect` | Shift-drag rectangular selection |
|
|
233
|
+
| `map.lassoSelect` | Free-form lasso selection |
|
|
234
|
+
| `map.vertexEdit` | Topology-aware vertex editing |
|
|
235
|
+
| `map.drawFeature` | Point, polyline, and polygon drawing |
|
|
236
|
+
| `map.transformFeature` | Translate, rotate, or scale selected features |
|
|
127
237
|
|
|
128
|
-
|
|
129
|
-
// Whole-dataset CLI ops
|
|
130
|
-
await map.ops.clipLayer({ source: 'roads', target: 'streets', mask: 'park' });
|
|
131
|
-
await map.ops.dissolveLayer({ source: 'us', target: 'counties', field: 'STATE' });
|
|
132
|
-
await map.ops.bufferLayer({ source: 'roads', target: 'streets', radius: 50 });
|
|
133
|
-
await map.ops.simplifyLayer({ source: 'world', percentage: 10 });
|
|
134
|
-
await map.ops.projectLayer({ source: 'world', crs: 'EPSG:3857' });
|
|
135
|
-
|
|
136
|
-
// Layer management
|
|
137
|
-
await map.ops.renameLayer({ source: 's', target: 'old', name: 'new' });
|
|
138
|
-
await map.ops.mergeLayers({ source: 's', targets: ['a', 'b'], name: 'combined' });
|
|
139
|
-
await map.ops.splitLayer({ source: 's', target: 'us', expression: 'STATE' });
|
|
140
|
-
await map.ops.dropLayer({ source: 's', target: 'tmp' });
|
|
141
|
-
|
|
142
|
-
// Attribute / data
|
|
143
|
-
await map.ops.applyExpression({ source: 's', target: 'l', expression: 'this.area = $.area' });
|
|
144
|
-
await map.ops.filterFeatures({ source: 's', target: 'l', expression: 'this.pop > 1e6' });
|
|
145
|
-
await map.ops.joinTable({
|
|
146
|
-
source: 's', target: 'states',
|
|
147
|
-
data: { csv: csvBytes }, keys: ['STATE_FIPS', 'fips'],
|
|
148
|
-
});
|
|
238
|
+
## Building Your Own UI
|
|
149
239
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
await map.ops.snapLayer({ source: 's', target: 'l', interval: 0.001 });
|
|
153
|
-
const report = await map.ops.checkGeometry({ source: 's' });
|
|
154
|
-
// → { ok: true, value: { ok, intersections: [...], intersectionCount } }
|
|
240
|
+
Built-in controls are optional. A custom toolbar can call the public map API and
|
|
241
|
+
handlers directly:
|
|
155
242
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
243
|
+
```ts
|
|
244
|
+
toolbar.querySelector('[data-tool="box"]').onclick = () => {
|
|
245
|
+
map.lassoSelect.disable();
|
|
246
|
+
map.boxSelect.enable();
|
|
247
|
+
};
|
|
159
248
|
|
|
160
|
-
|
|
249
|
+
toolbar.querySelector('[data-tool="undo"]').onclick = () => map.undo();
|
|
250
|
+
toolbar.querySelector('[data-tool="redo"]').onclick = () => map.redo();
|
|
161
251
|
|
|
162
|
-
|
|
252
|
+
map.on('historychange', (e) => {
|
|
253
|
+
undoButton.disabled = !e.canUndo;
|
|
254
|
+
redoButton.disabled = !e.canRedo;
|
|
255
|
+
});
|
|
256
|
+
```
|
|
163
257
|
|
|
164
|
-
|
|
258
|
+
For a custom control that still plugs into the corner layout, implement:
|
|
165
259
|
|
|
166
260
|
```ts
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
261
|
+
const control = {
|
|
262
|
+
onAdd(map) {
|
|
263
|
+
const el = document.createElement('div');
|
|
264
|
+
el.textContent = 'My tool';
|
|
265
|
+
return el;
|
|
266
|
+
},
|
|
267
|
+
onRemove() {},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
map.addControl(control, 'top-right');
|
|
173
271
|
```
|
|
174
272
|
|
|
175
|
-
|
|
273
|
+
### Custom Box Selection
|
|
176
274
|
|
|
177
|
-
|
|
275
|
+
If you want your own rectangular selection UI instead of `BoxSelectControl`, use
|
|
276
|
+
the built-in handler as the gesture engine and keep your buttons outside
|
|
277
|
+
`emap`:
|
|
178
278
|
|
|
179
279
|
```ts
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
280
|
+
const boxButton = document.querySelector<HTMLButtonElement>('[data-tool="box"]')!;
|
|
281
|
+
const clearButton = document.querySelector<HTMLButtonElement>('[data-tool="clear"]')!;
|
|
282
|
+
|
|
283
|
+
map.boxSelect.setOptions({
|
|
284
|
+
layers: ['district-fill'],
|
|
285
|
+
dragActivator: 'shift',
|
|
286
|
+
dragThreshold: 4,
|
|
287
|
+
mode: 'replace',
|
|
288
|
+
});
|
|
183
289
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
}
|
|
298
|
+
|
|
299
|
+
function deactivateBoxSelect() {
|
|
300
|
+
map.boxSelect.disable();
|
|
301
|
+
delete boxButton.dataset.active;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
boxButton.onclick = () => {
|
|
305
|
+
if (map.boxSelect.isEnabled()) deactivateBoxSelect();
|
|
306
|
+
else activateBoxSelect();
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
clearButton.onclick = () => map.clearSelection();
|
|
310
|
+
|
|
311
|
+
map.on('selectionchange', (e) => {
|
|
312
|
+
selectionCount.textContent = String(e.selected.length);
|
|
186
313
|
});
|
|
187
314
|
```
|
|
188
315
|
|
|
189
|
-
|
|
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.
|
|
190
319
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
Register validators that run after every committed edit. Failures fire `validationfailed` — the engine doesn't auto-undo, the app decides.
|
|
320
|
+
## Feature Events and Highlighting
|
|
194
321
|
|
|
195
322
|
```ts
|
|
196
|
-
|
|
323
|
+
map.on('feature:click', { layers: ['district-fill'] }, (e) => {
|
|
324
|
+
console.log(e.ref, e.feature.properties);
|
|
325
|
+
map.select([e.ref]);
|
|
326
|
+
});
|
|
197
327
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
})
|
|
328
|
+
map.on('feature:enter', { layers: ['district-fill'] }, (e) => {
|
|
329
|
+
map.setHighlightedFeatures([e.ref], { color: '#1677ff', width: 2, fill: true });
|
|
330
|
+
});
|
|
201
331
|
|
|
202
|
-
map.on('
|
|
203
|
-
|
|
204
|
-
|
|
332
|
+
map.on('feature:leave', () => {
|
|
333
|
+
map.clearHighlightedFeatures();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
map.on('mousemove', (e) => {
|
|
337
|
+
status.textContent = `${e.mapCoord[0].toFixed(4)}, ${e.mapCoord[1].toFixed(4)}`;
|
|
205
338
|
});
|
|
206
339
|
```
|
|
207
340
|
|
|
208
|
-
|
|
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.
|
|
209
344
|
|
|
210
|
-
|
|
345
|
+
## Data Operations
|
|
211
346
|
|
|
212
|
-
|
|
347
|
+
All operations return `Promise<OpResult<T>>`:
|
|
213
348
|
|
|
214
349
|
```ts
|
|
215
|
-
const
|
|
216
|
-
|
|
350
|
+
const result = await map.ops.bufferLayer({
|
|
351
|
+
source: 'roads',
|
|
352
|
+
target: 'roads',
|
|
353
|
+
radius: 50,
|
|
354
|
+
});
|
|
217
355
|
|
|
218
|
-
|
|
219
|
-
|
|
356
|
+
if (!result.ok) {
|
|
357
|
+
console.error(result.error.kind, result.error.message);
|
|
220
358
|
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Common operation groups:
|
|
362
|
+
|
|
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`.
|
|
374
|
+
|
|
375
|
+
Expression-bearing APIs are disabled by default for safety:
|
|
221
376
|
|
|
222
|
-
|
|
377
|
+
```ts
|
|
378
|
+
const map = new Emap({ container: 'map', expressionPolicy: 'trusted' });
|
|
223
379
|
```
|
|
224
380
|
|
|
225
|
-
|
|
381
|
+
Use `'trusted'` only for application-owned expressions and trusted data.
|
|
226
382
|
|
|
227
|
-
|
|
383
|
+
## Transactions, Undo, and Validation
|
|
228
384
|
|
|
229
385
|
```ts
|
|
230
|
-
map.
|
|
231
|
-
|
|
232
|
-
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();
|
|
233
397
|
|
|
234
398
|
map.on('selectionchange', (e) => {
|
|
235
|
-
console.log(e.
|
|
399
|
+
console.log(e.selected, e.added, e.removed);
|
|
236
400
|
});
|
|
237
401
|
```
|
|
238
402
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
### Worker offloading
|
|
403
|
+
Register validators for post-commit checks:
|
|
242
404
|
|
|
243
405
|
```ts
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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);
|
|
250
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
|
|
251
421
|
|
|
252
|
-
|
|
253
|
-
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();
|
|
254
429
|
```
|
|
255
430
|
|
|
256
|
-
|
|
257
|
-
`
|
|
431
|
+
Camera methods are chainable and fire `movestart`, `move`, `moveend`,
|
|
432
|
+
`zoomstart`, `zoom`, and `zoomend` events.
|
|
258
433
|
|
|
259
|
-
|
|
260
|
-
`mapshaper-vendor.js` from the same directory. The worker loads the vendor file
|
|
261
|
-
with `importScripts('mapshaper-vendor.js')`.
|
|
434
|
+
## Worker Offloading
|
|
262
435
|
|
|
263
436
|
```ts
|
|
264
437
|
const map = new Emap({
|
|
265
438
|
container: 'map',
|
|
266
439
|
useWorker: 'auto',
|
|
440
|
+
workerThreshold: 200_000,
|
|
267
441
|
workerUrl: '/vendor/emap/emap-worker.js',
|
|
442
|
+
workerPoolSize: 2,
|
|
268
443
|
});
|
|
269
444
|
```
|
|
270
445
|
|
|
271
|
-
|
|
272
|
-
your app build:
|
|
446
|
+
When workers are enabled, serve these files from the same directory:
|
|
273
447
|
|
|
274
448
|
```text
|
|
275
449
|
node_modules/@zwishing/emap/dist/emap-worker.js
|
|
276
450
|
node_modules/@zwishing/emap/dist/mapshaper-vendor.js
|
|
277
451
|
```
|
|
278
452
|
|
|
279
|
-
|
|
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:
|
|
280
464
|
|
|
281
465
|
```ts
|
|
282
|
-
|
|
283
|
-
await map.loadSnapshot(blob); // restore datasets + history
|
|
466
|
+
import '@zwishing/emap/style.css';
|
|
284
467
|
```
|
|
285
468
|
|
|
286
|
-
|
|
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
|
+
```
|
|
287
484
|
|
|
288
|
-
|
|
485
|
+
## Built-In Controls
|
|
289
486
|
|
|
290
487
|
| Control | Purpose |
|
|
291
488
|
|---|---|
|
|
292
|
-
| `NavigationControl` | Zoom in
|
|
293
|
-
| `StatusControl` |
|
|
489
|
+
| `NavigationControl` | Zoom in, zoom out, reset |
|
|
490
|
+
| `StatusControl` | Pointer coordinates and CRS |
|
|
294
491
|
| `BasemapControl` | MapLibre raster basemap toggle |
|
|
295
|
-
| `
|
|
296
|
-
| `
|
|
297
|
-
| `
|
|
298
|
-
| `
|
|
299
|
-
| `BoxSelectControl` |
|
|
300
|
-
| `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.
|
|
301
518
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
## Architecture
|
|
519
|
+
## Development
|
|
305
520
|
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
├── source/ # TopologySource, DisplayArcs (LOD)
|
|
312
|
-
├── core/ # EventDispatcher, MapshaperWorkerPool, drag/wheel
|
|
313
|
-
├── geo/ # Viewport, Projection, Bounds, AffineTransform, CRS resolver
|
|
314
|
-
├── renderer/ # CanvasPainter (HTML5 Canvas 2D)
|
|
315
|
-
├── edit/ # EditHistory + per-command memento classes
|
|
316
|
-
├── ui/ # Control implementations + controls.css
|
|
317
|
-
├── worker/ # off-thread mapshaper entry
|
|
318
|
-
└── 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
|
|
319
526
|
```
|
|
320
527
|
|
|
321
|
-
|
|
528
|
+
Build outputs:
|
|
322
529
|
|
|
323
|
-
-
|
|
324
|
-
-
|
|
325
|
-
-
|
|
326
|
-
-
|
|
327
|
-
-
|
|
328
|
-
-
|
|
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.
|
|
329
536
|
|
|
330
|
-
|
|
537
|
+
Manual examples live in `test/examples/*.html`.
|
|
538
|
+
|
|
539
|
+
## Release Checklist
|
|
540
|
+
|
|
541
|
+
Before publishing a version:
|
|
331
542
|
|
|
332
543
|
```bash
|
|
333
|
-
|
|
334
|
-
npm run
|
|
335
|
-
npm run
|
|
336
|
-
npm run build
|
|
337
|
-
|
|
338
|
-
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
|
|
339
549
|
```
|
|
340
550
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
- `emap.js` — production IIFE bundle
|
|
344
|
-
- `emap.mjs` — ES module
|
|
345
|
-
- `emap-dev.js` — dev bundle with sourcemaps
|
|
346
|
-
- `emap-worker.js` — worker entry
|
|
347
|
-
- `mapshaper-vendor.js` — mapshaper vendor bundle (load before `emap.js` in IIFE mode)
|
|
348
|
-
- `index.d.ts` — TypeScript declarations
|
|
551
|
+
Then check:
|
|
349
552
|
|
|
350
|
-
|
|
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.
|
|
351
561
|
|
|
352
562
|
## License
|
|
353
563
|
|