@zwishing/emap 0.2.0 → 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 +212 -1
- package/FEATURES.md +455 -0
- package/README.md +415 -210
- 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 +7 -2
- 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,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
|
-
|
|
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
|
-
// Re-importing an existing source: setData is awaitable — it resolves
|
|
122
|
-
// once the dataset is ready, so getLayers()/getExtent() are safe after it.
|
|
123
|
-
// The `data` / `error` events still fire for event-driven consumers.
|
|
124
|
-
await source.setData({ filename: 'next.geojson', content: bytes });
|
|
125
|
-
source.getExtent();
|
|
126
|
-
source.getLayers();
|
|
218
|
+
handler.enable();
|
|
219
|
+
handler.disable();
|
|
220
|
+
handler.isEnabled();
|
|
221
|
+
handler.setOptions({...});
|
|
222
|
+
handler.getOptions();
|
|
127
223
|
```
|
|
128
224
|
|
|
129
|
-
|
|
225
|
+
Available named handlers:
|
|
130
226
|
|
|
131
|
-
|
|
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 |
|
|
132
237
|
|
|
133
|
-
|
|
134
|
-
// Whole-dataset CLI ops
|
|
135
|
-
await map.ops.clipLayer({ source: 'roads', target: 'streets', mask: 'park' });
|
|
136
|
-
await map.ops.dissolveLayer({ source: 'us', target: 'counties', field: 'STATE' });
|
|
137
|
-
await map.ops.bufferLayer({ source: 'roads', target: 'streets', radius: 50 });
|
|
138
|
-
await map.ops.simplifyLayer({ source: 'world', percentage: 10 });
|
|
139
|
-
await map.ops.projectLayer({ source: 'world', crs: 'EPSG:3857' });
|
|
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'],
|
|
153
|
-
});
|
|
238
|
+
## Building Your Own UI
|
|
154
239
|
|
|
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 } }
|
|
240
|
+
Built-in controls are optional. A custom toolbar can call the public map API and
|
|
241
|
+
handlers directly:
|
|
160
242
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
243
|
+
```ts
|
|
244
|
+
toolbar.querySelector('[data-tool="box"]').onclick = () => {
|
|
245
|
+
map.lassoSelect.disable();
|
|
246
|
+
map.boxSelect.enable();
|
|
247
|
+
};
|
|
164
248
|
|
|
165
|
-
|
|
249
|
+
toolbar.querySelector('[data-tool="undo"]').onclick = () => map.undo();
|
|
250
|
+
toolbar.querySelector('[data-tool="redo"]').onclick = () => map.redo();
|
|
166
251
|
|
|
167
|
-
|
|
252
|
+
map.on('historychange', (e) => {
|
|
253
|
+
undoButton.disabled = !e.canUndo;
|
|
254
|
+
redoButton.disabled = !e.canRedo;
|
|
255
|
+
});
|
|
256
|
+
```
|
|
168
257
|
|
|
169
|
-
|
|
258
|
+
For a custom control that still plugs into the corner layout, implement:
|
|
170
259
|
|
|
171
260
|
```ts
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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');
|
|
178
271
|
```
|
|
179
272
|
|
|
180
|
-
|
|
273
|
+
### Custom Box Selection
|
|
181
274
|
|
|
182
|
-
|
|
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`:
|
|
183
278
|
|
|
184
279
|
```ts
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
});
|
|
188
289
|
|
|
189
|
-
|
|
190
|
-
|
|
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);
|
|
191
313
|
});
|
|
192
314
|
```
|
|
193
315
|
|
|
194
|
-
|
|
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.
|
|
195
319
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
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
|
|
199
321
|
|
|
200
322
|
```ts
|
|
201
|
-
|
|
323
|
+
map.on('feature:click', { layers: ['district-fill'] }, (e) => {
|
|
324
|
+
console.log(e.ref, e.feature.properties);
|
|
325
|
+
map.select([e.ref]);
|
|
326
|
+
});
|
|
202
327
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
})
|
|
328
|
+
map.on('feature:enter', { layers: ['district-fill'] }, (e) => {
|
|
329
|
+
map.setHighlightedFeatures([e.ref], { color: '#1677ff', width: 2, fill: true });
|
|
330
|
+
});
|
|
206
331
|
|
|
207
|
-
map.on('
|
|
208
|
-
|
|
209
|
-
|
|
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)}`;
|
|
210
338
|
});
|
|
211
339
|
```
|
|
212
340
|
|
|
213
|
-
|
|
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.
|
|
214
344
|
|
|
215
|
-
|
|
345
|
+
## Data Operations
|
|
216
346
|
|
|
217
|
-
|
|
347
|
+
All operations return `Promise<OpResult<T>>`:
|
|
218
348
|
|
|
219
349
|
```ts
|
|
220
|
-
const
|
|
221
|
-
|
|
350
|
+
const result = await map.ops.bufferLayer({
|
|
351
|
+
source: 'roads',
|
|
352
|
+
target: 'roads',
|
|
353
|
+
radius: 50,
|
|
354
|
+
});
|
|
222
355
|
|
|
223
|
-
|
|
224
|
-
|
|
356
|
+
if (!result.ok) {
|
|
357
|
+
console.error(result.error.kind, result.error.message);
|
|
225
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:
|
|
226
376
|
|
|
227
|
-
|
|
377
|
+
```ts
|
|
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
|
|