@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,28 @@
|
|
|
1
|
+
/** Interaction state keyed by stable objectId (ADR-009). Knows nothing about layers, data or rendering. */
|
|
2
|
+
export class SelectionState {
|
|
3
|
+
now;
|
|
4
|
+
current;
|
|
5
|
+
constructor(now = Date.now) {
|
|
6
|
+
this.now = now;
|
|
7
|
+
this.current = { objectIds: [], source: 'init', timestamp: now() };
|
|
8
|
+
}
|
|
9
|
+
get() {
|
|
10
|
+
return { ...this.current, objectIds: [...this.current.objectIds] };
|
|
11
|
+
}
|
|
12
|
+
isSelected(objectId) {
|
|
13
|
+
return this.current.objectIds.includes(objectId);
|
|
14
|
+
}
|
|
15
|
+
/** Returns true when the selected ids changed. */
|
|
16
|
+
set(objectIds, source, primaryObjectId) {
|
|
17
|
+
const ids = [...new Set(objectIds)];
|
|
18
|
+
const primary = ids.length ? (primaryObjectId && ids.includes(primaryObjectId) ? primaryObjectId : ids[0]) : undefined;
|
|
19
|
+
const same = ids.length === this.current.objectIds.length && ids.every((id, i) => id === this.current.objectIds[i]) && primary === this.current.primaryObjectId;
|
|
20
|
+
if (same)
|
|
21
|
+
return false;
|
|
22
|
+
this.current = { objectIds: ids, source, timestamp: this.now(), ...(primary !== undefined ? { primaryObjectId: primary } : {}) };
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
clear(source) {
|
|
26
|
+
return this.set([], source);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type * as GeoJSON from 'geojson';
|
|
2
|
+
import type { Geometry, Position } from '../core/types.js';
|
|
3
|
+
/** Steps used when a circle has to become a polygon (rendering, GeoJSON export). */
|
|
4
|
+
export declare const CIRCLE_STEPS = 64;
|
|
5
|
+
export declare function toGeoJSON(geometry: Geometry): GeoJSON.Geometry;
|
|
6
|
+
/** Converts and validates. Throws MapEngineError('INVALID_GEOMETRY') on anything the model cannot hold. */
|
|
7
|
+
export declare function fromGeoJSON(value: GeoJSON.Geometry): Geometry;
|
|
8
|
+
/** `[longitude, latitude]`, the order renderer libraries expect for a center or a point. */
|
|
9
|
+
export declare function toLngLat(p: Position): [number, number];
|
|
10
|
+
/** From a renderer's `{ lng, lat }` object. */
|
|
11
|
+
export declare function fromLngLat(v: {
|
|
12
|
+
lng: number;
|
|
13
|
+
lat: number;
|
|
14
|
+
}): Position;
|
|
15
|
+
export declare function toFeature(geometry: Geometry, properties?: GeoJSON.GeoJsonProperties): GeoJSON.Feature;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GeoJSON boundary (ADR-006). This is the ONLY module that knows GeoJSON's
|
|
3
|
+
* `[longitude, latitude]` order. Everything else uses semantic Positions.
|
|
4
|
+
*/
|
|
5
|
+
import { circle as turfCircle } from '@turf/circle';
|
|
6
|
+
import { MapEngineError } from '../core/errors.js';
|
|
7
|
+
import { validateGeometry } from './geometry.js';
|
|
8
|
+
/** Altitude is GeoJSON's third coordinate (ADR-028). */
|
|
9
|
+
const toPair = (p) => (p.altitude === undefined ? [p.longitude, p.latitude] : [p.longitude, p.latitude, p.altitude]);
|
|
10
|
+
function fromPair(pair, where) {
|
|
11
|
+
if (!Array.isArray(pair) || pair.length < 2)
|
|
12
|
+
throw new MapEngineError('INVALID_GEOMETRY', `${where} is not a [longitude, latitude] pair`);
|
|
13
|
+
const p = { longitude: pair[0], latitude: pair[1] };
|
|
14
|
+
if (pair.length > 2 && pair[2] !== null && pair[2] !== undefined)
|
|
15
|
+
p.altitude = pair[2];
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
/** Steps used when a circle has to become a polygon (rendering, GeoJSON export). */
|
|
19
|
+
export const CIRCLE_STEPS = 64;
|
|
20
|
+
export function toGeoJSON(geometry) {
|
|
21
|
+
switch (geometry.type) {
|
|
22
|
+
case 'Point':
|
|
23
|
+
return { type: 'Point', coordinates: toPair(geometry.coordinates) };
|
|
24
|
+
case 'LineString':
|
|
25
|
+
return { type: 'LineString', coordinates: geometry.coordinates.map(toPair) };
|
|
26
|
+
case 'Polygon':
|
|
27
|
+
return { type: 'Polygon', coordinates: geometry.coordinates.map((ring) => ring.map(toPair)) };
|
|
28
|
+
case 'MultiPoint':
|
|
29
|
+
return { type: 'MultiPoint', coordinates: geometry.coordinates.map(toPair) };
|
|
30
|
+
case 'MultiLineString':
|
|
31
|
+
return { type: 'MultiLineString', coordinates: geometry.coordinates.map((line) => line.map(toPair)) };
|
|
32
|
+
case 'MultiPolygon':
|
|
33
|
+
return { type: 'MultiPolygon', coordinates: geometry.coordinates.map((poly) => poly.map((ring) => ring.map(toPair))) };
|
|
34
|
+
case 'Circle':
|
|
35
|
+
return turfCircle(toPair(geometry.center), geometry.radiusMeters, { steps: CIRCLE_STEPS, units: 'meters' }).geometry;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const pairs = (list, where) => {
|
|
39
|
+
if (!Array.isArray(list))
|
|
40
|
+
throw new MapEngineError('INVALID_GEOMETRY', `${where} is not an array`);
|
|
41
|
+
return list.map((p, i) => fromPair(p, `${where}[${i}]`));
|
|
42
|
+
};
|
|
43
|
+
/** Converts and validates. Throws MapEngineError('INVALID_GEOMETRY') on anything the model cannot hold. */
|
|
44
|
+
export function fromGeoJSON(value) {
|
|
45
|
+
let geometry;
|
|
46
|
+
switch (value.type) {
|
|
47
|
+
case 'Point':
|
|
48
|
+
geometry = { type: 'Point', coordinates: fromPair(value.coordinates, 'Point.coordinates') };
|
|
49
|
+
break;
|
|
50
|
+
case 'LineString':
|
|
51
|
+
geometry = { type: 'LineString', coordinates: pairs(value.coordinates, 'LineString.coordinates') };
|
|
52
|
+
break;
|
|
53
|
+
case 'Polygon':
|
|
54
|
+
geometry = { type: 'Polygon', coordinates: value.coordinates.map((ring, r) => pairs(ring, `Polygon.coordinates[${r}]`)) };
|
|
55
|
+
break;
|
|
56
|
+
case 'MultiPoint':
|
|
57
|
+
geometry = { type: 'MultiPoint', coordinates: pairs(value.coordinates, 'MultiPoint.coordinates') };
|
|
58
|
+
break;
|
|
59
|
+
case 'MultiLineString':
|
|
60
|
+
geometry = { type: 'MultiLineString', coordinates: value.coordinates.map((line, i) => pairs(line, `MultiLineString.coordinates[${i}]`)) };
|
|
61
|
+
break;
|
|
62
|
+
case 'MultiPolygon':
|
|
63
|
+
geometry = { type: 'MultiPolygon', coordinates: value.coordinates.map((poly, i) => poly.map((ring, r) => pairs(ring, `MultiPolygon.coordinates[${i}][${r}]`))) };
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
throw new MapEngineError('INVALID_GEOMETRY', `GeoJSON ${value.type} is not supported (Point, LineString, Polygon and their Multi forms)`);
|
|
67
|
+
}
|
|
68
|
+
const check = validateGeometry(geometry);
|
|
69
|
+
if (!check.valid)
|
|
70
|
+
throw new MapEngineError('INVALID_GEOMETRY', check.reason);
|
|
71
|
+
return geometry;
|
|
72
|
+
}
|
|
73
|
+
/** `[longitude, latitude]`, the order renderer libraries expect for a center or a point. */
|
|
74
|
+
export function toLngLat(p) {
|
|
75
|
+
return [p.longitude, p.latitude];
|
|
76
|
+
}
|
|
77
|
+
/** From a renderer's `{ lng, lat }` object. */
|
|
78
|
+
export function fromLngLat(v) {
|
|
79
|
+
return { latitude: v.lat, longitude: v.lng };
|
|
80
|
+
}
|
|
81
|
+
export function toFeature(geometry, properties = {}) {
|
|
82
|
+
return { type: 'Feature', geometry: toGeoJSON(geometry), properties };
|
|
83
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BoundingBox, Geometry, Position, ValidationResult } from '../core/types.js';
|
|
2
|
+
export declare function validatePosition(value: unknown, where?: string): ValidationResult;
|
|
3
|
+
/** Validates untrusted geometry (for example, from a provider). Never throws. */
|
|
4
|
+
export declare function validateGeometry(value: unknown): ValidationResult;
|
|
5
|
+
export { geometryFamily } from '../core/geometry-family.js';
|
|
6
|
+
/** The positions that bound a geometry (a circle contributes its four extreme points). */
|
|
7
|
+
export declare function positionsOf(geometry: Geometry): Position[];
|
|
8
|
+
export declare function geometryBounds(geometry: Geometry): BoundingBox;
|
|
9
|
+
/** Smallest bounds containing all inputs; undefined for an empty list. */
|
|
10
|
+
export declare function unionBounds(list: BoundingBox[]): BoundingBox | undefined;
|
|
11
|
+
export declare function boundsIntersect(a: BoundingBox, b: BoundingBox): boolean;
|
|
12
|
+
export declare function boundsContainPosition(b: BoundingBox, p: Position): boolean;
|
|
13
|
+
export declare function boundsToPolygon(b: BoundingBox): Geometry;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { kinks } from '@turf/kinks';
|
|
2
|
+
import { fromGeoJSON, toGeoJSON } from './geojson.js';
|
|
3
|
+
const invalid = (reason) => ({ valid: false, reason });
|
|
4
|
+
export function validatePosition(value, where = 'position') {
|
|
5
|
+
if (typeof value !== 'object' || value === null)
|
|
6
|
+
return invalid(`${where} is not an object`);
|
|
7
|
+
const { latitude, longitude } = value;
|
|
8
|
+
if (typeof latitude !== 'number' || !Number.isFinite(latitude))
|
|
9
|
+
return invalid(`${where}.latitude is not a finite number`);
|
|
10
|
+
if (typeof longitude !== 'number' || !Number.isFinite(longitude))
|
|
11
|
+
return invalid(`${where}.longitude is not a finite number`);
|
|
12
|
+
if (latitude < -90 || latitude > 90)
|
|
13
|
+
return invalid(`${where}.latitude ${latitude} is outside [-90, 90]`);
|
|
14
|
+
if (longitude < -180 || longitude > 180)
|
|
15
|
+
return invalid(`${where}.longitude ${longitude} is outside [-180, 180]`);
|
|
16
|
+
const { altitude } = value;
|
|
17
|
+
if (altitude !== undefined) {
|
|
18
|
+
if (typeof altitude !== 'number' || !Number.isFinite(altitude))
|
|
19
|
+
return invalid(`${where}.altitude is not a finite number of metres`);
|
|
20
|
+
// Below the deepest ocean floor or beyond geostationary orbit by far: a unit or parsing error, not a height.
|
|
21
|
+
if (altitude < -12_000 || altitude > 100_000_000)
|
|
22
|
+
return invalid(`${where}.altitude ${altitude} m is not a plausible height`);
|
|
23
|
+
}
|
|
24
|
+
return { valid: true };
|
|
25
|
+
}
|
|
26
|
+
const samePosition = (a, b) => a.latitude === b.latitude && a.longitude === b.longitude;
|
|
27
|
+
function validateLine(coords, where) {
|
|
28
|
+
if (!Array.isArray(coords))
|
|
29
|
+
return invalid(`${where} is not an array`);
|
|
30
|
+
if (coords.length < 2)
|
|
31
|
+
return invalid(`${where} needs at least 2 positions`);
|
|
32
|
+
for (let i = 0; i < coords.length; i++) {
|
|
33
|
+
const r = validatePosition(coords[i], `${where}[${i}]`);
|
|
34
|
+
if (!r.valid)
|
|
35
|
+
return r;
|
|
36
|
+
}
|
|
37
|
+
return { valid: true };
|
|
38
|
+
}
|
|
39
|
+
function validateRings(rings, where) {
|
|
40
|
+
if (!Array.isArray(rings) || rings.length === 0)
|
|
41
|
+
return invalid(`${where} needs at least one ring`);
|
|
42
|
+
for (let r = 0; r < rings.length; r++) {
|
|
43
|
+
const ring = rings[r];
|
|
44
|
+
if (!Array.isArray(ring))
|
|
45
|
+
return invalid(`${where} ring ${r} is not an array`);
|
|
46
|
+
if (ring.length < 4)
|
|
47
|
+
return invalid(`${where} ring ${r} needs at least 4 positions (closed)`);
|
|
48
|
+
for (let i = 0; i < ring.length; i++) {
|
|
49
|
+
const p = validatePosition(ring[i], `${where}[${r}][${i}]`);
|
|
50
|
+
if (!p.valid)
|
|
51
|
+
return p;
|
|
52
|
+
}
|
|
53
|
+
if (!samePosition(ring[0], ring[ring.length - 1]))
|
|
54
|
+
return invalid(`${where} ring ${r} is not closed`);
|
|
55
|
+
}
|
|
56
|
+
const crossing = selfIntersection(rings);
|
|
57
|
+
if (crossing)
|
|
58
|
+
return invalid(`${where} edges cross each other near ${crossing.latitude.toFixed(6)}, ${crossing.longitude.toFixed(6)} (self-intersecting)`);
|
|
59
|
+
return { valid: true };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Where a polygon's edges cross (one ring crossing itself, or rings crossing each other), or null (doc §64).
|
|
63
|
+
* Turf's check is quadratic, so results are remembered by a fingerprint of the coordinates: a large polygon that
|
|
64
|
+
* arrives on every load is checked once.
|
|
65
|
+
*/
|
|
66
|
+
const crossings = new Map();
|
|
67
|
+
const CROSSINGS_KEPT = 5000;
|
|
68
|
+
function selfIntersection(rings) {
|
|
69
|
+
let h1 = 0x811c9dc5;
|
|
70
|
+
let h2 = 0;
|
|
71
|
+
let n = 0;
|
|
72
|
+
for (const ring of rings) {
|
|
73
|
+
for (const p of ring) {
|
|
74
|
+
const x = Math.round(p.longitude * 1e7);
|
|
75
|
+
const y = Math.round(p.latitude * 1e7);
|
|
76
|
+
h1 = Math.imul(h1 ^ x, 0x9e3779b1);
|
|
77
|
+
h1 = Math.imul(h1 ^ y, 0x85ebca6b);
|
|
78
|
+
h2 = (h2 * 31 + x * 7 + y * 13) % 0x1fffffffffffff;
|
|
79
|
+
n++;
|
|
80
|
+
}
|
|
81
|
+
h1 = Math.imul(h1 ^ -1, 0xc2b2ae35); // ring boundary
|
|
82
|
+
}
|
|
83
|
+
const key = `${n}:${h1}:${h2}`;
|
|
84
|
+
const known = crossings.get(key);
|
|
85
|
+
if (known !== undefined)
|
|
86
|
+
return known;
|
|
87
|
+
// A point repeated in a row is harmless (common in GIS data) but Turf counts it as a crossing: drop repeats.
|
|
88
|
+
const deduped = rings.map((ring) => ring.filter((p, i) => i === 0 || !samePosition(p, ring[i - 1])));
|
|
89
|
+
let result = null;
|
|
90
|
+
try {
|
|
91
|
+
const hit = kinks(toGeoJSON({ type: 'Polygon', coordinates: deduped })).features[0];
|
|
92
|
+
if (hit)
|
|
93
|
+
result = fromGeoJSON(hit.geometry).coordinates;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
result = null; // degenerate rings (e.g. all repeats) are judged by the other checks, not here
|
|
97
|
+
}
|
|
98
|
+
if (crossings.size >= CROSSINGS_KEPT)
|
|
99
|
+
crossings.delete(crossings.keys().next().value);
|
|
100
|
+
crossings.set(key, result);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function validateParts(parts, where, each) {
|
|
104
|
+
if (!Array.isArray(parts) || parts.length === 0)
|
|
105
|
+
return invalid(`${where} needs at least one part`);
|
|
106
|
+
for (let i = 0; i < parts.length; i++) {
|
|
107
|
+
const r = each(parts[i], `${where}[${i}]`);
|
|
108
|
+
if (!r.valid)
|
|
109
|
+
return r;
|
|
110
|
+
}
|
|
111
|
+
return { valid: true };
|
|
112
|
+
}
|
|
113
|
+
/** Validates untrusted geometry (for example, from a provider). Never throws. */
|
|
114
|
+
export function validateGeometry(value) {
|
|
115
|
+
if (typeof value !== 'object' || value === null)
|
|
116
|
+
return invalid('geometry is not an object');
|
|
117
|
+
const g = value;
|
|
118
|
+
switch (g.type) {
|
|
119
|
+
case 'Point':
|
|
120
|
+
return validatePosition(g.coordinates, 'Point.coordinates');
|
|
121
|
+
case 'LineString':
|
|
122
|
+
return validateLine(g.coordinates, 'LineString.coordinates');
|
|
123
|
+
case 'Polygon':
|
|
124
|
+
return validateRings(g.coordinates, 'Polygon');
|
|
125
|
+
case 'MultiPoint':
|
|
126
|
+
return validateParts(g.coordinates, 'MultiPoint.coordinates', (p, at) => validatePosition(p, at));
|
|
127
|
+
case 'MultiLineString':
|
|
128
|
+
return validateParts(g.coordinates, 'MultiLineString.coordinates', validateLine);
|
|
129
|
+
case 'MultiPolygon':
|
|
130
|
+
return validateParts(g.coordinates, 'MultiPolygon', validateRings);
|
|
131
|
+
case 'Circle': {
|
|
132
|
+
const c = validatePosition(g.center, 'Circle.center');
|
|
133
|
+
if (!c.valid)
|
|
134
|
+
return c;
|
|
135
|
+
if (typeof g.radiusMeters !== 'number' || !Number.isFinite(g.radiusMeters) || g.radiusMeters <= 0)
|
|
136
|
+
return invalid('Circle.radiusMeters must be a number > 0');
|
|
137
|
+
return { valid: true };
|
|
138
|
+
}
|
|
139
|
+
default:
|
|
140
|
+
return invalid(`unsupported geometry type ${JSON.stringify(g.type)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export { geometryFamily } from '../core/geometry-family.js';
|
|
144
|
+
const METERS_PER_DEGREE = 111_320;
|
|
145
|
+
/** The positions that bound a geometry (a circle contributes its four extreme points). */
|
|
146
|
+
export function positionsOf(geometry) {
|
|
147
|
+
switch (geometry.type) {
|
|
148
|
+
case 'Point':
|
|
149
|
+
return [geometry.coordinates];
|
|
150
|
+
case 'LineString':
|
|
151
|
+
case 'MultiPoint':
|
|
152
|
+
return geometry.coordinates;
|
|
153
|
+
case 'Polygon':
|
|
154
|
+
case 'MultiLineString':
|
|
155
|
+
return geometry.coordinates.flat();
|
|
156
|
+
case 'MultiPolygon':
|
|
157
|
+
return geometry.coordinates.flat(2);
|
|
158
|
+
case 'Circle': {
|
|
159
|
+
const { latitude, longitude } = geometry.center;
|
|
160
|
+
const dLat = geometry.radiusMeters / METERS_PER_DEGREE;
|
|
161
|
+
const dLon = geometry.radiusMeters / (METERS_PER_DEGREE * Math.max(0.01, Math.cos((latitude * Math.PI) / 180)));
|
|
162
|
+
return [
|
|
163
|
+
{ latitude: Math.min(90, latitude + dLat), longitude },
|
|
164
|
+
{ latitude: Math.max(-90, latitude - dLat), longitude },
|
|
165
|
+
{ latitude, longitude: Math.min(180, longitude + dLon) },
|
|
166
|
+
{ latitude, longitude: Math.max(-180, longitude - dLon) },
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export function geometryBounds(geometry) {
|
|
172
|
+
let west = Infinity;
|
|
173
|
+
let south = Infinity;
|
|
174
|
+
let east = -Infinity;
|
|
175
|
+
let north = -Infinity;
|
|
176
|
+
for (const p of positionsOf(geometry)) {
|
|
177
|
+
if (p.longitude < west)
|
|
178
|
+
west = p.longitude;
|
|
179
|
+
if (p.longitude > east)
|
|
180
|
+
east = p.longitude;
|
|
181
|
+
if (p.latitude < south)
|
|
182
|
+
south = p.latitude;
|
|
183
|
+
if (p.latitude > north)
|
|
184
|
+
north = p.latitude;
|
|
185
|
+
}
|
|
186
|
+
return { west, south, east, north };
|
|
187
|
+
}
|
|
188
|
+
/** Smallest bounds containing all inputs; undefined for an empty list. */
|
|
189
|
+
export function unionBounds(list) {
|
|
190
|
+
if (!list.length)
|
|
191
|
+
return undefined;
|
|
192
|
+
return list.reduce((a, b) => ({ west: Math.min(a.west, b.west), south: Math.min(a.south, b.south), east: Math.max(a.east, b.east), north: Math.max(a.north, b.north) }));
|
|
193
|
+
}
|
|
194
|
+
export function boundsIntersect(a, b) {
|
|
195
|
+
return a.west <= b.east && a.east >= b.west && a.south <= b.north && a.north >= b.south;
|
|
196
|
+
}
|
|
197
|
+
export function boundsContainPosition(b, p) {
|
|
198
|
+
return p.longitude >= b.west && p.longitude <= b.east && p.latitude >= b.south && p.latitude <= b.north;
|
|
199
|
+
}
|
|
200
|
+
export function boundsToPolygon(b) {
|
|
201
|
+
const sw = { latitude: b.south, longitude: b.west };
|
|
202
|
+
return {
|
|
203
|
+
type: 'Polygon',
|
|
204
|
+
coordinates: [[sw, { latitude: b.south, longitude: b.east }, { latitude: b.north, longitude: b.east }, { latitude: b.north, longitude: b.west }, sw]],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SpatialObject } from '../core/types.js';
|
|
2
|
+
export interface ImportResult {
|
|
3
|
+
objects: SpatialObject[];
|
|
4
|
+
/** Features or rows that could not be used, with why (1-based row/feature numbers). */
|
|
5
|
+
skipped: Array<{
|
|
6
|
+
index: number;
|
|
7
|
+
reason: string;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
/** A FeatureCollection, a Feature or a bare Geometry. Ids come from `idProperty`, then `feature.id`, then a counter. */
|
|
11
|
+
export declare function objectsFromGeoJSON(value: unknown, options?: {
|
|
12
|
+
idProperty?: string;
|
|
13
|
+
idPrefix?: string;
|
|
14
|
+
}): ImportResult;
|
|
15
|
+
/** Rows with latitude/longitude columns → points. Numbers and booleans in text cells become real values. */
|
|
16
|
+
export declare function objectsFromRows(rows: Array<Record<string, unknown>>, options: {
|
|
17
|
+
latitude: string;
|
|
18
|
+
longitude: string;
|
|
19
|
+
id?: string;
|
|
20
|
+
idPrefix?: string;
|
|
21
|
+
}): ImportResult;
|
|
22
|
+
/** Best guess at which columns hold latitude, longitude and an id. The application should let the user correct it. */
|
|
23
|
+
export declare function guessCoordinateColumns(headers: string[]): {
|
|
24
|
+
latitude?: string;
|
|
25
|
+
longitude?: string;
|
|
26
|
+
id?: string;
|
|
27
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { fromGeoJSON } from './geojson.js';
|
|
2
|
+
import { validatePosition } from './geometry.js';
|
|
3
|
+
const scalar = (v) => {
|
|
4
|
+
if (typeof v !== 'string')
|
|
5
|
+
return v;
|
|
6
|
+
const t = v.trim();
|
|
7
|
+
if (t === '')
|
|
8
|
+
return undefined;
|
|
9
|
+
if (/^-?\d+(\.\d+)?([eE][-+]?\d+)?$/.test(t))
|
|
10
|
+
return Number(t);
|
|
11
|
+
if (t === 'true' || t === 'false')
|
|
12
|
+
return t === 'true';
|
|
13
|
+
return t;
|
|
14
|
+
};
|
|
15
|
+
/** A FeatureCollection, a Feature or a bare Geometry. Ids come from `idProperty`, then `feature.id`, then a counter. */
|
|
16
|
+
export function objectsFromGeoJSON(value, options = {}) {
|
|
17
|
+
const prefix = options.idPrefix ?? 'f';
|
|
18
|
+
const v = value;
|
|
19
|
+
const features = v?.type === 'FeatureCollection' && Array.isArray(v.features)
|
|
20
|
+
? v.features
|
|
21
|
+
: v?.type === 'Feature'
|
|
22
|
+
? [v]
|
|
23
|
+
: v && typeof v.type === 'string'
|
|
24
|
+
? [{ type: 'Feature', geometry: v, properties: {} }]
|
|
25
|
+
: [];
|
|
26
|
+
const out = { objects: [], skipped: [] };
|
|
27
|
+
if (!features.length)
|
|
28
|
+
out.skipped.push({ index: 0, reason: 'not GeoJSON (expected a FeatureCollection, Feature or Geometry)' });
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
features.forEach((f, i) => {
|
|
31
|
+
if (!f?.geometry)
|
|
32
|
+
return void out.skipped.push({ index: i + 1, reason: 'feature has no geometry' });
|
|
33
|
+
try {
|
|
34
|
+
const geometry = fromGeoJSON(f.geometry);
|
|
35
|
+
const properties = { ...(f.properties ?? {}) };
|
|
36
|
+
const raw = (options.idProperty ? properties[options.idProperty] : undefined) ?? f.id;
|
|
37
|
+
let objectId = raw !== undefined && raw !== null && raw !== '' ? String(raw) : `${prefix}-${i + 1}`;
|
|
38
|
+
if (seen.has(objectId))
|
|
39
|
+
objectId = `${objectId}~${i + 1}`;
|
|
40
|
+
seen.add(objectId);
|
|
41
|
+
out.objects.push({ objectId, geometry, properties });
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
out.skipped.push({ index: i + 1, reason: e instanceof Error ? e.message : String(e) });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** Rows with latitude/longitude columns → points. Numbers and booleans in text cells become real values. */
|
|
50
|
+
export function objectsFromRows(rows, options) {
|
|
51
|
+
const out = { objects: [], skipped: [] };
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
rows.forEach((row, i) => {
|
|
54
|
+
const latitude = Number(scalar(row[options.latitude]));
|
|
55
|
+
const longitude = Number(scalar(row[options.longitude]));
|
|
56
|
+
const position = { latitude, longitude };
|
|
57
|
+
const check = validatePosition(position);
|
|
58
|
+
if (!check.valid)
|
|
59
|
+
return void out.skipped.push({ index: i + 1, reason: check.reason });
|
|
60
|
+
const properties = {};
|
|
61
|
+
for (const [k, v] of Object.entries(row)) {
|
|
62
|
+
if (k === options.latitude || k === options.longitude)
|
|
63
|
+
continue;
|
|
64
|
+
const s = scalar(v);
|
|
65
|
+
if (s !== undefined)
|
|
66
|
+
properties[k] = s;
|
|
67
|
+
}
|
|
68
|
+
const raw = options.id ? row[options.id] : undefined;
|
|
69
|
+
let objectId = raw !== undefined && raw !== null && String(raw).trim() !== '' ? String(raw).trim() : `${options.idPrefix ?? 'row'}-${i + 1}`;
|
|
70
|
+
if (seen.has(objectId))
|
|
71
|
+
objectId = `${objectId}~${i + 1}`;
|
|
72
|
+
seen.add(objectId);
|
|
73
|
+
out.objects.push({ objectId, geometry: { type: 'Point', coordinates: position }, properties });
|
|
74
|
+
});
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
const LAT = ['latitude', 'lat', 'y', 'lat_deg'];
|
|
78
|
+
const LON = ['longitude', 'lon', 'lng', 'long', 'x', 'lon_deg'];
|
|
79
|
+
/** Real identifier columns first (exact names, then anything ending in "id"); a display name is only a last resort. */
|
|
80
|
+
const ID = ['id', 'objectid', 'object_id', 'asset_id', 'assetid', 'device_id', 'deviceid', 'tag', 'asset', 'device', 'code'];
|
|
81
|
+
const ID_LIKE = /(^|[\s_-])id$|[a-z]Id$|ID$/;
|
|
82
|
+
/** Best guess at which columns hold latitude, longitude and an id. The application should let the user correct it. */
|
|
83
|
+
export function guessCoordinateColumns(headers) {
|
|
84
|
+
const find = (names) => names.map((n) => headers.find((h) => h.trim().toLowerCase() === n)).find((h) => h !== undefined);
|
|
85
|
+
const out = {};
|
|
86
|
+
const lat = find(LAT);
|
|
87
|
+
const lon = find(LON);
|
|
88
|
+
const id = find(ID) ?? headers.find((h) => ID_LIKE.test(h.trim())) ?? find(['name', 'label', 'title']);
|
|
89
|
+
if (lat)
|
|
90
|
+
out.latitude = lat;
|
|
91
|
+
if (lon)
|
|
92
|
+
out.longitude = lon;
|
|
93
|
+
if (id)
|
|
94
|
+
out.id = id;
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { boundsContainPosition, boundsIntersect, geometryBounds, unionBounds, validateGeometry, validatePosition } from './geometry.js';
|
|
2
|
+
import { fromGeoJSON, toGeoJSON } from './geojson.js';
|
|
3
|
+
import { contains, distanceMeters, distanceToGeometryMeters, intersects, labelPoint, matchesRelation, within } from './operations.js';
|
|
4
|
+
import { areaSquareMeters, bearingDegrees, circlePolygon, destination, ellipsePolygon, lengthMeters, measure, sectorPolygon } from './measure.js';
|
|
5
|
+
/** The geometry API for engine modules and applications. Nothing outside `spatial/` imports Turf. */
|
|
6
|
+
export declare const spatial: {
|
|
7
|
+
readonly validate: typeof validateGeometry;
|
|
8
|
+
readonly validatePosition: typeof validatePosition;
|
|
9
|
+
readonly bounds: typeof geometryBounds;
|
|
10
|
+
readonly unionBounds: typeof unionBounds;
|
|
11
|
+
readonly boundsIntersect: typeof boundsIntersect;
|
|
12
|
+
readonly boundsContainPosition: typeof boundsContainPosition;
|
|
13
|
+
readonly within: typeof within;
|
|
14
|
+
readonly contains: typeof contains;
|
|
15
|
+
readonly intersects: typeof intersects;
|
|
16
|
+
readonly distanceMeters: typeof distanceMeters;
|
|
17
|
+
readonly distanceToGeometryMeters: typeof distanceToGeometryMeters;
|
|
18
|
+
readonly matches: typeof matchesRelation;
|
|
19
|
+
readonly labelPoint: typeof labelPoint;
|
|
20
|
+
readonly bearingDegrees: typeof bearingDegrees;
|
|
21
|
+
readonly destination: typeof destination;
|
|
22
|
+
readonly lengthMeters: typeof lengthMeters;
|
|
23
|
+
readonly areaSquareMeters: typeof areaSquareMeters;
|
|
24
|
+
readonly measure: typeof measure;
|
|
25
|
+
readonly circlePolygon: typeof circlePolygon;
|
|
26
|
+
readonly ellipsePolygon: typeof ellipsePolygon;
|
|
27
|
+
readonly sectorPolygon: typeof sectorPolygon;
|
|
28
|
+
readonly toGeoJSON: typeof toGeoJSON;
|
|
29
|
+
readonly fromGeoJSON: typeof fromGeoJSON;
|
|
30
|
+
};
|
|
31
|
+
export { RBushSpatialIndex, type SpatialIndex } from './spatial-index.js';
|
|
32
|
+
export { fromLngLat, toFeature, toLngLat } from './geojson.js';
|
|
33
|
+
export { matchesCondition, matchesQuery, rankByRelation, validateConditions } from './query.js';
|
|
34
|
+
export { boundsToPolygon, positionsOf } from './geometry.js';
|
|
35
|
+
export { guessCoordinateColumns, objectsFromGeoJSON, objectsFromRows, type ImportResult } from './import.js';
|
|
36
|
+
export { judgeSight, pathSamples, type SightProfilePoint, type SightResult } from './line-of-sight.js';
|
|
37
|
+
export { volumeMesh, type VolumeMesh, type VolumeShape } from './volume-mesh.js';
|
|
38
|
+
export type { Measurement } from './measure.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { boundsContainPosition, boundsIntersect, geometryBounds, unionBounds, validateGeometry, validatePosition } from './geometry.js';
|
|
2
|
+
import { fromGeoJSON, toGeoJSON } from './geojson.js';
|
|
3
|
+
import { contains, distanceMeters, distanceToGeometryMeters, intersects, labelPoint, matchesRelation, within } from './operations.js';
|
|
4
|
+
import { areaSquareMeters, bearingDegrees, circlePolygon, destination, ellipsePolygon, lengthMeters, measure, sectorPolygon } from './measure.js';
|
|
5
|
+
/** The geometry API for engine modules and applications. Nothing outside `spatial/` imports Turf. */
|
|
6
|
+
export const spatial = {
|
|
7
|
+
validate: validateGeometry,
|
|
8
|
+
validatePosition,
|
|
9
|
+
bounds: geometryBounds,
|
|
10
|
+
unionBounds,
|
|
11
|
+
boundsIntersect,
|
|
12
|
+
boundsContainPosition,
|
|
13
|
+
within,
|
|
14
|
+
contains,
|
|
15
|
+
intersects,
|
|
16
|
+
distanceMeters,
|
|
17
|
+
distanceToGeometryMeters,
|
|
18
|
+
matches: matchesRelation,
|
|
19
|
+
labelPoint,
|
|
20
|
+
bearingDegrees,
|
|
21
|
+
destination,
|
|
22
|
+
lengthMeters,
|
|
23
|
+
areaSquareMeters,
|
|
24
|
+
measure,
|
|
25
|
+
circlePolygon,
|
|
26
|
+
ellipsePolygon,
|
|
27
|
+
sectorPolygon,
|
|
28
|
+
toGeoJSON,
|
|
29
|
+
fromGeoJSON,
|
|
30
|
+
};
|
|
31
|
+
export { RBushSpatialIndex } from './spatial-index.js';
|
|
32
|
+
export { fromLngLat, toFeature, toLngLat } from './geojson.js';
|
|
33
|
+
export { matchesCondition, matchesQuery, rankByRelation, validateConditions } from './query.js';
|
|
34
|
+
export { boundsToPolygon, positionsOf } from './geometry.js';
|
|
35
|
+
export { guessCoordinateColumns, objectsFromGeoJSON, objectsFromRows } from './import.js';
|
|
36
|
+
export { judgeSight, pathSamples } from './line-of-sight.js';
|
|
37
|
+
export { volumeMesh } from './volume-mesh.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line of sight over the ground (phase 3E): given heights along a path, is the far end visible from the near end?
|
|
3
|
+
* Pure maths — the ground heights come from an ElevationProvider or the renderer's terrain.
|
|
4
|
+
*
|
|
5
|
+
* The Earth's curvature raises the ground between the two ends by d·(D−d)/(2·R′), with R′ the effective radius
|
|
6
|
+
* under standard atmospheric refraction (k = 0.13), so long sight lines are judged as a surveyor would.
|
|
7
|
+
*/
|
|
8
|
+
import type { Position } from '../core/types.js';
|
|
9
|
+
/** `count` points from `from` to `to` (both ends included), evenly spaced. Fine for the ranges sight is judged over. */
|
|
10
|
+
export declare function pathSamples(from: Position, to: Position, count: number): Position[];
|
|
11
|
+
export interface SightProfilePoint {
|
|
12
|
+
position: Position;
|
|
13
|
+
/** Ground height (metres above sea level); null where unknown. */
|
|
14
|
+
ground: number | null;
|
|
15
|
+
/** Height of the sight line there. */
|
|
16
|
+
sight: number;
|
|
17
|
+
/** Extra ground height from the Earth's curvature there. */
|
|
18
|
+
bulge: number;
|
|
19
|
+
}
|
|
20
|
+
export interface SightResult {
|
|
21
|
+
visible: boolean;
|
|
22
|
+
/** The first place the ground rises above the sight line. */
|
|
23
|
+
blockedAt?: Position;
|
|
24
|
+
/** The smallest gap between the sight line and the (curved) ground; negative when blocked. */
|
|
25
|
+
clearanceMeters: number;
|
|
26
|
+
distanceMeters: number;
|
|
27
|
+
/** Samples whose ground height was unknown (they cannot block, so the answer may be optimistic). */
|
|
28
|
+
unknownSamples: number;
|
|
29
|
+
profile: SightProfilePoint[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Judges a sight line. `fromHeight` and `toHeight` are the two ends' heights above sea level; `ground` holds the
|
|
33
|
+
* ground height at each of `samples` (from `pathSamples`). The two end samples never block (they are the ends).
|
|
34
|
+
*/
|
|
35
|
+
export declare function judgeSight(samples: Position[], ground: Array<number | null>, fromHeight: number, toHeight: number, refraction?: number): SightResult;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { distanceMeters } from './operations.js';
|
|
2
|
+
const EARTH_RADIUS = 6_371_008.8;
|
|
3
|
+
/** `count` points from `from` to `to` (both ends included), evenly spaced. Fine for the ranges sight is judged over. */
|
|
4
|
+
export function pathSamples(from, to, count) {
|
|
5
|
+
const n = Math.max(2, Math.floor(count));
|
|
6
|
+
return Array.from({ length: n }, (_, i) => {
|
|
7
|
+
const t = i / (n - 1);
|
|
8
|
+
return { latitude: from.latitude + (to.latitude - from.latitude) * t, longitude: from.longitude + (to.longitude - from.longitude) * t };
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Judges a sight line. `fromHeight` and `toHeight` are the two ends' heights above sea level; `ground` holds the
|
|
13
|
+
* ground height at each of `samples` (from `pathSamples`). The two end samples never block (they are the ends).
|
|
14
|
+
*/
|
|
15
|
+
export function judgeSight(samples, ground, fromHeight, toHeight, refraction = 0.13) {
|
|
16
|
+
const total = distanceMeters(samples[0], samples[samples.length - 1]);
|
|
17
|
+
const radius = EARTH_RADIUS / (1 - refraction);
|
|
18
|
+
let clearance = Infinity;
|
|
19
|
+
let blockedAt;
|
|
20
|
+
let unknown = 0;
|
|
21
|
+
const profile = samples.map((position, i) => {
|
|
22
|
+
const t = samples.length === 1 ? 0 : i / (samples.length - 1);
|
|
23
|
+
const d = total * t;
|
|
24
|
+
const bulge = (d * (total - d)) / (2 * radius);
|
|
25
|
+
const sight = fromHeight + (toHeight - fromHeight) * t;
|
|
26
|
+
const g = ground[i] ?? null;
|
|
27
|
+
if (i > 0 && i < samples.length - 1) {
|
|
28
|
+
if (g === null)
|
|
29
|
+
unknown++;
|
|
30
|
+
else {
|
|
31
|
+
const gap = sight - (g + bulge);
|
|
32
|
+
if (gap < clearance)
|
|
33
|
+
clearance = gap;
|
|
34
|
+
if (gap < 0 && !blockedAt)
|
|
35
|
+
blockedAt = position;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { position, ground: g, sight, bulge };
|
|
39
|
+
});
|
|
40
|
+
if (clearance === Infinity)
|
|
41
|
+
clearance = Math.min(fromHeight, toHeight) - Math.max(ground[0] ?? -Infinity, ground[ground.length - 1] ?? -Infinity);
|
|
42
|
+
return { visible: !blockedAt, ...(blockedAt ? { blockedAt } : {}), clearanceMeters: clearance, distanceMeters: total, unknownSamples: unknown, profile };
|
|
43
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Geometry, PolygonGeometry, Position } from '../core/types.js';
|
|
2
|
+
/** Initial bearing from a to b, degrees clockwise from north [0, 360). */
|
|
3
|
+
export declare function bearingDegrees(a: Position, b: Position): number;
|
|
4
|
+
/** The position `meters` away from `from` along `bearing` (degrees clockwise from north). */
|
|
5
|
+
export declare function destination(from: Position, bearing: number, meters: number): Position;
|
|
6
|
+
/** Length of a line (or the sum of a multi-line), metres. */
|
|
7
|
+
export declare function lengthMeters(geometry: Geometry): number;
|
|
8
|
+
/** Area of an area geometry, square metres (holes subtracted); 0 for points and lines. */
|
|
9
|
+
export declare function areaSquareMeters(geometry: Geometry): number;
|
|
10
|
+
export interface Measurement {
|
|
11
|
+
/** Lines: along the line. Areas: the outline (perimeter). */
|
|
12
|
+
lengthMeters: number;
|
|
13
|
+
areaSquareMeters: number;
|
|
14
|
+
/** First point to last, degrees clockwise from north (lines with two or more points). */
|
|
15
|
+
bearingDegrees?: number;
|
|
16
|
+
/** Lines: each segment's length and bearing. */
|
|
17
|
+
segments?: Array<{
|
|
18
|
+
lengthMeters: number;
|
|
19
|
+
bearingDegrees: number;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export declare function measure(geometry: Geometry): Measurement;
|
|
23
|
+
/** A circle as a polygon of `steps` points. */
|
|
24
|
+
export declare function circlePolygon(center: Position, radiusMeters: number, steps?: number): PolygonGeometry;
|
|
25
|
+
/** An ellipse as a polygon: semi-axes in metres, the major axis `orientation` degrees clockwise from north. */
|
|
26
|
+
export declare function ellipsePolygon(center: Position, semiMajor: number, semiMinor: number, orientation?: number, steps?: number): PolygonGeometry;
|
|
27
|
+
/** A sector (pie slice) from `near` to `far` metres between two bearings, as a polygon. */
|
|
28
|
+
export declare function sectorPolygon(center: Position, fromBearing: number, toBearing: number, near: number, far: number, steps?: number): PolygonGeometry;
|