@faclon-labs/app-sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +34 -1
  2. package/dist/SignIn.js +2 -2
  3. package/dist/config.d.ts +5 -0
  4. package/dist/config.js +1 -0
  5. package/dist/map/globe.d.ts +5 -0
  6. package/dist/map/globe.js +5 -0
  7. package/dist/map/index.d.ts +21 -0
  8. package/dist/map/index.js +21 -0
  9. package/dist/map/source.d.ts +46 -0
  10. package/dist/map/source.js +54 -0
  11. package/dist/map/testing.d.ts +5 -0
  12. package/dist/map/testing.js +5 -0
  13. package/dist/session.d.ts +1 -1
  14. package/dist/session.js +1 -1
  15. package/package.json +55 -5
  16. package/vendor/SOURCE.txt +1 -0
  17. package/vendor/map-engine/cesium/camera.d.ts +10 -0
  18. package/vendor/map-engine/cesium/camera.js +25 -0
  19. package/vendor/map-engine/cesium/index.d.ts +2 -0
  20. package/vendor/map-engine/cesium/index.js +2 -0
  21. package/vendor/map-engine/cesium/renderer.d.ts +149 -0
  22. package/vendor/map-engine/cesium/renderer.js +837 -0
  23. package/vendor/map-engine/commands/index.d.ts +2 -0
  24. package/vendor/map-engine/commands/index.js +2 -0
  25. package/vendor/map-engine/commands/post-message.d.ts +32 -0
  26. package/vendor/map-engine/commands/post-message.js +77 -0
  27. package/vendor/map-engine/commands/webmcp.d.ts +17 -0
  28. package/vendor/map-engine/commands/webmcp.js +59 -0
  29. package/vendor/map-engine/core/commands.d.ts +82 -0
  30. package/vendor/map-engine/core/commands.js +46 -0
  31. package/vendor/map-engine/core/emitter.d.ts +9 -0
  32. package/vendor/map-engine/core/emitter.js +38 -0
  33. package/vendor/map-engine/core/equal.d.ts +2 -0
  34. package/vendor/map-engine/core/equal.js +28 -0
  35. package/vendor/map-engine/core/errors.d.ts +20 -0
  36. package/vendor/map-engine/core/errors.js +34 -0
  37. package/vendor/map-engine/core/geometry-family.d.ts +3 -0
  38. package/vendor/map-engine/core/geometry-family.js +13 -0
  39. package/vendor/map-engine/core/index.d.ts +4 -0
  40. package/vendor/map-engine/core/index.js +3 -0
  41. package/vendor/map-engine/core/types.d.ts +709 -0
  42. package/vendor/map-engine/core/types.js +7 -0
  43. package/vendor/map-engine/data/annotations.d.ts +3 -0
  44. package/vendor/map-engine/data/annotations.js +63 -0
  45. package/vendor/map-engine/data/contracts.d.ts +172 -0
  46. package/vendor/map-engine/data/contracts.js +12 -0
  47. package/vendor/map-engine/data/index.d.ts +4 -0
  48. package/vendor/map-engine/data/index.js +3 -0
  49. package/vendor/map-engine/data/schema.d.ts +7 -0
  50. package/vendor/map-engine/data/schema.js +40 -0
  51. package/vendor/map-engine/geofence/evaluator.d.ts +35 -0
  52. package/vendor/map-engine/geofence/evaluator.js +172 -0
  53. package/vendor/map-engine/geofence/history.d.ts +9 -0
  54. package/vendor/map-engine/geofence/history.js +19 -0
  55. package/vendor/map-engine/geofence/index.d.ts +2 -0
  56. package/vendor/map-engine/geofence/index.js +2 -0
  57. package/vendor/map-engine/index.d.ts +27 -0
  58. package/vendor/map-engine/index.js +14 -0
  59. package/vendor/map-engine/layers/index.d.ts +1 -0
  60. package/vendor/map-engine/layers/index.js +1 -0
  61. package/vendor/map-engine/layers/layers.d.ts +49 -0
  62. package/vendor/map-engine/layers/layers.js +316 -0
  63. package/vendor/map-engine/maplibre/deck-host.d.ts +105 -0
  64. package/vendor/map-engine/maplibre/deck-host.js +401 -0
  65. package/vendor/map-engine/maplibre/icons.d.ts +17 -0
  66. package/vendor/map-engine/maplibre/icons.js +65 -0
  67. package/vendor/map-engine/maplibre/index.d.ts +3 -0
  68. package/vendor/map-engine/maplibre/index.js +2 -0
  69. package/vendor/map-engine/maplibre/map-like.d.ts +56 -0
  70. package/vendor/map-engine/maplibre/map-like.js +1 -0
  71. package/vendor/map-engine/maplibre/renderer.d.ts +173 -0
  72. package/vendor/map-engine/maplibre/renderer.js +889 -0
  73. package/vendor/map-engine/maplibre/style.d.ts +84 -0
  74. package/vendor/map-engine/maplibre/style.js +493 -0
  75. package/vendor/map-engine/present/decorations.d.ts +25 -0
  76. package/vendor/map-engine/present/decorations.js +115 -0
  77. package/vendor/map-engine/present/index.d.ts +2 -0
  78. package/vendor/map-engine/present/index.js +2 -0
  79. package/vendor/map-engine/present/present.d.ts +47 -0
  80. package/vendor/map-engine/present/present.js +149 -0
  81. package/vendor/map-engine/query/index.d.ts +1 -0
  82. package/vendor/map-engine/query/index.js +1 -0
  83. package/vendor/map-engine/query/planner.d.ts +26 -0
  84. package/vendor/map-engine/query/planner.js +47 -0
  85. package/vendor/map-engine/react/MapView.d.ts +87 -0
  86. package/vendor/map-engine/react/MapView.js +270 -0
  87. package/vendor/map-engine/react/context.d.ts +39 -0
  88. package/vendor/map-engine/react/context.js +94 -0
  89. package/vendor/map-engine/react/index.d.ts +2 -0
  90. package/vendor/map-engine/react/index.js +2 -0
  91. package/vendor/map-engine/react/store.d.ts +19 -0
  92. package/vendor/map-engine/react/store.js +18 -0
  93. package/vendor/map-engine/realtime/apply.d.ts +74 -0
  94. package/vendor/map-engine/realtime/apply.js +155 -0
  95. package/vendor/map-engine/realtime/index.d.ts +1 -0
  96. package/vendor/map-engine/realtime/index.js +1 -0
  97. package/vendor/map-engine/render/index.d.ts +1 -0
  98. package/vendor/map-engine/render/index.js +1 -0
  99. package/vendor/map-engine/render/offline.d.ts +8 -0
  100. package/vendor/map-engine/render/offline.js +47 -0
  101. package/vendor/map-engine/render/renderer.d.ts +74 -0
  102. package/vendor/map-engine/render/renderer.js +1 -0
  103. package/vendor/map-engine/render/terrarium.d.ts +48 -0
  104. package/vendor/map-engine/render/terrarium.js +110 -0
  105. package/vendor/map-engine/runtime/drawing.d.ts +28 -0
  106. package/vendor/map-engine/runtime/drawing.js +58 -0
  107. package/vendor/map-engine/runtime/index.d.ts +5 -0
  108. package/vendor/map-engine/runtime/index.js +2 -0
  109. package/vendor/map-engine/runtime/offline.d.ts +24 -0
  110. package/vendor/map-engine/runtime/offline.js +37 -0
  111. package/vendor/map-engine/runtime/overlays.d.ts +25 -0
  112. package/vendor/map-engine/runtime/overlays.js +65 -0
  113. package/vendor/map-engine/runtime/runtime.d.ts +4 -0
  114. package/vendor/map-engine/runtime/runtime.js +2105 -0
  115. package/vendor/map-engine/runtime/types.d.ts +407 -0
  116. package/vendor/map-engine/runtime/types.js +1 -0
  117. package/vendor/map-engine/scene/index.d.ts +1 -0
  118. package/vendor/map-engine/scene/index.js +1 -0
  119. package/vendor/map-engine/scene/scene.d.ts +31 -0
  120. package/vendor/map-engine/scene/scene.js +216 -0
  121. package/vendor/map-engine/selection/index.d.ts +1 -0
  122. package/vendor/map-engine/selection/index.js +1 -0
  123. package/vendor/map-engine/selection/selection.d.ts +12 -0
  124. package/vendor/map-engine/selection/selection.js +28 -0
  125. package/vendor/map-engine/spatial/geojson.d.ts +15 -0
  126. package/vendor/map-engine/spatial/geojson.js +83 -0
  127. package/vendor/map-engine/spatial/geometry.d.ts +13 -0
  128. package/vendor/map-engine/spatial/geometry.js +206 -0
  129. package/vendor/map-engine/spatial/import.d.ts +27 -0
  130. package/vendor/map-engine/spatial/import.js +96 -0
  131. package/vendor/map-engine/spatial/index.d.ts +38 -0
  132. package/vendor/map-engine/spatial/index.js +37 -0
  133. package/vendor/map-engine/spatial/line-of-sight.d.ts +35 -0
  134. package/vendor/map-engine/spatial/line-of-sight.js +43 -0
  135. package/vendor/map-engine/spatial/measure.d.ts +28 -0
  136. package/vendor/map-engine/spatial/measure.js +90 -0
  137. package/vendor/map-engine/spatial/operations.d.ts +21 -0
  138. package/vendor/map-engine/spatial/operations.js +171 -0
  139. package/vendor/map-engine/spatial/query.d.ts +17 -0
  140. package/vendor/map-engine/spatial/query.js +88 -0
  141. package/vendor/map-engine/spatial/spatial-index.d.ts +24 -0
  142. package/vendor/map-engine/spatial/spatial-index.js +36 -0
  143. package/vendor/map-engine/spatial/volume-mesh.d.ts +19 -0
  144. package/vendor/map-engine/spatial/volume-mesh.js +56 -0
  145. package/vendor/map-engine/temporal/index.d.ts +3 -0
  146. package/vendor/map-engine/temporal/index.js +3 -0
  147. package/vendor/map-engine/temporal/journal.d.ts +20 -0
  148. package/vendor/map-engine/temporal/journal.js +34 -0
  149. package/vendor/map-engine/temporal/playback.d.ts +41 -0
  150. package/vendor/map-engine/temporal/playback.js +80 -0
  151. package/vendor/map-engine/temporal/tracks.d.ts +33 -0
  152. package/vendor/map-engine/temporal/tracks.js +119 -0
  153. package/vendor/map-engine/testing/annotation-store.d.ts +24 -0
  154. package/vendor/map-engine/testing/annotation-store.js +43 -0
  155. package/vendor/map-engine/testing/contracts.d.ts +41 -0
  156. package/vendor/map-engine/testing/contracts.js +328 -0
  157. package/vendor/map-engine/testing/fixtures-monitoring.d.ts +22 -0
  158. package/vendor/map-engine/testing/fixtures-monitoring.js +103 -0
  159. package/vendor/map-engine/testing/fixtures.d.ts +18 -0
  160. package/vendor/map-engine/testing/fixtures.js +28 -0
  161. package/vendor/map-engine/testing/index.d.ts +7 -0
  162. package/vendor/map-engine/testing/index.js +7 -0
  163. package/vendor/map-engine/testing/mock-ontology.d.ts +13 -0
  164. package/vendor/map-engine/testing/mock-ontology.js +24 -0
  165. package/vendor/map-engine/testing/mock-provider.d.ts +97 -0
  166. package/vendor/map-engine/testing/mock-provider.js +369 -0
  167. package/vendor/map-engine/testing/test-renderer.d.ts +72 -0
  168. package/vendor/map-engine/testing/test-renderer.js +153 -0
  169. package/vendor/map-engine/viewport/index.d.ts +1 -0
  170. package/vendor/map-engine/viewport/index.js +1 -0
  171. package/vendor/map-engine/viewport/viewport.d.ts +19 -0
  172. package/vendor/map-engine/viewport/viewport.js +102 -0
  173. package/vendor/map-provider-uns/client.d.ts +75 -0
  174. package/vendor/map-provider-uns/client.js +111 -0
  175. package/vendor/map-provider-uns/index.d.ts +2 -0
  176. package/vendor/map-provider-uns/index.js +2 -0
  177. package/vendor/map-provider-uns/provider.d.ts +131 -0
  178. package/vendor/map-provider-uns/provider.js +561 -0
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Contract suites (doc §66): the rules every provider, renderer and spatial index must keep, written once and run
3
+ * against each implementation. They are plain checks — no test framework — so any runner can use them:
4
+ *
5
+ * for (const c of providerContract({ create, sample })) it(c.name, c.run);
6
+ *
7
+ * A failing check throws a ContractViolation that names the rule.
8
+ */
9
+ import { isAbortError } from '../core/errors.js';
10
+ import { hasCatalog } from '../data/contracts.js';
11
+ import { geometryBounds, validateGeometry, validatePosition } from '../spatial/geometry.js';
12
+ import { intersectsBounds } from '../spatial/operations.js';
13
+ export class ContractViolation extends Error {
14
+ name = 'ContractViolation';
15
+ }
16
+ function must(condition, message) {
17
+ if (!condition)
18
+ throw new ContractViolation(message);
19
+ }
20
+ const check = (name, run) => ({ name, run: async () => run() });
21
+ const isRealtime = (p) => typeof p.subscribe === 'function';
22
+ const isTrack = (p) => typeof p.getTracks === 'function';
23
+ function validObjects(list, where) {
24
+ must(Array.isArray(list), `${where} must resolve to an array`);
25
+ const ids = new Set();
26
+ for (const o of list) {
27
+ must(o && typeof o.objectId === 'string' && o.objectId !== '', `${where}: every object needs a non-empty string objectId`);
28
+ must(!ids.has(o.objectId), `${where}: objectId ${o.objectId} appears twice`);
29
+ ids.add(o.objectId);
30
+ const g = validateGeometry(o.geometry);
31
+ must(g.valid, `${where}: object ${o.objectId} has invalid geometry (${g.valid ? '' : g.reason})`);
32
+ must(o.observedAt === undefined || Number.isFinite(Date.parse(o.observedAt)), `${where}: object ${o.objectId} observedAt is not an ISO time`);
33
+ }
34
+ return list;
35
+ }
36
+ /**
37
+ * The SpatialDataProvider contract (doc §26, ADR-005). Providers may return MORE than a query asks for (the runtime
38
+ * re-checks every object), but never less: a matching object must not be left out.
39
+ */
40
+ export function providerContract(o) {
41
+ const { sample } = o;
42
+ const scope = sample.ontologyType ? { ontologyType: sample.ontologyType } : { objectIds: [sample.objectId] };
43
+ const find = async (p, q) => validObjects(await p.query(q), 'query()').find((x) => x.objectId === sample.objectId);
44
+ const checks = [
45
+ check('provider: query returns valid objects with unique ids, including the sample', async () => {
46
+ must(await find(o.create(), scope), `query(${JSON.stringify(scope)}) left out the sample ${sample.objectId}`);
47
+ }),
48
+ check('provider: never leaves out a matching object when every filter is set', async () => {
49
+ const p = o.create();
50
+ const first = await find(p, scope);
51
+ must(first, 'sample not found');
52
+ const b = geometryBounds(first.geometry);
53
+ const pad = 0.01;
54
+ const q = {
55
+ ...scope,
56
+ objectIds: [sample.objectId],
57
+ bounds: { west: b.west - pad, south: b.south - pad, east: b.east + pad, north: b.north + pad },
58
+ zoom: 12,
59
+ ...(sample.property ? { where: [{ property: sample.property.name, op: 'eq', value: sample.property.value }], filter: { [sample.property.name]: sample.property.value } } : {}),
60
+ };
61
+ must(await find(p, q), `query(${JSON.stringify(q)}) left out the sample it matches`);
62
+ }),
63
+ check('provider: honours bounds when it claims spatial query support', async () => {
64
+ const p = o.create();
65
+ if (!p.capabilities.supportsSpatialQuery || !o.outside)
66
+ return;
67
+ must(!(await find(p, { ...scope, bounds: o.outside })), 'supportsSpatialQuery is true but an object outside the bounds was returned');
68
+ }),
69
+ check('provider: rejects an aborted query with an AbortError', async () => {
70
+ const p = o.create();
71
+ await p.query(scope); // warm caches: an aborted call must reject even when it could answer from memory
72
+ const c = new AbortController();
73
+ c.abort();
74
+ let error = null;
75
+ try {
76
+ await p.query(scope, c.signal);
77
+ }
78
+ catch (e) {
79
+ error = e;
80
+ }
81
+ must(error !== null, 'query() resolved although its signal was already aborted');
82
+ must(isAbortError(error), `an aborted query must reject with an AbortError, got ${String(error)}`);
83
+ }),
84
+ check('provider: leaves the caller’s query unchanged', async () => {
85
+ const q = { ...scope, where: [{ property: 'x', op: 'exists' }], bounds: { west: -180, south: -85, east: 180, north: 85 } };
86
+ const before = JSON.stringify(q);
87
+ await o.create().query(q);
88
+ must(JSON.stringify(q) === before, 'query() changed the query object it was given');
89
+ }),
90
+ check('provider: returns copies (changing a result does not change the next one)', async () => {
91
+ const p = o.create();
92
+ const a = await find(p, scope);
93
+ must(a, 'sample not found');
94
+ // Change it in place, as a careless caller would: properties and geometry.
95
+ if (a.properties)
96
+ a.properties.__contract = 'mutated';
97
+ if (a.geometry.type === 'Point')
98
+ a.geometry.coordinates.latitude = -89;
99
+ const b = await find(p, scope);
100
+ must(b, 'sample not found the second time');
101
+ must(b.properties?.__contract === undefined, 'a later query returned properties changed by the caller');
102
+ must(b.geometry.type !== 'Point' || b.geometry.coordinates.latitude !== -89, 'a later query returned a geometry changed by the caller');
103
+ }),
104
+ check('provider: getCurrentState returns known ids and skips unknown ones', async () => {
105
+ const list = validObjects(await o.create().getCurrentState([sample.objectId, '__contract_unknown__']), 'getCurrentState()');
106
+ must(list.some((x) => x.objectId === sample.objectId), 'getCurrentState left out a known id');
107
+ must(!list.some((x) => x.objectId === '__contract_unknown__'), 'getCurrentState returned an id that does not exist');
108
+ }),
109
+ ];
110
+ const probe = o.create();
111
+ if (isRealtime(probe)) {
112
+ checks.push(check('realtime: subscribe returns an unsubscribe that can be called twice', () => {
113
+ const p = o.create();
114
+ const off = p.subscribe(scope, () => { });
115
+ must(typeof off === 'function', 'subscribe must return a function');
116
+ off();
117
+ off();
118
+ }));
119
+ }
120
+ if (isTrack(probe) && o.history) {
121
+ const range = o.history;
122
+ checks.push(check('history: tracks have time-ordered, valid points inside the range', async () => {
123
+ const tracks = await o.create().getTracks({ ...scope, geometryType: 'Point' }, range);
124
+ must(Array.isArray(tracks), 'getTracks must resolve to an array');
125
+ const ids = new Set();
126
+ const start = Date.parse(range.start);
127
+ const end = Date.parse(range.end);
128
+ for (const t of tracks) {
129
+ must(!ids.has(t.objectId), `track ${t.objectId} appears twice`);
130
+ ids.add(t.objectId);
131
+ let last = -Infinity;
132
+ for (const pt of t.points) {
133
+ const ms = Date.parse(pt.timestamp);
134
+ must(Number.isFinite(ms), `track ${t.objectId}: timestamp ${pt.timestamp} is not an ISO time`);
135
+ must(ms >= last, `track ${t.objectId}: points are not in time order`);
136
+ must(ms >= start && ms <= end, `track ${t.objectId}: point ${pt.timestamp} is outside the range`);
137
+ must(validatePosition(pt).valid, `track ${t.objectId}: point ${pt.timestamp} has an invalid position`);
138
+ last = ms;
139
+ }
140
+ }
141
+ must(tracks.some((t) => t.objectId === sample.objectId && t.points.length > 0), `no history for the sample ${sample.objectId} in the range`);
142
+ }));
143
+ }
144
+ if (hasCatalog(probe)) {
145
+ checks.push(check('catalog: datasets describe type, geometry family, count and properties', async () => {
146
+ const list = await o.create().listDatasets();
147
+ must(Array.isArray(list), 'listDatasets must resolve to an array');
148
+ for (const d of list) {
149
+ must(typeof d.ontologyType === 'string' && d.ontologyType !== '', 'every dataset needs an ontologyType');
150
+ must(['point', 'line', 'area', 'mixed'].includes(d.geometryFamily), `dataset ${String(d.ontologyType)}: geometryFamily must be point, line, area or mixed`);
151
+ must(typeof d.count === 'number' && d.count >= 0, `dataset ${String(d.ontologyType)}: count must be a number >= 0`);
152
+ must(Array.isArray(d.properties), `dataset ${String(d.ontologyType)}: properties must be an array`);
153
+ }
154
+ if (sample.ontologyType)
155
+ must(list.some((d) => d.ontologyType === sample.ontologyType), `the sample's type ${sample.ontologyType} is not listed`);
156
+ }));
157
+ }
158
+ return checks;
159
+ }
160
+ const VIEWPORT = { center: { latitude: 19.08, longitude: 72.88 }, zoom: 11 };
161
+ const LAYERS = [
162
+ { id: 'c-pts', source: { ontologyType: 'P' }, visible: true, zIndex: 3, visualization: { type: 'marker' } },
163
+ { id: 'c-lines', source: { ontologyType: 'L' }, visible: true, zIndex: 2, visualization: { type: 'line' } },
164
+ { id: 'c-areas', source: { ontologyType: 'A' }, visible: true, zIndex: 1, visualization: { type: 'polygon' } },
165
+ ];
166
+ const sceneObject = (layerId, object) => ({ key: `${layerId}:${object.objectId}`, layerId, object, selected: false, hovered: false });
167
+ const P1 = sceneObject('c-pts', { objectId: 'p1', geometry: { type: 'Point', coordinates: { latitude: 19.08, longitude: 72.88 } }, properties: { status: 'ok' } });
168
+ const L1 = sceneObject('c-lines', { objectId: 'l1', geometry: { type: 'LineString', coordinates: [{ latitude: 19.07, longitude: 72.87 }, { latitude: 19.09, longitude: 72.89 }] } });
169
+ const A1 = sceneObject('c-areas', {
170
+ objectId: 'a1',
171
+ geometry: { type: 'Polygon', coordinates: [[{ latitude: 19.06, longitude: 72.86 }, { latitude: 19.06, longitude: 72.9 }, { latitude: 19.1, longitude: 72.9 }, { latitude: 19.06, longitude: 72.86 }]] },
172
+ });
173
+ const SCENE = { viewport: VIEWPORT, layers: LAYERS, objects: [P1, L1, A1] };
174
+ /** Bounds a promise in time, so a renderer that hangs fails the check instead of the run. */
175
+ async function within(ms, p, what) {
176
+ let timer;
177
+ try {
178
+ return await Promise.race([p, new Promise((_, reject) => (timer = setTimeout(() => reject(new ContractViolation(`${what} did not settle within ${ms} ms`)), ms)))]);
179
+ }
180
+ finally {
181
+ clearTimeout(timer);
182
+ }
183
+ }
184
+ /** The Renderer contract (doc §22, §65): bidirectional, incremental, and fully released by destroy(). */
185
+ export function rendererContract(o) {
186
+ const start = async () => {
187
+ const r = o.create();
188
+ await within(5000, r.initialize(o.container(), { basemap: { kind: 'none' }, viewport: VIEWPORT }), 'initialize()');
189
+ return r;
190
+ };
191
+ return [
192
+ check('renderer: initialize resolves and the size is a real size', async () => {
193
+ const r = await start();
194
+ const s = r.getSize();
195
+ must(Number.isFinite(s.width) && Number.isFinite(s.height) && s.width >= 0 && s.height >= 0, 'getSize() must return finite, non-negative numbers');
196
+ r.destroy();
197
+ }),
198
+ check('renderer: draws a full scene, then patches (add, update, remove, layer change)', async () => {
199
+ const r = await start();
200
+ r.render(SCENE);
201
+ r.update({ updated: [{ ...P1, object: { ...P1.object, geometry: { type: 'Point', coordinates: { latitude: 19.081, longitude: 72.881 } } }, selected: true }] });
202
+ r.update({ added: [sceneObject('c-pts', { objectId: 'p2', geometry: { type: 'Point', coordinates: { latitude: 19.07, longitude: 72.87 } } })] });
203
+ r.update({ removed: ['c-lines:l1'] });
204
+ r.update({ layers: LAYERS.map((l) => (l.id === 'c-pts' ? { ...l, visible: false } : l)) });
205
+ r.update({ layers: LAYERS.filter((l) => l.id !== 'c-areas'), removed: ['c-areas:a1'] });
206
+ r.setViewport({ ...VIEWPORT, zoom: 12 });
207
+ r.resize();
208
+ r.destroy();
209
+ }),
210
+ check('renderer: capabilities, when declared, are yes/no flags it can stand behind', async () => {
211
+ const r = o.create();
212
+ const caps = r.capabilities ?? {};
213
+ for (const [k, v] of Object.entries(caps))
214
+ must(typeof v === 'boolean', `capabilities.${k} must be true or false`);
215
+ if (caps.elevation)
216
+ must(typeof r.sampleElevation === 'function', 'capabilities.elevation needs sampleElevation()');
217
+ if (caps.globe)
218
+ must(typeof r.setProjection === 'function', 'capabilities.globe needs setProjection()');
219
+ if (caps.terrain)
220
+ must(typeof r.setTerrain === 'function', 'capabilities.terrain needs setTerrain()');
221
+ if (caps.tiles3d)
222
+ must(typeof r.setTilesets === 'function', 'capabilities.tiles3d needs setTilesets()');
223
+ r.destroy();
224
+ }),
225
+ check('renderer: every listener returns an unsubscribe that can be called twice', async () => {
226
+ const r = await start();
227
+ for (const off of [r.onViewportChange(() => { }), r.onPick(() => { }), r.onMapClick(() => { }), r.onHover(() => { }), r.onError(() => { }), ...(r.onCameraMove ? [r.onCameraMove(() => { })] : [])]) {
228
+ must(typeof off === 'function', 'listener registration must return a function');
229
+ off();
230
+ off();
231
+ }
232
+ r.destroy();
233
+ }),
234
+ check('renderer: destroy is idempotent and later calls are harmless', async () => {
235
+ const r = await start();
236
+ r.render(SCENE);
237
+ r.destroy();
238
+ r.destroy();
239
+ r.update({ removed: ['c-pts:p1'] });
240
+ r.render(SCENE);
241
+ r.setViewport(VIEWPORT);
242
+ r.resize();
243
+ r.project?.(VIEWPORT.center); // null is fine; throwing is not
244
+ }),
245
+ check('renderer: can be initialized again after destroy (unmount, then mount)', async () => {
246
+ const r = await start();
247
+ r.render(SCENE);
248
+ r.destroy();
249
+ await within(5000, r.initialize(o.container(), { basemap: { kind: 'none' }, viewport: VIEWPORT }), 'second initialize()');
250
+ r.render(SCENE);
251
+ r.destroy();
252
+ }),
253
+ check('renderer: destroy during initialize settles it promptly', async () => {
254
+ const r = o.create();
255
+ const pending = r.initialize(o.container(), { basemap: { kind: 'none' }, viewport: VIEWPORT });
256
+ r.destroy();
257
+ await within(2000, pending.then(() => undefined, () => undefined), 'an initialize() cancelled by destroy()');
258
+ }),
259
+ check('renderer: setBasemap (when offered) keeps working, and the scene can be drawn again', async () => {
260
+ const r = await start();
261
+ if (!r.setBasemap)
262
+ return r.destroy();
263
+ r.render(SCENE);
264
+ await within(5000, r.setBasemap({ kind: 'none', background: '#FFFFFF' }), 'setBasemap()');
265
+ r.render(SCENE);
266
+ r.destroy();
267
+ }),
268
+ ];
269
+ }
270
+ // ---------------------------------------------------------------- spatial indexes
271
+ /** The SpatialIndex contract (doc §11): any index (RBush today, others later) answers like a full scan. */
272
+ export function spatialIndexContract(create) {
273
+ // A deterministic 20 × 20 grid of points plus some lines and areas (doc §69: no random coordinates).
274
+ const objects = [];
275
+ for (let i = 0; i < 20; i++)
276
+ for (let j = 0; j < 20; j++)
277
+ objects.push({ objectId: `p-${i}-${j}`, geometry: { type: 'Point', coordinates: { latitude: 10 + i * 0.1, longitude: 20 + j * 0.1 } } });
278
+ for (let k = 0; k < 10; k++) {
279
+ const a = 10 + k * 0.2;
280
+ objects.push({ objectId: `l-${k}`, geometry: { type: 'LineString', coordinates: [{ latitude: a, longitude: 20 }, { latitude: a + 0.3, longitude: 21.5 }] } });
281
+ objects.push({ objectId: `a-${k}`, geometry: { type: 'Circle', center: { latitude: a, longitude: 20.5 + k * 0.1 }, radiusMeters: 5000 } });
282
+ }
283
+ const boxes = [
284
+ { west: 20.25, south: 10.25, east: 20.75, north: 10.75 },
285
+ { west: 19, south: 9, east: 19.5, north: 9.5 },
286
+ { west: 20, south: 10, east: 22, north: 12 },
287
+ { west: 20.95, south: 11.05, east: 21.05, north: 11.15 },
288
+ ];
289
+ const ids = (list) => list.map((x) => x.objectId).sort();
290
+ const filled = () => {
291
+ const index = create();
292
+ for (const x of objects)
293
+ index.insert(x);
294
+ return index;
295
+ };
296
+ return [
297
+ check('index: search finds exactly what a full scan finds', () => {
298
+ const index = filled();
299
+ must(index.size === objects.length, `size is ${index.size}, expected ${objects.length}`);
300
+ for (const b of boxes) {
301
+ const want = ids(objects.filter((x) => intersectsBounds(x.geometry, b)));
302
+ const got = ids(index.search(b));
303
+ // An index returns candidates: it may add bounding-box neighbours, but never miss a match.
304
+ must(want.every((id) => got.includes(id)), `search(${JSON.stringify(b)}) missed ${want.filter((id) => !got.includes(id)).join(', ')}`);
305
+ for (const id of got)
306
+ must(objects.find((x) => x.objectId === id), `search returned unknown ${id}`);
307
+ }
308
+ }),
309
+ check('index: insert with an existing id replaces it', () => {
310
+ const index = filled();
311
+ index.insert({ objectId: 'p-0-0', geometry: { type: 'Point', coordinates: { latitude: -40, longitude: -40 } } });
312
+ must(index.size === objects.length, 'replacing an object changed the size');
313
+ must(!ids(index.search(boxes[2])).includes('p-0-0'), 'the old position is still indexed');
314
+ must(ids(index.search({ west: -41, south: -41, east: -39, north: -39 })).includes('p-0-0'), 'the new position is not indexed');
315
+ must(index.get('p-0-0').geometry.coordinates.latitude === -40, 'get() returns the old object');
316
+ }),
317
+ check('index: remove, get, all and clear', () => {
318
+ const index = filled();
319
+ index.remove('p-1-1');
320
+ index.remove('__missing__');
321
+ must(index.get('p-1-1') === undefined && index.size === objects.length - 1, 'remove() did not remove');
322
+ must(!ids(index.search(boxes[2])).includes('p-1-1'), 'a removed object is still found by search');
323
+ must(index.all().length === objects.length - 1, 'all() does not match size');
324
+ index.clear();
325
+ must(index.size === 0 && index.all().length === 0 && index.search(boxes[2]).length === 0, 'clear() left objects behind');
326
+ }),
327
+ ];
328
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Deterministic DSS-shaped fixtures (doc §68–69, Milestone 4): platforms, sensors and their coverage, observations,
3
+ * fused entities, zones, events and tracks. Fictional coordinates and names; no randomness, so a failure reproduces.
4
+ * Domain names live ONLY here and in applications — never in engine code.
5
+ */
6
+ import type { GeometryType, Position, TrackPoint } from '../core/types.js';
7
+ import type { MockRecord, MockSpatialProvider } from './mock-provider.js';
8
+ /** Every fixture timestamp is relative to this instant. */
9
+ export declare const DSS_EPOCH = "2026-10-01T08:00:00Z";
10
+ /** The types and the geometry each declares (what an ontology would say). */
11
+ export declare const DSS_ONTOLOGY: Record<string, GeometryType>;
12
+ /**
13
+ * Track paths, as functions of minutes from the epoch (0…60). Deterministic, smooth, and crossing the zones so
14
+ * geofencing and replay have something to show.
15
+ */
16
+ export declare const DSS_TRACKS: Record<string, (minute: number) => Position>;
17
+ /** All DSS fixture records, typed for the mock provider. */
18
+ export declare const DSS_RECORDS: MockRecord[];
19
+ /** Track points every minute from the epoch to +60 min, for every tracked object. */
20
+ export declare function dssTrackPoints(id: string): TrackPoint[];
21
+ /** Seeds the tracks into a mock provider (replay, trails, history-based geofence events). */
22
+ export declare function seedDssHistory(provider: MockSpatialProvider): void;
@@ -0,0 +1,103 @@
1
+ const p = (latitude, longitude) => ({ latitude, longitude });
2
+ const ring = (pts) => [...pts, pts[0]].map(([la, lo]) => p(la, lo));
3
+ /** Every fixture timestamp is relative to this instant. */
4
+ export const DSS_EPOCH = '2026-10-01T08:00:00Z';
5
+ const T0 = Date.parse(DSS_EPOCH);
6
+ const at = (minutes) => new Date(T0 + minutes * 60_000).toISOString();
7
+ /** The types and the geometry each declares (what an ontology would say). */
8
+ export const DSS_ONTOLOGY = {
9
+ Platform: 'Point',
10
+ Sensor: 'Point',
11
+ SensorCoverage: 'Circle',
12
+ Observation: 'Point',
13
+ FusedEntity: 'Point',
14
+ Zone: 'Polygon',
15
+ Event: 'Point',
16
+ };
17
+ /**
18
+ * Track paths, as functions of minutes from the epoch (0…60). Deterministic, smooth, and crossing the zones so
19
+ * geofencing and replay have something to show.
20
+ */
21
+ export const DSS_TRACKS = {
22
+ 'platform-001': (m) => p(19.08 + 0.02 * Math.sin(m / 9), 72.87 + 0.025 * Math.cos(m / 9)), // air patrol orbit
23
+ 'platform-002': (m) => p(19.13 - 0.0012 * m, 72.95 - 0.0015 * m), // air transit, north-east to south-west
24
+ 'platform-003': (m) => p(19.03 + 0.0009 * m, 72.84 + 0.0011 * m), // ground patrol along the road
25
+ 'fused-entity-001': (m) => p(19.15 - 0.0012 * m, 72.9 + 0.0002 * m), // unknown contact heading into the restricted zone
26
+ };
27
+ const heading = (id, m) => {
28
+ const a = DSS_TRACKS[id](m);
29
+ const b = DSS_TRACKS[id](m + 1);
30
+ return Math.round(((Math.atan2(b.longitude - a.longitude, b.latitude - a.latitude) * 180) / Math.PI + 360) % 360);
31
+ };
32
+ const point = (id, pos, properties, observedAt = DSS_EPOCH) => ({
33
+ objectId: id,
34
+ geometry: { type: 'Point', coordinates: pos },
35
+ properties,
36
+ observedAt,
37
+ });
38
+ const PLATFORMS = [
39
+ point('platform-001', DSS_TRACKS['platform-001'](60), { label: 'Falcon One', domain: 'air', role: 'surveillance', status: 'on-mission', fuel: 64, altitude: 1800, speed: 160, heading: heading('platform-001', 60) }, at(60)),
40
+ point('platform-002', DSS_TRACKS['platform-002'](60), { label: 'Falcon Two', domain: 'air', role: 'transport', status: 'on-mission', fuel: 22, altitude: 3200, speed: 240, heading: heading('platform-002', 60) }, at(60)),
41
+ point('platform-003', DSS_TRACKS['platform-003'](60), { label: 'Ranger 3', domain: 'ground', role: 'patrol', status: 'on-mission', fuel: 81, altitude: 0, speed: 38, heading: heading('platform-003', 60) }, at(60)),
42
+ point('platform-004', p(19.045, 72.905), { label: 'Ranger 4', domain: 'ground', role: 'patrol', status: 'standby', fuel: 100, altitude: 0, speed: 0, heading: 0 }),
43
+ ];
44
+ const SENSORS = [
45
+ { object: point('sensor-001', p(19.06, 72.86), { label: 'Radar North', kind: 'radar', status: 'online', rangeMeters: 9000 }), rangeMeters: 9000 },
46
+ { object: point('sensor-002', p(19.11, 72.93), { label: 'EO Tower 2', kind: 'electro-optical', status: 'online', rangeMeters: 5000 }), rangeMeters: 5000 },
47
+ { object: point('sensor-003', p(19.02, 72.92), { label: 'Acoustic 3', kind: 'acoustic', status: 'degraded', rangeMeters: 3000 }), rangeMeters: 3000 },
48
+ ];
49
+ const COVERAGE = SENSORS.map(({ object, rangeMeters }) => ({
50
+ objectId: `${object.objectId}-coverage`,
51
+ geometry: { type: 'Circle', center: object.geometry.coordinates, radiusMeters: rangeMeters },
52
+ properties: { label: `${object.properties.label} coverage`, sensorId: object.objectId, kind: object.properties.kind, status: object.properties.status },
53
+ }));
54
+ const CLASSES = ['vehicle', 'person', 'aircraft', 'unknown'];
55
+ /** A fixed, irregular-looking spread (golden-angle spiral around the sensors' area); deterministic, no randomness. */
56
+ const scatter = (i) => {
57
+ const a = i * 2.39996;
58
+ const r = 0.012 + 0.0045 * i;
59
+ return p(19.075 + r * Math.sin(a), 72.895 + r * 1.15 * Math.cos(a));
60
+ };
61
+ /** Twelve observations, each tied to a sensor, with a fixed confidence. */
62
+ const OBSERVATIONS = Array.from({ length: 12 }, (_, i) => point(`observation-${String(i + 1).padStart(3, '0')}`, scatter(i), { label: `OBS-${i + 1}`, sensorId: SENSORS[i % 3].object.objectId, classification: CLASSES[i % 4], confidence: 50 + ((i * 7) % 50) }, at(30 + i * 2)));
63
+ const FUSED = [
64
+ point('fused-entity-001', DSS_TRACKS['fused-entity-001'](60), { label: 'Contact Alpha', classification: 'unknown', threat: 'high', confidence: 78, sources: 3, heading: heading('fused-entity-001', 60) }, at(60)),
65
+ point('fused-entity-002', p(19.07, 72.94), { label: 'Contact Bravo', classification: 'vehicle', threat: 'medium', confidence: 64, sources: 2, heading: 270 }, at(55)),
66
+ point('fused-entity-003', p(19.01, 72.87), { label: 'Contact Charlie', classification: 'person', threat: 'low', confidence: 52, sources: 1, heading: 0 }, at(50)),
67
+ ];
68
+ const ZONES = [
69
+ { objectId: 'zone-001', geometry: { type: 'Polygon', coordinates: [ring([[19.1, 72.86], [19.1, 72.92], [19.06, 72.92], [19.06, 72.86]])] }, properties: { label: 'Restricted Area A', kind: 'restricted', level: 'high' } },
70
+ {
71
+ objectId: 'zone-002',
72
+ geometry: { type: 'MultiPolygon', coordinates: [[ring([[19.03, 72.82], [19.03, 72.85], [19.01, 72.85], [19.01, 72.82]])], [ring([[19.13, 72.95], [19.13, 72.98], [19.11, 72.98], [19.11, 72.95]])]] },
73
+ properties: { label: 'Patrol Areas', kind: 'patrol', level: 'medium' },
74
+ },
75
+ { objectId: 'zone-003', geometry: { type: 'Polygon', coordinates: [ring([[19.05, 72.95], [19.05, 72.99], [19.02, 72.99], [19.02, 72.95]])] }, properties: { label: 'Safe Zone', kind: 'safe', level: 'low' } },
76
+ ];
77
+ const EVENTS = [
78
+ point('event-001', p(19.09, 72.88), { label: 'Zone breach', type: 'zone-breach', severity: 'critical', relatedId: 'fused-entity-001' }, at(52)),
79
+ point('event-002', p(19.02, 72.92), { label: 'Sensor degraded', type: 'sensor-fault', severity: 'warning', relatedId: 'sensor-003' }, at(41)),
80
+ point('event-003', p(19.07, 72.94), { label: 'New contact', type: 'detection', severity: 'info', relatedId: 'fused-entity-002' }, at(55)),
81
+ ];
82
+ /** All DSS fixture records, typed for the mock provider. */
83
+ export const DSS_RECORDS = [
84
+ ...PLATFORMS.map((object) => ({ object, ontologyType: 'Platform' })),
85
+ ...SENSORS.map(({ object }) => ({ object, ontologyType: 'Sensor' })),
86
+ ...COVERAGE.map((object) => ({ object, ontologyType: 'SensorCoverage' })),
87
+ ...OBSERVATIONS.map((object) => ({ object, ontologyType: 'Observation' })),
88
+ ...FUSED.map((object) => ({ object, ontologyType: 'FusedEntity' })),
89
+ ...ZONES.map((object) => ({ object, ontologyType: 'Zone' })),
90
+ ...EVENTS.map((object) => ({ object, ontologyType: 'Event' })),
91
+ ];
92
+ /** Track points every minute from the epoch to +60 min, for every tracked object. */
93
+ export function dssTrackPoints(id) {
94
+ const path = DSS_TRACKS[id];
95
+ if (!path)
96
+ return [];
97
+ return Array.from({ length: 61 }, (_, m) => ({ timestamp: at(m), ...path(m) }));
98
+ }
99
+ /** Seeds the tracks into a mock provider (replay, trails, history-based geofence events). */
100
+ export function seedDssHistory(provider) {
101
+ for (const id of Object.keys(DSS_TRACKS))
102
+ provider.seedHistory(id, dssTrackPoints(id));
103
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Reference fixtures from doc 21 (first demo). Domain names live ONLY here and in apps — never in engine code.
3
+ * Fictional coordinates; WGS84.
4
+ */
5
+ import type { GeometryType, MapConfig, MapLayer, Position, SpatialObject } from '../core/types.js';
6
+ import type { MockRecord } from './mock-provider.js';
7
+ export declare const ZONE_ALPHA_RING: Position[];
8
+ export declare const UAV_12: SpatialObject;
9
+ export declare const UAV_14: SpatialObject;
10
+ export declare const VEHICLE_21: SpatialObject;
11
+ export declare const ZONE_ALPHA: SpatialObject;
12
+ export declare const ROAD_7: SpatialObject;
13
+ export declare const FIXTURE_RECORDS: MockRecord[];
14
+ export declare const FIXTURE_LAYERS: MapLayer[];
15
+ /** A viewport that shows every fixture at an 800×600 container. */
16
+ export declare const FIXTURE_CONFIG: MapConfig;
17
+ /** Spatial capabilities of the fixture ontology types. */
18
+ export declare const FIXTURE_ONTOLOGY: Record<string, GeometryType>;
@@ -0,0 +1,28 @@
1
+ const p = (latitude, longitude) => ({ latitude, longitude });
2
+ export const ZONE_ALPHA_RING = [p(19.1, 72.85), p(19.1, 72.9), p(19.06, 72.9), p(19.06, 72.85), p(19.1, 72.85)];
3
+ export const UAV_12 = { objectId: 'UAV-12', geometry: { type: 'Point', coordinates: p(19.08, 72.87) }, properties: { status: 'active', label: 'UAV-12' }, observedAt: '2026-10-01T08:00:00Z' };
4
+ export const UAV_14 = { objectId: 'UAV-14', geometry: { type: 'Point', coordinates: p(19.12, 72.93) }, properties: { status: 'standby', label: 'UAV-14' }, observedAt: '2026-10-01T08:00:00Z' };
5
+ export const VEHICLE_21 = { objectId: 'Vehicle-21', geometry: { type: 'Point', coordinates: p(19.05, 72.88) }, properties: { status: 'active', label: 'Vehicle-21' }, observedAt: '2026-10-01T08:00:00Z' };
6
+ export const ZONE_ALPHA = { objectId: 'Zone-Alpha', geometry: { type: 'Polygon', coordinates: [ZONE_ALPHA_RING] }, properties: { label: 'Zone Alpha' } };
7
+ export const ROAD_7 = { objectId: 'Road-7', geometry: { type: 'LineString', coordinates: [p(19.04, 72.84), p(19.07, 72.87), p(19.11, 72.92)] }, properties: { label: 'Road 7' } };
8
+ export const FIXTURE_RECORDS = [
9
+ { object: UAV_12, ontologyType: 'UAV' },
10
+ { object: UAV_14, ontologyType: 'UAV' },
11
+ { object: VEHICLE_21, ontologyType: 'Vehicle' },
12
+ { object: ZONE_ALPHA, ontologyType: 'Zone' },
13
+ { object: ROAD_7, ontologyType: 'Road' },
14
+ ];
15
+ export const FIXTURE_LAYERS = [
16
+ { id: 'zones', source: { ontologyType: 'Zone' }, visible: true, zIndex: 1, visualization: { type: 'polygon' }, interaction: { selectable: true, hoverable: true } },
17
+ { id: 'roads', source: { ontologyType: 'Road' }, visible: true, zIndex: 2, visualization: { type: 'line' } },
18
+ { id: 'vehicles', source: { ontologyType: 'Vehicle' }, visible: true, zIndex: 3, visualization: { type: 'marker' }, interaction: { selectable: true, hoverable: true } },
19
+ { id: 'uavs', source: { ontologyType: 'UAV' }, visible: true, zIndex: 4, visualization: { type: 'marker', style: [{ property: 'status', equals: 'active', style: { color: '#4CC3FF' } }, { style: { color: '#8C939C' } }] }, interaction: { selectable: true, hoverable: true } },
20
+ ];
21
+ /** A viewport that shows every fixture at an 800×600 container. */
22
+ export const FIXTURE_CONFIG = {
23
+ viewport: { center: p(19.08, 72.885), zoom: 11 },
24
+ basemap: { kind: 'none', background: '#0B100E' },
25
+ layers: FIXTURE_LAYERS,
26
+ };
27
+ /** Spatial capabilities of the fixture ontology types. */
28
+ export const FIXTURE_ONTOLOGY = { UAV: 'Point', Vehicle: 'Point', Zone: 'Polygon', Road: 'LineString' };
@@ -0,0 +1,7 @@
1
+ export { MockSpatialProvider, type MockProviderOptions, type MockRecord } from './mock-provider.js';
2
+ export { TestRenderer } from './test-renderer.js';
3
+ export { MemoryAnnotationStore } from './annotation-store.js';
4
+ export { MockOntologyResolver } from './mock-ontology.js';
5
+ export * from './fixtures.js';
6
+ export * from './fixtures-monitoring.js';
7
+ export { providerContract, rendererContract, spatialIndexContract, ContractViolation, type ContractCheck, type ProviderContractOptions, type RendererContractOptions } from './contracts.js';
@@ -0,0 +1,7 @@
1
+ export { MockSpatialProvider } from './mock-provider.js';
2
+ export { TestRenderer } from './test-renderer.js';
3
+ export { MemoryAnnotationStore } from './annotation-store.js';
4
+ export { MockOntologyResolver } from './mock-ontology.js';
5
+ export * from './fixtures.js';
6
+ export * from './fixtures-monitoring.js';
7
+ export { providerContract, rendererContract, spatialIndexContract, ContractViolation } from './contracts.js';
@@ -0,0 +1,13 @@
1
+ import type { GeometryType } from '../core/types.js';
2
+ import type { MissingCapability, OntologySpatialResolver, SpatialBinding } from '../data/contracts.js';
3
+ /**
4
+ * A stand-in for the Ontology (UNS) side: which types have a spatial capability, and of what geometry.
5
+ * Types not listed have none, so the engine must report them instead of guessing coordinates (doc 20).
6
+ */
7
+ export declare class MockOntologyResolver implements OntologySpatialResolver {
8
+ private readonly types;
9
+ /** Every type asked for, for asserting the engine caches lookups. */
10
+ readonly lookups: string[];
11
+ constructor(types: Record<string, GeometryType>);
12
+ resolve(ontologyType: string): Promise<SpatialBinding | MissingCapability>;
13
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A stand-in for the Ontology (UNS) side: which types have a spatial capability, and of what geometry.
3
+ * Types not listed have none, so the engine must report them instead of guessing coordinates (doc 20).
4
+ */
5
+ export class MockOntologyResolver {
6
+ types;
7
+ /** Every type asked for, for asserting the engine caches lookups. */
8
+ lookups = [];
9
+ constructor(types) {
10
+ this.types = types;
11
+ }
12
+ async resolve(ontologyType) {
13
+ this.lookups.push(ontologyType);
14
+ const geometryType = this.types[ontologyType];
15
+ if (!geometryType)
16
+ return { ontologyType, missing: 'spatial', reason: 'no spatial capability is declared for this type' };
17
+ return {
18
+ ontologyType,
19
+ geometryProperty: geometryType === 'Polygon' ? 'boundary' : geometryType === 'LineString' ? 'route' : 'location',
20
+ geometryType,
21
+ capabilities: { supportsSpatialQuery: true, supportsHistoricalQuery: false, supportsRealtime: true, supportsWrite: false },
22
+ };
23
+ }
24
+ }