@honua/react 0.0.20-alpha.0 → 0.1.1-beta.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/contract/discovery.d.ts +231 -0
- package/contract/discovery.d.ts.map +1 -0
- package/contract/discovery.js +554 -0
- package/contract/edit-sketch.d.ts +8 -0
- package/contract/edit-sketch.d.ts.map +1 -1
- package/contract/edit-sketch.js +15 -0
- package/contract/edit-snapping.d.ts +160 -0
- package/contract/edit-snapping.d.ts.map +1 -0
- package/contract/edit-snapping.js +675 -0
- package/contract/generated/projjson-v0.7-crs-validator.d.ts +35 -0
- package/contract/generated/projjson-v0.7-crs-validator.d.ts.map +1 -0
- package/contract/generated/projjson-v0.7-crs-validator.js +3941 -0
- package/contract/index.d.ts +12 -1
- package/contract/index.d.ts.map +1 -1
- package/contract/index.js +4 -0
- package/contract/pmtiles.d.ts +193 -0
- package/contract/pmtiles.d.ts.map +1 -0
- package/contract/pmtiles.js +195 -0
- package/contract/protocol-module.d.ts +70 -0
- package/contract/protocol-module.d.ts.map +1 -0
- package/contract/protocol-module.js +1 -0
- package/contract/schema-envelope.d.ts +26 -0
- package/contract/schema-envelope.d.ts.map +1 -0
- package/contract/schema-envelope.js +25 -0
- package/contract/schema.d.ts +582 -0
- package/contract/schema.d.ts.map +1 -0
- package/contract/schema.js +2483 -0
- package/contract/source-capability-support.d.ts +6 -0
- package/contract/source-capability-support.d.ts.map +1 -0
- package/contract/source-capability-support.js +154 -0
- package/contract/source.d.ts +57 -15
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +368 -65
- package/contract/types.d.ts +171 -6
- package/contract/types.d.ts.map +1 -1
- package/contract/types.js +14 -14
- package/core/auth/client-credentials-provider.d.ts +51 -0
- package/core/auth/client-credentials-provider.d.ts.map +1 -0
- package/core/auth/client-credentials-provider.js +115 -0
- package/core/auth/credential-store.d.ts +52 -0
- package/core/auth/credential-store.d.ts.map +1 -0
- package/core/auth/credential-store.js +82 -0
- package/core/auth/index.d.ts +23 -0
- package/core/auth/index.d.ts.map +1 -0
- package/core/auth/index.js +20 -0
- package/core/auth/oauth2-provider.d.ts +114 -0
- package/core/auth/oauth2-provider.d.ts.map +1 -0
- package/core/auth/oauth2-provider.js +408 -0
- package/core/auth/pkce.d.ts +25 -0
- package/core/auth/pkce.d.ts.map +1 -0
- package/core/auth/pkce.js +46 -0
- package/core/auth/static-providers.d.ts +20 -0
- package/core/auth/static-providers.d.ts.map +1 -0
- package/core/auth/static-providers.js +26 -0
- package/core/auth/token-endpoint.d.ts +40 -0
- package/core/auth/token-endpoint.d.ts.map +1 -0
- package/core/auth/token-endpoint.js +133 -0
- package/core/auth/types.d.ts +144 -0
- package/core/auth/types.d.ts.map +1 -0
- package/core/auth/types.js +8 -0
- package/core/bounded-json.d.ts +4 -0
- package/core/bounded-json.d.ts.map +1 -0
- package/core/bounded-json.js +38 -0
- package/core/cache-state.d.ts +9 -0
- package/core/cache-state.d.ts.map +1 -1
- package/core/cache-state.js +10 -0
- package/core/capabilities-xml.d.ts +33 -0
- package/core/capabilities-xml.d.ts.map +1 -0
- package/core/capabilities-xml.js +331 -0
- package/core/client.d.ts +35 -2
- package/core/client.d.ts.map +1 -1
- package/core/client.js +167 -29
- package/core/error-classifications.d.ts +153 -0
- package/core/error-classifications.d.ts.map +1 -0
- package/core/error-classifications.js +140 -0
- package/core/error-code-registry.d.ts +158 -0
- package/core/error-code-registry.d.ts.map +1 -0
- package/core/error-code-registry.js +143 -0
- package/core/error-envelope.d.ts +68 -0
- package/core/error-envelope.d.ts.map +1 -0
- package/core/error-envelope.js +357 -0
- package/core/errors.d.ts +73 -25
- package/core/errors.d.ts.map +1 -1
- package/core/errors.js +113 -46
- package/core/geoparquet-sql.d.ts +190 -0
- package/core/geoparquet-sql.d.ts.map +1 -0
- package/core/geoparquet-sql.js +368 -0
- package/core/odata-key-path.d.ts +12 -0
- package/core/odata-key-path.d.ts.map +1 -0
- package/core/odata-key-path.js +28 -0
- package/core/odata-write-codec.d.ts +66 -0
- package/core/odata-write-codec.d.ts.map +1 -0
- package/core/odata-write-codec.js +1083 -0
- package/core/odata.d.ts +59 -7
- package/core/odata.d.ts.map +1 -1
- package/core/odata.js +431 -99
- package/core/ogc-endpoint-layout.d.ts +70 -0
- package/core/ogc-endpoint-layout.d.ts.map +1 -0
- package/core/ogc-endpoint-layout.js +173 -0
- package/core/ogc-features.d.ts.map +1 -1
- package/core/ogc-features.js +35 -17
- package/core/ogc-maps.d.ts +7 -0
- package/core/ogc-maps.d.ts.map +1 -1
- package/core/ogc-maps.js +46 -7
- package/core/ogc-processes.d.ts +2 -1
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +33 -5
- package/core/ogc-records.d.ts +9 -0
- package/core/ogc-records.d.ts.map +1 -1
- package/core/ogc-records.js +78 -29
- package/core/ogc-tiles.d.ts +10 -1
- package/core/ogc-tiles.d.ts.map +1 -1
- package/core/ogc-tiles.js +56 -17
- package/core/path-utils.d.ts +9 -0
- package/core/path-utils.d.ts.map +1 -1
- package/core/path-utils.js +19 -0
- package/core/protocol-transport.d.ts +1 -0
- package/core/protocol-transport.d.ts.map +1 -1
- package/core/request-pipeline.d.ts.map +1 -1
- package/core/request-pipeline.js +1 -1
- package/core/spatial-filter.d.ts +8 -4
- package/core/spatial-filter.d.ts.map +1 -1
- package/core/spatial-filter.js +206 -16
- package/core/stac-static.d.ts +55 -0
- package/core/stac-static.d.ts.map +1 -0
- package/core/stac-static.js +386 -0
- package/core/stac.d.ts +12 -0
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +45 -15
- package/core/surfaces.d.ts +13 -0
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +127 -16
- package/core/types.d.ts +174 -7
- package/core/types.d.ts.map +1 -1
- package/core/wfs-capabilities.d.ts +12 -0
- package/core/wfs-capabilities.d.ts.map +1 -1
- package/core/wfs-capabilities.js +22 -3
- package/core/wfs.d.ts +13 -0
- package/core/wfs.d.ts.map +1 -1
- package/core/wfs.js +35 -5
- package/core/wms-capabilities.d.ts +56 -84
- package/core/wms-capabilities.d.ts.map +1 -1
- package/core/wms-capabilities.js +212 -509
- package/core/wmts-capabilities.d.ts +62 -62
- package/core/wmts-capabilities.d.ts.map +1 -1
- package/core/wmts-capabilities.js +224 -371
- package/esri-compat/editor.d.ts +8 -0
- package/esri-compat/editor.d.ts.map +1 -1
- package/esri-compat/editor.js +13 -0
- package/esri-compat/feature-layer.d.ts +2 -0
- package/esri-compat/feature-layer.d.ts.map +1 -1
- package/esri-compat/feature-layer.js +1 -0
- package/esri-compat/identity-manager.d.ts +43 -0
- package/esri-compat/identity-manager.d.ts.map +1 -1
- package/esri-compat/identity-manager.js +131 -1
- package/esri-compat/layer-list.d.ts +11 -0
- package/esri-compat/layer-list.d.ts.map +1 -1
- package/esri-compat/layer-list.js +49 -0
- package/esri-compat/legend.d.ts +10 -0
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +27 -0
- package/esri-compat/oauth-info.d.ts +15 -0
- package/esri-compat/oauth-info.d.ts.map +1 -1
- package/esri-compat/oauth-info.js +27 -0
- package/esri-compat/portal.d.ts +197 -0
- package/esri-compat/portal.d.ts.map +1 -0
- package/esri-compat/portal.js +251 -0
- package/esri-compat/renderer-objects.d.ts +40 -0
- package/esri-compat/renderer-objects.d.ts.map +1 -0
- package/esri-compat/renderer-objects.js +62 -0
- package/esri-compat/sketch.d.ts +43 -0
- package/esri-compat/sketch.d.ts.map +1 -1
- package/esri-compat/sketch.js +46 -0
- package/esri-compat/snapping.d.ts +43 -0
- package/esri-compat/snapping.d.ts.map +1 -0
- package/esri-compat/snapping.js +45 -0
- package/esri-compat/url.d.ts.map +1 -1
- package/esri-compat/url.js +1 -1
- package/esri-compat/widget-host.d.ts +84 -0
- package/esri-compat/widget-host.d.ts.map +1 -0
- package/esri-compat/widget-host.js +159 -0
- package/filter-registry/index.d.ts +145 -0
- package/filter-registry/index.d.ts.map +1 -0
- package/filter-registry/index.js +531 -0
- package/generated-app/errors.d.ts +18 -2
- package/generated-app/errors.d.ts.map +1 -1
- package/generated-app/errors.js +44 -11
- package/map/automatic-mount-integration.d.ts +134 -0
- package/map/automatic-mount-integration.d.ts.map +1 -0
- package/map/automatic-mount-integration.js +305 -0
- package/map/automatic-source-strategy.d.ts +125 -0
- package/map/automatic-source-strategy.d.ts.map +1 -0
- package/map/automatic-source-strategy.js +494 -0
- package/map/data-to-map-bridge.d.ts +293 -0
- package/map/data-to-map-bridge.d.ts.map +1 -0
- package/map/data-to-map-bridge.js +817 -0
- package/map/geojson-projection.d.ts +33 -0
- package/map/geojson-projection.d.ts.map +1 -0
- package/map/geojson-projection.js +118 -0
- package/map/index.d.ts +14 -0
- package/map/index.d.ts.map +1 -1
- package/map/index.js +8 -0
- package/map/raster-source-spec.d.ts +30 -0
- package/map/raster-source-spec.d.ts.map +1 -0
- package/map/raster-source-spec.js +190 -0
- package/map/raster-source-strategy.d.ts +65 -0
- package/map/raster-source-strategy.d.ts.map +1 -0
- package/map/raster-source-strategy.js +232 -0
- package/map/source-to-maplibre.d.ts +91 -0
- package/map/source-to-maplibre.d.ts.map +1 -0
- package/map/source-to-maplibre.js +454 -0
- package/map/temporal-playback.d.ts +139 -0
- package/map/temporal-playback.d.ts.map +1 -0
- package/map/temporal-playback.js +247 -0
- package/package.json +19 -1
- package/query-planner/canonical.d.ts +7 -0
- package/query-planner/canonical.d.ts.map +1 -0
- package/query-planner/canonical.js +145 -0
- package/query-planner/cql2-json.d.ts +17 -0
- package/query-planner/cql2-json.d.ts.map +1 -0
- package/query-planner/cql2-json.js +646 -0
- package/query-planner/diagnostics.d.ts +35 -0
- package/query-planner/diagnostics.d.ts.map +1 -0
- package/query-planner/diagnostics.js +373 -0
- package/query-planner/duckdb.d.ts +79 -0
- package/query-planner/duckdb.d.ts.map +1 -0
- package/query-planner/duckdb.js +758 -0
- package/query-planner/execution-context.d.ts +5 -0
- package/query-planner/execution-context.d.ts.map +1 -0
- package/query-planner/execution-context.js +50 -0
- package/query-planner/executor.d.ts +5 -0
- package/query-planner/executor.d.ts.map +1 -0
- package/query-planner/executor.js +223 -0
- package/query-planner/geoservices.d.ts +91 -0
- package/query-planner/geoservices.d.ts.map +1 -0
- package/query-planner/geoservices.js +667 -0
- package/query-planner/grpc.d.ts +100 -0
- package/query-planner/grpc.d.ts.map +1 -0
- package/query-planner/grpc.js +736 -0
- package/query-planner/index.d.ts +44 -0
- package/query-planner/index.d.ts.map +1 -0
- package/query-planner/index.js +30 -0
- package/query-planner/ir.d.ts +20 -0
- package/query-planner/ir.d.ts.map +1 -0
- package/query-planner/ir.js +417 -0
- package/query-planner/local-aggregate.d.ts +5 -0
- package/query-planner/local-aggregate.d.ts.map +1 -0
- package/query-planner/local-aggregate.js +126 -0
- package/query-planner/odata.d.ts +48 -0
- package/query-planner/odata.d.ts.map +1 -0
- package/query-planner/odata.js +662 -0
- package/query-planner/ogc-compiler.d.ts +55 -0
- package/query-planner/ogc-compiler.d.ts.map +1 -0
- package/query-planner/ogc-compiler.js +480 -0
- package/query-planner/ogc-features.d.ts +53 -0
- package/query-planner/ogc-features.d.ts.map +1 -0
- package/query-planner/ogc-features.js +617 -0
- package/query-planner/planner.d.ts +17 -0
- package/query-planner/planner.d.ts.map +1 -0
- package/query-planner/planner.js +818 -0
- package/query-planner/resource.d.ts +94 -0
- package/query-planner/resource.d.ts.map +1 -0
- package/query-planner/resource.js +526 -0
- package/query-planner/semantic-canonical.d.ts +15 -0
- package/query-planner/semantic-canonical.d.ts.map +1 -0
- package/query-planner/semantic-canonical.js +62 -0
- package/query-planner/semantic-compat.d.ts +10 -0
- package/query-planner/semantic-compat.d.ts.map +1 -0
- package/query-planner/semantic-compat.js +35 -0
- package/query-planner/semantic-compiler.d.ts +64 -0
- package/query-planner/semantic-compiler.d.ts.map +1 -0
- package/query-planner/semantic-compiler.js +81 -0
- package/query-planner/semantic-literals.d.ts +30 -0
- package/query-planner/semantic-literals.d.ts.map +1 -0
- package/query-planner/semantic-literals.js +133 -0
- package/query-planner/semantic-types.d.ts +265 -0
- package/query-planner/semantic-types.d.ts.map +1 -0
- package/query-planner/semantic-types.js +1 -0
- package/query-planner/semantic.d.ts +54 -0
- package/query-planner/semantic.d.ts.map +1 -0
- package/query-planner/semantic.js +1421 -0
- package/query-planner/types.d.ts +560 -0
- package/query-planner/types.d.ts.map +1 -0
- package/query-planner/types.js +49 -0
- package/query-planner/wfs-semantic.d.ts +58 -0
- package/query-planner/wfs-semantic.d.ts.map +1 -0
- package/query-planner/wfs-semantic.js +547 -0
- package/query-planner/wfs.d.ts +6 -0
- package/query-planner/wfs.d.ts.map +1 -0
- package/query-planner/wfs.js +71 -0
- package/react/external-map.d.ts +67 -0
- package/react/external-map.d.ts.map +1 -0
- package/react/external-map.js +63 -0
- package/react/honua-map.d.ts.map +1 -1
- package/react/honua-map.js +3 -2
- package/react/honua-source-layer.d.ts +110 -0
- package/react/honua-source-layer.d.ts.map +1 -0
- package/react/honua-source-layer.js +101 -0
- package/react/hooks.d.ts +28 -0
- package/react/hooks.d.ts.map +1 -1
- package/react/hooks.js +26 -4
- package/react/index.d.ts +8 -0
- package/react/index.d.ts.map +1 -1
- package/react/index.js +5 -0
- package/react/query-cache.d.ts +26 -4
- package/react/query-cache.d.ts.map +1 -1
- package/react/query-cache.js +51 -8
- package/react/selection.d.ts +177 -0
- package/react/selection.d.ts.map +1 -0
- package/react/selection.js +0 -0
- package/react/use-mounted-source.d.ts +69 -0
- package/react/use-mounted-source.d.ts.map +1 -0
- package/react/use-mounted-source.js +346 -0
- package/runtime/edit-snapping.d.ts +107 -0
- package/runtime/edit-snapping.d.ts.map +1 -0
- package/runtime/edit-snapping.js +190 -0
- package/runtime/errors.d.ts +3 -2
- package/runtime/errors.d.ts.map +1 -1
- package/runtime/errors.js +19 -2
- package/runtime/index.d.ts +8 -2
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +4 -4
- package/runtime/load-package.d.ts.map +1 -1
- package/runtime/load-package.js +8 -0
- package/runtime/map-package-validation.d.ts +0 -13
- package/runtime/map-package-validation.d.ts.map +1 -1
- package/runtime/map-package-validation.js +0 -12
- package/runtime/map-package.d.ts +1 -1
- package/runtime/map-package.d.ts.map +1 -1
- package/runtime/maplibre-renderer.d.ts +41 -0
- package/runtime/maplibre-renderer.d.ts.map +1 -0
- package/runtime/maplibre-renderer.js +322 -0
- package/runtime/pmtiles-protocol.d.ts +77 -0
- package/runtime/pmtiles-protocol.d.ts.map +1 -0
- package/runtime/pmtiles-protocol.js +120 -0
- package/runtime/query-tiles.d.ts +2 -1
- package/runtime/query-tiles.d.ts.map +1 -1
- package/runtime/query-tiles.js +6 -2
- package/runtime/source-bridge.d.ts +4 -4
- package/runtime/source-bridge.d.ts.map +1 -1
- package/runtime/source-bridge.js +36 -56
- package/runtime/style-interactions.d.ts +3 -2
- package/runtime/style-interactions.d.ts.map +1 -1
- package/runtime/style-interactions.js +11 -3
- package/runtime/terra-draw-sketch.d.ts +275 -0
- package/runtime/terra-draw-sketch.d.ts.map +1 -0
- package/runtime/terra-draw-sketch.js +404 -0
- package/source-capability-registry.d.ts +1 -0
- package/source-capability-registry.js +1 -0
- package/source-capability-types.d.ts +186 -0
- package/source-capability-types.js +7 -0
- package/style/index.d.ts +2 -0
- package/style/index.d.ts.map +1 -1
- package/style/index.js +2 -0
- package/style/renderers.d.ts +326 -0
- package/style/renderers.d.ts.map +1 -0
- package/style/renderers.js +517 -0
- package/webmap/convert-renderer.d.ts +24 -3
- package/webmap/convert-renderer.d.ts.map +1 -1
- package/webmap/convert-renderer.js +93 -112
- package/webmap/index.d.ts +1 -1
- package/webmap/index.d.ts.map +1 -1
- package/webmap/index.js +1 -1
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer-neutral snapping for edit-sketch workflows.
|
|
3
|
+
*
|
|
4
|
+
* The engine resolves a pointer position (screen space plus a caller-provided
|
|
5
|
+
* projection function) against a {@link SnapIndex} of loaded features and
|
|
6
|
+
* returns the nearest snap candidate with deterministic ordering:
|
|
7
|
+
*
|
|
8
|
+
* 1. smallest screen distance wins;
|
|
9
|
+
* 2. at equal distance, `vertex` beats `edge` beats `feature`;
|
|
10
|
+
* 3. remaining ties break by source id, then feature id, then vertex /
|
|
11
|
+
* segment ordinal.
|
|
12
|
+
*
|
|
13
|
+
* The index is a packed uniform grid over feature vertices, segments, and
|
|
14
|
+
* polygon bodies — no per-move full scans and no new runtime dependency.
|
|
15
|
+
* Geometry is GeoJSON-shaped (`Point`, `MultiPoint`, `LineString`,
|
|
16
|
+
* `MultiLineString`, `Polygon`, `MultiPolygon`, `GeometryCollection`).
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_SNAPPING_CONFIG = Object.freeze({
|
|
21
|
+
enabled: true,
|
|
22
|
+
tolerance: 12,
|
|
23
|
+
kinds: Object.freeze(["vertex", "edge"]),
|
|
24
|
+
sources: Object.freeze({}),
|
|
25
|
+
});
|
|
26
|
+
/** Merge a partial config over {@link DEFAULT_SNAPPING_CONFIG}. */
|
|
27
|
+
export function resolveSnappingConfig(input) {
|
|
28
|
+
return {
|
|
29
|
+
enabled: input?.enabled ?? DEFAULT_SNAPPING_CONFIG.enabled,
|
|
30
|
+
tolerance: input?.tolerance ?? DEFAULT_SNAPPING_CONFIG.tolerance,
|
|
31
|
+
kinds: [...(input?.kinds ?? DEFAULT_SNAPPING_CONFIG.kinds)],
|
|
32
|
+
sources: { ...(input?.sources ?? {}) },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const UNSNAPPED = Object.freeze({ snapped: false, candidates: Object.freeze([]) });
|
|
36
|
+
const KIND_RANK = Object.freeze({ vertex: 0, edge: 1, feature: 2 });
|
|
37
|
+
export function compareSnapCandidates(a, b) {
|
|
38
|
+
if (a.distance !== b.distance)
|
|
39
|
+
return a.distance - b.distance;
|
|
40
|
+
if (KIND_RANK[a.kind] !== KIND_RANK[b.kind])
|
|
41
|
+
return KIND_RANK[a.kind] - KIND_RANK[b.kind];
|
|
42
|
+
if (a.sourceId !== b.sourceId)
|
|
43
|
+
return a.sourceId < b.sourceId ? -1 : 1;
|
|
44
|
+
const idOrder = compareFeatureIds(a.featureId, b.featureId);
|
|
45
|
+
if (idOrder !== 0)
|
|
46
|
+
return idOrder;
|
|
47
|
+
return ordinalOf(a) - ordinalOf(b);
|
|
48
|
+
}
|
|
49
|
+
function ordinalOf(candidate) {
|
|
50
|
+
return candidate.vertexIndex ?? candidate.segmentIndex ?? -1;
|
|
51
|
+
}
|
|
52
|
+
function compareFeatureIds(a, b) {
|
|
53
|
+
if (typeof a === "number" && typeof b === "number")
|
|
54
|
+
return a - b;
|
|
55
|
+
const left = String(a);
|
|
56
|
+
const right = String(b);
|
|
57
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Uniform packed-grid spatial index over snap sources.
|
|
61
|
+
*
|
|
62
|
+
* Mutations mark the grid dirty; it is rebuilt lazily on the next query.
|
|
63
|
+
* Feature geometries are treated as immutable once handed to the index.
|
|
64
|
+
*/
|
|
65
|
+
export class SnapIndex {
|
|
66
|
+
#sources = new Map();
|
|
67
|
+
#grid;
|
|
68
|
+
#dirty = true;
|
|
69
|
+
#gridBuilds = 0;
|
|
70
|
+
/** Replace every indexed feature for a source. */
|
|
71
|
+
setSourceFeatures(sourceId, features) {
|
|
72
|
+
const indexed = new Map();
|
|
73
|
+
for (const feature of features)
|
|
74
|
+
indexed.set(feature.id, indexFeature(sourceId, feature));
|
|
75
|
+
this.#sources.set(sourceId, indexed);
|
|
76
|
+
this.invalidate();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Insert or replace a single feature. Returns the previous feature input
|
|
80
|
+
* (id + geometry) when one was indexed, so optimistic edits can restore it.
|
|
81
|
+
*/
|
|
82
|
+
upsertFeature(sourceId, feature) {
|
|
83
|
+
let indexed = this.#sources.get(sourceId);
|
|
84
|
+
if (!indexed) {
|
|
85
|
+
indexed = new Map();
|
|
86
|
+
this.#sources.set(sourceId, indexed);
|
|
87
|
+
}
|
|
88
|
+
const previous = indexed.get(feature.id);
|
|
89
|
+
indexed.set(feature.id, indexFeature(sourceId, feature));
|
|
90
|
+
this.invalidate();
|
|
91
|
+
return previous ? featureInputOf(previous) : undefined;
|
|
92
|
+
}
|
|
93
|
+
/** Remove a single feature. Returns the removed feature input, if any. */
|
|
94
|
+
removeFeature(sourceId, featureId) {
|
|
95
|
+
const indexed = this.#sources.get(sourceId);
|
|
96
|
+
const previous = indexed?.get(featureId);
|
|
97
|
+
if (indexed && previous) {
|
|
98
|
+
indexed.delete(featureId);
|
|
99
|
+
this.invalidate();
|
|
100
|
+
}
|
|
101
|
+
return previous ? featureInputOf(previous) : undefined;
|
|
102
|
+
}
|
|
103
|
+
removeSource(sourceId) {
|
|
104
|
+
if (this.#sources.delete(sourceId))
|
|
105
|
+
this.invalidate();
|
|
106
|
+
}
|
|
107
|
+
clear() {
|
|
108
|
+
this.#sources.clear();
|
|
109
|
+
this.invalidate();
|
|
110
|
+
}
|
|
111
|
+
/** Force a lazy grid rebuild on the next query (e.g. after realtime updates). */
|
|
112
|
+
invalidate() {
|
|
113
|
+
this.#dirty = true;
|
|
114
|
+
}
|
|
115
|
+
sourceIds() {
|
|
116
|
+
return [...this.#sources.keys()];
|
|
117
|
+
}
|
|
118
|
+
stats() {
|
|
119
|
+
let featureCount = 0;
|
|
120
|
+
let vertexCount = 0;
|
|
121
|
+
let segmentCount = 0;
|
|
122
|
+
for (const features of this.#sources.values()) {
|
|
123
|
+
featureCount += features.size;
|
|
124
|
+
for (const feature of features.values()) {
|
|
125
|
+
vertexCount += feature.vertices.length / 2;
|
|
126
|
+
segmentCount += feature.segments.length / 4;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
sourceCount: this.#sources.size,
|
|
131
|
+
featureCount,
|
|
132
|
+
vertexCount,
|
|
133
|
+
segmentCount,
|
|
134
|
+
gridBuilds: this.#gridBuilds,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Resolve a pointer against this index. See {@link resolveSnapCandidate}
|
|
139
|
+
* for the deterministic ordering contract.
|
|
140
|
+
*/
|
|
141
|
+
resolve(input, config) {
|
|
142
|
+
const resolved = resolveSnappingConfig(config);
|
|
143
|
+
if (!resolved.enabled || resolved.tolerance < 0)
|
|
144
|
+
return UNSNAPPED;
|
|
145
|
+
const radius = searchRadius(input, resolved.tolerance);
|
|
146
|
+
const scope = this.#query(input.position[0] - radius.x, input.position[1] - radius.y, input.position[0] + radius.x, input.position[1] + radius.y);
|
|
147
|
+
return resolveScope(scope, input, resolved);
|
|
148
|
+
}
|
|
149
|
+
/** Collect grid entries intersecting a geographic search box. */
|
|
150
|
+
#query(minX, minY, maxX, maxY) {
|
|
151
|
+
const grid = this.#ensureGrid();
|
|
152
|
+
if (!grid)
|
|
153
|
+
return { vertices: [], segments: [], features: [] };
|
|
154
|
+
const colStart = clampIndex(Math.floor((minX - grid.minX) / grid.cellWidth), grid.columns);
|
|
155
|
+
const colEnd = clampIndex(Math.floor((maxX - grid.minX) / grid.cellWidth), grid.columns);
|
|
156
|
+
const rowStart = clampIndex(Math.floor((minY - grid.minY) / grid.cellHeight), grid.rows);
|
|
157
|
+
const rowEnd = clampIndex(Math.floor((maxY - grid.minY) / grid.cellHeight), grid.rows);
|
|
158
|
+
const vertices = [];
|
|
159
|
+
const seenSegments = new Set();
|
|
160
|
+
const segments = [];
|
|
161
|
+
const seenFeatures = new Set();
|
|
162
|
+
const features = [];
|
|
163
|
+
for (let row = rowStart; row <= rowEnd; row += 1) {
|
|
164
|
+
for (let col = colStart; col <= colEnd; col += 1) {
|
|
165
|
+
const cell = grid.cells.get(row * grid.columns + col);
|
|
166
|
+
if (!cell)
|
|
167
|
+
continue;
|
|
168
|
+
for (const entry of cell.vertices)
|
|
169
|
+
vertices.push(entry);
|
|
170
|
+
for (const entry of cell.segments) {
|
|
171
|
+
const key = `${entry[0].sourceId}|${String(entry[0].featureId)}|${entry[1]}`;
|
|
172
|
+
if (seenSegments.has(key))
|
|
173
|
+
continue;
|
|
174
|
+
seenSegments.add(key);
|
|
175
|
+
segments.push(entry);
|
|
176
|
+
}
|
|
177
|
+
for (const feature of cell.features) {
|
|
178
|
+
if (seenFeatures.has(feature))
|
|
179
|
+
continue;
|
|
180
|
+
seenFeatures.add(feature);
|
|
181
|
+
features.push(feature);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { vertices, segments, features };
|
|
186
|
+
}
|
|
187
|
+
#ensureGrid() {
|
|
188
|
+
if (!this.#dirty)
|
|
189
|
+
return this.#grid;
|
|
190
|
+
this.#grid = this.#buildGrid();
|
|
191
|
+
this.#dirty = false;
|
|
192
|
+
this.#gridBuilds += 1;
|
|
193
|
+
return this.#grid;
|
|
194
|
+
}
|
|
195
|
+
#buildGrid() {
|
|
196
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
197
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
198
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
199
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
200
|
+
let primitiveCount = 0;
|
|
201
|
+
const indexed = [];
|
|
202
|
+
for (const features of this.#sources.values()) {
|
|
203
|
+
for (const feature of features.values()) {
|
|
204
|
+
if (!feature.bbox)
|
|
205
|
+
continue;
|
|
206
|
+
indexed.push(feature);
|
|
207
|
+
minX = Math.min(minX, feature.bbox[0]);
|
|
208
|
+
minY = Math.min(minY, feature.bbox[1]);
|
|
209
|
+
maxX = Math.max(maxX, feature.bbox[2]);
|
|
210
|
+
maxY = Math.max(maxY, feature.bbox[3]);
|
|
211
|
+
primitiveCount += feature.vertices.length / 2 + feature.segments.length / 4;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (indexed.length === 0)
|
|
215
|
+
return undefined;
|
|
216
|
+
const axisCells = Math.min(128, Math.max(1, Math.ceil(Math.sqrt(primitiveCount))));
|
|
217
|
+
const width = maxX - minX;
|
|
218
|
+
const height = maxY - minY;
|
|
219
|
+
const grid = {
|
|
220
|
+
minX,
|
|
221
|
+
minY,
|
|
222
|
+
cellWidth: width > 0 ? width / axisCells : 1,
|
|
223
|
+
cellHeight: height > 0 ? height / axisCells : 1,
|
|
224
|
+
columns: width > 0 ? axisCells : 1,
|
|
225
|
+
rows: height > 0 ? axisCells : 1,
|
|
226
|
+
cells: new Map(),
|
|
227
|
+
};
|
|
228
|
+
const cellAt = (col, row) => {
|
|
229
|
+
const key = row * grid.columns + col;
|
|
230
|
+
let cell = grid.cells.get(key);
|
|
231
|
+
if (!cell) {
|
|
232
|
+
cell = { vertices: [], segments: [], features: [] };
|
|
233
|
+
grid.cells.set(key, cell);
|
|
234
|
+
}
|
|
235
|
+
return cell;
|
|
236
|
+
};
|
|
237
|
+
const insertBbox = (bminX, bminY, bmaxX, bmaxY, insert) => {
|
|
238
|
+
const colStart = clampIndex(Math.floor((bminX - grid.minX) / grid.cellWidth), grid.columns);
|
|
239
|
+
const colEnd = clampIndex(Math.floor((bmaxX - grid.minX) / grid.cellWidth), grid.columns);
|
|
240
|
+
const rowStart = clampIndex(Math.floor((bminY - grid.minY) / grid.cellHeight), grid.rows);
|
|
241
|
+
const rowEnd = clampIndex(Math.floor((bmaxY - grid.minY) / grid.cellHeight), grid.rows);
|
|
242
|
+
for (let row = rowStart; row <= rowEnd; row += 1) {
|
|
243
|
+
for (let col = colStart; col <= colEnd; col += 1)
|
|
244
|
+
insert(cellAt(col, row));
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
for (const feature of indexed) {
|
|
248
|
+
const vertices = feature.vertices;
|
|
249
|
+
for (let i = 0; i < vertices.length; i += 2) {
|
|
250
|
+
const ordinal = i / 2;
|
|
251
|
+
insertBbox(vertices[i], vertices[i + 1], vertices[i], vertices[i + 1], (cell) => cell.vertices.push([feature, ordinal]));
|
|
252
|
+
}
|
|
253
|
+
const segments = feature.segments;
|
|
254
|
+
for (let i = 0; i < segments.length; i += 4) {
|
|
255
|
+
const ordinal = i / 4;
|
|
256
|
+
insertBbox(Math.min(segments[i], segments[i + 2]), Math.min(segments[i + 1], segments[i + 3]), Math.max(segments[i], segments[i + 2]), Math.max(segments[i + 1], segments[i + 3]), (cell) => cell.segments.push([feature, ordinal]));
|
|
257
|
+
}
|
|
258
|
+
if (feature.bbox && (feature.rings.length > 0 || feature.segments.length > 0 || feature.vertices.length > 0)) {
|
|
259
|
+
insertBbox(feature.bbox[0], feature.bbox[1], feature.bbox[2], feature.bbox[3], (cell) => cell.features.push(feature));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return grid;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
export function createSnapIndex() {
|
|
266
|
+
return new SnapIndex();
|
|
267
|
+
}
|
|
268
|
+
// ── Resolution ────────────────────────────────────────────────
|
|
269
|
+
/**
|
|
270
|
+
* Resolve the pointer against the index and return the nearest snap candidate
|
|
271
|
+
* under the deterministic ordering documented on this module.
|
|
272
|
+
*/
|
|
273
|
+
export function resolveSnapCandidate(index, input, config) {
|
|
274
|
+
return index.resolve(input, config);
|
|
275
|
+
}
|
|
276
|
+
function resolveScope(scope, input, resolved) {
|
|
277
|
+
const kindsBySource = new Map();
|
|
278
|
+
const kindsFor = (sourceId) => {
|
|
279
|
+
if (!kindsBySource.has(sourceId))
|
|
280
|
+
kindsBySource.set(sourceId, sourceKinds(resolved, sourceId));
|
|
281
|
+
return kindsBySource.get(sourceId);
|
|
282
|
+
};
|
|
283
|
+
const tolerance = resolved.tolerance;
|
|
284
|
+
const candidates = [];
|
|
285
|
+
const bestByFeature = new Map();
|
|
286
|
+
const trackFeatureBest = (feature, candidate) => {
|
|
287
|
+
const kinds = kindsFor(feature.sourceId);
|
|
288
|
+
if (!kinds?.has("feature"))
|
|
289
|
+
return;
|
|
290
|
+
const featureCandidate = {
|
|
291
|
+
kind: "feature",
|
|
292
|
+
sourceId: candidate.sourceId,
|
|
293
|
+
featureId: candidate.featureId,
|
|
294
|
+
position: candidate.position,
|
|
295
|
+
screenPoint: candidate.screenPoint,
|
|
296
|
+
distance: candidate.distance,
|
|
297
|
+
};
|
|
298
|
+
const current = bestByFeature.get(feature);
|
|
299
|
+
if (!current || compareSnapCandidates(featureCandidate, current) < 0)
|
|
300
|
+
bestByFeature.set(feature, featureCandidate);
|
|
301
|
+
};
|
|
302
|
+
for (const [feature, ordinal] of scope.vertices) {
|
|
303
|
+
const kinds = kindsFor(feature.sourceId);
|
|
304
|
+
if (!kinds)
|
|
305
|
+
continue;
|
|
306
|
+
const x = feature.vertices[ordinal * 2];
|
|
307
|
+
const y = feature.vertices[ordinal * 2 + 1];
|
|
308
|
+
const screenPoint = input.project([x, y]);
|
|
309
|
+
const distance = screenDistance(screenPoint, input.point);
|
|
310
|
+
if (distance > tolerance)
|
|
311
|
+
continue;
|
|
312
|
+
const candidate = {
|
|
313
|
+
kind: "vertex",
|
|
314
|
+
sourceId: feature.sourceId,
|
|
315
|
+
featureId: feature.featureId,
|
|
316
|
+
position: [x, y],
|
|
317
|
+
screenPoint,
|
|
318
|
+
distance,
|
|
319
|
+
vertexIndex: ordinal,
|
|
320
|
+
};
|
|
321
|
+
if (kinds.has("vertex"))
|
|
322
|
+
candidates.push(candidate);
|
|
323
|
+
trackFeatureBest(feature, candidate);
|
|
324
|
+
}
|
|
325
|
+
for (const [feature, ordinal] of scope.segments) {
|
|
326
|
+
const kinds = kindsFor(feature.sourceId);
|
|
327
|
+
if (!kinds || (!kinds.has("edge") && !kinds.has("feature")))
|
|
328
|
+
continue;
|
|
329
|
+
const base = ordinal * 4;
|
|
330
|
+
const ax = feature.segments[base];
|
|
331
|
+
const ay = feature.segments[base + 1];
|
|
332
|
+
const bx = feature.segments[base + 2];
|
|
333
|
+
const by = feature.segments[base + 3];
|
|
334
|
+
const screenA = input.project([ax, ay]);
|
|
335
|
+
const screenB = input.project([bx, by]);
|
|
336
|
+
const t = closestSegmentParameter(screenA, screenB, input.point);
|
|
337
|
+
const screenPoint = {
|
|
338
|
+
x: screenA.x + (screenB.x - screenA.x) * t,
|
|
339
|
+
y: screenA.y + (screenB.y - screenA.y) * t,
|
|
340
|
+
};
|
|
341
|
+
const distance = screenDistance(screenPoint, input.point);
|
|
342
|
+
if (distance > tolerance)
|
|
343
|
+
continue;
|
|
344
|
+
const candidate = {
|
|
345
|
+
kind: "edge",
|
|
346
|
+
sourceId: feature.sourceId,
|
|
347
|
+
featureId: feature.featureId,
|
|
348
|
+
position: [ax + (bx - ax) * t, ay + (by - ay) * t],
|
|
349
|
+
screenPoint,
|
|
350
|
+
distance,
|
|
351
|
+
segmentIndex: ordinal,
|
|
352
|
+
};
|
|
353
|
+
if (kinds.has("edge"))
|
|
354
|
+
candidates.push(candidate);
|
|
355
|
+
trackFeatureBest(feature, candidate);
|
|
356
|
+
}
|
|
357
|
+
for (const feature of scope.features) {
|
|
358
|
+
const kinds = kindsFor(feature.sourceId);
|
|
359
|
+
if (!kinds?.has("feature") || feature.rings.length === 0)
|
|
360
|
+
continue;
|
|
361
|
+
if (!positionInRings(input.position, feature.rings))
|
|
362
|
+
continue;
|
|
363
|
+
bestByFeature.set(feature, {
|
|
364
|
+
kind: "feature",
|
|
365
|
+
sourceId: feature.sourceId,
|
|
366
|
+
featureId: feature.featureId,
|
|
367
|
+
position: [input.position[0], input.position[1]],
|
|
368
|
+
screenPoint: { x: input.point.x, y: input.point.y },
|
|
369
|
+
distance: 0,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
for (const candidate of bestByFeature.values())
|
|
373
|
+
candidates.push(candidate);
|
|
374
|
+
if (candidates.length === 0)
|
|
375
|
+
return UNSNAPPED;
|
|
376
|
+
candidates.sort(compareSnapCandidates);
|
|
377
|
+
return { snapped: true, candidate: candidates[0], candidates };
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Return `geometry` with its active sketch vertex replaced by `position`.
|
|
381
|
+
*
|
|
382
|
+
* The active vertex is the last drawn coordinate: the point itself for
|
|
383
|
+
* `Point`, the final coordinate for `MultiPoint` / `LineString` /
|
|
384
|
+
* `MultiLineString`, and the final non-closing vertex of the last ring for
|
|
385
|
+
* `Polygon` / `MultiPolygon` (ring closure is preserved). Unknown geometry
|
|
386
|
+
* shapes are returned unchanged.
|
|
387
|
+
*/
|
|
388
|
+
export function withSnappedActiveVertex(geometry, position) {
|
|
389
|
+
if (!geometry || typeof geometry.type !== "string")
|
|
390
|
+
return geometry;
|
|
391
|
+
const type = geometry.type;
|
|
392
|
+
const coordinates = geometry.coordinates;
|
|
393
|
+
const point = [position[0], position[1]];
|
|
394
|
+
if (type === "Point" && Array.isArray(coordinates)) {
|
|
395
|
+
return { ...geometry, coordinates: point };
|
|
396
|
+
}
|
|
397
|
+
if ((type === "MultiPoint" || type === "LineString") && Array.isArray(coordinates) && coordinates.length > 0) {
|
|
398
|
+
const next = [...coordinates];
|
|
399
|
+
next[next.length - 1] = point;
|
|
400
|
+
return { ...geometry, coordinates: next };
|
|
401
|
+
}
|
|
402
|
+
if (type === "MultiLineString" && Array.isArray(coordinates) && coordinates.length > 0) {
|
|
403
|
+
const lines = coordinates.map((line) => (Array.isArray(line) ? [...line] : line));
|
|
404
|
+
const last = lines[lines.length - 1];
|
|
405
|
+
if (!Array.isArray(last) || last.length === 0)
|
|
406
|
+
return geometry;
|
|
407
|
+
last[last.length - 1] = point;
|
|
408
|
+
return { ...geometry, coordinates: lines };
|
|
409
|
+
}
|
|
410
|
+
if ((type === "Polygon" || type === "MultiPolygon") && Array.isArray(coordinates) && coordinates.length > 0) {
|
|
411
|
+
const outer = coordinates.map((entry) => (Array.isArray(entry) ? [...entry] : entry));
|
|
412
|
+
const rings = type === "Polygon" ? outer : outer[outer.length - 1];
|
|
413
|
+
if (!Array.isArray(rings) || rings.length === 0)
|
|
414
|
+
return geometry;
|
|
415
|
+
if (type === "MultiPolygon") {
|
|
416
|
+
outer[outer.length - 1] = rings.map((ring) => (Array.isArray(ring) ? [...ring] : ring));
|
|
417
|
+
}
|
|
418
|
+
const targetRings = type === "Polygon" ? outer : outer[outer.length - 1];
|
|
419
|
+
const ring = targetRings[targetRings.length - 1];
|
|
420
|
+
if (!Array.isArray(ring) || ring.length === 0)
|
|
421
|
+
return geometry;
|
|
422
|
+
const nextRing = [...ring];
|
|
423
|
+
const first = nextRing[0];
|
|
424
|
+
const lastIndex = nextRing.length - 1;
|
|
425
|
+
const closed = nextRing.length > 1 &&
|
|
426
|
+
Array.isArray(first) &&
|
|
427
|
+
Array.isArray(nextRing[lastIndex]) &&
|
|
428
|
+
first[0] === nextRing[lastIndex][0] &&
|
|
429
|
+
first[1] === nextRing[lastIndex][1];
|
|
430
|
+
const activeIndex = closed ? nextRing.length - 2 : nextRing.length - 1;
|
|
431
|
+
if (activeIndex < 0)
|
|
432
|
+
return geometry;
|
|
433
|
+
nextRing[activeIndex] = point;
|
|
434
|
+
if (closed && activeIndex === 0)
|
|
435
|
+
nextRing[lastIndex] = [...point];
|
|
436
|
+
targetRings[targetRings.length - 1] = nextRing;
|
|
437
|
+
return { ...geometry, coordinates: outer };
|
|
438
|
+
}
|
|
439
|
+
return geometry;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Optimistic edit-session hooks that keep a {@link SnapIndex} in sync with
|
|
443
|
+
* an edit workflow's lifecycle: `apply` upserts (or removes, for deletes)
|
|
444
|
+
* the edited feature, `rollback` restores the pre-edit index entry, and
|
|
445
|
+
* `commit` finalizes it. Pass the result as the `optimistic` option of
|
|
446
|
+
* `createEditSession` / `createEditSketchWorkflow`.
|
|
447
|
+
*/
|
|
448
|
+
export function createSnapIndexEditSessionHooks(index, options = {}) {
|
|
449
|
+
const journal = [];
|
|
450
|
+
const inner = options.inner;
|
|
451
|
+
return {
|
|
452
|
+
async apply(snapshot) {
|
|
453
|
+
const sourceId = String(snapshot.sourceId);
|
|
454
|
+
const featureId = snapshot.feature.id;
|
|
455
|
+
let previous;
|
|
456
|
+
if (featureId !== undefined) {
|
|
457
|
+
previous =
|
|
458
|
+
snapshot.kind === "delete"
|
|
459
|
+
? index.removeFeature(sourceId, featureId)
|
|
460
|
+
: index.upsertFeature(sourceId, { id: featureId, geometry: snapshot.feature.geometry ?? null });
|
|
461
|
+
}
|
|
462
|
+
journal.push({ sourceId, featureId, previous });
|
|
463
|
+
await inner?.apply?.(snapshot);
|
|
464
|
+
},
|
|
465
|
+
async rollback(snapshot, result) {
|
|
466
|
+
const entry = journal.pop();
|
|
467
|
+
if (entry && entry.featureId !== undefined) {
|
|
468
|
+
if (entry.previous)
|
|
469
|
+
index.upsertFeature(entry.sourceId, entry.previous);
|
|
470
|
+
else
|
|
471
|
+
index.removeFeature(entry.sourceId, entry.featureId);
|
|
472
|
+
}
|
|
473
|
+
await inner?.rollback?.(snapshot, result);
|
|
474
|
+
},
|
|
475
|
+
async commit(snapshot, result) {
|
|
476
|
+
const entry = journal.pop();
|
|
477
|
+
const committedId = result.committedFeatureId;
|
|
478
|
+
if (entry && snapshot.kind !== "delete" && committedId !== undefined && committedId !== entry.featureId) {
|
|
479
|
+
// The optimistic apply could not index the feature under its
|
|
480
|
+
// committed id (server-assigned id on create, or a re-keyed id), so
|
|
481
|
+
// index the committed geometry now.
|
|
482
|
+
if (entry.featureId !== undefined)
|
|
483
|
+
index.removeFeature(entry.sourceId, entry.featureId);
|
|
484
|
+
index.upsertFeature(entry.sourceId, { id: committedId, geometry: snapshot.feature.geometry ?? null });
|
|
485
|
+
}
|
|
486
|
+
await inner?.commit?.(snapshot, result);
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
// ── Internal helpers ──────────────────────────────────────────
|
|
491
|
+
function sourceKinds(config, sourceId) {
|
|
492
|
+
const entry = config.sources[sourceId];
|
|
493
|
+
if (entry === false)
|
|
494
|
+
return undefined;
|
|
495
|
+
if (entry === undefined || entry === true)
|
|
496
|
+
return new Set(config.kinds);
|
|
497
|
+
if (entry.enabled === false)
|
|
498
|
+
return undefined;
|
|
499
|
+
return new Set(entry.kinds ?? config.kinds);
|
|
500
|
+
}
|
|
501
|
+
function searchRadius(input, tolerance) {
|
|
502
|
+
// Estimate geographic units per screen pixel around the pointer by probing
|
|
503
|
+
// the projection, then over-scan by 50% to stay safe under local distortion.
|
|
504
|
+
const [x, y] = input.position;
|
|
505
|
+
const probe = 1e-6 * Math.max(1, Math.abs(x), Math.abs(y));
|
|
506
|
+
const origin = input.project([x, y]);
|
|
507
|
+
const alongX = input.project([x + probe, y]);
|
|
508
|
+
const alongY = input.project([x, y + probe]);
|
|
509
|
+
const pixelsPerUnitX = screenDistance(alongX, origin) / probe;
|
|
510
|
+
const pixelsPerUnitY = screenDistance(alongY, origin) / probe;
|
|
511
|
+
return {
|
|
512
|
+
x: pixelsPerUnitX > 0 ? (tolerance / pixelsPerUnitX) * 1.5 : Number.POSITIVE_INFINITY,
|
|
513
|
+
y: pixelsPerUnitY > 0 ? (tolerance / pixelsPerUnitY) * 1.5 : Number.POSITIVE_INFINITY,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function screenDistance(a, b) {
|
|
517
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
518
|
+
}
|
|
519
|
+
function closestSegmentParameter(a, b, point) {
|
|
520
|
+
const dx = b.x - a.x;
|
|
521
|
+
const dy = b.y - a.y;
|
|
522
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
523
|
+
if (lengthSquared === 0)
|
|
524
|
+
return 0;
|
|
525
|
+
const t = ((point.x - a.x) * dx + (point.y - a.y) * dy) / lengthSquared;
|
|
526
|
+
return Math.min(1, Math.max(0, t));
|
|
527
|
+
}
|
|
528
|
+
function positionInRings(position, polygons) {
|
|
529
|
+
for (const rings of polygons) {
|
|
530
|
+
if (rings.length === 0)
|
|
531
|
+
continue;
|
|
532
|
+
if (!positionInRing(position, rings[0]))
|
|
533
|
+
continue;
|
|
534
|
+
let inHole = false;
|
|
535
|
+
for (let i = 1; i < rings.length; i += 1) {
|
|
536
|
+
if (positionInRing(position, rings[i])) {
|
|
537
|
+
inHole = true;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!inHole)
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
function positionInRing(position, ring) {
|
|
547
|
+
const [x, y] = position;
|
|
548
|
+
let inside = false;
|
|
549
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {
|
|
550
|
+
const [xi, yi] = ring[i];
|
|
551
|
+
const [xj, yj] = ring[j];
|
|
552
|
+
if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi)
|
|
553
|
+
inside = !inside;
|
|
554
|
+
}
|
|
555
|
+
return inside;
|
|
556
|
+
}
|
|
557
|
+
function clampIndex(value, size) {
|
|
558
|
+
if (Number.isNaN(value))
|
|
559
|
+
return 0;
|
|
560
|
+
return Math.min(size - 1, Math.max(0, value));
|
|
561
|
+
}
|
|
562
|
+
function featureInputOf(feature) {
|
|
563
|
+
return { id: feature.featureId, geometry: feature.geometry };
|
|
564
|
+
}
|
|
565
|
+
function indexFeature(sourceId, feature) {
|
|
566
|
+
const accumulator = { vertices: [], segments: [], rings: [] };
|
|
567
|
+
collectGeometry(feature.geometry ?? null, accumulator);
|
|
568
|
+
return {
|
|
569
|
+
sourceId,
|
|
570
|
+
featureId: feature.id,
|
|
571
|
+
vertices: accumulator.vertices,
|
|
572
|
+
segments: accumulator.segments,
|
|
573
|
+
rings: accumulator.rings,
|
|
574
|
+
bbox: bboxOf(accumulator.vertices),
|
|
575
|
+
geometry: feature.geometry ?? null,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function bboxOf(vertices) {
|
|
579
|
+
if (vertices.length === 0)
|
|
580
|
+
return undefined;
|
|
581
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
582
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
583
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
584
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
585
|
+
for (let i = 0; i < vertices.length; i += 2) {
|
|
586
|
+
minX = Math.min(minX, vertices[i]);
|
|
587
|
+
maxX = Math.max(maxX, vertices[i]);
|
|
588
|
+
minY = Math.min(minY, vertices[i + 1]);
|
|
589
|
+
maxY = Math.max(maxY, vertices[i + 1]);
|
|
590
|
+
}
|
|
591
|
+
return [minX, minY, maxX, maxY];
|
|
592
|
+
}
|
|
593
|
+
function collectGeometry(geometry, accumulator) {
|
|
594
|
+
if (!geometry || typeof geometry.type !== "string")
|
|
595
|
+
return;
|
|
596
|
+
const type = geometry.type;
|
|
597
|
+
const coordinates = geometry.coordinates;
|
|
598
|
+
if (type === "GeometryCollection" && Array.isArray(geometry.geometries)) {
|
|
599
|
+
for (const nested of geometry.geometries) {
|
|
600
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
601
|
+
collectGeometry(nested, accumulator);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (type === "Point") {
|
|
607
|
+
const position = toPosition(coordinates);
|
|
608
|
+
if (position)
|
|
609
|
+
accumulator.vertices.push(position[0], position[1]);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (type === "MultiPoint" && Array.isArray(coordinates)) {
|
|
613
|
+
for (const entry of coordinates) {
|
|
614
|
+
const position = toPosition(entry);
|
|
615
|
+
if (position)
|
|
616
|
+
accumulator.vertices.push(position[0], position[1]);
|
|
617
|
+
}
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (type === "LineString") {
|
|
621
|
+
collectPath(coordinates, accumulator, false);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (type === "MultiLineString" && Array.isArray(coordinates)) {
|
|
625
|
+
for (const line of coordinates)
|
|
626
|
+
collectPath(line, accumulator, false);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (type === "Polygon") {
|
|
630
|
+
collectPolygon(coordinates, accumulator);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (type === "MultiPolygon" && Array.isArray(coordinates)) {
|
|
634
|
+
for (const polygon of coordinates)
|
|
635
|
+
collectPolygon(polygon, accumulator);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function collectPolygon(coordinates, accumulator) {
|
|
639
|
+
if (!Array.isArray(coordinates))
|
|
640
|
+
return;
|
|
641
|
+
const rings = [];
|
|
642
|
+
for (const ring of coordinates) {
|
|
643
|
+
const positions = collectPath(ring, accumulator, true);
|
|
644
|
+
if (positions.length >= 3)
|
|
645
|
+
rings.push(positions);
|
|
646
|
+
}
|
|
647
|
+
if (rings.length > 0)
|
|
648
|
+
accumulator.rings.push(rings);
|
|
649
|
+
}
|
|
650
|
+
function collectPath(coordinates, accumulator, isRing) {
|
|
651
|
+
if (!Array.isArray(coordinates))
|
|
652
|
+
return [];
|
|
653
|
+
const positions = [];
|
|
654
|
+
for (const entry of coordinates) {
|
|
655
|
+
const position = toPosition(entry);
|
|
656
|
+
if (position)
|
|
657
|
+
positions.push(position);
|
|
658
|
+
}
|
|
659
|
+
const closed = isRing &&
|
|
660
|
+
positions.length > 1 &&
|
|
661
|
+
positions[0][0] === positions[positions.length - 1][0] &&
|
|
662
|
+
positions[0][1] === positions[positions.length - 1][1];
|
|
663
|
+
const vertexCount = closed ? positions.length - 1 : positions.length;
|
|
664
|
+
for (let i = 0; i < vertexCount; i += 1)
|
|
665
|
+
accumulator.vertices.push(positions[i][0], positions[i][1]);
|
|
666
|
+
for (let i = 0; i < positions.length - 1; i += 1) {
|
|
667
|
+
accumulator.segments.push(positions[i][0], positions[i][1], positions[i + 1][0], positions[i + 1][1]);
|
|
668
|
+
}
|
|
669
|
+
return positions;
|
|
670
|
+
}
|
|
671
|
+
function toPosition(value) {
|
|
672
|
+
return Array.isArray(value) && typeof value[0] === "number" && typeof value[1] === "number"
|
|
673
|
+
? [value[0], value[1]]
|
|
674
|
+
: undefined;
|
|
675
|
+
}
|