@faclon-labs/app-sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -1
- package/dist/IOSenseApp.js +26 -6
- package/dist/SignIn.js +2 -2
- package/dist/auth.d.ts +7 -0
- package/dist/auth.js +35 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/map/globe.d.ts +5 -0
- package/dist/map/globe.js +5 -0
- package/dist/map/index.d.ts +21 -0
- package/dist/map/index.js +21 -0
- package/dist/map/source.d.ts +48 -0
- package/dist/map/source.js +73 -0
- package/dist/map/testing.d.ts +5 -0
- package/dist/map/testing.js +5 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +1 -1
- package/package.json +55 -5
- package/vendor/SOURCE.txt +1 -0
- package/vendor/map-engine/cesium/camera.d.ts +10 -0
- package/vendor/map-engine/cesium/camera.js +25 -0
- package/vendor/map-engine/cesium/index.d.ts +2 -0
- package/vendor/map-engine/cesium/index.js +2 -0
- package/vendor/map-engine/cesium/renderer.d.ts +149 -0
- package/vendor/map-engine/cesium/renderer.js +837 -0
- package/vendor/map-engine/commands/index.d.ts +2 -0
- package/vendor/map-engine/commands/index.js +2 -0
- package/vendor/map-engine/commands/post-message.d.ts +32 -0
- package/vendor/map-engine/commands/post-message.js +77 -0
- package/vendor/map-engine/commands/webmcp.d.ts +17 -0
- package/vendor/map-engine/commands/webmcp.js +59 -0
- package/vendor/map-engine/core/commands.d.ts +82 -0
- package/vendor/map-engine/core/commands.js +46 -0
- package/vendor/map-engine/core/emitter.d.ts +9 -0
- package/vendor/map-engine/core/emitter.js +38 -0
- package/vendor/map-engine/core/equal.d.ts +2 -0
- package/vendor/map-engine/core/equal.js +28 -0
- package/vendor/map-engine/core/errors.d.ts +20 -0
- package/vendor/map-engine/core/errors.js +34 -0
- package/vendor/map-engine/core/geometry-family.d.ts +3 -0
- package/vendor/map-engine/core/geometry-family.js +13 -0
- package/vendor/map-engine/core/index.d.ts +4 -0
- package/vendor/map-engine/core/index.js +3 -0
- package/vendor/map-engine/core/types.d.ts +709 -0
- package/vendor/map-engine/core/types.js +7 -0
- package/vendor/map-engine/data/annotations.d.ts +3 -0
- package/vendor/map-engine/data/annotations.js +63 -0
- package/vendor/map-engine/data/contracts.d.ts +172 -0
- package/vendor/map-engine/data/contracts.js +12 -0
- package/vendor/map-engine/data/index.d.ts +4 -0
- package/vendor/map-engine/data/index.js +3 -0
- package/vendor/map-engine/data/schema.d.ts +7 -0
- package/vendor/map-engine/data/schema.js +40 -0
- package/vendor/map-engine/geofence/evaluator.d.ts +35 -0
- package/vendor/map-engine/geofence/evaluator.js +172 -0
- package/vendor/map-engine/geofence/history.d.ts +9 -0
- package/vendor/map-engine/geofence/history.js +19 -0
- package/vendor/map-engine/geofence/index.d.ts +2 -0
- package/vendor/map-engine/geofence/index.js +2 -0
- package/vendor/map-engine/index.d.ts +27 -0
- package/vendor/map-engine/index.js +14 -0
- package/vendor/map-engine/layers/index.d.ts +1 -0
- package/vendor/map-engine/layers/index.js +1 -0
- package/vendor/map-engine/layers/layers.d.ts +49 -0
- package/vendor/map-engine/layers/layers.js +316 -0
- package/vendor/map-engine/maplibre/deck-host.d.ts +105 -0
- package/vendor/map-engine/maplibre/deck-host.js +401 -0
- package/vendor/map-engine/maplibre/icons.d.ts +17 -0
- package/vendor/map-engine/maplibre/icons.js +65 -0
- package/vendor/map-engine/maplibre/index.d.ts +3 -0
- package/vendor/map-engine/maplibre/index.js +2 -0
- package/vendor/map-engine/maplibre/map-like.d.ts +56 -0
- package/vendor/map-engine/maplibre/map-like.js +1 -0
- package/vendor/map-engine/maplibre/renderer.d.ts +173 -0
- package/vendor/map-engine/maplibre/renderer.js +889 -0
- package/vendor/map-engine/maplibre/style.d.ts +84 -0
- package/vendor/map-engine/maplibre/style.js +493 -0
- package/vendor/map-engine/present/decorations.d.ts +25 -0
- package/vendor/map-engine/present/decorations.js +115 -0
- package/vendor/map-engine/present/index.d.ts +2 -0
- package/vendor/map-engine/present/index.js +2 -0
- package/vendor/map-engine/present/present.d.ts +47 -0
- package/vendor/map-engine/present/present.js +149 -0
- package/vendor/map-engine/query/index.d.ts +1 -0
- package/vendor/map-engine/query/index.js +1 -0
- package/vendor/map-engine/query/planner.d.ts +26 -0
- package/vendor/map-engine/query/planner.js +47 -0
- package/vendor/map-engine/react/MapView.d.ts +87 -0
- package/vendor/map-engine/react/MapView.js +270 -0
- package/vendor/map-engine/react/context.d.ts +39 -0
- package/vendor/map-engine/react/context.js +94 -0
- package/vendor/map-engine/react/index.d.ts +2 -0
- package/vendor/map-engine/react/index.js +2 -0
- package/vendor/map-engine/react/store.d.ts +19 -0
- package/vendor/map-engine/react/store.js +18 -0
- package/vendor/map-engine/realtime/apply.d.ts +74 -0
- package/vendor/map-engine/realtime/apply.js +155 -0
- package/vendor/map-engine/realtime/index.d.ts +1 -0
- package/vendor/map-engine/realtime/index.js +1 -0
- package/vendor/map-engine/render/index.d.ts +1 -0
- package/vendor/map-engine/render/index.js +1 -0
- package/vendor/map-engine/render/offline.d.ts +8 -0
- package/vendor/map-engine/render/offline.js +47 -0
- package/vendor/map-engine/render/renderer.d.ts +74 -0
- package/vendor/map-engine/render/renderer.js +1 -0
- package/vendor/map-engine/render/terrarium.d.ts +48 -0
- package/vendor/map-engine/render/terrarium.js +110 -0
- package/vendor/map-engine/runtime/drawing.d.ts +28 -0
- package/vendor/map-engine/runtime/drawing.js +58 -0
- package/vendor/map-engine/runtime/index.d.ts +5 -0
- package/vendor/map-engine/runtime/index.js +2 -0
- package/vendor/map-engine/runtime/offline.d.ts +24 -0
- package/vendor/map-engine/runtime/offline.js +37 -0
- package/vendor/map-engine/runtime/overlays.d.ts +25 -0
- package/vendor/map-engine/runtime/overlays.js +65 -0
- package/vendor/map-engine/runtime/runtime.d.ts +4 -0
- package/vendor/map-engine/runtime/runtime.js +2105 -0
- package/vendor/map-engine/runtime/types.d.ts +407 -0
- package/vendor/map-engine/runtime/types.js +1 -0
- package/vendor/map-engine/scene/index.d.ts +1 -0
- package/vendor/map-engine/scene/index.js +1 -0
- package/vendor/map-engine/scene/scene.d.ts +31 -0
- package/vendor/map-engine/scene/scene.js +216 -0
- package/vendor/map-engine/selection/index.d.ts +1 -0
- package/vendor/map-engine/selection/index.js +1 -0
- package/vendor/map-engine/selection/selection.d.ts +12 -0
- package/vendor/map-engine/selection/selection.js +28 -0
- package/vendor/map-engine/spatial/geojson.d.ts +15 -0
- package/vendor/map-engine/spatial/geojson.js +83 -0
- package/vendor/map-engine/spatial/geometry.d.ts +13 -0
- package/vendor/map-engine/spatial/geometry.js +206 -0
- package/vendor/map-engine/spatial/import.d.ts +27 -0
- package/vendor/map-engine/spatial/import.js +96 -0
- package/vendor/map-engine/spatial/index.d.ts +38 -0
- package/vendor/map-engine/spatial/index.js +37 -0
- package/vendor/map-engine/spatial/line-of-sight.d.ts +35 -0
- package/vendor/map-engine/spatial/line-of-sight.js +43 -0
- package/vendor/map-engine/spatial/measure.d.ts +28 -0
- package/vendor/map-engine/spatial/measure.js +90 -0
- package/vendor/map-engine/spatial/operations.d.ts +21 -0
- package/vendor/map-engine/spatial/operations.js +171 -0
- package/vendor/map-engine/spatial/query.d.ts +17 -0
- package/vendor/map-engine/spatial/query.js +88 -0
- package/vendor/map-engine/spatial/spatial-index.d.ts +24 -0
- package/vendor/map-engine/spatial/spatial-index.js +36 -0
- package/vendor/map-engine/spatial/volume-mesh.d.ts +19 -0
- package/vendor/map-engine/spatial/volume-mesh.js +56 -0
- package/vendor/map-engine/temporal/index.d.ts +3 -0
- package/vendor/map-engine/temporal/index.js +3 -0
- package/vendor/map-engine/temporal/journal.d.ts +20 -0
- package/vendor/map-engine/temporal/journal.js +34 -0
- package/vendor/map-engine/temporal/playback.d.ts +41 -0
- package/vendor/map-engine/temporal/playback.js +80 -0
- package/vendor/map-engine/temporal/tracks.d.ts +33 -0
- package/vendor/map-engine/temporal/tracks.js +119 -0
- package/vendor/map-engine/testing/annotation-store.d.ts +24 -0
- package/vendor/map-engine/testing/annotation-store.js +43 -0
- package/vendor/map-engine/testing/contracts.d.ts +41 -0
- package/vendor/map-engine/testing/contracts.js +328 -0
- package/vendor/map-engine/testing/fixtures-monitoring.d.ts +22 -0
- package/vendor/map-engine/testing/fixtures-monitoring.js +103 -0
- package/vendor/map-engine/testing/fixtures.d.ts +18 -0
- package/vendor/map-engine/testing/fixtures.js +28 -0
- package/vendor/map-engine/testing/index.d.ts +7 -0
- package/vendor/map-engine/testing/index.js +7 -0
- package/vendor/map-engine/testing/mock-ontology.d.ts +13 -0
- package/vendor/map-engine/testing/mock-ontology.js +24 -0
- package/vendor/map-engine/testing/mock-provider.d.ts +97 -0
- package/vendor/map-engine/testing/mock-provider.js +369 -0
- package/vendor/map-engine/testing/test-renderer.d.ts +72 -0
- package/vendor/map-engine/testing/test-renderer.js +153 -0
- package/vendor/map-engine/viewport/index.d.ts +1 -0
- package/vendor/map-engine/viewport/index.js +1 -0
- package/vendor/map-engine/viewport/viewport.d.ts +19 -0
- package/vendor/map-engine/viewport/viewport.js +102 -0
- package/vendor/map-provider-uns/client.d.ts +75 -0
- package/vendor/map-provider-uns/client.js +111 -0
- package/vendor/map-provider-uns/index.d.ts +2 -0
- package/vendor/map-provider-uns/index.js +2 -0
- package/vendor/map-provider-uns/provider.d.ts +131 -0
- package/vendor/map-provider-uns/provider.js +561 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measurements and generated shapes on the ground (doc 28 G9, doc 29 §14): length, area, bearing, destination,
|
|
3
|
+
* circles, ellipses and sectors as polygons. Spherical Earth (mean radius); good to well under 1 % at map scales.
|
|
4
|
+
*/
|
|
5
|
+
import { area as turfArea } from '@turf/area';
|
|
6
|
+
import { toGeoJSON } from './geojson.js';
|
|
7
|
+
import { distanceMeters } from './operations.js';
|
|
8
|
+
const R = 6_371_008.8;
|
|
9
|
+
const rad = (d) => (d * Math.PI) / 180;
|
|
10
|
+
const deg = (r) => (r * 180) / Math.PI;
|
|
11
|
+
/** Initial bearing from a to b, degrees clockwise from north [0, 360). */
|
|
12
|
+
export function bearingDegrees(a, b) {
|
|
13
|
+
const f1 = rad(a.latitude);
|
|
14
|
+
const f2 = rad(b.latitude);
|
|
15
|
+
const dl = rad(b.longitude - a.longitude);
|
|
16
|
+
const y = Math.sin(dl) * Math.cos(f2);
|
|
17
|
+
const x = Math.cos(f1) * Math.sin(f2) - Math.sin(f1) * Math.cos(f2) * Math.cos(dl);
|
|
18
|
+
return (deg(Math.atan2(y, x)) + 360) % 360;
|
|
19
|
+
}
|
|
20
|
+
/** The position `meters` away from `from` along `bearing` (degrees clockwise from north). */
|
|
21
|
+
export function destination(from, bearing, meters) {
|
|
22
|
+
const d = meters / R;
|
|
23
|
+
const t = rad(bearing);
|
|
24
|
+
const f1 = rad(from.latitude);
|
|
25
|
+
const l1 = rad(from.longitude);
|
|
26
|
+
const f2 = Math.asin(Math.sin(f1) * Math.cos(d) + Math.cos(f1) * Math.sin(d) * Math.cos(t));
|
|
27
|
+
const l2 = l1 + Math.atan2(Math.sin(t) * Math.sin(d) * Math.cos(f1), Math.cos(d) - Math.sin(f1) * Math.sin(f2));
|
|
28
|
+
return { latitude: deg(f2), longitude: ((deg(l2) + 540) % 360) - 180 };
|
|
29
|
+
}
|
|
30
|
+
/** Length of a line (or the sum of a multi-line), metres. */
|
|
31
|
+
export function lengthMeters(geometry) {
|
|
32
|
+
const along = (pts) => pts.slice(1).reduce((sum, p, i) => sum + distanceMeters(pts[i], p), 0);
|
|
33
|
+
switch (geometry.type) {
|
|
34
|
+
case 'LineString':
|
|
35
|
+
return along(geometry.coordinates);
|
|
36
|
+
case 'MultiLineString':
|
|
37
|
+
return geometry.coordinates.reduce((s, l) => s + along(l), 0);
|
|
38
|
+
case 'Polygon':
|
|
39
|
+
return along(geometry.coordinates[0] ?? []);
|
|
40
|
+
case 'MultiPolygon':
|
|
41
|
+
return geometry.coordinates.reduce((s, p) => s + along(p[0] ?? []), 0);
|
|
42
|
+
case 'Circle':
|
|
43
|
+
return 2 * Math.PI * geometry.radiusMeters;
|
|
44
|
+
default:
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Area of an area geometry, square metres (holes subtracted); 0 for points and lines. */
|
|
49
|
+
export function areaSquareMeters(geometry) {
|
|
50
|
+
if (geometry.type === 'Circle')
|
|
51
|
+
return Math.PI * geometry.radiusMeters ** 2;
|
|
52
|
+
if (geometry.type !== 'Polygon' && geometry.type !== 'MultiPolygon')
|
|
53
|
+
return 0;
|
|
54
|
+
return turfArea({ type: 'Feature', geometry: toGeoJSON(geometry), properties: {} });
|
|
55
|
+
}
|
|
56
|
+
export function measure(geometry) {
|
|
57
|
+
const out = { lengthMeters: lengthMeters(geometry), areaSquareMeters: areaSquareMeters(geometry) };
|
|
58
|
+
if (geometry.type === 'LineString' && geometry.coordinates.length >= 2) {
|
|
59
|
+
const pts = geometry.coordinates;
|
|
60
|
+
out.bearingDegrees = bearingDegrees(pts[0], pts[pts.length - 1]);
|
|
61
|
+
out.segments = pts.slice(1).map((p, i) => ({ lengthMeters: distanceMeters(pts[i], p), bearingDegrees: bearingDegrees(pts[i], p) }));
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
const ring = (pts) => ({ type: 'Polygon', coordinates: [[...pts, pts[0]]] });
|
|
66
|
+
/** A circle as a polygon of `steps` points. */
|
|
67
|
+
export function circlePolygon(center, radiusMeters, steps = 64) {
|
|
68
|
+
return ring(Array.from({ length: steps }, (_, i) => destination(center, (i * 360) / steps, radiusMeters)));
|
|
69
|
+
}
|
|
70
|
+
/** An ellipse as a polygon: semi-axes in metres, the major axis `orientation` degrees clockwise from north. */
|
|
71
|
+
export function ellipsePolygon(center, semiMajor, semiMinor, orientation = 0, steps = 64) {
|
|
72
|
+
const pts = [];
|
|
73
|
+
for (let i = 0; i < steps; i++) {
|
|
74
|
+
const t = (i * 2 * Math.PI) / steps;
|
|
75
|
+
const along = semiMajor * Math.cos(t);
|
|
76
|
+
const across = semiMinor * Math.sin(t);
|
|
77
|
+
const r = Math.hypot(along, across);
|
|
78
|
+
pts.push(destination(center, orientation + deg(Math.atan2(across, along)), r));
|
|
79
|
+
}
|
|
80
|
+
return ring(pts);
|
|
81
|
+
}
|
|
82
|
+
/** A sector (pie slice) from `near` to `far` metres between two bearings, as a polygon. */
|
|
83
|
+
export function sectorPolygon(center, fromBearing, toBearing, near, far, steps = 24) {
|
|
84
|
+
const span = (((toBearing - fromBearing) % 360) + 360) % 360 || 360;
|
|
85
|
+
const arc = (r, reverse) => {
|
|
86
|
+
const out = Array.from({ length: steps + 1 }, (_, i) => destination(center, fromBearing + (span * i) / steps, r));
|
|
87
|
+
return reverse ? out.reverse() : out;
|
|
88
|
+
};
|
|
89
|
+
return ring(near > 0 ? [...arc(far, false), ...arc(near, true)] : [center, ...arc(far, false)]);
|
|
90
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BoundingBox, Geometry, Position, SpatialRelation } from '../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* True when every part of `a` lies inside `b` (OGC "within"). Each part of `a` must lie within one part of `b`, which
|
|
4
|
+
* is exact for valid multi-geometries (their parts do not overlap). Points in a circle use the true distance.
|
|
5
|
+
*/
|
|
6
|
+
export declare function within(a: Geometry, b: Geometry): boolean;
|
|
7
|
+
/** True when `a` contains `b`. Defined as `within(b, a)` so both relations share one implementation. */
|
|
8
|
+
export declare function contains(a: Geometry, b: Geometry): boolean;
|
|
9
|
+
export declare function intersects(a: Geometry, b: Geometry): boolean;
|
|
10
|
+
/** Great-circle distance in metres. */
|
|
11
|
+
export declare function distanceMeters(a: Position, b: Position): number;
|
|
12
|
+
/** Shortest distance in metres from a position to a geometry. Zero when inside an area or on a line. */
|
|
13
|
+
export declare function distanceToGeometryMeters(position: Position, geometry: Geometry): number;
|
|
14
|
+
export declare function intersectsBounds(geometry: Geometry, bounds: BoundingBox): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Where one label for a geometry goes (one per object, never one per map tile): a point, the middle point of a
|
|
17
|
+
* line, a circle's centre, or a point guaranteed inside an area — the largest part of a multipolygon.
|
|
18
|
+
*/
|
|
19
|
+
export declare function labelPoint(geometry: Geometry, placement?: 'center' | 'top'): Position;
|
|
20
|
+
/** Does `geometry` satisfy the relation? Candidate filtering by bounds happens first, exact geometry second. */
|
|
21
|
+
export declare function matchesRelation(geometry: Geometry, relation: SpatialRelation): boolean;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spatial relations. Turf is an implementation detail of this file (ADR: no GIS maths from scratch,
|
|
3
|
+
* and the rest of the engine never imports Turf).
|
|
4
|
+
*/
|
|
5
|
+
import { area as turfArea } from '@turf/area';
|
|
6
|
+
import { booleanIntersects } from '@turf/boolean-intersects';
|
|
7
|
+
import { booleanWithin } from '@turf/boolean-within';
|
|
8
|
+
import { distance as turfDistance } from '@turf/distance';
|
|
9
|
+
import { pointToLineDistance } from '@turf/point-to-line-distance';
|
|
10
|
+
import { lineIntersect } from '@turf/line-intersect';
|
|
11
|
+
import { pointOnFeature } from '@turf/point-on-feature';
|
|
12
|
+
import { pointToPolygonDistance } from '@turf/point-to-polygon-distance';
|
|
13
|
+
import { boundsIntersect, boundsToPolygon, geometryBounds } from './geometry.js';
|
|
14
|
+
import { fromGeoJSON, toFeature, toGeoJSON } from './geojson.js';
|
|
15
|
+
const DIMENSION = { Point: 0, LineString: 1, Polygon: 2 };
|
|
16
|
+
const point = (p) => toFeature({ type: 'Point', coordinates: p });
|
|
17
|
+
/** A geometry as simple parts. A circle becomes its polygon here; tests that can use the true radius do. */
|
|
18
|
+
function parts(g) {
|
|
19
|
+
switch (g.type) {
|
|
20
|
+
case 'MultiPoint':
|
|
21
|
+
return g.coordinates.map((coordinates) => ({ type: 'Point', coordinates }));
|
|
22
|
+
case 'MultiLineString':
|
|
23
|
+
return g.coordinates.map((coordinates) => ({ type: 'LineString', coordinates }));
|
|
24
|
+
case 'MultiPolygon':
|
|
25
|
+
return g.coordinates.map((coordinates) => ({ type: 'Polygon', coordinates }));
|
|
26
|
+
case 'Circle': {
|
|
27
|
+
const poly = toGeoJSON(g);
|
|
28
|
+
return [{ type: 'Polygon', coordinates: poly.coordinates.map((ring) => ring.map(([longitude, latitude]) => ({ latitude: latitude, longitude: longitude }))) }];
|
|
29
|
+
}
|
|
30
|
+
default:
|
|
31
|
+
return [g];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function simpleWithin(a, b) {
|
|
35
|
+
// A geometry cannot lie within a geometry of lower dimension (Turf throws for these pairs).
|
|
36
|
+
if (DIMENSION[a.type] > DIMENSION[b.type])
|
|
37
|
+
return false;
|
|
38
|
+
if (!boundsIntersect(geometryBounds(a), geometryBounds(b)))
|
|
39
|
+
return false;
|
|
40
|
+
if (a.type === 'Point' && b.type === 'Point') {
|
|
41
|
+
return a.coordinates.latitude === b.coordinates.latitude && a.coordinates.longitude === b.coordinates.longitude;
|
|
42
|
+
}
|
|
43
|
+
return booleanWithin(toFeature(a), toFeature(b));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* True when every part of `a` lies inside `b` (OGC "within"). Each part of `a` must lie within one part of `b`, which
|
|
47
|
+
* is exact for valid multi-geometries (their parts do not overlap). Points in a circle use the true distance.
|
|
48
|
+
*/
|
|
49
|
+
export function within(a, b) {
|
|
50
|
+
if (b.type === 'Circle' && (a.type === 'Point' || a.type === 'MultiPoint')) {
|
|
51
|
+
const pts = a.type === 'Point' ? [a.coordinates] : a.coordinates;
|
|
52
|
+
return pts.every((p) => distanceMeters(p, b.center) <= b.radiusMeters);
|
|
53
|
+
}
|
|
54
|
+
if (!boundsIntersect(geometryBounds(a), geometryBounds(b)))
|
|
55
|
+
return false;
|
|
56
|
+
const bp = parts(b);
|
|
57
|
+
return parts(a).every((pa) => bp.some((pb) => simpleWithin(pa, pb)));
|
|
58
|
+
}
|
|
59
|
+
/** True when `a` contains `b`. Defined as `within(b, a)` so both relations share one implementation. */
|
|
60
|
+
export function contains(a, b) {
|
|
61
|
+
return within(b, a);
|
|
62
|
+
}
|
|
63
|
+
export function intersects(a, b) {
|
|
64
|
+
if (!boundsIntersect(geometryBounds(a), geometryBounds(b)))
|
|
65
|
+
return false;
|
|
66
|
+
if (b.type === 'Circle' && a.type === 'Point')
|
|
67
|
+
return distanceMeters(a.coordinates, b.center) <= b.radiusMeters;
|
|
68
|
+
if (a.type === 'Circle' && b.type === 'Point')
|
|
69
|
+
return distanceMeters(b.coordinates, a.center) <= a.radiusMeters;
|
|
70
|
+
const bp = parts(b);
|
|
71
|
+
return parts(a).some((pa) => bp.some((pb) => booleanIntersects(toFeature(pa), toFeature(pb))));
|
|
72
|
+
}
|
|
73
|
+
/** Great-circle distance in metres. */
|
|
74
|
+
export function distanceMeters(a, b) {
|
|
75
|
+
return turfDistance(point(a), point(b), { units: 'meters' });
|
|
76
|
+
}
|
|
77
|
+
function simpleDistance(position, geometry) {
|
|
78
|
+
switch (geometry.type) {
|
|
79
|
+
case 'Point':
|
|
80
|
+
return distanceMeters(position, geometry.coordinates);
|
|
81
|
+
case 'LineString':
|
|
82
|
+
return pointToLineDistance(point(position), toGeoJSON(geometry), { units: 'meters' });
|
|
83
|
+
case 'Polygon':
|
|
84
|
+
return Math.max(0, pointToPolygonDistance(point(position), toGeoJSON(geometry), { units: 'meters' }));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Shortest distance in metres from a position to a geometry. Zero when inside an area or on a line. */
|
|
88
|
+
export function distanceToGeometryMeters(position, geometry) {
|
|
89
|
+
if (geometry.type === 'Circle')
|
|
90
|
+
return Math.max(0, distanceMeters(position, geometry.center) - geometry.radiusMeters);
|
|
91
|
+
return Math.min(...parts(geometry).map((p) => simpleDistance(position, p)));
|
|
92
|
+
}
|
|
93
|
+
export function intersectsBounds(geometry, bounds) {
|
|
94
|
+
if (!boundsIntersect(geometryBounds(geometry), bounds))
|
|
95
|
+
return false;
|
|
96
|
+
// Points whose box meets the bounds are inside them; no exact test needed.
|
|
97
|
+
if (geometry.type === 'Point')
|
|
98
|
+
return true;
|
|
99
|
+
return intersects(geometry, boundsToPolygon(bounds));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Where one label for a geometry goes (one per object, never one per map tile): a point, the middle point of a
|
|
103
|
+
* line, a circle's centre, or a point guaranteed inside an area — the largest part of a multipolygon.
|
|
104
|
+
*/
|
|
105
|
+
export function labelPoint(geometry, placement = 'center') {
|
|
106
|
+
if (placement === 'top' && (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon' || geometry.type === 'Circle')) {
|
|
107
|
+
const top = topLabelPoint(geometry);
|
|
108
|
+
if (top)
|
|
109
|
+
return top;
|
|
110
|
+
}
|
|
111
|
+
switch (geometry.type) {
|
|
112
|
+
case 'Point':
|
|
113
|
+
return geometry.coordinates;
|
|
114
|
+
case 'Circle':
|
|
115
|
+
return geometry.center;
|
|
116
|
+
case 'MultiPoint':
|
|
117
|
+
return geometry.coordinates[0];
|
|
118
|
+
case 'LineString':
|
|
119
|
+
return geometry.coordinates[Math.floor(geometry.coordinates.length / 2)];
|
|
120
|
+
case 'MultiLineString': {
|
|
121
|
+
const longest = geometry.coordinates.reduce((a, b) => (b.length > a.length ? b : a));
|
|
122
|
+
return longest[Math.floor(longest.length / 2)];
|
|
123
|
+
}
|
|
124
|
+
case 'Polygon':
|
|
125
|
+
case 'MultiPolygon': {
|
|
126
|
+
const parts = geometry.type === 'Polygon' ? [geometry.coordinates] : geometry.coordinates;
|
|
127
|
+
const polys = parts.map((coordinates) => toFeature({ type: 'Polygon', coordinates }));
|
|
128
|
+
const largest = polys.reduce((a, b) => (turfArea(b) > turfArea(a) ? b : a));
|
|
129
|
+
return fromGeoJSON(pointOnFeature(largest).geometry).coordinates;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A point just inside the top of an area: on a line a little below the northmost point, the middle of the widest
|
|
135
|
+
* stretch that lies inside the area. Null when no such stretch exists (degenerate shapes).
|
|
136
|
+
*/
|
|
137
|
+
function topLabelPoint(geometry) {
|
|
138
|
+
const b = geometryBounds(geometry);
|
|
139
|
+
const lat = b.north - (b.north - b.south) * 0.06;
|
|
140
|
+
const across = toFeature({ type: 'LineString', coordinates: [{ latitude: lat, longitude: b.west - 1 }, { latitude: lat, longitude: b.east + 1 }] });
|
|
141
|
+
const area = toFeature(geometry);
|
|
142
|
+
const crossings = lineIntersect(across, area)
|
|
143
|
+
.features.map((f) => fromGeoJSON(f.geometry).coordinates.longitude)
|
|
144
|
+
.sort((x, y) => x - y);
|
|
145
|
+
let best = null;
|
|
146
|
+
for (let i = 0; i + 1 < crossings.length; i += 2) {
|
|
147
|
+
const w = crossings[i];
|
|
148
|
+
const e = crossings[i + 1];
|
|
149
|
+
if (!best || e - w > best[1] - best[0])
|
|
150
|
+
best = [w, e];
|
|
151
|
+
}
|
|
152
|
+
return best ? { latitude: lat, longitude: (best[0] + best[1]) / 2 } : null;
|
|
153
|
+
}
|
|
154
|
+
/** Does `geometry` satisfy the relation? Candidate filtering by bounds happens first, exact geometry second. */
|
|
155
|
+
export function matchesRelation(geometry, relation) {
|
|
156
|
+
switch (relation.relation) {
|
|
157
|
+
case 'bbox':
|
|
158
|
+
return intersectsBounds(geometry, relation.bounds);
|
|
159
|
+
case 'within':
|
|
160
|
+
return within(geometry, relation.geometry);
|
|
161
|
+
case 'contains':
|
|
162
|
+
return contains(geometry, relation.geometry);
|
|
163
|
+
case 'intersects':
|
|
164
|
+
return intersects(geometry, relation.geometry);
|
|
165
|
+
case 'near':
|
|
166
|
+
return distanceToGeometryMeters(relation.position, geometry) <= relation.distanceMeters;
|
|
167
|
+
case 'nearest':
|
|
168
|
+
// Per object, only the distance cap applies; choosing the closest N is a ranking (rankByRelation).
|
|
169
|
+
return relation.maxDistanceMeters === undefined || distanceToGeometryMeters(relation.position, geometry) <= relation.maxDistanceMeters;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PropertyCondition, SpatialObject, SpatialQuery, ValidationResult } from '../core/types.js';
|
|
2
|
+
/** Does a property value satisfy one condition? Numeric operators fail on values that are not numbers. */
|
|
3
|
+
export declare function matchesCondition(value: unknown, c: PropertyCondition): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Does an object satisfy the client-checkable parts of a query (ids, properties filter and conditions, bounds,
|
|
6
|
+
* relation)? `zoom` and `time` are provider hints and never filter here.
|
|
7
|
+
* `ontologyType` is not checkable here — objects do not carry their type; providers and subscriptions scope it.
|
|
8
|
+
* Cheap checks first, exact geometry last (candidate → exact, doc 13).
|
|
9
|
+
*/
|
|
10
|
+
export declare function matchesQuery(object: SpatialObject, query: SpatialQuery): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Applies the ranking part of a query: for `nearest`, the `count` closest objects, closest first.
|
|
13
|
+
* Every other query keeps its order. Call after `matchesQuery` filtering.
|
|
14
|
+
*/
|
|
15
|
+
export declare function rankByRelation(objects: SpatialObject[], query: SpatialQuery): SpatialObject[];
|
|
16
|
+
/** Checks property conditions (doc §10): known operator, and a value of the shape the operator needs. */
|
|
17
|
+
export declare function validateConditions(value: unknown, where?: string): ValidationResult;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { deepEqual } from '../core/equal.js';
|
|
2
|
+
import { geometryFamily } from '../core/geometry-family.js';
|
|
3
|
+
import { distanceToGeometryMeters, intersectsBounds, matchesRelation } from './operations.js';
|
|
4
|
+
const asNumber = (v) => (typeof v === 'number' ? v : typeof v === 'string' && v.trim() !== '' ? Number(v) : Number.NaN);
|
|
5
|
+
/** Does a property value satisfy one condition? Numeric operators fail on values that are not numbers. */
|
|
6
|
+
export function matchesCondition(value, c) {
|
|
7
|
+
switch (c.op) {
|
|
8
|
+
case 'exists':
|
|
9
|
+
return value !== null && value !== undefined;
|
|
10
|
+
case 'eq':
|
|
11
|
+
return deepEqual(value, c.value);
|
|
12
|
+
case 'neq':
|
|
13
|
+
return !deepEqual(value, c.value);
|
|
14
|
+
case 'in':
|
|
15
|
+
return Array.isArray(c.value) && c.value.some((x) => deepEqual(value, x));
|
|
16
|
+
case 'between': {
|
|
17
|
+
const n = asNumber(value);
|
|
18
|
+
return Array.isArray(c.value) && n >= c.value[0] && n <= c.value[1];
|
|
19
|
+
}
|
|
20
|
+
case 'gt':
|
|
21
|
+
return asNumber(value) > c.value;
|
|
22
|
+
case 'gte':
|
|
23
|
+
return asNumber(value) >= c.value;
|
|
24
|
+
case 'lt':
|
|
25
|
+
return asNumber(value) < c.value;
|
|
26
|
+
case 'lte':
|
|
27
|
+
return asNumber(value) <= c.value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Does an object satisfy the client-checkable parts of a query (ids, properties filter and conditions, bounds,
|
|
32
|
+
* relation)? `zoom` and `time` are provider hints and never filter here.
|
|
33
|
+
* `ontologyType` is not checkable here — objects do not carry their type; providers and subscriptions scope it.
|
|
34
|
+
* Cheap checks first, exact geometry last (candidate → exact, doc 13).
|
|
35
|
+
*/
|
|
36
|
+
export function matchesQuery(object, query) {
|
|
37
|
+
// A geometry type in a query stands for its family: a Point query also returns MultiPoint objects.
|
|
38
|
+
if (query.geometryType && geometryFamily(object.geometry.type) !== geometryFamily(query.geometryType))
|
|
39
|
+
return false;
|
|
40
|
+
if (query.objectIds && !query.objectIds.includes(object.objectId))
|
|
41
|
+
return false;
|
|
42
|
+
if (query.filter && !Object.entries(query.filter).every(([k, v]) => deepEqual(object.properties?.[k], v)))
|
|
43
|
+
return false;
|
|
44
|
+
if (query.where && !query.where.every((c) => matchesCondition(object.properties?.[c.property], c)))
|
|
45
|
+
return false;
|
|
46
|
+
if (query.bounds && !intersectsBounds(object.geometry, query.bounds))
|
|
47
|
+
return false;
|
|
48
|
+
if (query.spatial && !matchesRelation(object.geometry, query.spatial))
|
|
49
|
+
return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Applies the ranking part of a query: for `nearest`, the `count` closest objects, closest first.
|
|
54
|
+
* Every other query keeps its order. Call after `matchesQuery` filtering.
|
|
55
|
+
*/
|
|
56
|
+
export function rankByRelation(objects, query) {
|
|
57
|
+
const r = query.spatial;
|
|
58
|
+
if (!r || r.relation !== 'nearest')
|
|
59
|
+
return objects;
|
|
60
|
+
return objects
|
|
61
|
+
.map((o) => ({ o, d: distanceToGeometryMeters(r.position, o.geometry) }))
|
|
62
|
+
.sort((a, b) => a.d - b.d || a.o.objectId.localeCompare(b.o.objectId))
|
|
63
|
+
.slice(0, Math.max(0, Math.floor(r.count)))
|
|
64
|
+
.map((x) => x.o);
|
|
65
|
+
}
|
|
66
|
+
const CONDITION_OPS = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in'];
|
|
67
|
+
/** Checks property conditions (doc §10): known operator, and a value of the shape the operator needs. */
|
|
68
|
+
export function validateConditions(value, where = 'where') {
|
|
69
|
+
if (!Array.isArray(value))
|
|
70
|
+
return { valid: false, reason: `${where} must be an array of conditions` };
|
|
71
|
+
for (const [i, c] of value.entries()) {
|
|
72
|
+
const at = `${where}[${i}]`;
|
|
73
|
+
if (typeof c !== 'object' || c === null)
|
|
74
|
+
return { valid: false, reason: `${at} is not an object` };
|
|
75
|
+
const { property, op, value: v } = c;
|
|
76
|
+
if (typeof property !== 'string' || property === '')
|
|
77
|
+
return { valid: false, reason: `${at}.property must be a non-empty string` };
|
|
78
|
+
if (op !== 'exists' && !CONDITION_OPS.includes(op))
|
|
79
|
+
return { valid: false, reason: `${at}.op "${String(op)}" is not one of exists, ${CONDITION_OPS.join(', ')}` };
|
|
80
|
+
if (op === 'between' && !(Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === 'number')))
|
|
81
|
+
return { valid: false, reason: `${at} "between" needs value [min, max]` };
|
|
82
|
+
if (op === 'in' && !Array.isArray(v))
|
|
83
|
+
return { valid: false, reason: `${at} "in" needs an array value` };
|
|
84
|
+
if (['gt', 'gte', 'lt', 'lte'].includes(op) && typeof v !== 'number')
|
|
85
|
+
return { valid: false, reason: `${at} "${op}" needs a number value` };
|
|
86
|
+
}
|
|
87
|
+
return { valid: true };
|
|
88
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BoundingBox, SpatialObject } from '../core/types.js';
|
|
2
|
+
/** Index of objects the client has loaded. It is a lookup structure, not a data store or a security filter. */
|
|
3
|
+
export interface SpatialIndex {
|
|
4
|
+
/** Inserts or replaces by objectId. */
|
|
5
|
+
insert(object: SpatialObject): void;
|
|
6
|
+
remove(objectId: string): void;
|
|
7
|
+
get(objectId: string): SpatialObject | undefined;
|
|
8
|
+
/** Candidates whose bounds intersect `bounds`. Exact relation tests are the caller's job. */
|
|
9
|
+
search(bounds: BoundingBox): SpatialObject[];
|
|
10
|
+
all(): SpatialObject[];
|
|
11
|
+
clear(): void;
|
|
12
|
+
readonly size: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class RBushSpatialIndex implements SpatialIndex {
|
|
15
|
+
private readonly tree;
|
|
16
|
+
private readonly byId;
|
|
17
|
+
insert(object: SpatialObject): void;
|
|
18
|
+
remove(objectId: string): void;
|
|
19
|
+
get(objectId: string): SpatialObject | undefined;
|
|
20
|
+
search(bounds: BoundingBox): SpatialObject[];
|
|
21
|
+
all(): SpatialObject[];
|
|
22
|
+
clear(): void;
|
|
23
|
+
get size(): number;
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import RBush from 'rbush';
|
|
2
|
+
import { geometryBounds } from './geometry.js';
|
|
3
|
+
export class RBushSpatialIndex {
|
|
4
|
+
tree = new RBush();
|
|
5
|
+
byId = new Map();
|
|
6
|
+
insert(object) {
|
|
7
|
+
this.remove(object.objectId);
|
|
8
|
+
const b = geometryBounds(object.geometry);
|
|
9
|
+
const entry = { minX: b.west, minY: b.south, maxX: b.east, maxY: b.north, object };
|
|
10
|
+
this.tree.insert(entry);
|
|
11
|
+
this.byId.set(object.objectId, entry);
|
|
12
|
+
}
|
|
13
|
+
remove(objectId) {
|
|
14
|
+
const entry = this.byId.get(objectId);
|
|
15
|
+
if (!entry)
|
|
16
|
+
return;
|
|
17
|
+
this.tree.remove(entry);
|
|
18
|
+
this.byId.delete(objectId);
|
|
19
|
+
}
|
|
20
|
+
get(objectId) {
|
|
21
|
+
return this.byId.get(objectId)?.object;
|
|
22
|
+
}
|
|
23
|
+
search(bounds) {
|
|
24
|
+
return this.tree.search({ minX: bounds.west, minY: bounds.south, maxX: bounds.east, maxY: bounds.north }).map((e) => e.object);
|
|
25
|
+
}
|
|
26
|
+
all() {
|
|
27
|
+
return [...this.byId.values()].map((e) => e.object);
|
|
28
|
+
}
|
|
29
|
+
clear() {
|
|
30
|
+
this.tree.clear();
|
|
31
|
+
this.byId.clear();
|
|
32
|
+
}
|
|
33
|
+
get size() {
|
|
34
|
+
return this.byId.size;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage volumes as meshes (phase 3E): a dome (sphere cap from the horizon up) or a wedge (a sector between an
|
|
3
|
+
* azimuth ± beamWidth/2 and two elevation angles), at unit radius in local metres — x east, y north, z up — so a
|
|
4
|
+
* renderer scales it by the range and turns it by the azimuth. Closed surfaces (outer shell + side faces), flat
|
|
5
|
+
* normals, plain triangles. Geometry only: what a sensor can actually see is the data side's concern.
|
|
6
|
+
*/
|
|
7
|
+
export interface VolumeShape {
|
|
8
|
+
shape: 'dome' | 'wedge';
|
|
9
|
+
/** Degrees; wedge only. */
|
|
10
|
+
beamWidth?: number;
|
|
11
|
+
/** Degrees above the horizon. Defaults 0 and 90. */
|
|
12
|
+
minElevation?: number;
|
|
13
|
+
maxElevation?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface VolumeMesh {
|
|
16
|
+
positions: Float32Array;
|
|
17
|
+
normals: Float32Array;
|
|
18
|
+
}
|
|
19
|
+
export declare function volumeMesh(v: VolumeShape, segments?: number): VolumeMesh;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const rad = (d) => (d * Math.PI) / 180;
|
|
2
|
+
/** A point on the unit sphere at an azimuth (clockwise from north) and elevation. */
|
|
3
|
+
function onSphere(azimuthDeg, elevationDeg) {
|
|
4
|
+
const az = rad(azimuthDeg);
|
|
5
|
+
const el = rad(elevationDeg);
|
|
6
|
+
return [Math.cos(el) * Math.sin(az), Math.cos(el) * Math.cos(az), Math.sin(el)];
|
|
7
|
+
}
|
|
8
|
+
export function volumeMesh(v, segments = 32) {
|
|
9
|
+
const wedge = v.shape === 'wedge';
|
|
10
|
+
const half = wedge ? Math.min(180, (v.beamWidth ?? 60) / 2) : 180;
|
|
11
|
+
const lo = v.minElevation ?? 0;
|
|
12
|
+
const hi = v.maxElevation ?? 90;
|
|
13
|
+
const az0 = -half;
|
|
14
|
+
const az1 = half;
|
|
15
|
+
const nAz = Math.max(4, Math.round((segments * (az1 - az0)) / 360));
|
|
16
|
+
const nEl = Math.max(2, Math.round((segments * (hi - lo)) / 180));
|
|
17
|
+
const pos = [];
|
|
18
|
+
const nor = [];
|
|
19
|
+
const tri = (a, b, c) => {
|
|
20
|
+
const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
21
|
+
const w = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
22
|
+
let n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]];
|
|
23
|
+
const len = Math.hypot(n[0], n[1], n[2]) || 1;
|
|
24
|
+
n = n.map((x) => x / len);
|
|
25
|
+
pos.push(...a, ...b, ...c);
|
|
26
|
+
nor.push(...n, ...n, ...n);
|
|
27
|
+
};
|
|
28
|
+
const O = [0, 0, 0];
|
|
29
|
+
const at = (i, j) => onSphere(az0 + ((az1 - az0) * i) / nAz, lo + ((hi - lo) * j) / nEl);
|
|
30
|
+
// Outer shell.
|
|
31
|
+
for (let i = 0; i < nAz; i++) {
|
|
32
|
+
for (let j = 0; j < nEl; j++) {
|
|
33
|
+
const a = at(i, j);
|
|
34
|
+
const b = at(i + 1, j);
|
|
35
|
+
const c = at(i + 1, j + 1);
|
|
36
|
+
const d = at(i, j + 1);
|
|
37
|
+
tri(a, b, c);
|
|
38
|
+
tri(a, c, d);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Lower and upper cones (fans to the origin), unless they are flat at the horizon or closed at the zenith.
|
|
42
|
+
if (lo > -90)
|
|
43
|
+
for (let i = 0; i < nAz; i++)
|
|
44
|
+
tri(O, at(i + 1, 0), at(i, 0));
|
|
45
|
+
if (hi < 90)
|
|
46
|
+
for (let i = 0; i < nAz; i++)
|
|
47
|
+
tri(O, at(i, nEl), at(i + 1, nEl));
|
|
48
|
+
// Side faces of a wedge.
|
|
49
|
+
if (wedge && half < 180) {
|
|
50
|
+
for (let j = 0; j < nEl; j++) {
|
|
51
|
+
tri(O, at(0, j), at(0, j + 1));
|
|
52
|
+
tri(O, at(nAz, j + 1), at(nAz, j));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { positions: new Float32Array(pos), normals: new Float32Array(nor) };
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebuilding objects at a time from an event journal (doc 29 §7–8). Pure: entries in, state out. The journal itself
|
|
3
|
+
* comes from a provider; the engine never stores history.
|
|
4
|
+
*/
|
|
5
|
+
import type { SpatialObject } from '../core/types.js';
|
|
6
|
+
/** The journal kinds the reconstruction understands; any other kind carrying an `object` counts as an update. */
|
|
7
|
+
interface Entry {
|
|
8
|
+
at: string;
|
|
9
|
+
objectId: string;
|
|
10
|
+
kind: string;
|
|
11
|
+
object?: SpatialObject;
|
|
12
|
+
}
|
|
13
|
+
/** Entries grouped by object, oldest first (stable for equal times). */
|
|
14
|
+
export declare function groupJournal<E extends Entry>(entries: E[]): Map<string, E[]>;
|
|
15
|
+
/**
|
|
16
|
+
* One object's state at `atMs`: its last entry at or before that time. Absent (null) when it did not exist yet,
|
|
17
|
+
* or its last entry says it was removed. `undefined` when the journal says nothing about it at all.
|
|
18
|
+
*/
|
|
19
|
+
export declare function journalStateAt<E extends Entry>(entries: E[] | undefined, atMs: number): SpatialObject | null | undefined;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Entries grouped by object, oldest first (stable for equal times). */
|
|
2
|
+
export function groupJournal(entries) {
|
|
3
|
+
const out = new Map();
|
|
4
|
+
for (const e of entries) {
|
|
5
|
+
if (!e || typeof e.objectId !== 'string' || !Number.isFinite(Date.parse(e.at)))
|
|
6
|
+
continue;
|
|
7
|
+
let list = out.get(e.objectId);
|
|
8
|
+
if (!list)
|
|
9
|
+
out.set(e.objectId, (list = []));
|
|
10
|
+
list.push(e);
|
|
11
|
+
}
|
|
12
|
+
for (const list of out.values())
|
|
13
|
+
list.sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* One object's state at `atMs`: its last entry at or before that time. Absent (null) when it did not exist yet,
|
|
18
|
+
* or its last entry says it was removed. `undefined` when the journal says nothing about it at all.
|
|
19
|
+
*/
|
|
20
|
+
export function journalStateAt(entries, atMs) {
|
|
21
|
+
if (!entries?.length)
|
|
22
|
+
return undefined;
|
|
23
|
+
let last;
|
|
24
|
+
for (const e of entries) {
|
|
25
|
+
if (Date.parse(e.at) > atMs)
|
|
26
|
+
break;
|
|
27
|
+
last = e;
|
|
28
|
+
}
|
|
29
|
+
if (!last)
|
|
30
|
+
return null; // first known after this time: not there yet
|
|
31
|
+
if (last.kind === 'removed' || !last.object)
|
|
32
|
+
return null;
|
|
33
|
+
return last.object;
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A playback clock for replay (doc 07). The application owns time (ADR-010): it creates a Playback, shows its
|
|
3
|
+
* controls, and passes each tick to `runtime.setTime`. The engine never runs a clock on its own.
|
|
4
|
+
*/
|
|
5
|
+
import type { TimeRange } from '../core/types.js';
|
|
6
|
+
export interface PlaybackOptions {
|
|
7
|
+
range: TimeRange;
|
|
8
|
+
/** Start position. Default: range start. */
|
|
9
|
+
at?: string;
|
|
10
|
+
/** Simulated seconds per real second. Default 1. */
|
|
11
|
+
speed?: number;
|
|
12
|
+
/** Called with the new time on every tick and seek. */
|
|
13
|
+
onTime: (at: string) => void;
|
|
14
|
+
/** Called when playback reaches the end of the range (and stops). */
|
|
15
|
+
onEnd?: () => void;
|
|
16
|
+
/** Schedules the next frame. Default: requestAnimationFrame, else a 50 ms timer. */
|
|
17
|
+
frame?: (cb: (nowMs: number) => void) => void;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
}
|
|
20
|
+
export declare class Playback {
|
|
21
|
+
private readonly options;
|
|
22
|
+
private atMs;
|
|
23
|
+
private startMs;
|
|
24
|
+
private endMs;
|
|
25
|
+
private speed;
|
|
26
|
+
private playing;
|
|
27
|
+
private lastReal;
|
|
28
|
+
private generation;
|
|
29
|
+
private readonly frame;
|
|
30
|
+
private readonly now;
|
|
31
|
+
constructor(options: PlaybackOptions);
|
|
32
|
+
get time(): string;
|
|
33
|
+
get isPlaying(): boolean;
|
|
34
|
+
get rate(): number;
|
|
35
|
+
get range(): TimeRange;
|
|
36
|
+
play(): void;
|
|
37
|
+
pause(): void;
|
|
38
|
+
seek(at: string): void;
|
|
39
|
+
setSpeed(speed: number): void;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|