@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.
Files changed (183) hide show
  1. package/README.md +39 -1
  2. package/dist/IOSenseApp.js +26 -6
  3. package/dist/SignIn.js +2 -2
  4. package/dist/auth.d.ts +7 -0
  5. package/dist/auth.js +35 -1
  6. package/dist/config.d.ts +5 -0
  7. package/dist/config.js +1 -0
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/map/globe.d.ts +5 -0
  11. package/dist/map/globe.js +5 -0
  12. package/dist/map/index.d.ts +21 -0
  13. package/dist/map/index.js +21 -0
  14. package/dist/map/source.d.ts +48 -0
  15. package/dist/map/source.js +73 -0
  16. package/dist/map/testing.d.ts +5 -0
  17. package/dist/map/testing.js +5 -0
  18. package/dist/session.d.ts +1 -1
  19. package/dist/session.js +1 -1
  20. package/package.json +55 -5
  21. package/vendor/SOURCE.txt +1 -0
  22. package/vendor/map-engine/cesium/camera.d.ts +10 -0
  23. package/vendor/map-engine/cesium/camera.js +25 -0
  24. package/vendor/map-engine/cesium/index.d.ts +2 -0
  25. package/vendor/map-engine/cesium/index.js +2 -0
  26. package/vendor/map-engine/cesium/renderer.d.ts +149 -0
  27. package/vendor/map-engine/cesium/renderer.js +837 -0
  28. package/vendor/map-engine/commands/index.d.ts +2 -0
  29. package/vendor/map-engine/commands/index.js +2 -0
  30. package/vendor/map-engine/commands/post-message.d.ts +32 -0
  31. package/vendor/map-engine/commands/post-message.js +77 -0
  32. package/vendor/map-engine/commands/webmcp.d.ts +17 -0
  33. package/vendor/map-engine/commands/webmcp.js +59 -0
  34. package/vendor/map-engine/core/commands.d.ts +82 -0
  35. package/vendor/map-engine/core/commands.js +46 -0
  36. package/vendor/map-engine/core/emitter.d.ts +9 -0
  37. package/vendor/map-engine/core/emitter.js +38 -0
  38. package/vendor/map-engine/core/equal.d.ts +2 -0
  39. package/vendor/map-engine/core/equal.js +28 -0
  40. package/vendor/map-engine/core/errors.d.ts +20 -0
  41. package/vendor/map-engine/core/errors.js +34 -0
  42. package/vendor/map-engine/core/geometry-family.d.ts +3 -0
  43. package/vendor/map-engine/core/geometry-family.js +13 -0
  44. package/vendor/map-engine/core/index.d.ts +4 -0
  45. package/vendor/map-engine/core/index.js +3 -0
  46. package/vendor/map-engine/core/types.d.ts +709 -0
  47. package/vendor/map-engine/core/types.js +7 -0
  48. package/vendor/map-engine/data/annotations.d.ts +3 -0
  49. package/vendor/map-engine/data/annotations.js +63 -0
  50. package/vendor/map-engine/data/contracts.d.ts +172 -0
  51. package/vendor/map-engine/data/contracts.js +12 -0
  52. package/vendor/map-engine/data/index.d.ts +4 -0
  53. package/vendor/map-engine/data/index.js +3 -0
  54. package/vendor/map-engine/data/schema.d.ts +7 -0
  55. package/vendor/map-engine/data/schema.js +40 -0
  56. package/vendor/map-engine/geofence/evaluator.d.ts +35 -0
  57. package/vendor/map-engine/geofence/evaluator.js +172 -0
  58. package/vendor/map-engine/geofence/history.d.ts +9 -0
  59. package/vendor/map-engine/geofence/history.js +19 -0
  60. package/vendor/map-engine/geofence/index.d.ts +2 -0
  61. package/vendor/map-engine/geofence/index.js +2 -0
  62. package/vendor/map-engine/index.d.ts +27 -0
  63. package/vendor/map-engine/index.js +14 -0
  64. package/vendor/map-engine/layers/index.d.ts +1 -0
  65. package/vendor/map-engine/layers/index.js +1 -0
  66. package/vendor/map-engine/layers/layers.d.ts +49 -0
  67. package/vendor/map-engine/layers/layers.js +316 -0
  68. package/vendor/map-engine/maplibre/deck-host.d.ts +105 -0
  69. package/vendor/map-engine/maplibre/deck-host.js +401 -0
  70. package/vendor/map-engine/maplibre/icons.d.ts +17 -0
  71. package/vendor/map-engine/maplibre/icons.js +65 -0
  72. package/vendor/map-engine/maplibre/index.d.ts +3 -0
  73. package/vendor/map-engine/maplibre/index.js +2 -0
  74. package/vendor/map-engine/maplibre/map-like.d.ts +56 -0
  75. package/vendor/map-engine/maplibre/map-like.js +1 -0
  76. package/vendor/map-engine/maplibre/renderer.d.ts +173 -0
  77. package/vendor/map-engine/maplibre/renderer.js +889 -0
  78. package/vendor/map-engine/maplibre/style.d.ts +84 -0
  79. package/vendor/map-engine/maplibre/style.js +493 -0
  80. package/vendor/map-engine/present/decorations.d.ts +25 -0
  81. package/vendor/map-engine/present/decorations.js +115 -0
  82. package/vendor/map-engine/present/index.d.ts +2 -0
  83. package/vendor/map-engine/present/index.js +2 -0
  84. package/vendor/map-engine/present/present.d.ts +47 -0
  85. package/vendor/map-engine/present/present.js +149 -0
  86. package/vendor/map-engine/query/index.d.ts +1 -0
  87. package/vendor/map-engine/query/index.js +1 -0
  88. package/vendor/map-engine/query/planner.d.ts +26 -0
  89. package/vendor/map-engine/query/planner.js +47 -0
  90. package/vendor/map-engine/react/MapView.d.ts +87 -0
  91. package/vendor/map-engine/react/MapView.js +270 -0
  92. package/vendor/map-engine/react/context.d.ts +39 -0
  93. package/vendor/map-engine/react/context.js +94 -0
  94. package/vendor/map-engine/react/index.d.ts +2 -0
  95. package/vendor/map-engine/react/index.js +2 -0
  96. package/vendor/map-engine/react/store.d.ts +19 -0
  97. package/vendor/map-engine/react/store.js +18 -0
  98. package/vendor/map-engine/realtime/apply.d.ts +74 -0
  99. package/vendor/map-engine/realtime/apply.js +155 -0
  100. package/vendor/map-engine/realtime/index.d.ts +1 -0
  101. package/vendor/map-engine/realtime/index.js +1 -0
  102. package/vendor/map-engine/render/index.d.ts +1 -0
  103. package/vendor/map-engine/render/index.js +1 -0
  104. package/vendor/map-engine/render/offline.d.ts +8 -0
  105. package/vendor/map-engine/render/offline.js +47 -0
  106. package/vendor/map-engine/render/renderer.d.ts +74 -0
  107. package/vendor/map-engine/render/renderer.js +1 -0
  108. package/vendor/map-engine/render/terrarium.d.ts +48 -0
  109. package/vendor/map-engine/render/terrarium.js +110 -0
  110. package/vendor/map-engine/runtime/drawing.d.ts +28 -0
  111. package/vendor/map-engine/runtime/drawing.js +58 -0
  112. package/vendor/map-engine/runtime/index.d.ts +5 -0
  113. package/vendor/map-engine/runtime/index.js +2 -0
  114. package/vendor/map-engine/runtime/offline.d.ts +24 -0
  115. package/vendor/map-engine/runtime/offline.js +37 -0
  116. package/vendor/map-engine/runtime/overlays.d.ts +25 -0
  117. package/vendor/map-engine/runtime/overlays.js +65 -0
  118. package/vendor/map-engine/runtime/runtime.d.ts +4 -0
  119. package/vendor/map-engine/runtime/runtime.js +2105 -0
  120. package/vendor/map-engine/runtime/types.d.ts +407 -0
  121. package/vendor/map-engine/runtime/types.js +1 -0
  122. package/vendor/map-engine/scene/index.d.ts +1 -0
  123. package/vendor/map-engine/scene/index.js +1 -0
  124. package/vendor/map-engine/scene/scene.d.ts +31 -0
  125. package/vendor/map-engine/scene/scene.js +216 -0
  126. package/vendor/map-engine/selection/index.d.ts +1 -0
  127. package/vendor/map-engine/selection/index.js +1 -0
  128. package/vendor/map-engine/selection/selection.d.ts +12 -0
  129. package/vendor/map-engine/selection/selection.js +28 -0
  130. package/vendor/map-engine/spatial/geojson.d.ts +15 -0
  131. package/vendor/map-engine/spatial/geojson.js +83 -0
  132. package/vendor/map-engine/spatial/geometry.d.ts +13 -0
  133. package/vendor/map-engine/spatial/geometry.js +206 -0
  134. package/vendor/map-engine/spatial/import.d.ts +27 -0
  135. package/vendor/map-engine/spatial/import.js +96 -0
  136. package/vendor/map-engine/spatial/index.d.ts +38 -0
  137. package/vendor/map-engine/spatial/index.js +37 -0
  138. package/vendor/map-engine/spatial/line-of-sight.d.ts +35 -0
  139. package/vendor/map-engine/spatial/line-of-sight.js +43 -0
  140. package/vendor/map-engine/spatial/measure.d.ts +28 -0
  141. package/vendor/map-engine/spatial/measure.js +90 -0
  142. package/vendor/map-engine/spatial/operations.d.ts +21 -0
  143. package/vendor/map-engine/spatial/operations.js +171 -0
  144. package/vendor/map-engine/spatial/query.d.ts +17 -0
  145. package/vendor/map-engine/spatial/query.js +88 -0
  146. package/vendor/map-engine/spatial/spatial-index.d.ts +24 -0
  147. package/vendor/map-engine/spatial/spatial-index.js +36 -0
  148. package/vendor/map-engine/spatial/volume-mesh.d.ts +19 -0
  149. package/vendor/map-engine/spatial/volume-mesh.js +56 -0
  150. package/vendor/map-engine/temporal/index.d.ts +3 -0
  151. package/vendor/map-engine/temporal/index.js +3 -0
  152. package/vendor/map-engine/temporal/journal.d.ts +20 -0
  153. package/vendor/map-engine/temporal/journal.js +34 -0
  154. package/vendor/map-engine/temporal/playback.d.ts +41 -0
  155. package/vendor/map-engine/temporal/playback.js +80 -0
  156. package/vendor/map-engine/temporal/tracks.d.ts +33 -0
  157. package/vendor/map-engine/temporal/tracks.js +119 -0
  158. package/vendor/map-engine/testing/annotation-store.d.ts +24 -0
  159. package/vendor/map-engine/testing/annotation-store.js +43 -0
  160. package/vendor/map-engine/testing/contracts.d.ts +41 -0
  161. package/vendor/map-engine/testing/contracts.js +328 -0
  162. package/vendor/map-engine/testing/fixtures-monitoring.d.ts +22 -0
  163. package/vendor/map-engine/testing/fixtures-monitoring.js +103 -0
  164. package/vendor/map-engine/testing/fixtures.d.ts +18 -0
  165. package/vendor/map-engine/testing/fixtures.js +28 -0
  166. package/vendor/map-engine/testing/index.d.ts +7 -0
  167. package/vendor/map-engine/testing/index.js +7 -0
  168. package/vendor/map-engine/testing/mock-ontology.d.ts +13 -0
  169. package/vendor/map-engine/testing/mock-ontology.js +24 -0
  170. package/vendor/map-engine/testing/mock-provider.d.ts +97 -0
  171. package/vendor/map-engine/testing/mock-provider.js +369 -0
  172. package/vendor/map-engine/testing/test-renderer.d.ts +72 -0
  173. package/vendor/map-engine/testing/test-renderer.js +153 -0
  174. package/vendor/map-engine/viewport/index.d.ts +1 -0
  175. package/vendor/map-engine/viewport/index.js +1 -0
  176. package/vendor/map-engine/viewport/viewport.d.ts +19 -0
  177. package/vendor/map-engine/viewport/viewport.js +102 -0
  178. package/vendor/map-provider-uns/client.d.ts +75 -0
  179. package/vendor/map-provider-uns/client.js +111 -0
  180. package/vendor/map-provider-uns/index.d.ts +2 -0
  181. package/vendor/map-provider-uns/index.js +2 -0
  182. package/vendor/map-provider-uns/provider.d.ts +131 -0
  183. package/vendor/map-provider-uns/provider.js +561 -0
package/README.md CHANGED
@@ -12,6 +12,11 @@ Wrap the app in `<IOSenseApp>`. It takes the session IOSense hands over — the
12
12
  token (`?token=`, exchanged once), a bearer on the URL, or the `.iosense.io` cookie — takes the token
13
13
  out of the address, and keeps it in this browser. With no session it shows a sign-in screen.
14
14
 
15
+ Inside IOSense (the Studio's view of a deployed app) the app is framed by a page that already has the
16
+ viewer's session: from 0.3, `<IOSenseApp>` asks its parent for it (`postMessage`, about a second at
17
+ most) and the Studio answers with the bearer, sent to the app's own origin only. No SSO token is made
18
+ or exchanged. Only an IOSense origin (`*.iosense.io`, `*.iocompute.ai`, localhost) is believed.
19
+
15
20
  ```tsx
16
21
  import { IOSenseApp } from '@faclon-labs/app-sdk';
17
22
 
@@ -51,4 +56,37 @@ const { data, loading, error } = useRequest(url, { auth: 'iosense' });
51
56
 
52
57
  `auth: 'iosense'` adds the viewer's token and is refused for any host that is not IOSense's.
53
58
 
54
- Peer dependency: React 18 or later.
59
+ ## Maps (`@faclon-labs/app-sdk/map`)
60
+
61
+ The IO Studio Map Engine on UNS data: a declarative `MapConfig`, the React `MapView`, the MapLibre renderer, the
62
+ UNS provider, and the agent commands. Install MapLibre next to the SDK:
63
+
64
+ ```bash
65
+ npm install @faclon-labs/app-sdk maplibre-gl
66
+ ```
67
+
68
+ ```tsx
69
+ import 'maplibre-gl/dist/maplibre-gl.css';
70
+ import { MapView, MapLibreRenderer, useUnsMapSource, type MapConfig } from '@faclon-labs/app-sdk/map';
71
+ // Vite: without this line the map stays empty (the engine warns after 8 s).
72
+ import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
73
+
74
+ function PlantMap({ config }: { config: MapConfig }) {
75
+ const source = useUnsMapSource({ workspace: 'ws_…' }); // the viewer's session; the UNS decides what they see
76
+ return <MapView style={{ height: '100vh' }} config={config} {...source.props} createRenderer={() => new MapLibreRenderer({ workerUrl })} />;
77
+ }
78
+ ```
79
+
80
+ - `source.uns.listDatasets()` lists every spatial class in the workspace with its properties.
81
+ - On sample data (`auth="optional"`, nobody signed in) the map shows `sample` records you pass to
82
+ `useUnsMapSource`. `@faclon-labs/app-sdk/map/testing` has fixtures and an in-memory provider.
83
+ - The UNS host is `configure({ unsBaseUrl })`, else worked out from `apiBaseUrl`
84
+ (`appserver.X` → `uns-backend-server.X`). **The UNS backend answers only IOSense's own origins (CORS)**: an app
85
+ served from elsewhere needs a pass-through that allows it, set as `unsBaseUrl`.
86
+ - 3D globe: `npm install cesium`, then `import { CesiumRenderer } from '@faclon-labs/app-sdk/map/globe'` and
87
+ `createRenderer={() => new CesiumRenderer()}`. A 2D app never downloads Cesium.
88
+
89
+ The engine comes from the MapEngine repo: `npm run build` copies it into `vendor/` (set `MAP_ENGINE_DIR` if the
90
+ checkout is not next to io-lens-mfe). `scripts/smoke-map.sh` installs the packed SDK into a fresh app and checks it.
91
+
92
+ Peer dependencies: React 18 or later; `maplibre-gl` (v5+) for `./map`; `cesium` for `./map/globe`.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useState } from 'react';
3
- import { adoptHostSession, clearToken, exchangeSsoToken, getToken, setToken, takePendingSsoToken } from './auth.js';
3
+ import { adoptHostSession, clearToken, exchangeSsoToken, getToken, isEmbedded, requestHostSession, setToken, takePendingSsoToken } from './auth.js';
4
4
  import { configure, getConfig } from './config.js';
5
5
  import { publishSession } from './request.js';
6
6
  import { SAMPLE_NOTICE, SessionContext, useIOSense } from './session.js';
@@ -20,10 +20,30 @@ export function IOSenseApp({ name, auth = 'required', sample, apiBaseUrl, signIn
20
20
  const [phase, setPhase] = useState(() => {
21
21
  if (auth === 'none')
22
22
  return { kind: 'ready', token: null };
23
- adoptHostSession();
23
+ const how = adoptHostSession();
24
24
  const sso = takePendingSsoToken();
25
- return sso ? { kind: 'exchanging', sso } : { kind: 'ready', token: getToken() };
25
+ if (sso)
26
+ return { kind: 'exchanging', sso };
27
+ // Framed, with nothing handed over on the URL or in the cookie: the parent may hold the session
28
+ // (IOSense does). A session kept from before is asked about too — the parent's viewer wins.
29
+ if (isEmbedded() && (how === 'none' || how === 'stored'))
30
+ return { kind: 'asking' };
31
+ return { kind: 'ready', token: getToken() };
26
32
  });
33
+ useEffect(() => {
34
+ if (phase.kind !== 'asking')
35
+ return;
36
+ let current = true;
37
+ requestHostSession().then((handed) => {
38
+ if (handed)
39
+ setToken(handed);
40
+ if (current)
41
+ setPhase({ kind: 'ready', token: getToken() });
42
+ });
43
+ return () => {
44
+ current = false;
45
+ };
46
+ }, [phase]);
27
47
  useEffect(() => {
28
48
  if (phase.kind !== 'exchanging')
29
49
  return;
@@ -55,9 +75,9 @@ export function IOSenseApp({ name, auth = 'required', sample, apiBaseUrl, signIn
55
75
  }), [name, token, api, sample, auth]);
56
76
  // For `request()`, which is called from event handlers where there is no hook to read from.
57
77
  publishSession({ token, apiBaseUrl: api });
58
- // A blank moment while the SSO token is exchanged: not the sign-in screen, which would be wrong
59
- // for the half second before the session lands.
60
- if (phase.kind === 'exchanging')
78
+ // A blank moment while the SSO token is exchanged or the parent is asked: not the sign-in screen,
79
+ // which would be wrong for the half second before the session lands.
80
+ if (phase.kind === 'exchanging' || phase.kind === 'asking')
61
81
  return null;
62
82
  if (auth === 'required' && !token) {
63
83
  const onSignedIn = (bearer) => {
package/dist/SignIn.js CHANGED
@@ -40,7 +40,7 @@ export function SignIn({ appName, apiBaseUrl, onSignedIn }) {
40
40
  }
41
41
  catch (err) {
42
42
  // The server's own words ("Invalid or expired token.") say what to do: mint a new one.
43
- setError(`${err instanceof Error ? err.message : 'IOSense did not accept this token.'} An SSO token works once — copy a fresh one.`);
43
+ setError(`${err instanceof Error ? err.message : 'IOSense did not accept this token.'} An SSO token works once. Copy a fresh one.`);
44
44
  }
45
45
  finally {
46
46
  setBusy(false);
@@ -68,5 +68,5 @@ export function SignIn({ appName, apiBaseUrl, onSignedIn }) {
68
68
  onSignedIn(value);
69
69
  }
70
70
  const canSubmit = !busy && token.trim().length > 0;
71
- return (_jsx("div", { "data-iosense-signin": true, style: { minHeight: '100vh', display: 'grid', placeItems: 'center', padding: 24, background: 'var(--background, #f7f7f4)', color: 'var(--foreground, #161616)', fontFamily: 'inherit' }, children: _jsxs("form", { onSubmit: submit, style: { width: 'min(380px, 100%)', display: 'flex', flexDirection: 'column', gap: 12, padding: 24, borderRadius: 12, background: 'var(--card, #fff)', border: '1px solid var(--border, #e2e0da)' }, children: [_jsxs("div", { children: [_jsxs("h1", { style: { margin: 0, fontSize: 18, fontWeight: 600 }, children: ["Sign in to ", appName] }), _jsx("p", { style: { margin: '4px 0 0', fontSize: 13, color: 'var(--muted-foreground, #5c5c5c)' }, children: "This app reads your plant through IOSense. Paste an SSO token from your IOSense session." })] }), _jsxs("label", { style: { display: 'flex', flexDirection: 'column', gap: 4, fontSize: 13, fontWeight: 500 }, children: ["SSO token", _jsx("input", { name: "token", autoComplete: "off", autoFocus: true, spellCheck: false, placeholder: "Paste an SSO token from IOSense", value: token, onChange: (e) => setToken(e.target.value), disabled: busy, style: { width: '100%', boxSizing: 'border-box', height: 40, padding: '0 12px', border: '1px solid var(--border, #d4d4d8)', borderRadius: 8, background: 'var(--background, #fff)', color: 'inherit', font: 'inherit' } })] }), error && (_jsx("p", { role: "alert", style: { margin: 0, fontSize: 13, color: 'var(--destructive, #b4302c)' }, children: error })), _jsx("button", { type: "submit", disabled: !canSubmit, style: { height: 40, border: 0, borderRadius: 999, background: 'var(--primary, #1364f1)', color: 'var(--primary-foreground, #fff)', font: 'inherit', fontWeight: 500, cursor: busy ? 'progress' : 'pointer', opacity: canSubmit ? 1 : 0.6 }, children: busy ? 'Checking…' : 'Continue' }), _jsx("p", { style: { margin: 0, fontSize: 12, color: 'var(--muted-foreground, #5c5c5c)' }, children: "Opened from the IOSense launchpad, this app signs you in on its own \u2014 this screen is for opening it by its own address." })] }) }));
71
+ return (_jsx("div", { "data-iosense-signin": true, style: { minHeight: '100vh', display: 'grid', placeItems: 'center', padding: 24, background: 'var(--background, #f7f7f4)', color: 'var(--foreground, #161616)', fontFamily: 'inherit' }, children: _jsxs("form", { onSubmit: submit, style: { width: 'min(380px, 100%)', display: 'flex', flexDirection: 'column', gap: 12, padding: 24, borderRadius: 12, background: 'var(--card, #fff)', border: '1px solid var(--border, #e2e0da)' }, children: [_jsxs("div", { children: [_jsxs("h1", { style: { margin: 0, fontSize: 18, fontWeight: 600 }, children: ["Sign in to ", appName] }), _jsx("p", { style: { margin: '4px 0 0', fontSize: 13, color: 'var(--muted-foreground, #5c5c5c)' }, children: "This app reads your plant through IOSense. Paste an SSO token from your IOSense session." })] }), _jsxs("label", { style: { display: 'flex', flexDirection: 'column', gap: 4, fontSize: 13, fontWeight: 500 }, children: ["SSO token", _jsx("input", { name: "token", autoComplete: "off", autoFocus: true, spellCheck: false, placeholder: "Paste an SSO token from IOSense", value: token, onChange: (e) => setToken(e.target.value), disabled: busy, style: { width: '100%', boxSizing: 'border-box', height: 40, padding: '0 12px', border: '1px solid var(--border, #d4d4d8)', borderRadius: 8, background: 'var(--background, #fff)', color: 'inherit', font: 'inherit' } })] }), error && (_jsx("p", { role: "alert", style: { margin: 0, fontSize: 13, color: 'var(--destructive, #b4302c)' }, children: error })), _jsx("button", { type: "submit", disabled: !canSubmit, style: { height: 40, border: 0, borderRadius: 999, background: 'var(--primary, #1364f1)', color: 'var(--primary-foreground, #fff)', font: 'inherit', fontWeight: 500, cursor: busy ? 'progress' : 'pointer', opacity: canSubmit ? 1 : 0.6 }, children: busy ? 'Checking…' : 'Continue' }), _jsx("p", { style: { margin: 0, fontSize: 12, color: 'var(--muted-foreground, #5c5c5c)' }, children: "Opened from the IOSense launchpad, this app signs you in on its own. This screen is for opening it by its own address." })] }) }));
72
72
  }
package/dist/auth.d.ts CHANGED
@@ -15,6 +15,13 @@ export declare function setToken(token: string): void;
15
15
  export declare function clearToken(): void;
16
16
  /** True when this page runs inside another page's frame — the IOSense launchpad, normally. */
17
17
  export declare function isEmbedded(): boolean;
18
+ export declare const isTrustedHostOrigin: (origin: string) => boolean;
19
+ /**
20
+ * Ask the page this app is framed in for the viewer's session. Inside IOSense the parent answers at
21
+ * once with the bearer it already has, sent to this app's origin only; anywhere else nobody answers
22
+ * and this resolves null after `timeoutMs`. The question carries no secret, so it may go to any parent.
23
+ */
24
+ export declare function requestHostSession(timeoutMs?: number): Promise<string | null>;
18
25
  /**
19
26
  * Take the session the host handed over, and take the token out of the URL. Called once, before
20
27
  * the first render (`<IOSenseApp>` does it). Returns how the session arrived.
package/dist/auth.js CHANGED
@@ -2,7 +2,10 @@ import { getConfig } from './config.js';
2
2
  /**
3
3
  * The IOSense session: how an app gets it, and where it keeps it.
4
4
  *
5
- * Four ways in, in order:
5
+ * Five ways in, in order:
6
+ * 0. **Handed by the frame's parent** — inside IOSense (the Studio's view of a deployed app) the
7
+ * parent already holds the viewer's bearer and posts it to this frame (`requestHostSession`):
8
+ * no token to make, nothing to exchange, nothing in the URL. Only an IOSense origin is believed.
6
9
  * 1. **Launchpad, SSO mode** — IOSense opens the app as `…?token=<sso>`: a 64-hex SSO token,
7
10
  * exchanged ONCE for the bearer (`exchangeSsoToken`) before the app renders.
8
11
  * 2. **A bearer on the URL** (`?token=<jwt>`), taken as it is.
@@ -93,6 +96,37 @@ export function isEmbedded() {
93
96
  return true;
94
97
  }
95
98
  }
99
+ /** The pages that may hand this app a session: IOSense's own hosts, the Studio's, and a developer's machine. */
100
+ const HOST_ORIGIN = /^https:\/\/([a-z0-9-]+\.)*(iosense\.io|iocompute\.ai)$|^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i;
101
+ export const isTrustedHostOrigin = (origin) => HOST_ORIGIN.test(origin);
102
+ /**
103
+ * Ask the page this app is framed in for the viewer's session. Inside IOSense the parent answers at
104
+ * once with the bearer it already has, sent to this app's origin only; anywhere else nobody answers
105
+ * and this resolves null after `timeoutMs`. The question carries no secret, so it may go to any parent.
106
+ */
107
+ export function requestHostSession(timeoutMs = 1000) {
108
+ if (typeof window === 'undefined' || !isEmbedded())
109
+ return Promise.resolve(null);
110
+ return new Promise((resolve) => {
111
+ let timer;
112
+ const done = (token) => {
113
+ window.removeEventListener('message', heard);
114
+ clearTimeout(timer);
115
+ resolve(token);
116
+ };
117
+ const heard = (event) => {
118
+ if (event.source !== window.parent || !isTrustedHostOrigin(event.origin))
119
+ return;
120
+ const data = event.data;
121
+ if (!data || data.source !== 'iolens-studio' || data.type !== 'session')
122
+ return;
123
+ done(typeof data.token === 'string' && data.token.trim() ? data.token.trim() : null);
124
+ };
125
+ window.addEventListener('message', heard);
126
+ timer = setTimeout(() => done(null), timeoutMs);
127
+ window.parent.postMessage({ source: 'iosense-app', type: 'session-request', v: 1 }, '*');
128
+ });
129
+ }
96
130
  function readCookie(name) {
97
131
  const prefix = `${name}=`;
98
132
  for (const part of document.cookie.split(';')) {
package/dist/config.d.ts CHANGED
@@ -12,6 +12,11 @@ export interface IOSenseConfig {
12
12
  tokenCookie: string;
13
13
  /** Where the session is kept in this browser. */
14
14
  storageKey: string;
15
+ /**
16
+ * The UNS backend the map reads (`@faclon-labs/app-sdk/map`). Empty: worked out from `apiBaseUrl`
17
+ * (`appserver.X` → `uns-backend-server.X`).
18
+ */
19
+ unsBaseUrl: string;
15
20
  }
16
21
  export declare function configure(next: Partial<IOSenseConfig>): void;
17
22
  export declare const getConfig: () => Readonly<IOSenseConfig>;
package/dist/config.js CHANGED
@@ -3,6 +3,7 @@ const config = {
3
3
  ssoExchangeUrl: 'https://connector.iosense.io/api/retrieve-sso-token',
4
4
  tokenCookie: 'AccessToken',
5
5
  storageKey: 'bearer_token',
6
+ unsBaseUrl: '',
6
7
  };
7
8
  export function configure(next) {
8
9
  for (const [key, value] of Object.entries(next)) {
package/dist/index.d.ts CHANGED
@@ -13,4 +13,4 @@ export { useLatest, useSeries, useUser, type LatestOptions, type LatestResult, t
13
13
  export { fetchLatest, fetchSeries, isSampleTopic, type SeriesPoint } from './data.js';
14
14
  export { request, useRequest, RequestError, isIOSenseHost, type RequestOptions, type UseRequestOptions, type UseRequestResult } from './request.js';
15
15
  export { configure, getConfig, type IOSenseConfig } from './config.js';
16
- export { adoptHostSession, exchangeSsoToken, getToken, setToken, clearToken, isSsoToken, isEmbedded } from './auth.js';
16
+ export { adoptHostSession, exchangeSsoToken, getToken, setToken, clearToken, isSsoToken, isEmbedded, requestHostSession, isTrustedHostOrigin } from './auth.js';
package/dist/index.js CHANGED
@@ -13,4 +13,4 @@ export { useLatest, useSeries, useUser } from './hooks.js';
13
13
  export { fetchLatest, fetchSeries, isSampleTopic } from './data.js';
14
14
  export { request, useRequest, RequestError, isIOSenseHost } from './request.js';
15
15
  export { configure, getConfig } from './config.js';
16
- export { adoptHostSession, exchangeSsoToken, getToken, setToken, clearToken, isSsoToken, isEmbedded } from './auth.js';
16
+ export { adoptHostSession, exchangeSsoToken, getToken, setToken, clearToken, isSsoToken, isEmbedded, requestHostSession, isTrustedHostOrigin } from './auth.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @faclon-labs/app-sdk/map/globe — the 3D globe renderer (Cesium). Install `cesium` next to the SDK; pass
3
+ * `createRenderer={() => new CesiumRenderer()}` to `MapView`. Everything else comes from `@faclon-labs/app-sdk/map`.
4
+ */
5
+ export * from '../../vendor/map-engine/cesium/index.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @faclon-labs/app-sdk/map/globe — the 3D globe renderer (Cesium). Install `cesium` next to the SDK; pass
3
+ * `createRenderer={() => new CesiumRenderer()}` to `MapView`. Everything else comes from `@faclon-labs/app-sdk/map`.
4
+ */
5
+ export * from '../../vendor/map-engine/cesium/index.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @faclon-labs/app-sdk/map — maps on UNS data.
3
+ *
4
+ * The IO Studio Map Engine (runtime, contracts, spatial API, commands, agent transports), its React view and
5
+ * MapLibre renderer, and the UNS provider — plus `useUnsMapSource`, which connects the provider to the viewer's
6
+ * session. The 3D globe (Cesium) is a separate entry, `@faclon-labs/app-sdk/map/globe`, so a 2D app never
7
+ * downloads it.
8
+ *
9
+ * ```tsx
10
+ * import { MapView, MapLibreRenderer, useUnsMapSource } from '@faclon-labs/app-sdk/map';
11
+ * import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
12
+ *
13
+ * const source = useUnsMapSource({ workspace: 'ws_…' });
14
+ * <MapView {...source.props} config={config} createRenderer={() => new MapLibreRenderer({ workerUrl })} />
15
+ * ```
16
+ */
17
+ export * from '../../vendor/map-engine/index.js';
18
+ export * from '../../vendor/map-engine/react/index.js';
19
+ export * from '../../vendor/map-engine/maplibre/index.js';
20
+ export * from '../../vendor/map-provider-uns/index.js';
21
+ export { useUnsMapSource, unsBaseUrlFor, type UnsMapSource, type UnsMapSourceOptions, type UnsMapSourceProps } from './source.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @faclon-labs/app-sdk/map — maps on UNS data.
3
+ *
4
+ * The IO Studio Map Engine (runtime, contracts, spatial API, commands, agent transports), its React view and
5
+ * MapLibre renderer, and the UNS provider — plus `useUnsMapSource`, which connects the provider to the viewer's
6
+ * session. The 3D globe (Cesium) is a separate entry, `@faclon-labs/app-sdk/map/globe`, so a 2D app never
7
+ * downloads it.
8
+ *
9
+ * ```tsx
10
+ * import { MapView, MapLibreRenderer, useUnsMapSource } from '@faclon-labs/app-sdk/map';
11
+ * import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
12
+ *
13
+ * const source = useUnsMapSource({ workspace: 'ws_…' });
14
+ * <MapView {...source.props} config={config} createRenderer={() => new MapLibreRenderer({ workerUrl })} />
15
+ * ```
16
+ */
17
+ export * from '../../vendor/map-engine/index.js';
18
+ export * from '../../vendor/map-engine/react/index.js';
19
+ export * from '../../vendor/map-engine/maplibre/index.js';
20
+ export * from '../../vendor/map-provider-uns/index.js';
21
+ export { useUnsMapSource, unsBaseUrlFor } from './source.js';
@@ -0,0 +1,48 @@
1
+ import type { AuthContext, JournalProvider, LinkProvider, OntologySpatialResolver, RealtimeProvider, SpatialDataProvider, SpatialObject, TrackProvider } from '../../vendor/map-engine/index.js';
2
+ import { UnsSpatialProvider } from '../../vendor/map-provider-uns/index.js';
3
+ import { type MockRecord } from '../../vendor/map-engine/testing/index.js';
4
+ export interface UnsMapSourceOptions {
5
+ /** The UNS workspace id (`ws_…`) whose spatial nodes the map shows. None: the map shows `sample`. */
6
+ workspace?: string;
7
+ /** Shown as the datasets' source. */
8
+ workspaceName?: string;
9
+ /** The UNS backend. Default: `configure({ unsBaseUrl })`, else worked out from the session's `apiBaseUrl`. */
10
+ unsBaseUrl?: string;
11
+ /** How often live values are read. Default 3000 ms. */
12
+ pollMs?: number;
13
+ /**
14
+ * What the map shows when there is no workspace, or while the app is on sample data (nobody signed in with
15
+ * `auth="optional"`). Give each record the `ontologyType` your layers ask for. A new array moves the map's
16
+ * objects to it (added, changed and removed by `objectId`), so simulated live data is just a new array.
17
+ * Default: nothing — the layers stay empty.
18
+ */
19
+ sample?: Array<MockRecord | SpatialObject>;
20
+ }
21
+ /** Spread into `<MapView>`: the data side of a map. */
22
+ export interface UnsMapSourceProps {
23
+ provider: SpatialDataProvider;
24
+ realtime: RealtimeProvider;
25
+ history: TrackProvider;
26
+ ontology: OntologySpatialResolver;
27
+ journal: JournalProvider;
28
+ links: LinkProvider;
29
+ auth: AuthContext | null;
30
+ }
31
+ export interface UnsMapSource {
32
+ props: UnsMapSourceProps;
33
+ /** The UNS provider (null on sample data): `listDatasets()` gives every spatial class with its properties. */
34
+ uns: UnsSpatialProvider | null;
35
+ /** True while the map shows the app's sample records rather than the plant's. */
36
+ sample: boolean;
37
+ }
38
+ /** `https://appserver.X` → `https://uns-backend-server.X`. Any other host: IOSense's production UNS. */
39
+ export declare function unsBaseUrlFor(apiBaseUrl: string): string;
40
+ /**
41
+ * The UNS as a map data source, on the viewer's session: the token is read on every request, so a refreshed
42
+ * session is used at once, and handed to the engine as `auth` so the UNS decides what is visible. Inside
43
+ * `<IOSenseApp>` only.
44
+ *
45
+ * The UNS backend answers only IOSense's own origins (CORS). An app served from elsewhere sets `unsBaseUrl` to a
46
+ * pass-through that allows it.
47
+ */
48
+ export declare function useUnsMapSource(options: UnsMapSourceOptions): UnsMapSource;
@@ -0,0 +1,73 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { UnsClient, UnsSpatialProvider } from '../../vendor/map-provider-uns/index.js';
3
+ import { MockOntologyResolver, MockSpatialProvider } from '../../vendor/map-engine/testing/index.js';
4
+ import { getConfig } from '../config.js';
5
+ import { useIOSense } from '../session.js';
6
+ /** `https://appserver.X` → `https://uns-backend-server.X`. Any other host: IOSense's production UNS. */
7
+ export function unsBaseUrlFor(apiBaseUrl) {
8
+ const m = /^(https?:\/\/)appserver\.(.+?)\/?$/.exec(apiBaseUrl);
9
+ return m ? `${m[1]}uns-backend-server.${m[2]}` : 'https://uns-backend-server.iosense.io';
10
+ }
11
+ const recordOf = (r) => ('object' in r ? r : { object: r });
12
+ /** Classes and their geometry, written into `into` — the resolver keeps that object, so later records count too. */
13
+ function learnOntology(records, into) {
14
+ for (const r of records.map(recordOf))
15
+ if (r.ontologyType)
16
+ into[r.ontologyType] = r.object.geometry.type;
17
+ return into;
18
+ }
19
+ /**
20
+ * The UNS as a map data source, on the viewer's session: the token is read on every request, so a refreshed
21
+ * session is used at once, and handed to the engine as `auth` so the UNS decides what is visible. Inside
22
+ * `<IOSenseApp>` only.
23
+ *
24
+ * The UNS backend answers only IOSense's own origins (CORS). An app served from elsewhere sets `unsBaseUrl` to a
25
+ * pass-through that allows it.
26
+ */
27
+ export function useUnsMapSource(options) {
28
+ const session = useIOSense();
29
+ const { token, apiBaseUrl } = session;
30
+ const sample = session.sample || !options.workspace;
31
+ const tokenRef = useRef(token);
32
+ tokenRef.current = token;
33
+ const base = (options.unsBaseUrl || getConfig().unsBaseUrl || unsBaseUrlFor(apiBaseUrl)).replace(/\/$/, '');
34
+ // The map is built once per switch into sample mode; later arrays are applied to it (below), never rebuilt.
35
+ const sampleRef = useRef(options.sample);
36
+ sampleRef.current = options.sample;
37
+ const built = useMemo(() => {
38
+ if (sample) {
39
+ const records = sampleRef.current ?? [];
40
+ const types = learnOntology(records, {});
41
+ const mock = new MockSpatialProvider(records, { latencyMs: 0 });
42
+ return { data: mock, mock, types, applied: records, ontology: new MockOntologyResolver(types), uns: null };
43
+ }
44
+ const client = new UnsClient({ baseUrl: base, token: () => tokenRef.current });
45
+ const uns = new UnsSpatialProvider({
46
+ client,
47
+ workspace: options.workspace,
48
+ ...(options.workspaceName ? { workspaceName: options.workspaceName } : {}),
49
+ ...(options.pollMs ? { pollMs: options.pollMs } : {}),
50
+ });
51
+ return { data: uns, mock: null, types: {}, applied: [], ontology: uns.ontology, uns };
52
+ }, [sample, base, options.workspace, options.workspaceName, options.pollMs]);
53
+ // A new sample array: what changed goes to the map as a live update.
54
+ useEffect(() => {
55
+ const next = options.sample;
56
+ if (!built.mock || !next || next === built.applied)
57
+ return;
58
+ const before = new Set(built.applied.map((r) => recordOf(r).object.objectId));
59
+ learnOntology(next, built.types);
60
+ for (const r of next.map(recordOf)) {
61
+ before.delete(r.object.objectId);
62
+ void built.mock.upsert(r.object, r.ontologyType ? { ontologyType: r.ontologyType } : undefined);
63
+ }
64
+ for (const id of before)
65
+ void built.mock.remove(id);
66
+ built.applied = next;
67
+ }, [built, options.sample]);
68
+ // No dispose on unmount: polling stops by itself when the map's runtime unsubscribes, and React's dev double
69
+ // effects would otherwise cut a live map's subscription.
70
+ const auth = useMemo(() => (token && !sample ? { token } : null), [token, sample]);
71
+ const props = useMemo(() => ({ provider: built.data, realtime: built.data, history: built.data, ontology: built.ontology, journal: built.data, links: built.data, auth }), [built, auth]);
72
+ return { props, uns: built.uns, sample };
73
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @faclon-labs/app-sdk/map/testing — an in-memory provider, a renderer that draws nothing, fixtures and contract
3
+ * checks, for tests and sample data.
4
+ */
5
+ export * from '../../vendor/map-engine/testing/index.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @faclon-labs/app-sdk/map/testing — an in-memory provider, a renderer that draws nothing, fixtures and contract
3
+ * checks, for tests and sample data.
4
+ */
5
+ export * from '../../vendor/map-engine/testing/index.js';
package/dist/session.d.ts CHANGED
@@ -13,7 +13,7 @@ export interface IOSenseSession {
13
13
  /** Forget the session in this browser and show the sign-in screen again. */
14
14
  signOut: () => void;
15
15
  }
16
- export declare const SAMPLE_NOTICE = "Sample data \u2014 not live plant readings";
16
+ export declare const SAMPLE_NOTICE = "Sample data: not live plant readings";
17
17
  export declare const SessionContext: import("react").Context<IOSenseSession | null>;
18
18
  /** The session of the nearest `<IOSenseApp>`. Throws outside one. */
19
19
  export declare function useIOSense(): IOSenseSession;
package/dist/session.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createContext, useContext } from 'react';
2
- export const SAMPLE_NOTICE = 'Sample data — not live plant readings';
2
+ export const SAMPLE_NOTICE = 'Sample data: not live plant readings';
3
3
  export const SessionContext = createContext(null);
4
4
  /** The session of the nearest `<IOSenseApp>`. Throws outside one. */
5
5
  export function useIOSense() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@faclon-labs/app-sdk",
3
- "version": "0.1.0",
4
- "description": "IOSense sign-in and plant data for React apps: the launchpad SSO session, UNS latest values and series, the signed-in user, and requests to IOSense's APIs.",
3
+ "version": "0.3.0",
4
+ "description": "IOSense sign-in, plant data and maps for React apps: the launchpad SSO session, UNS latest values and series, the signed-in user, requests to IOSense's APIs, and the Map Engine on UNS (./map).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -10,28 +10,78 @@
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js"
13
+ },
14
+ "./map": {
15
+ "types": "./dist/map/index.d.ts",
16
+ "import": "./dist/map/index.js"
17
+ },
18
+ "./map/globe": {
19
+ "types": "./dist/map/globe.d.ts",
20
+ "import": "./dist/map/globe.js"
21
+ },
22
+ "./map/testing": {
23
+ "types": "./dist/map/testing.d.ts",
24
+ "import": "./dist/map/testing.js"
13
25
  }
14
26
  },
15
27
  "files": [
16
28
  "dist",
29
+ "vendor",
17
30
  "README.md"
18
31
  ],
19
32
  "sideEffects": false,
33
+ "dependencies": {
34
+ "@deck.gl/core": "~9.4.0",
35
+ "@deck.gl/layers": "~9.4.0",
36
+ "@deck.gl/mapbox": "~9.4.0",
37
+ "@deck.gl/mesh-layers": "~9.4.0",
38
+ "@loaders.gl/core": "^4.5.2",
39
+ "@loaders.gl/gltf": "^4.5.2",
40
+ "@luma.gl/core": "~9.4.2",
41
+ "@turf/area": "^7.4.0",
42
+ "@turf/boolean-contains": "^7.4.0",
43
+ "@turf/boolean-intersects": "^7.4.0",
44
+ "@turf/boolean-within": "^7.4.0",
45
+ "@turf/circle": "^7.4.0",
46
+ "@turf/distance": "^7.4.0",
47
+ "@turf/kinks": "^7.4.0",
48
+ "@turf/line-intersect": "^7.4.0",
49
+ "@turf/point-on-feature": "^7.4.0",
50
+ "@turf/point-to-line-distance": "^7.4.0",
51
+ "@turf/point-to-polygon-distance": "^7.4.0",
52
+ "@types/geojson": "7946.0.16",
53
+ "maplibre-contour": "^0.1.1",
54
+ "pmtiles": "^4.5.0",
55
+ "rbush": "^4.0.1"
56
+ },
20
57
  "peerDependencies": {
21
- "react": ">=18"
58
+ "react": ">=18",
59
+ "maplibre-gl": ">=5.0.0",
60
+ "cesium": ">=1.120.0"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "maplibre-gl": {
64
+ "optional": true
65
+ },
66
+ "cesium": {
67
+ "optional": true
68
+ }
22
69
  },
23
70
  "keywords": [
24
71
  "iosense",
25
72
  "faclon",
26
73
  "uns",
27
74
  "react",
28
- "sso"
75
+ "sso",
76
+ "map",
77
+ "maplibre"
29
78
  ],
30
79
  "license": "UNLICENSED",
31
80
  "publishConfig": {
32
81
  "access": "public"
33
82
  },
34
83
  "scripts": {
35
- "build": "rm -rf dist && tsc -p tsconfig.json"
84
+ "vendor": "node scripts/vendor-map-engine.mjs",
85
+ "build": "node scripts/vendor-map-engine.mjs && rm -rf dist && tsc -p tsconfig.json"
36
86
  }
37
87
  }
@@ -0,0 +1 @@
1
+ Built from MapEngine c5b1f59 — do not edit; run npm run vendor.
@@ -0,0 +1,10 @@
1
+ /** Ground metres per screen pixel at a zoom and latitude (web-map convention). */
2
+ export declare function metresPerPixel(zoom: number, latitude: number): number;
3
+ /** Distance from the camera to the centre that shows `viewHeightPx` pixels of ground at `zoom` (vertical FOV in radians). */
4
+ export declare function rangeForZoom(zoom: number, latitude: number, viewHeightPx: number, fovY: number): number;
5
+ /** The inverse of rangeForZoom. */
6
+ export declare function zoomForRange(range: number, latitude: number, viewHeightPx: number, fovY: number): number;
7
+ /** Engine pitch (0 = looking straight down, up to 85) → camera pitch in degrees (−90 = straight down). */
8
+ export declare const cameraPitch: (pitch: number | undefined) => number;
9
+ /** Camera pitch in degrees → engine pitch. */
10
+ export declare const enginePitch: (cameraPitchDeg: number) => number;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The engine's viewport (centre, zoom, bearing, pitch) ↔ a 3D camera (Cesium). Pure maths, testable without WebGL.
3
+ *
4
+ * Zoom keeps its web-map meaning: at zoom z the ground under the centre measures 2π·R·cos(lat) / (256·2^z) metres
5
+ * per pixel. A camera that frames the view height at that scale stands `range` metres from the centre.
6
+ */
7
+ const EARTH_RADIUS = 6_378_137;
8
+ /** Ground metres per screen pixel at a zoom and latitude (web-map convention). */
9
+ export function metresPerPixel(zoom, latitude) {
10
+ return (2 * Math.PI * EARTH_RADIUS * Math.cos((latitude * Math.PI) / 180)) / (256 * 2 ** zoom);
11
+ }
12
+ /** Distance from the camera to the centre that shows `viewHeightPx` pixels of ground at `zoom` (vertical FOV in radians). */
13
+ export function rangeForZoom(zoom, latitude, viewHeightPx, fovY) {
14
+ return (metresPerPixel(zoom, latitude) * viewHeightPx) / 2 / Math.tan(fovY / 2);
15
+ }
16
+ /** The inverse of rangeForZoom. */
17
+ export function zoomForRange(range, latitude, viewHeightPx, fovY) {
18
+ const mpp = (2 * range * Math.tan(fovY / 2)) / Math.max(1, viewHeightPx);
19
+ const z = Math.log2((2 * Math.PI * EARTH_RADIUS * Math.cos((latitude * Math.PI) / 180)) / (256 * mpp));
20
+ return Math.max(0, Math.min(24, z));
21
+ }
22
+ /** Engine pitch (0 = looking straight down, up to 85) → camera pitch in degrees (−90 = straight down). */
23
+ export const cameraPitch = (pitch) => -90 + Math.max(0, Math.min(85, pitch ?? 0));
24
+ /** Camera pitch in degrees → engine pitch. */
25
+ export const enginePitch = (cameraPitchDeg) => Math.max(0, Math.min(85, 90 + cameraPitchDeg));
@@ -0,0 +1,2 @@
1
+ export { CesiumRenderer, DEFAULT_CESIUM_THEME, type CesiumRendererOptions, type CesiumTheme } from './renderer.js';
2
+ export { metresPerPixel, rangeForZoom, zoomForRange } from './camera.js';
@@ -0,0 +1,2 @@
1
+ export { CesiumRenderer, DEFAULT_CESIUM_THEME } from './renderer.js';
2
+ export { metresPerPixel, rangeForZoom, zoomForRange } from './camera.js';