@zwishing/emap 0.1.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 +38 -0
- package/LICENSE +373 -0
- package/README.md +294 -0
- package/SECURITY.md +56 -0
- package/dist/adapter/mapshaper-adapter.d.ts +282 -0
- package/dist/core/drag-pan-handler.d.ts +28 -0
- package/dist/core/events.d.ts +16 -0
- package/dist/core/interactions.d.ts +20 -0
- package/dist/core/mapshaper-worker-pool.d.ts +151 -0
- package/dist/core/tween.d.ts +26 -0
- package/dist/edit/commands/composite.d.ts +16 -0
- package/dist/edit/commands/dataset-replace.d.ts +43 -0
- package/dist/edit/commands/feature-affine.d.ts +72 -0
- package/dist/edit/commands/feature-create.d.ts +47 -0
- package/dist/edit/commands/feature-delete.d.ts +72 -0
- package/dist/edit/commands/feature-property-change.d.ts +34 -0
- package/dist/edit/commands/feature-translate.d.ts +55 -0
- package/dist/edit/commands/field-add.d.ts +24 -0
- package/dist/edit/commands/field-remove.d.ts +20 -0
- package/dist/edit/commands/field-rename.d.ts +19 -0
- package/dist/edit/commands/split-shared-arcs.d.ts +71 -0
- package/dist/edit/commands/vertex-delete.d.ts +26 -0
- package/dist/edit/commands/vertex-insert.d.ts +26 -0
- package/dist/edit/commands/vertex-move.d.ts +45 -0
- package/dist/edit/edit-command.d.ts +72 -0
- package/dist/edit/edit-history.d.ts +130 -0
- package/dist/edit/transaction.d.ts +59 -0
- package/dist/emap-worker.js +1 -0
- package/dist/emap.css +157 -0
- package/dist/emap.js +5 -0
- package/dist/emap.mjs +5 -0
- package/dist/geo/bounds.d.ts +18 -0
- package/dist/geo/crs-resolver.d.ts +35 -0
- package/dist/geo/projection.d.ts +28 -0
- package/dist/geo/transform.d.ts +19 -0
- package/dist/geo/viewport.d.ts +52 -0
- package/dist/index.d.ts +86 -0
- package/dist/map/attribute-ops.d.ts +61 -0
- package/dist/map/command-args.d.ts +28 -0
- package/dist/map/edit-sessions.d.ts +97 -0
- package/dist/map/edit-state-store.d.ts +41 -0
- package/dist/map/emap-host.d.ts +79 -0
- package/dist/map/feature-accessor.d.ts +43 -0
- package/dist/map/feature-query.d.ts +58 -0
- package/dist/map/highlight-manager.d.ts +17 -0
- package/dist/map/layer-registry.d.ts +33 -0
- package/dist/map/layer.d.ts +29 -0
- package/dist/map/map.d.ts +386 -0
- package/dist/map/mapshaper-ops.d.ts +56 -0
- package/dist/map/op-result.d.ts +46 -0
- package/dist/map/ops/_context.d.ts +41 -0
- package/dist/map/ops/_runner.d.ts +55 -0
- package/dist/map/ops/affine.d.ts +4 -0
- package/dist/map/ops/buffer.d.ts +4 -0
- package/dist/map/ops/check-geometry.d.ts +4 -0
- package/dist/map/ops/clean.d.ts +4 -0
- package/dist/map/ops/clip-erase.d.ts +5 -0
- package/dist/map/ops/data-fill.d.ts +4 -0
- package/dist/map/ops/dissolve.d.ts +20 -0
- package/dist/map/ops/divide.d.ts +4 -0
- package/dist/map/ops/drop-layer.d.ts +4 -0
- package/dist/map/ops/each-filter.d.ts +5 -0
- package/dist/map/ops/explode.d.ts +4 -0
- package/dist/map/ops/filter-fields.d.ts +4 -0
- package/dist/map/ops/filter-geom.d.ts +4 -0
- package/dist/map/ops/filter-islands.d.ts +4 -0
- package/dist/map/ops/filter-slivers.d.ts +4 -0
- package/dist/map/ops/innerlines.d.ts +4 -0
- package/dist/map/ops/intersection-points.d.ts +4 -0
- package/dist/map/ops/join-table.d.ts +4 -0
- package/dist/map/ops/lines.d.ts +4 -0
- package/dist/map/ops/merge-layers.d.ts +4 -0
- package/dist/map/ops/mosaic.d.ts +4 -0
- package/dist/map/ops/points.d.ts +4 -0
- package/dist/map/ops/polygons.d.ts +4 -0
- package/dist/map/ops/project.d.ts +4 -0
- package/dist/map/ops/rebuild-topology.d.ts +4 -0
- package/dist/map/ops/rename-fields.d.ts +4 -0
- package/dist/map/ops/rename-layer.d.ts +4 -0
- package/dist/map/ops/simplify.d.ts +4 -0
- package/dist/map/ops/snap.d.ts +4 -0
- package/dist/map/ops/sort-features.d.ts +4 -0
- package/dist/map/ops/split-layer.d.ts +4 -0
- package/dist/map/ops/union.d.ts +4 -0
- package/dist/map/ops/unique-features.d.ts +4 -0
- package/dist/map/selection.d.ts +73 -0
- package/dist/map/types.d.ts +1072 -0
- package/dist/map/worker-routing.d.ts +40 -0
- package/dist/mapshaper-vendor.js +1 -0
- package/dist/renderer/canvas-painter.d.ts +50 -0
- package/dist/renderer/edit-overlay-renderer.d.ts +22 -0
- package/dist/renderer/painter.d.ts +52 -0
- package/dist/shim.d.ts +1 -0
- package/dist/source/display-arcs.d.ts +49 -0
- package/dist/source/layer-utils.d.ts +12 -0
- package/dist/source/mapshaper-runner.d.ts +22 -0
- package/dist/source/source.d.ts +80 -0
- package/dist/source/topology-source.d.ts +145 -0
- package/dist/types/mapshaper-types.d.ts +182 -0
- package/dist/ui/basemap-control.d.ts +35 -0
- package/dist/ui/box-select-control.d.ts +67 -0
- package/dist/ui/control.d.ts +6 -0
- package/dist/ui/draw-feature-control.d.ts +82 -0
- package/dist/ui/edit-toolbar.d.ts +27 -0
- package/dist/ui/history-control.d.ts +29 -0
- package/dist/ui/lasso-select-control.d.ts +96 -0
- package/dist/ui/navigation-control.d.ts +16 -0
- package/dist/ui/simplify-control.d.ts +40 -0
- package/dist/ui/status-control.d.ts +23 -0
- package/dist/ui/vertex-edit-control.d.ts +111 -0
- package/dist/validation/builtin/topology.d.ts +19 -0
- package/dist/validation/registry.d.ts +23 -0
- package/dist/validation/validator.d.ts +47 -0
- package/package.json +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# emap
|
|
2
|
+
|
|
3
|
+
A topology-oriented geographic map rendering and editing engine built around [Mapshaper](https://github.com/mbloch/mapshaper) data structures and HTML5 Canvas. Browser-only, framework-agnostic TypeScript.
|
|
4
|
+
|
|
5
|
+
## What you get
|
|
6
|
+
|
|
7
|
+
- **Topology rendering** — draws shared-arc datasets directly without re-tessellating per layer
|
|
8
|
+
- **Loaders** — GeoJSON, TopoJSON, ZIP Shapefile (with per-archive resource limits)
|
|
9
|
+
- **Editing** — pan / wheel-zoom / hover, plus vertex, feature, and topology-aware editing
|
|
10
|
+
- **30+ data ops** under `emap.ops.*` — clip, erase, dissolve, buffer, simplify, project, snap, clean, mosaic, union, divide, points / lines / polygons conversions, join-table, filter, sort, uniq, split, merge, rename, …
|
|
11
|
+
- **Worker pool** — expensive ops dispatch to off-thread workers automatically based on dataset size + command family
|
|
12
|
+
- **Undo / redo** with command coalescing, dataset-replace barriers, and structured `OpResult` errors
|
|
13
|
+
- **Selection model** — single / multi / box / lasso, with attribute-only ops preserving feature ids
|
|
14
|
+
- **Snapshots** — pack / unpack a full session (datasets + history) to a compact binary blob
|
|
15
|
+
- **HTML5 Canvas 2D rendering** — `CanvasPainter` with batched-stroke optimization for large topology datasets
|
|
16
|
+
- **Built-in controls** — navigation, status, basemap, vertex edit, feature draw, history, box / lasso selection
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add emap
|
|
22
|
+
# or
|
|
23
|
+
npm install emap
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Hello map
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<!DOCTYPE html>
|
|
30
|
+
<html>
|
|
31
|
+
<head>
|
|
32
|
+
<link rel="stylesheet" href="node_modules/emap/dist/emap.css" />
|
|
33
|
+
<style>#map { width: 100vw; height: 100vh; margin: 0; }</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<div id="map"></div>
|
|
37
|
+
<script src="node_modules/emap/dist/mapshaper-vendor.js"></script>
|
|
38
|
+
<script src="node_modules/emap/dist/emap.js"></script>
|
|
39
|
+
<script>
|
|
40
|
+
const { Emap, TopologySource, NavigationControl, EditToolbar } = emap;
|
|
41
|
+
|
|
42
|
+
const map = new Emap({ container: 'map' });
|
|
43
|
+
map.addControl(new NavigationControl(), 'top-left');
|
|
44
|
+
map.addControl(new EditToolbar(), 'top-left');
|
|
45
|
+
|
|
46
|
+
(async () => {
|
|
47
|
+
const source = await TopologySource.fromUrl(
|
|
48
|
+
'china',
|
|
49
|
+
'https://geojson.cn/api/china/1.6.3/china.topo.json',
|
|
50
|
+
);
|
|
51
|
+
map.addSource('china', source);
|
|
52
|
+
map.setExtent(source.getExtent());
|
|
53
|
+
})();
|
|
54
|
+
</script>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Live demos in `test/examples/*.html` cover every feature area below.
|
|
60
|
+
|
|
61
|
+
## API tour
|
|
62
|
+
|
|
63
|
+
### Core map
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const map = new Emap({ container: 'map' });
|
|
67
|
+
|
|
68
|
+
map.addSource(id, source);
|
|
69
|
+
map.addLayer({ id, source: 'china', type: 'fill', paint: { 'fill-color': '#eee' } });
|
|
70
|
+
map.setExtent(source.getExtent());
|
|
71
|
+
|
|
72
|
+
map.queryFeatures(point, { layers: ['borders'] });
|
|
73
|
+
map.on('click', (e) => { /* … */ });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Sources
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const source = await TopologySource.fromUrl('id', url); // GeoJSON / TopoJSON / ZIP
|
|
80
|
+
const source2 = await TopologySource.fromBytes('id', buf); // ArrayBuffer / Uint8Array
|
|
81
|
+
source.getExtent();
|
|
82
|
+
source.getLayers();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Editing operations (`emap.ops`)
|
|
86
|
+
|
|
87
|
+
All ops return `Promise<OpResult>` — a discriminated `{ ok: true, value? } | { ok: false, error: OpError }`. Errors are typed (`not-found`, `validation`, `expression-disabled`, `mapshaper`, `host-removed`).
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// Whole-dataset CLI ops
|
|
91
|
+
await map.ops.clipLayer({ source: 'roads', target: 'streets', mask: 'park' });
|
|
92
|
+
await map.ops.dissolveLayer({ source: 'us', target: 'counties', field: 'STATE' });
|
|
93
|
+
await map.ops.bufferLayer({ source: 'roads', target: 'streets', radius: 50 });
|
|
94
|
+
await map.ops.simplifyLayer({ source: 'world', percentage: 10 });
|
|
95
|
+
await map.ops.projectLayer({ source: 'world', crs: 'EPSG:3857' });
|
|
96
|
+
|
|
97
|
+
// Layer management
|
|
98
|
+
await map.ops.renameLayer({ source: 's', target: 'old', name: 'new' });
|
|
99
|
+
await map.ops.mergeLayers({ source: 's', targets: ['a', 'b'], name: 'combined' });
|
|
100
|
+
await map.ops.splitLayer({ source: 's', target: 'us', expression: 'STATE' });
|
|
101
|
+
await map.ops.dropLayer({ source: 's', target: 'tmp' });
|
|
102
|
+
|
|
103
|
+
// Attribute / data
|
|
104
|
+
await map.ops.applyExpression({ source: 's', target: 'l', expression: 'this.area = $.area' });
|
|
105
|
+
await map.ops.filterFeatures({ source: 's', target: 'l', expression: 'this.pop > 1e6' });
|
|
106
|
+
await map.ops.joinTable({
|
|
107
|
+
source: 's', target: 'states',
|
|
108
|
+
data: { csv: csvBytes }, keys: ['STATE_FIPS', 'fips'],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Topology repair / inspection
|
|
112
|
+
await map.ops.cleanLayer({ source: 's', target: 'l' });
|
|
113
|
+
await map.ops.snapLayer({ source: 's', target: 'l', interval: 0.001 });
|
|
114
|
+
const report = await map.ops.checkGeometry({ source: 's' });
|
|
115
|
+
// → { ok: true, value: { ok, intersections: [...], intersectionCount } }
|
|
116
|
+
|
|
117
|
+
// Selection-driven
|
|
118
|
+
await map.ops.mergeSelected();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
A full list of ops + their option types lives in `src/index.ts` (search for `*Options`).
|
|
122
|
+
|
|
123
|
+
### Transactions
|
|
124
|
+
|
|
125
|
+
Stage multiple ops as one atomic edit — commit collapses them into a single undo entry, rollback restores every touched source's dataset (and the selection) to the pre-transaction state.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const tx = map.beginTransaction();
|
|
129
|
+
const r1 = await map.ops.clipLayer({ source, target, mask });
|
|
130
|
+
if (!r1.ok) { tx.rollback(); return; }
|
|
131
|
+
const r2 = await map.ops.dissolveLayer({ source, target, field: 'STATE' });
|
|
132
|
+
if (!r2.ok) { tx.rollback(); return; }
|
|
133
|
+
await tx.commit('Process boundaries');
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Nesting is not supported — opening a transaction while one is active throws.
|
|
137
|
+
|
|
138
|
+
### Undo / redo
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
map.undo();
|
|
142
|
+
map.redo();
|
|
143
|
+
map.clearHistory();
|
|
144
|
+
|
|
145
|
+
map.on('historychange', (e) => {
|
|
146
|
+
console.log(e.canUndo, e.canRedo, e.label);
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Vertex drags / feature translates within a session merge into one undo step. Dataset-replace ops (clip, dissolve, …) keep prior commands in the stack but flag them as `stale` for the UI to render appropriately — there's no destructive `clear`.
|
|
151
|
+
|
|
152
|
+
### Validation hooks
|
|
153
|
+
|
|
154
|
+
Register validators that run after every committed edit. Failures fire `validationfailed` — the engine doesn't auto-undo, the app decides.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { topologyValidator } from 'emap';
|
|
158
|
+
|
|
159
|
+
const unregister = map.validators.register(topologyValidator({
|
|
160
|
+
sources: ['china'],
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
map.on('validationfailed', (e) => {
|
|
164
|
+
console.warn(e.results); // [{ validator, report }, ...]
|
|
165
|
+
// Optionally: map.undo();
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Custom validators implement `Validator { name, phase: 'after-commit', run(host, change) }` and return `{ ok, issues: [{ severity, message, ref? }] }`.
|
|
170
|
+
|
|
171
|
+
### Feature accessor
|
|
172
|
+
|
|
173
|
+
Read one feature (or iterate a layer) without indexing into raw `layer.shapes` / `layer.data.getRecords()`:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const f = map.features.get({ source: 'us', layer: 'states', id: 12 });
|
|
177
|
+
if (f) console.log(f.properties.NAME, f.geometry);
|
|
178
|
+
|
|
179
|
+
for (const f of map.features.iter('us', 'states')) {
|
|
180
|
+
/* … */
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
map.features.count('us', 'states'); // O(1)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`f.properties` is `Object.freeze`d so accidental mutation can't leak back into the dataset.
|
|
187
|
+
|
|
188
|
+
### Selection
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
map.selection.add({ source: 'us', layer: 'counties', id: 42 });
|
|
192
|
+
map.selection.toggle(ref);
|
|
193
|
+
map.selection.clear();
|
|
194
|
+
|
|
195
|
+
map.on('selectionchange', (e) => {
|
|
196
|
+
console.log(e.added, e.removed, e.current);
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Attribute-only ops (each / join / rename-fields / sort) preserve the selection across the operation. Shape-changing or topology-rebuilding ops clear it.
|
|
201
|
+
|
|
202
|
+
### Worker offloading
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const map = new Emap({
|
|
206
|
+
container: 'map',
|
|
207
|
+
workerUrl: '/emap-worker.js', // built alongside dist/emap.js
|
|
208
|
+
workerMode: 'auto', // 'auto' | true | false
|
|
209
|
+
workerThreshold: 200_000, // vertex count
|
|
210
|
+
workerPoolSize: 2, // multiple workers (PR-22a)
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
map.on('workerjobstart', (e) => console.log('start', e.label));
|
|
214
|
+
map.on('workerjobend', (e) => console.log('end', e.durationMs));
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The router classifies each op as cheap / expensive and overrides the threshold accordingly. Override the decision globally with `MapOptions.workerRouting?: (info) => boolean`.
|
|
218
|
+
|
|
219
|
+
### Snapshots
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const blob = await map.exportSnapshot(); // → Uint8Array
|
|
223
|
+
await map.loadSnapshot(blob); // restore datasets + history
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The export is a mapshaper-pack with magic-byte validation on load.
|
|
227
|
+
|
|
228
|
+
### Controls
|
|
229
|
+
|
|
230
|
+
| Control | Purpose |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `NavigationControl` | Zoom in/out + reset |
|
|
233
|
+
| `StatusControl` | Cursor coordinates + EPSG |
|
|
234
|
+
| `BasemapControl` | MapLibre raster basemap toggle |
|
|
235
|
+
| `EditToolbar` | Mode switcher (vertex / feature / draw) |
|
|
236
|
+
| `VertexEditControl` | Topology-aware vertex dragging |
|
|
237
|
+
| `DrawFeatureControl` | Polygon / polyline / point drawing with edge snapping |
|
|
238
|
+
| `HistoryControl` | Visual undo/redo stack with stale-entry markers |
|
|
239
|
+
| `BoxSelectControl` | Drag-to-select bounding box |
|
|
240
|
+
| `LassoSelectControl` | Free-form selection lasso |
|
|
241
|
+
|
|
242
|
+
All controls implement `onAdd(map)` / `onRemove()` and accept `'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'`.
|
|
243
|
+
|
|
244
|
+
## Architecture
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
src/
|
|
248
|
+
├── map/ # Emap orchestrator, ops facade, selection, history
|
|
249
|
+
│ └── ops/ # one file per data op (clip, dissolve, buffer, …)
|
|
250
|
+
├── adapter/ # MapshaperAdapter — sole bridge to mapshaper.internal.*
|
|
251
|
+
├── source/ # TopologySource, DisplayArcs (LOD)
|
|
252
|
+
├── core/ # EventDispatcher, MapshaperWorkerPool, drag/wheel
|
|
253
|
+
├── geo/ # Viewport, Projection, Bounds, AffineTransform, CRS resolver
|
|
254
|
+
├── renderer/ # CanvasPainter (HTML5 Canvas 2D)
|
|
255
|
+
├── edit/ # EditHistory + per-command memento classes
|
|
256
|
+
├── ui/ # Control implementations + controls.css
|
|
257
|
+
├── worker/ # off-thread mapshaper entry
|
|
258
|
+
└── types/ # central mapshaper type definitions
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Design pillars:
|
|
262
|
+
|
|
263
|
+
- **One adapter, no leaks.** `MapshaperAdapter` is the only place `mapshaper.internal.*` is touched. Tests stub it; future mapshaper upgrades reroute through it.
|
|
264
|
+
- **Structured commands, no string-build.** Ops emit `ParsedCommand[]` directly — there is no CLI string between the op and mapshaper, on either thread.
|
|
265
|
+
- **Memento commands.** Every edit captures enough state to round-trip do/undo without referencing live dataset internals.
|
|
266
|
+
- **Effect-aware dataset replace.** Each op declares whether it changes shape / topology / attributes / CRS; selection is preserved or cleared accordingly.
|
|
267
|
+
- **Affine coordinate math everywhere.** All viewport conversions pass through `AffineTransform`, never ad-hoc matrix math.
|
|
268
|
+
- **Batch rendering.** `CanvasPainter` groups 25 paths per `stroke()` — measured optimization for large topology datasets.
|
|
269
|
+
|
|
270
|
+
## Development
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
pnpm install
|
|
274
|
+
npm run dev # watch + sourcemaps
|
|
275
|
+
npm run dev-build # one-shot dev build
|
|
276
|
+
npm run build # production (minified) + .d.ts
|
|
277
|
+
npm test # vitest, ~760 unit tests
|
|
278
|
+
npm run typecheck # strict tsc
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Build outputs in `dist/`:
|
|
282
|
+
|
|
283
|
+
- `emap.js` — production IIFE bundle
|
|
284
|
+
- `emap.mjs` — ES module
|
|
285
|
+
- `emap-dev.js` — dev bundle with sourcemaps
|
|
286
|
+
- `emap-worker.js` — worker entry
|
|
287
|
+
- `mapshaper-vendor.js` — mapshaper vendor bundle (load before `emap.js` in IIFE mode)
|
|
288
|
+
- `index.d.ts` — TypeScript declarations
|
|
289
|
+
|
|
290
|
+
Manual testing: open any `test/examples/*.html` after building.
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
[MPL-2.0](LICENSE)
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Expression evaluation policy
|
|
4
|
+
|
|
5
|
+
Several `MapshaperOps` APIs accept JavaScript expression strings that
|
|
6
|
+
mapshaper executes inside its own runner:
|
|
7
|
+
|
|
8
|
+
- `applyExpression({ expression })`
|
|
9
|
+
- `filterFeatures({ expression })`
|
|
10
|
+
- `sortFeatures({ expression })`
|
|
11
|
+
- `splitLayer({ expression })`
|
|
12
|
+
- `joinTable({ where, calc })`
|
|
13
|
+
- `mosaicLayer({ calc })`
|
|
14
|
+
- ... and any other op that forwards `where=` / `calc=` to mapshaper.
|
|
15
|
+
|
|
16
|
+
These expressions execute with full access to the JavaScript runtime
|
|
17
|
+
(no sandbox). Treating an externally-supplied expression string as data
|
|
18
|
+
is a remote code execution vulnerability.
|
|
19
|
+
|
|
20
|
+
### Default: `'disabled'`
|
|
21
|
+
|
|
22
|
+
`Emap` defaults `expressionPolicy` to `'disabled'`. Calls that carry a
|
|
23
|
+
non-empty expression string return `false` and emit an `error` event
|
|
24
|
+
with the message `"<op>: expression evaluation is disabled"`.
|
|
25
|
+
|
|
26
|
+
### Opting in: `'trusted'`
|
|
27
|
+
|
|
28
|
+
Apps that load only their own data — and never accept layer config,
|
|
29
|
+
filter strings, or join expressions from end users or third-party
|
|
30
|
+
sources — can opt in:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const map = new Emap({ container: 'map', expressionPolicy: 'trusted' });
|
|
34
|
+
|
|
35
|
+
// or, to flip at runtime
|
|
36
|
+
map.setExpressionPolicy('trusted');
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Set the policy back to `'disabled'` before processing any
|
|
40
|
+
externally-sourced strings.
|
|
41
|
+
|
|
42
|
+
### Breaking change
|
|
43
|
+
|
|
44
|
+
Prior to this release the default was `'trusted'`. Apps that broke
|
|
45
|
+
after upgrading should either:
|
|
46
|
+
|
|
47
|
+
1. Pass `expressionPolicy: 'trusted'` if all expression strings are
|
|
48
|
+
first-party and audited, or
|
|
49
|
+
2. Stop forwarding user-supplied strings to expression-accepting APIs
|
|
50
|
+
and keep the secure default.
|
|
51
|
+
|
|
52
|
+
## Reporting a vulnerability
|
|
53
|
+
|
|
54
|
+
Please report security issues via GitHub's "Report a vulnerability"
|
|
55
|
+
flow on this repository. Do not open public issues for unpatched
|
|
56
|
+
security bugs.
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type { ArcCollection, MapshaperDataset, MapshaperLayer } from '../types/mapshaper-types';
|
|
2
|
+
/** Plain-object form produced by `exportDatasetsToPack` / consumed by `restoreSessionData`. */
|
|
3
|
+
export type PackedSession = Record<string, unknown> & {
|
|
4
|
+
datasets?: unknown[];
|
|
5
|
+
};
|
|
6
|
+
/** Single export-file output — `exportFileContent` / `exportPackedDatasets` shape. */
|
|
7
|
+
export interface ExportedFile {
|
|
8
|
+
filename: string;
|
|
9
|
+
content: string | Uint8Array | ArrayBuffer;
|
|
10
|
+
}
|
|
11
|
+
/** A single segment-intersection record; mapshaper carries extra fields we don't expose. */
|
|
12
|
+
export interface SegmentIntersection {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** Bounding box returned by `getDatasetBounds` / `getLayerBounds`. */
|
|
18
|
+
export interface MapshaperBBox {
|
|
19
|
+
xmin: number;
|
|
20
|
+
ymin: number;
|
|
21
|
+
xmax: number;
|
|
22
|
+
ymax: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* One entry in a parsed mapshaper command pipeline. Equivalent to what
|
|
26
|
+
* `parseCommands('-clip source=mask')` returns: a `name` (kebab-case op
|
|
27
|
+
* name like `'clip'`, `'rename-layers'`, `'filter-fields'`) plus an
|
|
28
|
+
* `options` bag whose keys match mapshaper's snake_case internal naming
|
|
29
|
+
* (e.g. `cap_style`, `gap_fill_area`, `no_replace`).
|
|
30
|
+
*
|
|
31
|
+
* Hand-built ParsedCommands skip mapshaper's CLI parser entirely, which
|
|
32
|
+
* removes the quoting / escaping concern that drove PR-4 phase 1: option
|
|
33
|
+
* values are typed JS values (`string`, `number`, `boolean`, `string[]`)
|
|
34
|
+
* rather than tokens interpolated into a shell-like grammar.
|
|
35
|
+
*
|
|
36
|
+
* Op modules under `src/map/ops/` build these directly via
|
|
37
|
+
* `OpContext.runDatasetCommand(commands, ...)`.
|
|
38
|
+
*/
|
|
39
|
+
export interface ParsedCommand {
|
|
40
|
+
name: string;
|
|
41
|
+
options: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
/** Files injected onto a `-i` command's `input` option. */
|
|
44
|
+
export type RunnerInputFiles = Record<string, Uint8Array | string>;
|
|
45
|
+
/**
|
|
46
|
+
* The mapshaper internal API surface the editor depends on. Methods are
|
|
47
|
+
* grouped by purpose to make it obvious which mapshaper subsystem each one
|
|
48
|
+
* pokes; signatures match the underlying mapshaper functions exactly so the
|
|
49
|
+
* default adapter implementation can stay a one-line delegation.
|
|
50
|
+
*/
|
|
51
|
+
export interface MapshaperAdapter {
|
|
52
|
+
/**
|
|
53
|
+
* Move every vertex in `vertexIds` to `to`, preserving topology. Mapshaper
|
|
54
|
+
* mutates the per-arc `xx`/`yy` slices in place and updates affected arc
|
|
55
|
+
* bounds. Used by VertexMoveCommand for both do() and undo().
|
|
56
|
+
*/
|
|
57
|
+
snapVertices(vertexIds: number[], to: [number, number], arcs: ArcCollection): void;
|
|
58
|
+
/**
|
|
59
|
+
* Insert `point` immediately before the vertex at index `vertexIndex`,
|
|
60
|
+
* shifting the rest of the arc forward. Returns the index of the newly
|
|
61
|
+
* inserted vertex (== `vertexIndex` of the original arg).
|
|
62
|
+
*
|
|
63
|
+
* Note: the underlying mapshaper signature is `(arcs, i, point)` — there
|
|
64
|
+
* is no separate `arcId` parameter; mapshaper resolves the arc internally
|
|
65
|
+
* from the global vertex index. The tuple-based naming in the plan was
|
|
66
|
+
* inaccurate; this signature matches actual mapshaper.
|
|
67
|
+
*/
|
|
68
|
+
insertVertex(arcs: ArcCollection, vertexIndex: number, point: [number, number]): void;
|
|
69
|
+
/** Remove the vertex at the given global vertex index. */
|
|
70
|
+
deleteVertex(arcs: ArcCollection, vertexIndex: number): void;
|
|
71
|
+
/** Append a new empty arc to the collection; subsequent `appendVertex`
|
|
72
|
+
* calls write into it. */
|
|
73
|
+
appendEmptyArc(arcs: ArcCollection): void;
|
|
74
|
+
/**
|
|
75
|
+
* Append `[x, y]` onto the arc most recently created via `appendEmptyArc`.
|
|
76
|
+
* Mapshaper resolves the target arc implicitly; pass the coord pair
|
|
77
|
+
* directly (mapshaper accepts `[x, y]` rather than separate args).
|
|
78
|
+
*/
|
|
79
|
+
appendVertex(arcs: ArcCollection, point: [number, number]): void;
|
|
80
|
+
/** Pop the most recently appended arc. Used by FeatureCreateCommand.undo. */
|
|
81
|
+
deleteLastArc(arcs: ArcCollection): void;
|
|
82
|
+
/**
|
|
83
|
+
* Find every vertex id that shares the same topological node as the
|
|
84
|
+
* vertex closest to `point` within the given `shape`. Returns an empty
|
|
85
|
+
* array when no vertex is close enough.
|
|
86
|
+
*/
|
|
87
|
+
findNearestVertices(point: [number, number], shape: number[][], arcs: ArcCollection): number[];
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a global vertex index back to its parent arc id, given the
|
|
90
|
+
* `ii` (per-arc start-index) array from `ArcCollection.getVertexData()`.
|
|
91
|
+
* Used by VertexEditControl when mapping a vertex hit to a single arc.
|
|
92
|
+
*/
|
|
93
|
+
findArcIdFromVertexId(vertexId: number, ii: Int32Array | Uint32Array): number;
|
|
94
|
+
/**
|
|
95
|
+
* `true` iff the vertex at `idx` is the start- or end-vertex of its
|
|
96
|
+
* arc — those endpoints are shared topological nodes and cannot be
|
|
97
|
+
* deleted without breaking adjacency.
|
|
98
|
+
*/
|
|
99
|
+
vertexIsArcEndpoint(idx: number, arcs: ArcCollection): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Walk every segment of every arc referenced by `shape`, invoking `cb`
|
|
102
|
+
* with the (i, j) global-vertex pair plus the shared `xx` / `yy`
|
|
103
|
+
* arrays. Used by VertexEditControl to find the closest edge under
|
|
104
|
+
* the cursor for drag-to-insert.
|
|
105
|
+
*/
|
|
106
|
+
forEachSegmentInShape(shape: number[][], arcs: ArcCollection, cb: (i: number, j: number, xx: Float64Array, yy: Float64Array) => void): void;
|
|
107
|
+
/**
|
|
108
|
+
* Project (px, py) onto the segment from (ax, ay) to (bx, by). The
|
|
109
|
+
* returned coord is clamped to the segment endpoints when the
|
|
110
|
+
* projection falls outside them. `snapArg` mirrors mapshaper's
|
|
111
|
+
* snap-tolerance argument and is forwarded as-is.
|
|
112
|
+
*/
|
|
113
|
+
findClosestPointOnSeg(px: number, py: number, ax: number, ay: number, bx: number, by: number, snapArg: number): [number, number];
|
|
114
|
+
/**
|
|
115
|
+
* Pre-pass run by every overlay command (clip / dissolve / union / …):
|
|
116
|
+
* inserts vertices at every detected segment intersection and rebuilds
|
|
117
|
+
* shared-arc topology. Mutates `dataset.arcs` in place. Surfaces as
|
|
118
|
+
* `Emap.ops.rebuildTopology()` when called explicitly.
|
|
119
|
+
*/
|
|
120
|
+
addIntersectionCuts(dataset: MapshaperDataset, opts?: {
|
|
121
|
+
snap_interval?: number;
|
|
122
|
+
no_snap?: boolean;
|
|
123
|
+
rebuild_topology?: boolean;
|
|
124
|
+
}): void;
|
|
125
|
+
/**
|
|
126
|
+
* Read-only segment-intersection scan; populates the report consumed
|
|
127
|
+
* by `Emap.ops.checkGeometry`. Does NOT mutate the arcs.
|
|
128
|
+
*/
|
|
129
|
+
findSegmentIntersections(arcs: ArcCollection, opts?: {
|
|
130
|
+
tolerance?: number;
|
|
131
|
+
}): SegmentIntersection[];
|
|
132
|
+
/**
|
|
133
|
+
* Materialise a multi-point layer at every intersection found by
|
|
134
|
+
* {@link findSegmentIntersections}. Used by
|
|
135
|
+
* `Emap.ops.intersectionPointsLayer` to expose intersections as a
|
|
136
|
+
* pickable / editable layer.
|
|
137
|
+
*/
|
|
138
|
+
getIntersectionLayer(intersections: SegmentIntersection[], target: MapshaperLayer, arcs: ArcCollection): MapshaperLayer;
|
|
139
|
+
/**
|
|
140
|
+
* Average segment length across the arc collection. Used by
|
|
141
|
+
* `DisplayArcs` to pick a sensible per-frame simplification floor.
|
|
142
|
+
*/
|
|
143
|
+
getAvgSegment(arcs: ArcCollection): number;
|
|
144
|
+
/**
|
|
145
|
+
* Fast (non-Visvalingam) simplification: drops vertices below a
|
|
146
|
+
* length threshold. Used to pre-build a coarse "always-on" LOD copy
|
|
147
|
+
* the renderer falls back to at zoomed-out scales.
|
|
148
|
+
*/
|
|
149
|
+
simplifyArcsFast(arcs: ArcCollection, segLength: number): ArcCollection;
|
|
150
|
+
/**
|
|
151
|
+
* Returns `true` iff the layer carries renderable shape data (a
|
|
152
|
+
* `geometry_type` plus a non-empty `shapes` array). Used by the
|
|
153
|
+
* projection module to decide whether a layer participates in the
|
|
154
|
+
* extent rebuild.
|
|
155
|
+
*/
|
|
156
|
+
layerHasGeometry(layer: MapshaperLayer): boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Bounds of one layer's shapes against an arc collection. Returns a
|
|
159
|
+
* mapshaper-internal Bounds-like object — typed loosely because the
|
|
160
|
+
* caller only reads `xmin/ymin/xmax/ymax`.
|
|
161
|
+
*/
|
|
162
|
+
getLayerBounds(layer: MapshaperLayer, arcs?: ArcCollection): MapshaperBBox;
|
|
163
|
+
/** Bounding box across every layer in the dataset. */
|
|
164
|
+
getDatasetBounds(dataset: MapshaperDataset): MapshaperBBox;
|
|
165
|
+
/**
|
|
166
|
+
* Pack one or more datasets into a structured-clone-friendly object
|
|
167
|
+
* suitable for `postMessage` (worker round-trips) or
|
|
168
|
+
* {@link pack} (`.msx` snapshot bytes). `compact: true` bakes
|
|
169
|
+
* simplification thresholds into `zz` and gzip-compresses arc
|
|
170
|
+
* buffers; `false` skips that for the worker pipeline.
|
|
171
|
+
*/
|
|
172
|
+
exportDatasetsToPack(datasets: MapshaperDataset[], opts?: {
|
|
173
|
+
compact?: boolean;
|
|
174
|
+
}): Promise<PackedSession>;
|
|
175
|
+
/**
|
|
176
|
+
* Inverse of {@link exportDatasetsToPack}. Decodes a packed session
|
|
177
|
+
* (typically produced by the worker) back into live datasets.
|
|
178
|
+
*/
|
|
179
|
+
restoreSessionData(packed: PackedSession): Promise<{
|
|
180
|
+
datasets: MapshaperDataset[];
|
|
181
|
+
}>;
|
|
182
|
+
/** Materialise a `.msx` snapshot byte buffer from a packed session. */
|
|
183
|
+
pack(obj: PackedSession): Uint8Array;
|
|
184
|
+
/** Inverse of {@link pack}: `.msx` bytes → packed session object. */
|
|
185
|
+
unpackSessionData(bytes: Uint8Array): Promise<PackedSession>;
|
|
186
|
+
/** Convenience: pack + bytes in one call (`.msx` export pipeline). */
|
|
187
|
+
exportPackedDatasets(datasets: MapshaperDataset[], opts?: {
|
|
188
|
+
compact?: boolean;
|
|
189
|
+
}): Promise<ExportedFile[]>;
|
|
190
|
+
/** Plain-text export (GeoJSON / TopoJSON). */
|
|
191
|
+
exportFileContent(dataset: MapshaperDataset, opts: {
|
|
192
|
+
format: string;
|
|
193
|
+
}): ExportedFile[];
|
|
194
|
+
/**
|
|
195
|
+
* Deep copy of `dataset` — separate arc storage, separate per-layer shape
|
|
196
|
+
* arrays. mapshaper's `_runDatasetCommand` snapshots this for undo before
|
|
197
|
+
* letting the in-place clip / dissolve / buffer pipeline run.
|
|
198
|
+
*/
|
|
199
|
+
copyDataset(dataset: MapshaperDataset): MapshaperDataset;
|
|
200
|
+
/**
|
|
201
|
+
* Run a CLI-style command string starting with `-i` (import) and resolve
|
|
202
|
+
* with the first default-target dataset. `inputFiles` is injected as the
|
|
203
|
+
* import command's `input` option, matching the GUI import path.
|
|
204
|
+
*/
|
|
205
|
+
runImport(cmd: string, inputFiles: RunnerInputFiles): Promise<MapshaperDataset>;
|
|
206
|
+
/**
|
|
207
|
+
* Run a CLI-style command (sans `-i`) against an existing dataset and
|
|
208
|
+
* resolve with the resulting dataset. `inputFiles` is forwarded to every
|
|
209
|
+
* parsed command's `options.input` so `-join FILE` / similar can resolve
|
|
210
|
+
* relative names. Rejects if mapshaper produced no default target
|
|
211
|
+
* (no-op command — see PR-21 / L1).
|
|
212
|
+
*/
|
|
213
|
+
runOnDataset(cmd: string, dataset: MapshaperDataset, inputFiles?: RunnerInputFiles): Promise<MapshaperDataset>;
|
|
214
|
+
/**
|
|
215
|
+
* Variant of {@link runOnDataset} that accepts a pre-parsed
|
|
216
|
+
* `ParsedCommand[]` and skips the CLI parser entirely. Op modules
|
|
217
|
+
* use this so option values flow as typed JS (numbers / arrays /
|
|
218
|
+
* booleans) rather than as tokens interpolated into a shell-like
|
|
219
|
+
* grammar — eliminates the entire quoting / escaping concern.
|
|
220
|
+
*
|
|
221
|
+
* `inputFiles` is wired onto every command's `options.input` exactly
|
|
222
|
+
* like the string variant, so `-join FILE` works the same way.
|
|
223
|
+
*/
|
|
224
|
+
runOnDatasetParsed(commands: ParsedCommand[], dataset: MapshaperDataset, inputFiles?: RunnerInputFiles): Promise<MapshaperDataset>;
|
|
225
|
+
}
|
|
226
|
+
export declare class DefaultMapshaperAdapter implements MapshaperAdapter {
|
|
227
|
+
snapVertices(vertexIds: number[], to: [number, number], arcs: ArcCollection): void;
|
|
228
|
+
insertVertex(arcs: ArcCollection, vertexIndex: number, point: [number, number]): void;
|
|
229
|
+
deleteVertex(arcs: ArcCollection, vertexIndex: number): void;
|
|
230
|
+
appendEmptyArc(arcs: ArcCollection): void;
|
|
231
|
+
appendVertex(arcs: ArcCollection, point: [number, number]): void;
|
|
232
|
+
deleteLastArc(arcs: ArcCollection): void;
|
|
233
|
+
findNearestVertices(point: [number, number], shape: number[][], arcs: ArcCollection): number[];
|
|
234
|
+
findArcIdFromVertexId(vertexId: number, ii: Int32Array | Uint32Array): number;
|
|
235
|
+
vertexIsArcEndpoint(idx: number, arcs: ArcCollection): boolean;
|
|
236
|
+
forEachSegmentInShape(shape: number[][], arcs: ArcCollection, cb: (i: number, j: number, xx: Float64Array, yy: Float64Array) => void): void;
|
|
237
|
+
findClosestPointOnSeg(px: number, py: number, ax: number, ay: number, bx: number, by: number, snapArg: number): [number, number];
|
|
238
|
+
addIntersectionCuts(dataset: MapshaperDataset, opts?: {
|
|
239
|
+
snap_interval?: number;
|
|
240
|
+
no_snap?: boolean;
|
|
241
|
+
rebuild_topology?: boolean;
|
|
242
|
+
}): void;
|
|
243
|
+
findSegmentIntersections(arcs: ArcCollection, opts?: {
|
|
244
|
+
tolerance?: number;
|
|
245
|
+
}): SegmentIntersection[];
|
|
246
|
+
getIntersectionLayer(intersections: SegmentIntersection[], target: MapshaperLayer, arcs: ArcCollection): MapshaperLayer;
|
|
247
|
+
getAvgSegment(arcs: ArcCollection): number;
|
|
248
|
+
simplifyArcsFast(arcs: ArcCollection, segLength: number): ArcCollection;
|
|
249
|
+
layerHasGeometry(layer: MapshaperLayer): boolean;
|
|
250
|
+
getLayerBounds(layer: MapshaperLayer, arcs?: ArcCollection): MapshaperBBox;
|
|
251
|
+
getDatasetBounds(dataset: MapshaperDataset): MapshaperBBox;
|
|
252
|
+
exportDatasetsToPack(datasets: MapshaperDataset[], opts?: {
|
|
253
|
+
compact?: boolean;
|
|
254
|
+
}): Promise<PackedSession>;
|
|
255
|
+
restoreSessionData(packed: PackedSession): Promise<{
|
|
256
|
+
datasets: MapshaperDataset[];
|
|
257
|
+
}>;
|
|
258
|
+
pack(obj: PackedSession): Uint8Array;
|
|
259
|
+
unpackSessionData(bytes: Uint8Array): Promise<PackedSession>;
|
|
260
|
+
exportPackedDatasets(datasets: MapshaperDataset[], opts?: {
|
|
261
|
+
compact?: boolean;
|
|
262
|
+
}): Promise<ExportedFile[]>;
|
|
263
|
+
exportFileContent(dataset: MapshaperDataset, opts: {
|
|
264
|
+
format: string;
|
|
265
|
+
}): ExportedFile[];
|
|
266
|
+
copyDataset(dataset: MapshaperDataset): MapshaperDataset;
|
|
267
|
+
runImport(cmd: string, inputFiles: RunnerInputFiles): Promise<MapshaperDataset>;
|
|
268
|
+
runOnDataset(cmd: string, dataset: MapshaperDataset, inputFiles?: RunnerInputFiles): Promise<MapshaperDataset>;
|
|
269
|
+
runOnDatasetParsed(commands: ParsedCommand[], dataset: MapshaperDataset, inputFiles?: RunnerInputFiles): Promise<MapshaperDataset>;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Process-wide singleton adapter. Constructed lazily on first access so
|
|
273
|
+
* importing this module doesn't pay the mapshaper bundle cost up-front for
|
|
274
|
+
* call sites that inject their own adapter explicitly.
|
|
275
|
+
*/
|
|
276
|
+
export declare function getDefaultMapshaperAdapter(): MapshaperAdapter;
|
|
277
|
+
/**
|
|
278
|
+
* Swap the process-wide adapter — tests use this to inject a mock that
|
|
279
|
+
* records calls without invoking the real mapshaper bundle. Pass `null` to
|
|
280
|
+
* reset to the lazy-default behaviour.
|
|
281
|
+
*/
|
|
282
|
+
export declare function setDefaultMapshaperAdapter(adapter: MapshaperAdapter | null): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { EventDispatcher } from './events';
|
|
2
|
+
/**
|
|
3
|
+
* Handles mouse drag-to-pan interaction.
|
|
4
|
+
* Matches the MouseWheel pattern — extends EventDispatcher and emits events.
|
|
5
|
+
*
|
|
6
|
+
* Events:
|
|
7
|
+
* 'pan' — { dx: number, dy: number } — pixel delta to translate
|
|
8
|
+
* 'panstart' — drag started
|
|
9
|
+
* 'panend' — drag ended
|
|
10
|
+
*/
|
|
11
|
+
export declare class DragPanHandler extends EventDispatcher {
|
|
12
|
+
private _element;
|
|
13
|
+
private _isDragging;
|
|
14
|
+
private _lastMousePos;
|
|
15
|
+
private _enabled;
|
|
16
|
+
private _onMouseDownBound;
|
|
17
|
+
private _onMouseMoveBound;
|
|
18
|
+
private _onMouseUpBound;
|
|
19
|
+
constructor(element: HTMLElement);
|
|
20
|
+
/** Temporarily disable pan (e.g. during vertex editing) */
|
|
21
|
+
setEnabled(enabled: boolean): void;
|
|
22
|
+
isEnabled(): boolean;
|
|
23
|
+
isDragging(): boolean;
|
|
24
|
+
destroy(): void;
|
|
25
|
+
private _onMouseDown;
|
|
26
|
+
private _onMouseMove;
|
|
27
|
+
private _onMouseUp;
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Event listener function type with typed event data */
|
|
2
|
+
export type EventListener<T = Record<string, unknown>> = (event: {
|
|
3
|
+
type: string;
|
|
4
|
+
} & T) => void;
|
|
5
|
+
export declare class EventDispatcher {
|
|
6
|
+
private _listeners;
|
|
7
|
+
on<T = Record<string, unknown>>(type: string, listener: EventListener<T>): this;
|
|
8
|
+
/**
|
|
9
|
+
* Subscribe to `type` for exactly one delivery. The wrapper unsubscribes
|
|
10
|
+
* itself before invoking the user listener, so re-subscribing or firing
|
|
11
|
+
* synchronously inside the handler is safe.
|
|
12
|
+
*/
|
|
13
|
+
once<T = Record<string, unknown>>(type: string, listener: EventListener<T>): this;
|
|
14
|
+
off<T = Record<string, unknown>>(type: string, listener: EventListener<T>): this;
|
|
15
|
+
fire<T = Record<string, unknown>>(type: string, data?: T): this;
|
|
16
|
+
}
|