@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +373 -0
  3. package/README.md +294 -0
  4. package/SECURITY.md +56 -0
  5. package/dist/adapter/mapshaper-adapter.d.ts +282 -0
  6. package/dist/core/drag-pan-handler.d.ts +28 -0
  7. package/dist/core/events.d.ts +16 -0
  8. package/dist/core/interactions.d.ts +20 -0
  9. package/dist/core/mapshaper-worker-pool.d.ts +151 -0
  10. package/dist/core/tween.d.ts +26 -0
  11. package/dist/edit/commands/composite.d.ts +16 -0
  12. package/dist/edit/commands/dataset-replace.d.ts +43 -0
  13. package/dist/edit/commands/feature-affine.d.ts +72 -0
  14. package/dist/edit/commands/feature-create.d.ts +47 -0
  15. package/dist/edit/commands/feature-delete.d.ts +72 -0
  16. package/dist/edit/commands/feature-property-change.d.ts +34 -0
  17. package/dist/edit/commands/feature-translate.d.ts +55 -0
  18. package/dist/edit/commands/field-add.d.ts +24 -0
  19. package/dist/edit/commands/field-remove.d.ts +20 -0
  20. package/dist/edit/commands/field-rename.d.ts +19 -0
  21. package/dist/edit/commands/split-shared-arcs.d.ts +71 -0
  22. package/dist/edit/commands/vertex-delete.d.ts +26 -0
  23. package/dist/edit/commands/vertex-insert.d.ts +26 -0
  24. package/dist/edit/commands/vertex-move.d.ts +45 -0
  25. package/dist/edit/edit-command.d.ts +72 -0
  26. package/dist/edit/edit-history.d.ts +130 -0
  27. package/dist/edit/transaction.d.ts +59 -0
  28. package/dist/emap-worker.js +1 -0
  29. package/dist/emap.css +157 -0
  30. package/dist/emap.js +5 -0
  31. package/dist/emap.mjs +5 -0
  32. package/dist/geo/bounds.d.ts +18 -0
  33. package/dist/geo/crs-resolver.d.ts +35 -0
  34. package/dist/geo/projection.d.ts +28 -0
  35. package/dist/geo/transform.d.ts +19 -0
  36. package/dist/geo/viewport.d.ts +52 -0
  37. package/dist/index.d.ts +86 -0
  38. package/dist/map/attribute-ops.d.ts +61 -0
  39. package/dist/map/command-args.d.ts +28 -0
  40. package/dist/map/edit-sessions.d.ts +97 -0
  41. package/dist/map/edit-state-store.d.ts +41 -0
  42. package/dist/map/emap-host.d.ts +79 -0
  43. package/dist/map/feature-accessor.d.ts +43 -0
  44. package/dist/map/feature-query.d.ts +58 -0
  45. package/dist/map/highlight-manager.d.ts +17 -0
  46. package/dist/map/layer-registry.d.ts +33 -0
  47. package/dist/map/layer.d.ts +29 -0
  48. package/dist/map/map.d.ts +386 -0
  49. package/dist/map/mapshaper-ops.d.ts +56 -0
  50. package/dist/map/op-result.d.ts +46 -0
  51. package/dist/map/ops/_context.d.ts +41 -0
  52. package/dist/map/ops/_runner.d.ts +55 -0
  53. package/dist/map/ops/affine.d.ts +4 -0
  54. package/dist/map/ops/buffer.d.ts +4 -0
  55. package/dist/map/ops/check-geometry.d.ts +4 -0
  56. package/dist/map/ops/clean.d.ts +4 -0
  57. package/dist/map/ops/clip-erase.d.ts +5 -0
  58. package/dist/map/ops/data-fill.d.ts +4 -0
  59. package/dist/map/ops/dissolve.d.ts +20 -0
  60. package/dist/map/ops/divide.d.ts +4 -0
  61. package/dist/map/ops/drop-layer.d.ts +4 -0
  62. package/dist/map/ops/each-filter.d.ts +5 -0
  63. package/dist/map/ops/explode.d.ts +4 -0
  64. package/dist/map/ops/filter-fields.d.ts +4 -0
  65. package/dist/map/ops/filter-geom.d.ts +4 -0
  66. package/dist/map/ops/filter-islands.d.ts +4 -0
  67. package/dist/map/ops/filter-slivers.d.ts +4 -0
  68. package/dist/map/ops/innerlines.d.ts +4 -0
  69. package/dist/map/ops/intersection-points.d.ts +4 -0
  70. package/dist/map/ops/join-table.d.ts +4 -0
  71. package/dist/map/ops/lines.d.ts +4 -0
  72. package/dist/map/ops/merge-layers.d.ts +4 -0
  73. package/dist/map/ops/mosaic.d.ts +4 -0
  74. package/dist/map/ops/points.d.ts +4 -0
  75. package/dist/map/ops/polygons.d.ts +4 -0
  76. package/dist/map/ops/project.d.ts +4 -0
  77. package/dist/map/ops/rebuild-topology.d.ts +4 -0
  78. package/dist/map/ops/rename-fields.d.ts +4 -0
  79. package/dist/map/ops/rename-layer.d.ts +4 -0
  80. package/dist/map/ops/simplify.d.ts +4 -0
  81. package/dist/map/ops/snap.d.ts +4 -0
  82. package/dist/map/ops/sort-features.d.ts +4 -0
  83. package/dist/map/ops/split-layer.d.ts +4 -0
  84. package/dist/map/ops/union.d.ts +4 -0
  85. package/dist/map/ops/unique-features.d.ts +4 -0
  86. package/dist/map/selection.d.ts +73 -0
  87. package/dist/map/types.d.ts +1072 -0
  88. package/dist/map/worker-routing.d.ts +40 -0
  89. package/dist/mapshaper-vendor.js +1 -0
  90. package/dist/renderer/canvas-painter.d.ts +50 -0
  91. package/dist/renderer/edit-overlay-renderer.d.ts +22 -0
  92. package/dist/renderer/painter.d.ts +52 -0
  93. package/dist/shim.d.ts +1 -0
  94. package/dist/source/display-arcs.d.ts +49 -0
  95. package/dist/source/layer-utils.d.ts +12 -0
  96. package/dist/source/mapshaper-runner.d.ts +22 -0
  97. package/dist/source/source.d.ts +80 -0
  98. package/dist/source/topology-source.d.ts +145 -0
  99. package/dist/types/mapshaper-types.d.ts +182 -0
  100. package/dist/ui/basemap-control.d.ts +35 -0
  101. package/dist/ui/box-select-control.d.ts +67 -0
  102. package/dist/ui/control.d.ts +6 -0
  103. package/dist/ui/draw-feature-control.d.ts +82 -0
  104. package/dist/ui/edit-toolbar.d.ts +27 -0
  105. package/dist/ui/history-control.d.ts +29 -0
  106. package/dist/ui/lasso-select-control.d.ts +96 -0
  107. package/dist/ui/navigation-control.d.ts +16 -0
  108. package/dist/ui/simplify-control.d.ts +40 -0
  109. package/dist/ui/status-control.d.ts +23 -0
  110. package/dist/ui/vertex-edit-control.d.ts +111 -0
  111. package/dist/validation/builtin/topology.d.ts +19 -0
  112. package/dist/validation/registry.d.ts +23 -0
  113. package/dist/validation/validator.d.ts +47 -0
  114. 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
+ }