tangram-core 0.2.0__cp313-cp313-win_amd64.whl

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 (171) hide show
  1. tangram_core/App.vue +335 -0
  2. tangram_core/__init__.py +5 -0
  3. tangram_core/__main__.py +141 -0
  4. tangram_core/_core.cp313-win_amd64.pyd +0 -0
  5. tangram_core/_core.pyi +38 -0
  6. tangram_core/api.ts +456 -0
  7. tangram_core/backend.py +335 -0
  8. tangram_core/colour.ts +71 -0
  9. tangram_core/config.py +122 -0
  10. tangram_core/dist-frontend/aggregation-layers.js +521 -0
  11. tangram_core/dist-frontend/aggregation-layers.js.map +1 -0
  12. tangram_core/dist-frontend/assets/_commonjsHelpers-CqkleIqs.js +2 -0
  13. tangram_core/dist-frontend/assets/_commonjsHelpers-CqkleIqs.js.map +1 -0
  14. tangram_core/dist-frontend/assets/array-utils-flat-wyE8tIYR.js +11 -0
  15. tangram_core/dist-frontend/assets/array-utils-flat-wyE8tIYR.js.map +1 -0
  16. tangram_core/dist-frontend/assets/assert-hrfsarFU.js +3 -0
  17. tangram_core/dist-frontend/assets/assert-hrfsarFU.js.map +1 -0
  18. tangram_core/dist-frontend/assets/b612-latin-400-italic-DePNXA0a.woff +0 -0
  19. tangram_core/dist-frontend/assets/b612-latin-400-italic-a-4GLPtl.woff2 +0 -0
  20. tangram_core/dist-frontend/assets/b612-latin-400-normal-CC98FVm_.woff2 +0 -0
  21. tangram_core/dist-frontend/assets/b612-latin-400-normal-JbZ7xwUX.woff +0 -0
  22. tangram_core/dist-frontend/assets/b612-latin-700-normal-B_Snq1wd.woff +0 -0
  23. tangram_core/dist-frontend/assets/b612-latin-700-normal-BinQrnoB.woff2 +0 -0
  24. tangram_core/dist-frontend/assets/clip-extension-DTCP51Ak.js +26 -0
  25. tangram_core/dist-frontend/assets/clip-extension-DTCP51Ak.js.map +1 -0
  26. tangram_core/dist-frontend/assets/color-CUNNsFV-.js +17 -0
  27. tangram_core/dist-frontend/assets/color-CUNNsFV-.js.map +1 -0
  28. tangram_core/dist-frontend/assets/cube-geometry-CzJ_uBWa.js +2 -0
  29. tangram_core/dist-frontend/assets/cube-geometry-CzJ_uBWa.js.map +1 -0
  30. tangram_core/dist-frontend/assets/deep-equal-uriyKJca.js +2 -0
  31. tangram_core/dist-frontend/assets/deep-equal-uriyKJca.js.map +1 -0
  32. tangram_core/dist-frontend/assets/fly-to-interpolator-DlKiy9_S.js +2 -0
  33. tangram_core/dist-frontend/assets/fly-to-interpolator-DlKiy9_S.js.map +1 -0
  34. tangram_core/dist-frontend/assets/geojson-layer-CLhXLxdI.js +1010 -0
  35. tangram_core/dist-frontend/assets/geojson-layer-CLhXLxdI.js.map +1 -0
  36. tangram_core/dist-frontend/assets/globe-view-DKhftlA1.js +94 -0
  37. tangram_core/dist-frontend/assets/globe-view-DKhftlA1.js.map +1 -0
  38. tangram_core/dist-frontend/assets/globe-viewport-CPES4D4P.js +2 -0
  39. tangram_core/dist-frontend/assets/globe-viewport-CPES4D4P.js.map +1 -0
  40. tangram_core/dist-frontend/assets/image-loader-ClbNCMXW.js +2 -0
  41. tangram_core/dist-frontend/assets/image-loader-ClbNCMXW.js.map +1 -0
  42. tangram_core/dist-frontend/assets/inconsolata-latin-400-normal-DTZQ6lD6.woff2 +0 -0
  43. tangram_core/dist-frontend/assets/inconsolata-latin-400-normal-HYADljCo.woff +0 -0
  44. tangram_core/dist-frontend/assets/inconsolata-latin-700-normal-ByjKuJjN.woff2 +0 -0
  45. tangram_core/dist-frontend/assets/inconsolata-latin-700-normal-DzgUY3Rl.woff +0 -0
  46. tangram_core/dist-frontend/assets/inconsolata-latin-ext-400-normal-BaHVOdFB.woff2 +0 -0
  47. tangram_core/dist-frontend/assets/inconsolata-latin-ext-400-normal-yvPjCxxx.woff +0 -0
  48. tangram_core/dist-frontend/assets/inconsolata-latin-ext-700-normal-D0Kpgs_9.woff2 +0 -0
  49. tangram_core/dist-frontend/assets/inconsolata-latin-ext-700-normal-Dlt-daqV.woff +0 -0
  50. tangram_core/dist-frontend/assets/inconsolata-vietnamese-400-normal-ByiM2lek.woff +0 -0
  51. tangram_core/dist-frontend/assets/inconsolata-vietnamese-400-normal-DfC_iMic.woff2 +0 -0
  52. tangram_core/dist-frontend/assets/inconsolata-vietnamese-700-normal-DLCFFAUf.woff +0 -0
  53. tangram_core/dist-frontend/assets/inconsolata-vietnamese-700-normal-DuasYmn8.woff2 +0 -0
  54. tangram_core/dist-frontend/assets/index-UPPakSLR.css +1 -0
  55. tangram_core/dist-frontend/assets/index-r8T0kY2p.js +821 -0
  56. tangram_core/dist-frontend/assets/index-r8T0kY2p.js.map +1 -0
  57. tangram_core/dist-frontend/assets/layer-DO63TrsS.js +555 -0
  58. tangram_core/dist-frontend/assets/layer-DO63TrsS.js.map +1 -0
  59. tangram_core/dist-frontend/assets/layer-extension-CZ3zsHuN.js +2 -0
  60. tangram_core/dist-frontend/assets/layer-extension-CZ3zsHuN.js.map +1 -0
  61. tangram_core/dist-frontend/assets/mesh-layers-BSECKarm.js +1123 -0
  62. tangram_core/dist-frontend/assets/mesh-layers-BSECKarm.js.map +1 -0
  63. tangram_core/dist-frontend/assets/orthographic-viewport-CzZmHDEZ.js +2 -0
  64. tangram_core/dist-frontend/assets/orthographic-viewport-CzZmHDEZ.js.map +1 -0
  65. tangram_core/dist-frontend/assets/pick-layers-pass-xhWsgZtf.js +2 -0
  66. tangram_core/dist-frontend/assets/pick-layers-pass-xhWsgZtf.js.map +1 -0
  67. tangram_core/dist-frontend/assets/project-CrvReKGW.js +760 -0
  68. tangram_core/dist-frontend/assets/project-CrvReKGW.js.map +1 -0
  69. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-italic-4qS3_zkX.woff2 +0 -0
  70. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-italic-CDK-EZBY.woff +0 -0
  71. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-normal-Bgns473E.woff +0 -0
  72. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-normal-_T2aQlWs.woff2 +0 -0
  73. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-500-normal-CvEVpWxD.woff +0 -0
  74. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-500-normal-s4PklZE0.woff2 +0 -0
  75. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-700-normal-9RN-Z7cI.woff2 +0 -0
  76. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-700-normal-BGMkBBYx.woff +0 -0
  77. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-italic-C7erd-g8.woff +0 -0
  78. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-italic-DR5R5TWx.woff2 +0 -0
  79. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-normal-DGo1Ayjq.woff2 +0 -0
  80. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-normal-WtM1l1qc.woff +0 -0
  81. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-500-normal-C8FNIdXm.woff2 +0 -0
  82. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-500-normal-TLDmfi3Q.woff +0 -0
  83. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-700-normal-CTXjXnze.woff2 +0 -0
  84. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-700-normal-CWPRiRXS.woff +0 -0
  85. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-italic-CR6qj4Z4.woff2 +0 -0
  86. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-italic-DHRaIs10.woff +0 -0
  87. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-normal-D5vBSIyg.woff2 +0 -0
  88. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-normal-FabMgVmk.woff +0 -0
  89. tangram_core/dist-frontend/assets/roboto-condensed-greek-500-normal-BIN62cw9.woff +0 -0
  90. tangram_core/dist-frontend/assets/roboto-condensed-greek-500-normal-Hsn-wDIp.woff2 +0 -0
  91. tangram_core/dist-frontend/assets/roboto-condensed-greek-700-normal-89Up2Xly.woff +0 -0
  92. tangram_core/dist-frontend/assets/roboto-condensed-greek-700-normal-DWMOA2VK.woff2 +0 -0
  93. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-italic-D_BR-3LG.woff2 +0 -0
  94. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-italic-om57GXsO.woff +0 -0
  95. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-normal-BICmKrXV.woff2 +0 -0
  96. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-normal-D2e7XwB1.woff +0 -0
  97. tangram_core/dist-frontend/assets/roboto-condensed-latin-500-normal-3p2daRJW.woff2 +0 -0
  98. tangram_core/dist-frontend/assets/roboto-condensed-latin-500-normal-Dc9bsamC.woff +0 -0
  99. tangram_core/dist-frontend/assets/roboto-condensed-latin-700-normal-BOl6B_hI.woff +0 -0
  100. tangram_core/dist-frontend/assets/roboto-condensed-latin-700-normal-DRbp0YnP.woff2 +0 -0
  101. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-italic-BXrkWnoY.woff +0 -0
  102. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-italic-Bhem1d5z.woff2 +0 -0
  103. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-normal-DT8nEsYA.woff +0 -0
  104. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-normal-OHaX69iP.woff2 +0 -0
  105. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-500-normal-CcSTXKtO.woff2 +0 -0
  106. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-500-normal-JgPl2bDS.woff +0 -0
  107. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-700-normal-B004qtqu.woff2 +0 -0
  108. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-700-normal-O6H_RRvN.woff +0 -0
  109. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-italic-BwUYFJ2t.woff2 +0 -0
  110. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-italic-DV8QogUk.woff +0 -0
  111. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-normal-0o1laQ-g.woff2 +0 -0
  112. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-normal-CPsdS8_S.woff +0 -0
  113. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-500-normal-G9shSJ2z.woff +0 -0
  114. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-500-normal-TFWhjk13.woff2 +0 -0
  115. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-700-normal-BtNeb9D6.woff +0 -0
  116. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-700-normal-D35V1G0s.woff2 +0 -0
  117. tangram_core/dist-frontend/assets/shader-BJmsOfPx.js +843 -0
  118. tangram_core/dist-frontend/assets/shader-BJmsOfPx.js.map +1 -0
  119. tangram_core/dist-frontend/assets/solid-polygon-layer-DiarVGxh.js +392 -0
  120. tangram_core/dist-frontend/assets/solid-polygon-layer-DiarVGxh.js.map +1 -0
  121. tangram_core/dist-frontend/assets/tesselator-49Dw9L5A.js +2 -0
  122. tangram_core/dist-frontend/assets/tesselator-49Dw9L5A.js.map +1 -0
  123. tangram_core/dist-frontend/assets/webgl-developer-tools-CZl8qVFg.js +7 -0
  124. tangram_core/dist-frontend/assets/webgl-developer-tools-CZl8qVFg.js.map +1 -0
  125. tangram_core/dist-frontend/assets/webgl-device-BY0-CUP6.js +3 -0
  126. tangram_core/dist-frontend/assets/webgl-device-BY0-CUP6.js.map +1 -0
  127. tangram_core/dist-frontend/assets/widget-BbOeHGj0.js +2 -0
  128. tangram_core/dist-frontend/assets/widget-BbOeHGj0.js.map +1 -0
  129. tangram_core/dist-frontend/core.js +60 -0
  130. tangram_core/dist-frontend/core.js.map +1 -0
  131. tangram_core/dist-frontend/extensions.js +609 -0
  132. tangram_core/dist-frontend/extensions.js.map +1 -0
  133. tangram_core/dist-frontend/favicon.ico +0 -0
  134. tangram_core/dist-frontend/favicon.png +0 -0
  135. tangram_core/dist-frontend/font-awesome.min.css +4 -0
  136. tangram_core/dist-frontend/fonts/FontAwesome.otf +0 -0
  137. tangram_core/dist-frontend/fonts/fontawesome-webfont.eot +0 -0
  138. tangram_core/dist-frontend/fonts/fontawesome-webfont.svg +2671 -0
  139. tangram_core/dist-frontend/fonts/fontawesome-webfont.ttf +0 -0
  140. tangram_core/dist-frontend/fonts/fontawesome-webfont.woff +0 -0
  141. tangram_core/dist-frontend/fonts/fontawesome-webfont.woff2 +0 -0
  142. tangram_core/dist-frontend/geo-layers.js +115 -0
  143. tangram_core/dist-frontend/geo-layers.js.map +1 -0
  144. tangram_core/dist-frontend/index.html +38 -0
  145. tangram_core/dist-frontend/json.js +3 -0
  146. tangram_core/dist-frontend/json.js.map +1 -0
  147. tangram_core/dist-frontend/layers.js +268 -0
  148. tangram_core/dist-frontend/layers.js.map +1 -0
  149. tangram_core/dist-frontend/lit-html.js +7 -0
  150. tangram_core/dist-frontend/lit-html.js.map +1 -0
  151. tangram_core/dist-frontend/mapbox.js +2 -0
  152. tangram_core/dist-frontend/mapbox.js.map +1 -0
  153. tangram_core/dist-frontend/maplibre-gl.js +59 -0
  154. tangram_core/dist-frontend/maplibre-gl.js.map +1 -0
  155. tangram_core/dist-frontend/mesh-layers.js +2 -0
  156. tangram_core/dist-frontend/mesh-layers.js.map +1 -0
  157. tangram_core/dist-frontend/rs1090_wasm.js +813 -0
  158. tangram_core/dist-frontend/rs1090_wasm_bg.wasm +0 -0
  159. tangram_core/dist-frontend/vue.esm-browser.prod.js +13 -0
  160. tangram_core/dist-frontend/widgets.js +3 -0
  161. tangram_core/dist-frontend/widgets.js.map +1 -0
  162. tangram_core/main.ts +16 -0
  163. tangram_core/plugin.py +70 -0
  164. tangram_core/plugin.ts +41 -0
  165. tangram_core/redis.py +89 -0
  166. tangram_core/user.css +114 -0
  167. tangram_core/vite-plugin-tangram.mjs +88 -0
  168. tangram_core-0.2.0.dist-info/METADATA +37 -0
  169. tangram_core-0.2.0.dist-info/RECORD +171 -0
  170. tangram_core-0.2.0.dist-info/WHEEL +4 -0
  171. tangram_core-0.2.0.dist-info/entry_points.txt +2 -0
tangram_core/api.ts ADDED
@@ -0,0 +1,456 @@
1
+ import {
2
+ App,
3
+ ref,
4
+ Component,
5
+ computed,
6
+ reactive,
7
+ shallowRef,
8
+ watch,
9
+ type ShallowRef,
10
+ type Ref
11
+ } from "vue";
12
+ import type { Map as MaplibreMap, LngLatBounds, StyleSpecification } from "maplibre-gl";
13
+ import { MapboxOverlay } from "@deck.gl/mapbox";
14
+ import { Socket, Channel } from "phoenix";
15
+
16
+ class NotImplementedError extends Error {
17
+ constructor(message = "this function is not yet implemented.") {
18
+ super(message);
19
+ this.name = "NotImplementedError";
20
+ }
21
+ }
22
+
23
+ export interface ChannelConfig {
24
+ url: string;
25
+ }
26
+
27
+ export interface MapConfig {
28
+ style: string | StyleSpecification;
29
+ attribution: string;
30
+ center_lat: number;
31
+ center_lon: number;
32
+ zoom: number;
33
+ pitch: number;
34
+ bearing: number;
35
+ lang: string;
36
+ }
37
+
38
+ export interface TangramConfig {
39
+ channel: ChannelConfig;
40
+ map: MapConfig;
41
+ }
42
+
43
+ export type EntityId = string;
44
+
45
+ export interface Disposable {
46
+ dispose(): void;
47
+ }
48
+
49
+ export interface Entity<TState extends EntityState = EntityState> {
50
+ readonly id: EntityId;
51
+ readonly type: string;
52
+ readonly state: TState;
53
+ }
54
+
55
+ export type EntityState = unknown;
56
+
57
+ export interface IPosition {
58
+ readonly lat: number;
59
+ readonly lng: number;
60
+ }
61
+
62
+ export interface ITimestamp {
63
+ readonly timestamp: Date;
64
+ }
65
+
66
+ export type WidgetLocation = "TopBar" | "SideBar" | "MapOverlay";
67
+
68
+ /* Local time. Not to be confused with the server time. */
69
+ export class TimeApi implements Disposable {
70
+ readonly now = ref(new Date());
71
+ readonly isPlaying = ref(false);
72
+
73
+ constructor() {}
74
+
75
+ dispose() {}
76
+
77
+ play(): void {
78
+ throw new NotImplementedError();
79
+ }
80
+ pause(): void {
81
+ throw new NotImplementedError();
82
+ }
83
+ seek(time: Date): void {
84
+ throw new NotImplementedError();
85
+ }
86
+ }
87
+
88
+ export class UiApi {
89
+ private app: App;
90
+ readonly widgets = reactive<Record<WidgetLocation, { id: string }[]>>({
91
+ TopBar: [],
92
+ SideBar: [],
93
+ MapOverlay: []
94
+ });
95
+ readonly isSidebarCollapsed = ref(true);
96
+
97
+ constructor(app: App) {
98
+ this.app = app;
99
+ }
100
+
101
+ openSidebar = (): void => {
102
+ this.isSidebarCollapsed.value = false;
103
+ };
104
+
105
+ closeSidebar = (): void => {
106
+ this.isSidebarCollapsed.value = true;
107
+ };
108
+
109
+ toggleSidebar = (): void => {
110
+ this.isSidebarCollapsed.value = !this.isSidebarCollapsed.value;
111
+ };
112
+
113
+ registerWidget(
114
+ id: string,
115
+ location: WidgetLocation,
116
+ component: Component
117
+ ): Disposable {
118
+ this.app.component(id, component);
119
+
120
+ const widget = { id };
121
+ this.widgets[location].push(widget);
122
+
123
+ return {
124
+ dispose: () => {
125
+ const locationWidgets = this.widgets[location];
126
+ const index = locationWidgets.findIndex(w => w.id === id);
127
+ if (index > -1) {
128
+ locationWidgets.splice(index, 1);
129
+ }
130
+ }
131
+ };
132
+ }
133
+ }
134
+ // NOTE: we use arrow functions to capture lexical `this` properly.
135
+
136
+ export class MapApi implements Disposable {
137
+ private tangramApi: TangramApi;
138
+
139
+ constructor(tangramApi: TangramApi) {
140
+ this.tangramApi = tangramApi;
141
+ }
142
+
143
+ readonly map = shallowRef<MaplibreMap | null>(null);
144
+ private overlay = shallowRef<MapboxOverlay | null>(null);
145
+ readonly layers = shallowRef<any[]>([]);
146
+ readonly isReady = computed(() => !!this.map.value);
147
+
148
+ readonly center = ref({ lng: 0, lat: 0 });
149
+ readonly zoom = ref(0);
150
+ readonly pitch = ref(0);
151
+ readonly bearing = ref(0);
152
+ readonly bounds: Ref<Readonly<LngLatBounds> | null> = ref(null);
153
+
154
+ private updateState = () => {
155
+ if (!this.map.value) return;
156
+ const map = this.map.value;
157
+ this.center.value = map.getCenter();
158
+ this.zoom.value = map.getZoom();
159
+ this.pitch.value = map.getPitch();
160
+ this.bearing.value = map.getBearing();
161
+ (this.bounds as Ref).value = map.getBounds();
162
+ };
163
+
164
+ initialize = (mapInstance: MaplibreMap) => {
165
+ this.map.value = mapInstance;
166
+ this.overlay.value = new MapboxOverlay({
167
+ interleaved: false,
168
+ onHover: info => {
169
+ const canvas = this.map.value?.getCanvas();
170
+ if (canvas) {
171
+ canvas.style.cursor = info.object ? "pointer" : "";
172
+ }
173
+ },
174
+ onClick: info => {
175
+ if (!info.object) {
176
+ this.tangramApi.state.deselectActiveEntity();
177
+ }
178
+ }
179
+ });
180
+ this.map.value.addControl(this.overlay.value);
181
+
182
+ watch(
183
+ this.layers,
184
+ newLayers => {
185
+ this.overlay.value?.setProps({ layers: newLayers });
186
+ },
187
+ { deep: true }
188
+ );
189
+
190
+ const onMapLoad = () => {
191
+ this.updateState();
192
+ this.map.value?.off("load", onMapLoad);
193
+ };
194
+ this.map.value.on("load", onMapLoad);
195
+
196
+ this.map.value.on("moveend", this.updateState);
197
+ this.map.value.on("zoomend", this.updateState);
198
+ this.map.value.on("pitchend", this.updateState);
199
+ this.map.value.on("rotateend", this.updateState);
200
+ };
201
+
202
+ dispose = () => {
203
+ this.map.value?.remove();
204
+ this.map.value = null;
205
+ };
206
+
207
+ getMapInstance = (): MaplibreMap => {
208
+ if (!this.map.value) {
209
+ throw new Error("map not initialized");
210
+ }
211
+ return this.map.value;
212
+ };
213
+
214
+ addLayer(layer: any): Disposable {
215
+ this.layers.value = [...this.layers.value, layer];
216
+ return {
217
+ dispose: () => {
218
+ this.layers.value = this.layers.value.filter(l => l !== layer);
219
+ }
220
+ };
221
+ }
222
+
223
+ setLayer(layer: any): Disposable {
224
+ const index = this.layers.value.findIndex(l => l.id === layer.id);
225
+ if (index >= 0) {
226
+ const newLayers = [...this.layers.value];
227
+ newLayers[index] = layer;
228
+ this.layers.value = newLayers;
229
+ } else {
230
+ this.layers.value = [...this.layers.value, layer];
231
+ }
232
+ return {
233
+ dispose: () => {
234
+ this.layers.value = this.layers.value.filter(l => l.id !== layer.id);
235
+ }
236
+ };
237
+ }
238
+ }
239
+
240
+ // TODO: in the future, entities may simply mean rows in a arrow table (e.g. from a parquet file)
241
+ // NOTE: the server may return entities within the map bounding box
242
+ // so the entities stored may not represent the full set of entities
243
+ // we thus do not provide a "total entity count" in this api.
244
+ export class StateApi {
245
+ readonly entitiesByType: Map<string, ShallowRef<Map<EntityId, Entity>>> = new Map();
246
+ readonly totalCounts: Ref<ReadonlyMap<string, number>> = ref(new Map());
247
+
248
+ private readonly _selection = shallowRef<{ id: string; type: string } | null>(null);
249
+
250
+ readonly activeEntity = computed(() => {
251
+ const sel = this._selection.value;
252
+ if (!sel) return null;
253
+ const bucket = this.entitiesByType.get(sel.type);
254
+ return bucket?.value.get(sel.id) || null;
255
+ });
256
+
257
+ registerEntityType = (type: string): void => {
258
+ if (!this.entitiesByType.has(type)) {
259
+ this.entitiesByType.set(type, shallowRef(new Map()));
260
+ }
261
+ };
262
+
263
+ getEntitiesByType = <T extends EntityState>(
264
+ type: string
265
+ ): Ref<ReadonlyMap<EntityId, Entity<T>>> => {
266
+ if (!this.entitiesByType.has(type)) {
267
+ this.entitiesByType.set(type, shallowRef(new Map()));
268
+ }
269
+ return this.entitiesByType.get(type) as Ref<ReadonlyMap<EntityId, Entity<T>>>;
270
+ };
271
+
272
+ replaceAllEntitiesByType = (type: string, newEntities: Entity[]): void => {
273
+ let bucket = this.entitiesByType.get(type);
274
+ if (!bucket) {
275
+ bucket = shallowRef(new Map());
276
+ this.entitiesByType.set(type, bucket);
277
+ }
278
+
279
+ const newMap = new Map<EntityId, Entity>();
280
+ for (const entity of newEntities) {
281
+ newMap.set(entity.id, entity);
282
+ }
283
+ bucket.value = newMap;
284
+ };
285
+
286
+ setActiveEntity = (entity: Entity): void => {
287
+ this._selection.value = { id: entity.id, type: entity.type };
288
+ };
289
+
290
+ deselectActiveEntity = (): void => {
291
+ this._selection.value = null;
292
+ };
293
+
294
+ setTotalCount = (type: string, count: number): void => {
295
+ const newMap = new Map(this.totalCounts.value);
296
+ newMap.set(type, count);
297
+ this.totalCounts.value = newMap;
298
+ };
299
+ }
300
+
301
+ export class RealtimeApi {
302
+ private socket: Socket | null = null;
303
+ private channels: Map<string, Channel> = new Map();
304
+ private channelConfig: ChannelConfig;
305
+ private connectionPromise: Promise<void> | null = null;
306
+ private connectionId: string | null = null;
307
+
308
+ constructor(config: TangramConfig) {
309
+ this.channelConfig = config.channel;
310
+ }
311
+
312
+ getConnectionId(): string | null {
313
+ return this.connectionId;
314
+ }
315
+
316
+ async ensureConnected(): Promise<string> {
317
+ await this.connect();
318
+ if (!this.connectionId) {
319
+ throw new Error("connection id unavailable after connect");
320
+ }
321
+ return this.connectionId;
322
+ }
323
+
324
+ private async fetchToken(channel: string): Promise<{ id: string; token: string }> {
325
+ const tokenUrl = `${this.channelConfig.url}/token`;
326
+ const body: Record<string, string> = { channel };
327
+ if (this.connectionId) {
328
+ body.id = this.connectionId;
329
+ }
330
+
331
+ const resp = await fetch(tokenUrl, {
332
+ method: "POST",
333
+ headers: { "Content-Type": "application/json" },
334
+ body: JSON.stringify(body)
335
+ });
336
+ if (!resp.ok) {
337
+ throw new Error(`failed to fetch token: ${resp.statusText}`);
338
+ }
339
+ return resp.json();
340
+ }
341
+
342
+ private connect(): Promise<void> {
343
+ if (!this.connectionPromise) {
344
+ this.connectionPromise = (async () => {
345
+ try {
346
+ if (this.socket?.isConnected()) return;
347
+
348
+ const { id, token: userToken } = await this.fetchToken("system");
349
+ this.connectionId = id;
350
+
351
+ // NOTE: phoenix appends `/websocket` automatically, do not add it here.
352
+ const socketUrl = this.channelConfig.url.replace(/^http/, "ws");
353
+ this.socket = new Socket(socketUrl, { params: { userToken } });
354
+
355
+ await new Promise<void>((resolve, reject) => {
356
+ this.socket!.connect();
357
+ this.socket!.onOpen(() => {
358
+ console.log("ws connected");
359
+ resolve();
360
+ });
361
+ this.socket!.onError(e => {
362
+ console.error("ws connection error:", e);
363
+ this.connectionPromise = null;
364
+ reject(e);
365
+ });
366
+ });
367
+ } catch (e) {
368
+ this.connectionPromise = null;
369
+ throw e;
370
+ }
371
+ })();
372
+ }
373
+ return this.connectionPromise!;
374
+ }
375
+
376
+ private async getChannel(topic: string): Promise<Channel> {
377
+ await this.connect();
378
+ if (!this.socket) throw new Error("socket connection failed");
379
+
380
+ if (this.channels.has(topic)) {
381
+ const channel = this.channels.get(topic)!;
382
+ if (channel.state === "joined") {
383
+ return channel;
384
+ }
385
+ }
386
+
387
+ const { token } = await this.fetchToken(topic);
388
+ const channel = this.socket.channel(topic, { token });
389
+ this.channels.set(topic, channel);
390
+
391
+ return new Promise((resolve, reject) => {
392
+ channel
393
+ .join()
394
+ .receive("ok", () => resolve(channel))
395
+ .receive("error", reason => reject(reason))
396
+ .receive("timeout", () => reject("channel join timeout"));
397
+ });
398
+ }
399
+
400
+ private parseTopicEvent(topic: string): [string, string] {
401
+ const parts = topic.split(":");
402
+ if (parts.length < 2) throw new Error(`invalid topic:event format: ${topic}`);
403
+ const event = parts.pop()!;
404
+ const channelTopic = parts.join(":");
405
+ return [channelTopic, event];
406
+ }
407
+
408
+ async subscribe<T>(
409
+ topic: string,
410
+ callback: (payload: T) => void
411
+ ): Promise<Disposable> {
412
+ const [channelTopic, event] = this.parseTopicEvent(topic);
413
+ const channel = await this.getChannel(channelTopic);
414
+ const ref = channel.on(event, callback);
415
+ return { dispose: () => channel.off(event, ref) };
416
+ }
417
+
418
+ async publish<T>(topic: string, payload: T): Promise<void> {
419
+ const [channelTopic, event] = this.parseTopicEvent(topic);
420
+ const channel = await this.getChannel(channelTopic);
421
+ channel.push(event, payload);
422
+ }
423
+ }
424
+
425
+ export class TangramApi {
426
+ readonly time: TimeApi;
427
+ readonly ui: UiApi;
428
+ readonly map: MapApi;
429
+ readonly state: StateApi;
430
+ readonly realtime: RealtimeApi;
431
+ readonly config: TangramConfig;
432
+
433
+ private constructor(
434
+ private app: App,
435
+ config: TangramConfig
436
+ ) {
437
+ this.config = config;
438
+ this.realtime = new RealtimeApi(config);
439
+ this.ui = new UiApi(this.app);
440
+ this.time = new TimeApi();
441
+ this.map = new MapApi(this);
442
+ this.state = new StateApi();
443
+ }
444
+
445
+ public static async create(app: App): Promise<TangramApi> {
446
+ const config: TangramConfig = await fetch("/config").then(res => {
447
+ if (!res.ok) throw new Error("failed to fetch `/config`!");
448
+ return res.json();
449
+ });
450
+ return new TangramApi(app, config);
451
+ }
452
+
453
+ getVueApp(): App {
454
+ return this.app;
455
+ }
456
+ }