tangram-core 0.3.0__cp310-cp310-manylinux_2_28_aarch64.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.
- tangram_core/App.vue +441 -0
- tangram_core/CommandPalette.vue +200 -0
- tangram_core/HighlightText.vue +32 -0
- tangram_core/__Timeline.vue +300 -0
- tangram_core/__init__.py +5 -0
- tangram_core/__main__.py +331 -0
- tangram_core/_core.cpython-310-aarch64-linux-gnu.so +0 -0
- tangram_core/_core.pyi +38 -0
- tangram_core/api.ts +652 -0
- tangram_core/backend.py +458 -0
- tangram_core/components.ts +2 -0
- tangram_core/config.py +167 -0
- tangram_core/dist-frontend/aggregation-layers.js +521 -0
- tangram_core/dist-frontend/aggregation-layers.js.map +1 -0
- tangram_core/dist-frontend/assets/_commonjsHelpers-CqkleIqs.js +2 -0
- tangram_core/dist-frontend/assets/_commonjsHelpers-CqkleIqs.js.map +1 -0
- tangram_core/dist-frontend/assets/array-utils-flat-BBMak426.js +11 -0
- tangram_core/dist-frontend/assets/array-utils-flat-BBMak426.js.map +1 -0
- tangram_core/dist-frontend/assets/assert-cyW4mg7q.js +3 -0
- tangram_core/dist-frontend/assets/assert-cyW4mg7q.js.map +1 -0
- tangram_core/dist-frontend/assets/b612-latin-400-italic-DePNXA0a.woff +0 -0
- tangram_core/dist-frontend/assets/b612-latin-400-italic-a-4GLPtl.woff2 +0 -0
- tangram_core/dist-frontend/assets/b612-latin-400-normal-CC98FVm_.woff2 +0 -0
- tangram_core/dist-frontend/assets/b612-latin-400-normal-JbZ7xwUX.woff +0 -0
- tangram_core/dist-frontend/assets/b612-latin-700-normal-B_Snq1wd.woff +0 -0
- tangram_core/dist-frontend/assets/b612-latin-700-normal-BinQrnoB.woff2 +0 -0
- tangram_core/dist-frontend/assets/clip-extension-D-rbmFPj.js +26 -0
- tangram_core/dist-frontend/assets/clip-extension-D-rbmFPj.js.map +1 -0
- tangram_core/dist-frontend/assets/color-CUNNsFV-.js +17 -0
- tangram_core/dist-frontend/assets/color-CUNNsFV-.js.map +1 -0
- tangram_core/dist-frontend/assets/cube-geometry-v0HQ793i.js +2 -0
- tangram_core/dist-frontend/assets/cube-geometry-v0HQ793i.js.map +1 -0
- tangram_core/dist-frontend/assets/deep-equal-BTW2ZN6S.js +2 -0
- tangram_core/dist-frontend/assets/deep-equal-BTW2ZN6S.js.map +1 -0
- tangram_core/dist-frontend/assets/fly-to-interpolator-CIXGjOdo.js +2 -0
- tangram_core/dist-frontend/assets/fly-to-interpolator-CIXGjOdo.js.map +1 -0
- tangram_core/dist-frontend/assets/geojson-layer-DgMOQ4Qu.js +1010 -0
- tangram_core/dist-frontend/assets/geojson-layer-DgMOQ4Qu.js.map +1 -0
- tangram_core/dist-frontend/assets/globe-view-Day_n1iB.js +94 -0
- tangram_core/dist-frontend/assets/globe-view-Day_n1iB.js.map +1 -0
- tangram_core/dist-frontend/assets/globe-viewport-tqhQW7C4.js +2 -0
- tangram_core/dist-frontend/assets/globe-viewport-tqhQW7C4.js.map +1 -0
- tangram_core/dist-frontend/assets/image-loader-hHJsndO6.js +2 -0
- tangram_core/dist-frontend/assets/image-loader-hHJsndO6.js.map +1 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-400-normal-DTZQ6lD6.woff2 +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-400-normal-HYADljCo.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-700-normal-ByjKuJjN.woff2 +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-700-normal-DzgUY3Rl.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-ext-400-normal-BaHVOdFB.woff2 +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-ext-400-normal-yvPjCxxx.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-ext-700-normal-D0Kpgs_9.woff2 +0 -0
- tangram_core/dist-frontend/assets/inconsolata-latin-ext-700-normal-Dlt-daqV.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-vietnamese-400-normal-ByiM2lek.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-vietnamese-400-normal-DfC_iMic.woff2 +0 -0
- tangram_core/dist-frontend/assets/inconsolata-vietnamese-700-normal-DLCFFAUf.woff +0 -0
- tangram_core/dist-frontend/assets/inconsolata-vietnamese-700-normal-DuasYmn8.woff2 +0 -0
- tangram_core/dist-frontend/assets/index-CcogpxdD.js +824 -0
- tangram_core/dist-frontend/assets/index-CcogpxdD.js.map +1 -0
- tangram_core/dist-frontend/assets/index-SSLdizTv.css +1 -0
- tangram_core/dist-frontend/assets/layer-DPcO4AXQ.js +555 -0
- tangram_core/dist-frontend/assets/layer-DPcO4AXQ.js.map +1 -0
- tangram_core/dist-frontend/assets/layer-extension-CYwTXf73.js +2 -0
- tangram_core/dist-frontend/assets/layer-extension-CYwTXf73.js.map +1 -0
- tangram_core/dist-frontend/assets/mesh-layers-wiqredoy.js +1123 -0
- tangram_core/dist-frontend/assets/mesh-layers-wiqredoy.js.map +1 -0
- tangram_core/dist-frontend/assets/orthographic-viewport-B4nCj5tn.js +2 -0
- tangram_core/dist-frontend/assets/orthographic-viewport-B4nCj5tn.js.map +1 -0
- tangram_core/dist-frontend/assets/pick-layers-pass-C-3k0wbN.js +2 -0
- tangram_core/dist-frontend/assets/pick-layers-pass-C-3k0wbN.js.map +1 -0
- tangram_core/dist-frontend/assets/project-BTjD2Imj.js +760 -0
- tangram_core/dist-frontend/assets/project-BTjD2Imj.js.map +1 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-italic-4qS3_zkX.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-italic-CDK-EZBY.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-normal-Bgns473E.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-normal-_T2aQlWs.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-500-normal-CvEVpWxD.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-500-normal-s4PklZE0.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-700-normal-9RN-Z7cI.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-700-normal-BGMkBBYx.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-italic-C7erd-g8.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-italic-DR5R5TWx.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-normal-DGo1Ayjq.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-normal-WtM1l1qc.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-500-normal-C8FNIdXm.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-500-normal-TLDmfi3Q.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-700-normal-CTXjXnze.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-700-normal-CWPRiRXS.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-400-italic-CR6qj4Z4.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-400-italic-DHRaIs10.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-400-normal-D5vBSIyg.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-400-normal-FabMgVmk.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-500-normal-BIN62cw9.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-500-normal-Hsn-wDIp.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-700-normal-89Up2Xly.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-greek-700-normal-DWMOA2VK.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-400-italic-D_BR-3LG.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-400-italic-om57GXsO.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-400-normal-BICmKrXV.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-400-normal-D2e7XwB1.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-500-normal-3p2daRJW.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-500-normal-Dc9bsamC.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-700-normal-BOl6B_hI.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-700-normal-DRbp0YnP.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-italic-BXrkWnoY.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-italic-Bhem1d5z.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-normal-DT8nEsYA.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-normal-OHaX69iP.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-500-normal-CcSTXKtO.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-500-normal-JgPl2bDS.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-700-normal-B004qtqu.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-700-normal-O6H_RRvN.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-italic-BwUYFJ2t.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-italic-DV8QogUk.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-normal-0o1laQ-g.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-normal-CPsdS8_S.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-500-normal-G9shSJ2z.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-500-normal-TFWhjk13.woff2 +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-700-normal-BtNeb9D6.woff +0 -0
- tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-700-normal-D35V1G0s.woff2 +0 -0
- tangram_core/dist-frontend/assets/shader-Cbdysp2j.js +843 -0
- tangram_core/dist-frontend/assets/shader-Cbdysp2j.js.map +1 -0
- tangram_core/dist-frontend/assets/solid-polygon-layer-DJFl_7Ca.js +392 -0
- tangram_core/dist-frontend/assets/solid-polygon-layer-DJFl_7Ca.js.map +1 -0
- tangram_core/dist-frontend/assets/tesselator-CENyUZ2p.js +2 -0
- tangram_core/dist-frontend/assets/tesselator-CENyUZ2p.js.map +1 -0
- tangram_core/dist-frontend/assets/webgl-developer-tools-utTNOsNf.js +7 -0
- tangram_core/dist-frontend/assets/webgl-developer-tools-utTNOsNf.js.map +1 -0
- tangram_core/dist-frontend/assets/webgl-device-BYRB-GQX.js +3 -0
- tangram_core/dist-frontend/assets/webgl-device-BYRB-GQX.js.map +1 -0
- tangram_core/dist-frontend/assets/widget-BjgEeHAL.js +2 -0
- tangram_core/dist-frontend/assets/widget-BjgEeHAL.js.map +1 -0
- tangram_core/dist-frontend/core.js +60 -0
- tangram_core/dist-frontend/core.js.map +1 -0
- tangram_core/dist-frontend/extensions.js +609 -0
- tangram_core/dist-frontend/extensions.js.map +1 -0
- tangram_core/dist-frontend/favicon.ico +0 -0
- tangram_core/dist-frontend/favicon.png +0 -0
- tangram_core/dist-frontend/geo-layers.js +115 -0
- tangram_core/dist-frontend/geo-layers.js.map +1 -0
- tangram_core/dist-frontend/index.html +39 -0
- tangram_core/dist-frontend/json.js +3 -0
- tangram_core/dist-frontend/json.js.map +1 -0
- tangram_core/dist-frontend/layers.js +268 -0
- tangram_core/dist-frontend/layers.js.map +1 -0
- tangram_core/dist-frontend/mapbox.js +2 -0
- tangram_core/dist-frontend/mapbox.js.map +1 -0
- tangram_core/dist-frontend/mesh-layers.js +2 -0
- tangram_core/dist-frontend/mesh-layers.js.map +1 -0
- tangram_core/dist-frontend/widgets.js +3 -0
- tangram_core/dist-frontend/widgets.js.map +1 -0
- tangram_core/main.ts +28 -0
- tangram_core/package.json +62 -0
- tangram_core/plugin.py +109 -0
- tangram_core/plugin.ts +47 -0
- tangram_core/redis.py +89 -0
- tangram_core/user.css +114 -0
- tangram_core/utils.ts +143 -0
- tangram_core/vite-plugin-tangram.mjs +155 -0
- tangram_core-0.3.0.dist-info/METADATA +101 -0
- tangram_core-0.3.0.dist-info/RECORD +162 -0
- tangram_core-0.3.0.dist-info/WHEEL +4 -0
- tangram_core-0.3.0.dist-info/entry_points.txt +2 -0
tangram_core/api.ts
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
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 type { Layer } from "@deck.gl/core";
|
|
15
|
+
import { Socket, Channel } from "phoenix";
|
|
16
|
+
|
|
17
|
+
class NotImplementedError extends Error {
|
|
18
|
+
constructor(message = "this function is not yet implemented.") {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "NotImplementedError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ChannelConfig {
|
|
25
|
+
url: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MapConfig {
|
|
29
|
+
style: string | StyleSpecification;
|
|
30
|
+
attribution: string;
|
|
31
|
+
center_lat: number;
|
|
32
|
+
center_lon: number;
|
|
33
|
+
zoom: number;
|
|
34
|
+
pitch: number;
|
|
35
|
+
bearing: number;
|
|
36
|
+
lang: string;
|
|
37
|
+
min_zoom: number;
|
|
38
|
+
max_zoom: number;
|
|
39
|
+
max_pitch: number;
|
|
40
|
+
allow_pitch: boolean;
|
|
41
|
+
allow_bearing: boolean;
|
|
42
|
+
enable_3d: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TangramConfig {
|
|
46
|
+
channel: ChannelConfig;
|
|
47
|
+
map: MapConfig;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type EntityId = string;
|
|
51
|
+
|
|
52
|
+
export interface Disposable {
|
|
53
|
+
dispose(): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Entity<TState extends EntityState = EntityState> {
|
|
57
|
+
readonly id: EntityId;
|
|
58
|
+
readonly type: string;
|
|
59
|
+
readonly state: TState;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type EntityState = unknown;
|
|
63
|
+
|
|
64
|
+
export interface IPosition {
|
|
65
|
+
readonly lat: number;
|
|
66
|
+
readonly lng: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ITimestamp {
|
|
70
|
+
readonly timestamp: Date;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type WidgetLocation = "TopBar" | "SideBar" | "MapOverlay";
|
|
74
|
+
|
|
75
|
+
/* Local time. Not to be confused with the server time. */
|
|
76
|
+
export class TimeApi implements Disposable {
|
|
77
|
+
readonly now = ref(new Date());
|
|
78
|
+
readonly isPlaying = ref(false);
|
|
79
|
+
|
|
80
|
+
constructor() {}
|
|
81
|
+
|
|
82
|
+
dispose() {}
|
|
83
|
+
|
|
84
|
+
play(): void {
|
|
85
|
+
throw new NotImplementedError();
|
|
86
|
+
}
|
|
87
|
+
pause(): void {
|
|
88
|
+
throw new NotImplementedError();
|
|
89
|
+
}
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
91
|
+
seek(_time: Date): void {
|
|
92
|
+
throw new NotImplementedError();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface WidgetOptions {
|
|
97
|
+
priority?: number;
|
|
98
|
+
title?: string;
|
|
99
|
+
relevantFor?: string | string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface WidgetEntry extends WidgetOptions {
|
|
103
|
+
id: string;
|
|
104
|
+
priority: number;
|
|
105
|
+
isCollapsed: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class UiApi {
|
|
109
|
+
private app: App;
|
|
110
|
+
readonly widgets = reactive<Record<WidgetLocation, WidgetEntry[]>>({
|
|
111
|
+
TopBar: [],
|
|
112
|
+
SideBar: [],
|
|
113
|
+
MapOverlay: []
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
constructor(app: App) {
|
|
117
|
+
this.app = app;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
registerWidget(
|
|
121
|
+
id: string,
|
|
122
|
+
location: WidgetLocation,
|
|
123
|
+
component: Component,
|
|
124
|
+
options: WidgetOptions = {}
|
|
125
|
+
): Disposable {
|
|
126
|
+
this.app.component(id, component);
|
|
127
|
+
|
|
128
|
+
// TODO: deckgl map overlays order
|
|
129
|
+
const { priority = 0, title, relevantFor } = options;
|
|
130
|
+
const effectivePriority = location === "MapOverlay" ? 0 : priority;
|
|
131
|
+
|
|
132
|
+
const widget: WidgetEntry = {
|
|
133
|
+
id,
|
|
134
|
+
priority: effectivePriority,
|
|
135
|
+
title,
|
|
136
|
+
relevantFor,
|
|
137
|
+
isCollapsed: false
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
this.widgets[location].push(widget);
|
|
141
|
+
this.widgets[location].sort((a, b) => b.priority - a.priority);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
dispose: () => {
|
|
145
|
+
const locationWidgets = this.widgets[location];
|
|
146
|
+
const index = locationWidgets.findIndex(w => w.id === id);
|
|
147
|
+
if (index > -1) {
|
|
148
|
+
locationWidgets.splice(index, 1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// NOTE: we use arrow functions to capture lexical `this` properly.
|
|
155
|
+
|
|
156
|
+
export class MapApi implements Disposable {
|
|
157
|
+
private tangramApi: TangramApi;
|
|
158
|
+
|
|
159
|
+
constructor(tangramApi: TangramApi) {
|
|
160
|
+
this.tangramApi = tangramApi;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
readonly map = shallowRef<MaplibreMap | null>(null);
|
|
164
|
+
private overlay = shallowRef<MapboxOverlay | null>(null);
|
|
165
|
+
readonly layers = shallowRef<Layer[]>([]);
|
|
166
|
+
readonly isReady = computed(() => !!this.map.value);
|
|
167
|
+
|
|
168
|
+
readonly center = ref({ lng: 0, lat: 0 });
|
|
169
|
+
readonly zoom = ref(0);
|
|
170
|
+
readonly pitch = ref(0);
|
|
171
|
+
readonly bearing = ref(0);
|
|
172
|
+
readonly bounds: Ref<Readonly<LngLatBounds> | null> = ref(null);
|
|
173
|
+
|
|
174
|
+
private updateState = () => {
|
|
175
|
+
if (!this.map.value) return;
|
|
176
|
+
const map = this.map.value;
|
|
177
|
+
this.center.value = map.getCenter();
|
|
178
|
+
this.zoom.value = map.getZoom();
|
|
179
|
+
this.pitch.value = map.getPitch();
|
|
180
|
+
this.bearing.value = map.getBearing();
|
|
181
|
+
(this.bounds as Ref).value = map.getBounds();
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
initialize = (mapInstance: MaplibreMap) => {
|
|
185
|
+
this.map.value = mapInstance;
|
|
186
|
+
this.overlay.value = new MapboxOverlay({
|
|
187
|
+
interleaved: false,
|
|
188
|
+
onHover: info => {
|
|
189
|
+
const canvas = this.map.value?.getCanvas();
|
|
190
|
+
if (canvas) {
|
|
191
|
+
canvas.style.cursor = info.object ? "pointer" : "";
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
onClick: info => {
|
|
195
|
+
if (!info.object) {
|
|
196
|
+
this.tangramApi.state.clearSelection();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
this.map.value.addControl(this.overlay.value);
|
|
201
|
+
|
|
202
|
+
watch(
|
|
203
|
+
this.layers,
|
|
204
|
+
newLayers => {
|
|
205
|
+
this.overlay.value?.setProps({ layers: newLayers });
|
|
206
|
+
},
|
|
207
|
+
{ deep: true }
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const onMapLoad = () => {
|
|
211
|
+
this.updateState();
|
|
212
|
+
this.map.value?.off("load", onMapLoad);
|
|
213
|
+
};
|
|
214
|
+
this.map.value.on("load", onMapLoad);
|
|
215
|
+
|
|
216
|
+
this.map.value.on("moveend", this.updateState);
|
|
217
|
+
this.map.value.on("zoomend", this.updateState);
|
|
218
|
+
this.map.value.on("pitchend", this.updateState);
|
|
219
|
+
this.map.value.on("rotateend", this.updateState);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
dispose = () => {
|
|
223
|
+
this.map.value?.remove();
|
|
224
|
+
this.map.value = null;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
getMapInstance = (): MaplibreMap => {
|
|
228
|
+
if (!this.map.value) {
|
|
229
|
+
throw new Error("map not initialized");
|
|
230
|
+
}
|
|
231
|
+
return this.map.value;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
addLayer(layer: Layer): Disposable {
|
|
235
|
+
this.layers.value = [...this.layers.value, layer];
|
|
236
|
+
return {
|
|
237
|
+
dispose: () => {
|
|
238
|
+
this.layers.value = this.layers.value.filter(l => l !== layer);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
setLayer(layer: Layer): Disposable {
|
|
244
|
+
const index = this.layers.value.findIndex(l => l.id === layer.id);
|
|
245
|
+
if (index >= 0) {
|
|
246
|
+
const newLayers = [...this.layers.value];
|
|
247
|
+
newLayers[index] = layer;
|
|
248
|
+
this.layers.value = newLayers;
|
|
249
|
+
} else {
|
|
250
|
+
this.layers.value = [...this.layers.value, layer];
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
dispose: () => {
|
|
254
|
+
this.layers.value = this.layers.value.filter(l => l.id !== layer.id);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// TODO: in the future, entities may simply mean rows in a arrow table (e.g. from a parquet file)
|
|
261
|
+
// NOTE: the server may return entities within the map bounding box
|
|
262
|
+
// so the entities stored may not represent the full set of entities
|
|
263
|
+
// we thus do not provide a "total entity count" in this api.
|
|
264
|
+
export class StateApi {
|
|
265
|
+
readonly entitiesByType: Map<string, ShallowRef<Map<EntityId, Entity>>> = new Map();
|
|
266
|
+
readonly totalCounts: Ref<ReadonlyMap<string, number>> = ref(new Map());
|
|
267
|
+
|
|
268
|
+
readonly activeEntities = shallowRef<Map<EntityId, Entity>>(new Map());
|
|
269
|
+
|
|
270
|
+
registerEntityType = (type: string): void => {
|
|
271
|
+
if (!this.entitiesByType.has(type)) {
|
|
272
|
+
this.entitiesByType.set(type, shallowRef(new Map()));
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
getEntitiesByType = <T extends EntityState>(
|
|
277
|
+
type: string
|
|
278
|
+
): Ref<ReadonlyMap<EntityId, Entity<T>>> => {
|
|
279
|
+
if (!this.entitiesByType.has(type)) {
|
|
280
|
+
this.entitiesByType.set(type, shallowRef(new Map()));
|
|
281
|
+
}
|
|
282
|
+
return this.entitiesByType.get(type) as Ref<ReadonlyMap<EntityId, Entity<T>>>;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
replaceAllEntitiesByType = (type: string, newEntities: Entity[]): void => {
|
|
286
|
+
let bucket = this.entitiesByType.get(type);
|
|
287
|
+
if (!bucket) {
|
|
288
|
+
bucket = shallowRef(new Map());
|
|
289
|
+
this.entitiesByType.set(type, bucket);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const newMap = new Map<EntityId, Entity>();
|
|
293
|
+
for (const entity of newEntities) {
|
|
294
|
+
newMap.set(entity.id, entity);
|
|
295
|
+
}
|
|
296
|
+
bucket.value = newMap;
|
|
297
|
+
|
|
298
|
+
const currentActive = new Map(this.activeEntities.value);
|
|
299
|
+
let changed = false;
|
|
300
|
+
for (const [id, entity] of currentActive) {
|
|
301
|
+
if (entity.type === type) {
|
|
302
|
+
const fresh = newMap.get(id);
|
|
303
|
+
if (fresh) {
|
|
304
|
+
currentActive.set(id, fresh);
|
|
305
|
+
changed = true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (changed) {
|
|
310
|
+
this.activeEntities.value = currentActive;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
selectEntity = (entity: Entity, exclusive: boolean = true): void => {
|
|
315
|
+
if (exclusive) {
|
|
316
|
+
const newMap = new Map();
|
|
317
|
+
newMap.set(entity.id, entity);
|
|
318
|
+
this.activeEntities.value = newMap;
|
|
319
|
+
} else {
|
|
320
|
+
const newMap = new Map(this.activeEntities.value);
|
|
321
|
+
newMap.set(entity.id, entity);
|
|
322
|
+
this.activeEntities.value = newMap;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
deselectEntity = (entityId: EntityId): void => {
|
|
327
|
+
const newMap = new Map(this.activeEntities.value);
|
|
328
|
+
if (newMap.delete(entityId)) {
|
|
329
|
+
this.activeEntities.value = newMap;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
clearSelection = (): void => {
|
|
334
|
+
if (this.activeEntities.value.size > 0) {
|
|
335
|
+
this.activeEntities.value = new Map();
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
setTotalCount = (type: string, count: number): void => {
|
|
340
|
+
const newMap = new Map(this.totalCounts.value);
|
|
341
|
+
newMap.set(type, count);
|
|
342
|
+
this.totalCounts.value = newMap;
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export class RealtimeApi {
|
|
347
|
+
private socket: Socket | null = null;
|
|
348
|
+
private channels: Map<string, Channel> = new Map();
|
|
349
|
+
private channelConfig: ChannelConfig;
|
|
350
|
+
private connectionPromise: Promise<void> | null = null;
|
|
351
|
+
private connectionId: string | null = null;
|
|
352
|
+
private joinPromises: Map<string, Promise<Channel>> = new Map();
|
|
353
|
+
|
|
354
|
+
constructor(config: TangramConfig) {
|
|
355
|
+
this.channelConfig = config.channel;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
getConnectionId(): string | null {
|
|
359
|
+
return this.connectionId;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async ensureConnected(): Promise<string> {
|
|
363
|
+
await this.connect();
|
|
364
|
+
if (!this.connectionId) {
|
|
365
|
+
throw new Error("connection id unavailable after connect");
|
|
366
|
+
}
|
|
367
|
+
return this.connectionId;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private async fetchToken(channel: string): Promise<{ id: string; token: string }> {
|
|
371
|
+
const tokenUrl = `${this.channelConfig.url}/token`;
|
|
372
|
+
const body: Record<string, string> = { channel };
|
|
373
|
+
if (this.connectionId) {
|
|
374
|
+
body.id = this.connectionId;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const resp = await fetch(tokenUrl, {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: { "Content-Type": "application/json" },
|
|
380
|
+
body: JSON.stringify(body)
|
|
381
|
+
});
|
|
382
|
+
if (!resp.ok) {
|
|
383
|
+
throw new Error(`failed to fetch token: ${resp.statusText}`);
|
|
384
|
+
}
|
|
385
|
+
return resp.json();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private connect(): Promise<void> {
|
|
389
|
+
if (!this.connectionPromise) {
|
|
390
|
+
this.connectionPromise = (async () => {
|
|
391
|
+
try {
|
|
392
|
+
if (this.socket?.isConnected()) return;
|
|
393
|
+
|
|
394
|
+
const { id, token: userToken } = await this.fetchToken("system");
|
|
395
|
+
this.connectionId = id;
|
|
396
|
+
|
|
397
|
+
// NOTE: phoenix appends `/websocket` automatically, do not add it here.
|
|
398
|
+
const socketUrl = this.channelConfig.url.replace(/^http/, "ws");
|
|
399
|
+
this.socket = new Socket(socketUrl, { params: { userToken } });
|
|
400
|
+
|
|
401
|
+
await new Promise<void>((resolve, reject) => {
|
|
402
|
+
this.socket!.connect();
|
|
403
|
+
this.socket!.onOpen(() => {
|
|
404
|
+
console.log("ws connected");
|
|
405
|
+
resolve();
|
|
406
|
+
});
|
|
407
|
+
this.socket!.onError(e => {
|
|
408
|
+
console.error("ws connection error:", e);
|
|
409
|
+
this.connectionPromise = null;
|
|
410
|
+
reject(e);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
} catch (e) {
|
|
414
|
+
this.connectionPromise = null;
|
|
415
|
+
throw e;
|
|
416
|
+
}
|
|
417
|
+
})();
|
|
418
|
+
}
|
|
419
|
+
return this.connectionPromise!;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Multiple plugins can subscribe to the *same* Phoenix channel topic,
|
|
424
|
+
* e.g. `streaming-<conn_id>` but listen to different events (e.g.
|
|
425
|
+
* `new-jet1090-data` or `new-ship162-data`).
|
|
426
|
+
*
|
|
427
|
+
* To avoid two plugins racing to create a channel instance which can
|
|
428
|
+
* overwrite each other, we store in-flight join promises in `joinPromises`
|
|
429
|
+
* map so both plugins receive the same `Channel` instance.
|
|
430
|
+
*/
|
|
431
|
+
private async getChannel(topic: string): Promise<Channel> {
|
|
432
|
+
await this.connect();
|
|
433
|
+
if (!this.socket) throw new Error("socket connection failed");
|
|
434
|
+
|
|
435
|
+
if (this.joinPromises.has(topic)) {
|
|
436
|
+
return this.joinPromises.get(topic)!;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (this.channels.has(topic)) {
|
|
440
|
+
const channel = this.channels.get(topic)!;
|
|
441
|
+
if (channel.state === "joined") {
|
|
442
|
+
return channel;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const joinPromise = (async () => {
|
|
447
|
+
try {
|
|
448
|
+
let channel = this.channels.get(topic);
|
|
449
|
+
if (!channel) {
|
|
450
|
+
const { token } = await this.fetchToken(topic);
|
|
451
|
+
channel = this.socket!.channel(topic, { token });
|
|
452
|
+
this.channels.set(topic, channel);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (channel.state === "joined") return channel;
|
|
456
|
+
|
|
457
|
+
await new Promise<void>((resolve, reject) => {
|
|
458
|
+
channel!
|
|
459
|
+
.join()
|
|
460
|
+
.receive("ok", () => resolve())
|
|
461
|
+
.receive("error", reason => reject(reason))
|
|
462
|
+
.receive("timeout", () => reject("channel join timeout"));
|
|
463
|
+
});
|
|
464
|
+
return channel!;
|
|
465
|
+
} catch (e) {
|
|
466
|
+
this.channels.delete(topic);
|
|
467
|
+
throw e;
|
|
468
|
+
}
|
|
469
|
+
})();
|
|
470
|
+
|
|
471
|
+
this.joinPromises.set(topic, joinPromise);
|
|
472
|
+
try {
|
|
473
|
+
return await joinPromise;
|
|
474
|
+
} finally {
|
|
475
|
+
this.joinPromises.delete(topic);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private parseTopicEvent(topic: string): [string, string] {
|
|
480
|
+
const parts = topic.split(":");
|
|
481
|
+
if (parts.length < 2) throw new Error(`invalid topic:event format: ${topic}`);
|
|
482
|
+
const event = parts.pop()!;
|
|
483
|
+
const channelTopic = parts.join(":");
|
|
484
|
+
return [channelTopic, event];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async subscribe<T>(
|
|
488
|
+
topic: string,
|
|
489
|
+
callback: (payload: T) => void
|
|
490
|
+
): Promise<Disposable> {
|
|
491
|
+
const [channelTopic, event] = this.parseTopicEvent(topic);
|
|
492
|
+
const channel = await this.getChannel(channelTopic);
|
|
493
|
+
const ref = channel.on(event, callback);
|
|
494
|
+
return { dispose: () => channel.off(event, ref) };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async publish<T>(topic: string, payload: T): Promise<void> {
|
|
498
|
+
const [channelTopic, event] = this.parseTopicEvent(topic);
|
|
499
|
+
const channel = await this.getChannel(channelTopic);
|
|
500
|
+
channel.push(event, payload);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Performs a request-response cycle over the websocket.
|
|
505
|
+
*/
|
|
506
|
+
async request<TRes, TReq = unknown>(
|
|
507
|
+
topic: string,
|
|
508
|
+
payload: TReq,
|
|
509
|
+
timeoutMs = 5000
|
|
510
|
+
): Promise<TRes> {
|
|
511
|
+
const [channelTopic, event] = this.parseTopicEvent(topic);
|
|
512
|
+
const channel = await this.getChannel(channelTopic);
|
|
513
|
+
const responseEvent = `${event}_result`;
|
|
514
|
+
const requestId = crypto.randomUUID();
|
|
515
|
+
|
|
516
|
+
return new Promise((resolve, reject) => {
|
|
517
|
+
const timer = setTimeout(() => {
|
|
518
|
+
cleanup();
|
|
519
|
+
reject(new Error("request timeout"));
|
|
520
|
+
}, timeoutMs);
|
|
521
|
+
|
|
522
|
+
const ref = channel.on(responseEvent, (msg: any) => {
|
|
523
|
+
if (msg.request_id === requestId) {
|
|
524
|
+
cleanup();
|
|
525
|
+
resolve(msg.data as TRes);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const cleanup = () => {
|
|
530
|
+
clearTimeout(timer);
|
|
531
|
+
channel.off(responseEvent, ref);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
channel.push(event, { ...payload, request_id: requestId });
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export interface SearchResult {
|
|
540
|
+
id: string;
|
|
541
|
+
score?: number;
|
|
542
|
+
component: Component;
|
|
543
|
+
props?: Record<string, unknown>;
|
|
544
|
+
onSelect?: () => void;
|
|
545
|
+
children?: SearchResult[];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export interface SearchProvider {
|
|
549
|
+
id: string;
|
|
550
|
+
name: string;
|
|
551
|
+
search: (query: string, signal: AbortSignal) => Promise<SearchResult[]>;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export class SearchApi {
|
|
555
|
+
private providers = new Map<string, SearchProvider>();
|
|
556
|
+
|
|
557
|
+
registerProvider(provider: SearchProvider): Disposable {
|
|
558
|
+
this.providers.set(provider.id, provider);
|
|
559
|
+
return {
|
|
560
|
+
dispose: () => {
|
|
561
|
+
this.providers.delete(provider.id);
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async search(
|
|
567
|
+
query: string,
|
|
568
|
+
signal: AbortSignal,
|
|
569
|
+
onResult: (results: SearchResult[]) => void
|
|
570
|
+
): Promise<void> {
|
|
571
|
+
await Promise.all(
|
|
572
|
+
Array.from(this.providers.values()).map(async provider => {
|
|
573
|
+
try {
|
|
574
|
+
const results = await provider.search(query, signal);
|
|
575
|
+
if (!signal.aborted && results.length > 0) {
|
|
576
|
+
onResult(results);
|
|
577
|
+
}
|
|
578
|
+
} catch (e) {
|
|
579
|
+
if (!signal.aborted) {
|
|
580
|
+
console.error(`search error in ${provider.id}:`, e);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export class TangramApi {
|
|
589
|
+
readonly time: TimeApi;
|
|
590
|
+
readonly ui: UiApi;
|
|
591
|
+
readonly map: MapApi;
|
|
592
|
+
readonly state: StateApi;
|
|
593
|
+
readonly realtime: RealtimeApi;
|
|
594
|
+
readonly search: SearchApi;
|
|
595
|
+
readonly config: TangramConfig;
|
|
596
|
+
|
|
597
|
+
private constructor(
|
|
598
|
+
private app: App,
|
|
599
|
+
config: TangramConfig
|
|
600
|
+
) {
|
|
601
|
+
this.config = config;
|
|
602
|
+
this.realtime = new RealtimeApi(config);
|
|
603
|
+
this.ui = new UiApi(this.app);
|
|
604
|
+
this.time = new TimeApi();
|
|
605
|
+
this.map = new MapApi(this);
|
|
606
|
+
this.state = new StateApi();
|
|
607
|
+
this.search = new SearchApi();
|
|
608
|
+
|
|
609
|
+
this.setupViewUpdates();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private setupViewUpdates() {
|
|
613
|
+
const updateView = () => {
|
|
614
|
+
const connId = this.realtime.getConnectionId();
|
|
615
|
+
if (!connId) return;
|
|
616
|
+
const bounds = this.map.bounds.value;
|
|
617
|
+
if (!bounds) return;
|
|
618
|
+
|
|
619
|
+
const selectedEntities = Array.from(this.state.activeEntities.value.values()).map(
|
|
620
|
+
e => ({
|
|
621
|
+
id: e.id,
|
|
622
|
+
typeName: e.type
|
|
623
|
+
})
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
const payload = {
|
|
627
|
+
connectionId: connId,
|
|
628
|
+
northEastLat: bounds.getNorthEast().lat,
|
|
629
|
+
northEastLng: bounds.getNorthEast().lng,
|
|
630
|
+
southWestLat: bounds.getSouthWest().lat,
|
|
631
|
+
southWestLng: bounds.getSouthWest().lng,
|
|
632
|
+
selectedEntities: selectedEntities
|
|
633
|
+
};
|
|
634
|
+
this.realtime.publish("system:bound-box", payload);
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
watch(this.map.bounds, updateView, { deep: true });
|
|
638
|
+
watch(this.state.activeEntities, updateView, { deep: true });
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
public static async create(app: App): Promise<TangramApi> {
|
|
642
|
+
const config: TangramConfig = await fetch("/config").then(res => {
|
|
643
|
+
if (!res.ok) throw new Error("failed to fetch `/config`!");
|
|
644
|
+
return res.json();
|
|
645
|
+
});
|
|
646
|
+
return new TangramApi(app, config);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
getVueApp(): App {
|
|
650
|
+
return this.app;
|
|
651
|
+
}
|
|
652
|
+
}
|