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.
Files changed (162) hide show
  1. tangram_core/App.vue +441 -0
  2. tangram_core/CommandPalette.vue +200 -0
  3. tangram_core/HighlightText.vue +32 -0
  4. tangram_core/__Timeline.vue +300 -0
  5. tangram_core/__init__.py +5 -0
  6. tangram_core/__main__.py +331 -0
  7. tangram_core/_core.cpython-310-aarch64-linux-gnu.so +0 -0
  8. tangram_core/_core.pyi +38 -0
  9. tangram_core/api.ts +652 -0
  10. tangram_core/backend.py +458 -0
  11. tangram_core/components.ts +2 -0
  12. tangram_core/config.py +167 -0
  13. tangram_core/dist-frontend/aggregation-layers.js +521 -0
  14. tangram_core/dist-frontend/aggregation-layers.js.map +1 -0
  15. tangram_core/dist-frontend/assets/_commonjsHelpers-CqkleIqs.js +2 -0
  16. tangram_core/dist-frontend/assets/_commonjsHelpers-CqkleIqs.js.map +1 -0
  17. tangram_core/dist-frontend/assets/array-utils-flat-BBMak426.js +11 -0
  18. tangram_core/dist-frontend/assets/array-utils-flat-BBMak426.js.map +1 -0
  19. tangram_core/dist-frontend/assets/assert-cyW4mg7q.js +3 -0
  20. tangram_core/dist-frontend/assets/assert-cyW4mg7q.js.map +1 -0
  21. tangram_core/dist-frontend/assets/b612-latin-400-italic-DePNXA0a.woff +0 -0
  22. tangram_core/dist-frontend/assets/b612-latin-400-italic-a-4GLPtl.woff2 +0 -0
  23. tangram_core/dist-frontend/assets/b612-latin-400-normal-CC98FVm_.woff2 +0 -0
  24. tangram_core/dist-frontend/assets/b612-latin-400-normal-JbZ7xwUX.woff +0 -0
  25. tangram_core/dist-frontend/assets/b612-latin-700-normal-B_Snq1wd.woff +0 -0
  26. tangram_core/dist-frontend/assets/b612-latin-700-normal-BinQrnoB.woff2 +0 -0
  27. tangram_core/dist-frontend/assets/clip-extension-D-rbmFPj.js +26 -0
  28. tangram_core/dist-frontend/assets/clip-extension-D-rbmFPj.js.map +1 -0
  29. tangram_core/dist-frontend/assets/color-CUNNsFV-.js +17 -0
  30. tangram_core/dist-frontend/assets/color-CUNNsFV-.js.map +1 -0
  31. tangram_core/dist-frontend/assets/cube-geometry-v0HQ793i.js +2 -0
  32. tangram_core/dist-frontend/assets/cube-geometry-v0HQ793i.js.map +1 -0
  33. tangram_core/dist-frontend/assets/deep-equal-BTW2ZN6S.js +2 -0
  34. tangram_core/dist-frontend/assets/deep-equal-BTW2ZN6S.js.map +1 -0
  35. tangram_core/dist-frontend/assets/fly-to-interpolator-CIXGjOdo.js +2 -0
  36. tangram_core/dist-frontend/assets/fly-to-interpolator-CIXGjOdo.js.map +1 -0
  37. tangram_core/dist-frontend/assets/geojson-layer-DgMOQ4Qu.js +1010 -0
  38. tangram_core/dist-frontend/assets/geojson-layer-DgMOQ4Qu.js.map +1 -0
  39. tangram_core/dist-frontend/assets/globe-view-Day_n1iB.js +94 -0
  40. tangram_core/dist-frontend/assets/globe-view-Day_n1iB.js.map +1 -0
  41. tangram_core/dist-frontend/assets/globe-viewport-tqhQW7C4.js +2 -0
  42. tangram_core/dist-frontend/assets/globe-viewport-tqhQW7C4.js.map +1 -0
  43. tangram_core/dist-frontend/assets/image-loader-hHJsndO6.js +2 -0
  44. tangram_core/dist-frontend/assets/image-loader-hHJsndO6.js.map +1 -0
  45. tangram_core/dist-frontend/assets/inconsolata-latin-400-normal-DTZQ6lD6.woff2 +0 -0
  46. tangram_core/dist-frontend/assets/inconsolata-latin-400-normal-HYADljCo.woff +0 -0
  47. tangram_core/dist-frontend/assets/inconsolata-latin-700-normal-ByjKuJjN.woff2 +0 -0
  48. tangram_core/dist-frontend/assets/inconsolata-latin-700-normal-DzgUY3Rl.woff +0 -0
  49. tangram_core/dist-frontend/assets/inconsolata-latin-ext-400-normal-BaHVOdFB.woff2 +0 -0
  50. tangram_core/dist-frontend/assets/inconsolata-latin-ext-400-normal-yvPjCxxx.woff +0 -0
  51. tangram_core/dist-frontend/assets/inconsolata-latin-ext-700-normal-D0Kpgs_9.woff2 +0 -0
  52. tangram_core/dist-frontend/assets/inconsolata-latin-ext-700-normal-Dlt-daqV.woff +0 -0
  53. tangram_core/dist-frontend/assets/inconsolata-vietnamese-400-normal-ByiM2lek.woff +0 -0
  54. tangram_core/dist-frontend/assets/inconsolata-vietnamese-400-normal-DfC_iMic.woff2 +0 -0
  55. tangram_core/dist-frontend/assets/inconsolata-vietnamese-700-normal-DLCFFAUf.woff +0 -0
  56. tangram_core/dist-frontend/assets/inconsolata-vietnamese-700-normal-DuasYmn8.woff2 +0 -0
  57. tangram_core/dist-frontend/assets/index-CcogpxdD.js +824 -0
  58. tangram_core/dist-frontend/assets/index-CcogpxdD.js.map +1 -0
  59. tangram_core/dist-frontend/assets/index-SSLdizTv.css +1 -0
  60. tangram_core/dist-frontend/assets/layer-DPcO4AXQ.js +555 -0
  61. tangram_core/dist-frontend/assets/layer-DPcO4AXQ.js.map +1 -0
  62. tangram_core/dist-frontend/assets/layer-extension-CYwTXf73.js +2 -0
  63. tangram_core/dist-frontend/assets/layer-extension-CYwTXf73.js.map +1 -0
  64. tangram_core/dist-frontend/assets/mesh-layers-wiqredoy.js +1123 -0
  65. tangram_core/dist-frontend/assets/mesh-layers-wiqredoy.js.map +1 -0
  66. tangram_core/dist-frontend/assets/orthographic-viewport-B4nCj5tn.js +2 -0
  67. tangram_core/dist-frontend/assets/orthographic-viewport-B4nCj5tn.js.map +1 -0
  68. tangram_core/dist-frontend/assets/pick-layers-pass-C-3k0wbN.js +2 -0
  69. tangram_core/dist-frontend/assets/pick-layers-pass-C-3k0wbN.js.map +1 -0
  70. tangram_core/dist-frontend/assets/project-BTjD2Imj.js +760 -0
  71. tangram_core/dist-frontend/assets/project-BTjD2Imj.js.map +1 -0
  72. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-italic-4qS3_zkX.woff2 +0 -0
  73. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-italic-CDK-EZBY.woff +0 -0
  74. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-normal-Bgns473E.woff +0 -0
  75. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-400-normal-_T2aQlWs.woff2 +0 -0
  76. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-500-normal-CvEVpWxD.woff +0 -0
  77. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-500-normal-s4PklZE0.woff2 +0 -0
  78. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-700-normal-9RN-Z7cI.woff2 +0 -0
  79. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-700-normal-BGMkBBYx.woff +0 -0
  80. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-italic-C7erd-g8.woff +0 -0
  81. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-italic-DR5R5TWx.woff2 +0 -0
  82. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-normal-DGo1Ayjq.woff2 +0 -0
  83. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-400-normal-WtM1l1qc.woff +0 -0
  84. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-500-normal-C8FNIdXm.woff2 +0 -0
  85. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-500-normal-TLDmfi3Q.woff +0 -0
  86. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-700-normal-CTXjXnze.woff2 +0 -0
  87. tangram_core/dist-frontend/assets/roboto-condensed-cyrillic-ext-700-normal-CWPRiRXS.woff +0 -0
  88. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-italic-CR6qj4Z4.woff2 +0 -0
  89. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-italic-DHRaIs10.woff +0 -0
  90. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-normal-D5vBSIyg.woff2 +0 -0
  91. tangram_core/dist-frontend/assets/roboto-condensed-greek-400-normal-FabMgVmk.woff +0 -0
  92. tangram_core/dist-frontend/assets/roboto-condensed-greek-500-normal-BIN62cw9.woff +0 -0
  93. tangram_core/dist-frontend/assets/roboto-condensed-greek-500-normal-Hsn-wDIp.woff2 +0 -0
  94. tangram_core/dist-frontend/assets/roboto-condensed-greek-700-normal-89Up2Xly.woff +0 -0
  95. tangram_core/dist-frontend/assets/roboto-condensed-greek-700-normal-DWMOA2VK.woff2 +0 -0
  96. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-italic-D_BR-3LG.woff2 +0 -0
  97. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-italic-om57GXsO.woff +0 -0
  98. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-normal-BICmKrXV.woff2 +0 -0
  99. tangram_core/dist-frontend/assets/roboto-condensed-latin-400-normal-D2e7XwB1.woff +0 -0
  100. tangram_core/dist-frontend/assets/roboto-condensed-latin-500-normal-3p2daRJW.woff2 +0 -0
  101. tangram_core/dist-frontend/assets/roboto-condensed-latin-500-normal-Dc9bsamC.woff +0 -0
  102. tangram_core/dist-frontend/assets/roboto-condensed-latin-700-normal-BOl6B_hI.woff +0 -0
  103. tangram_core/dist-frontend/assets/roboto-condensed-latin-700-normal-DRbp0YnP.woff2 +0 -0
  104. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-italic-BXrkWnoY.woff +0 -0
  105. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-italic-Bhem1d5z.woff2 +0 -0
  106. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-normal-DT8nEsYA.woff +0 -0
  107. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-400-normal-OHaX69iP.woff2 +0 -0
  108. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-500-normal-CcSTXKtO.woff2 +0 -0
  109. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-500-normal-JgPl2bDS.woff +0 -0
  110. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-700-normal-B004qtqu.woff2 +0 -0
  111. tangram_core/dist-frontend/assets/roboto-condensed-latin-ext-700-normal-O6H_RRvN.woff +0 -0
  112. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-italic-BwUYFJ2t.woff2 +0 -0
  113. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-italic-DV8QogUk.woff +0 -0
  114. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-normal-0o1laQ-g.woff2 +0 -0
  115. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-400-normal-CPsdS8_S.woff +0 -0
  116. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-500-normal-G9shSJ2z.woff +0 -0
  117. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-500-normal-TFWhjk13.woff2 +0 -0
  118. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-700-normal-BtNeb9D6.woff +0 -0
  119. tangram_core/dist-frontend/assets/roboto-condensed-vietnamese-700-normal-D35V1G0s.woff2 +0 -0
  120. tangram_core/dist-frontend/assets/shader-Cbdysp2j.js +843 -0
  121. tangram_core/dist-frontend/assets/shader-Cbdysp2j.js.map +1 -0
  122. tangram_core/dist-frontend/assets/solid-polygon-layer-DJFl_7Ca.js +392 -0
  123. tangram_core/dist-frontend/assets/solid-polygon-layer-DJFl_7Ca.js.map +1 -0
  124. tangram_core/dist-frontend/assets/tesselator-CENyUZ2p.js +2 -0
  125. tangram_core/dist-frontend/assets/tesselator-CENyUZ2p.js.map +1 -0
  126. tangram_core/dist-frontend/assets/webgl-developer-tools-utTNOsNf.js +7 -0
  127. tangram_core/dist-frontend/assets/webgl-developer-tools-utTNOsNf.js.map +1 -0
  128. tangram_core/dist-frontend/assets/webgl-device-BYRB-GQX.js +3 -0
  129. tangram_core/dist-frontend/assets/webgl-device-BYRB-GQX.js.map +1 -0
  130. tangram_core/dist-frontend/assets/widget-BjgEeHAL.js +2 -0
  131. tangram_core/dist-frontend/assets/widget-BjgEeHAL.js.map +1 -0
  132. tangram_core/dist-frontend/core.js +60 -0
  133. tangram_core/dist-frontend/core.js.map +1 -0
  134. tangram_core/dist-frontend/extensions.js +609 -0
  135. tangram_core/dist-frontend/extensions.js.map +1 -0
  136. tangram_core/dist-frontend/favicon.ico +0 -0
  137. tangram_core/dist-frontend/favicon.png +0 -0
  138. tangram_core/dist-frontend/geo-layers.js +115 -0
  139. tangram_core/dist-frontend/geo-layers.js.map +1 -0
  140. tangram_core/dist-frontend/index.html +39 -0
  141. tangram_core/dist-frontend/json.js +3 -0
  142. tangram_core/dist-frontend/json.js.map +1 -0
  143. tangram_core/dist-frontend/layers.js +268 -0
  144. tangram_core/dist-frontend/layers.js.map +1 -0
  145. tangram_core/dist-frontend/mapbox.js +2 -0
  146. tangram_core/dist-frontend/mapbox.js.map +1 -0
  147. tangram_core/dist-frontend/mesh-layers.js +2 -0
  148. tangram_core/dist-frontend/mesh-layers.js.map +1 -0
  149. tangram_core/dist-frontend/widgets.js +3 -0
  150. tangram_core/dist-frontend/widgets.js.map +1 -0
  151. tangram_core/main.ts +28 -0
  152. tangram_core/package.json +62 -0
  153. tangram_core/plugin.py +109 -0
  154. tangram_core/plugin.ts +47 -0
  155. tangram_core/redis.py +89 -0
  156. tangram_core/user.css +114 -0
  157. tangram_core/utils.ts +143 -0
  158. tangram_core/vite-plugin-tangram.mjs +155 -0
  159. tangram_core-0.3.0.dist-info/METADATA +101 -0
  160. tangram_core-0.3.0.dist-info/RECORD +162 -0
  161. tangram_core-0.3.0.dist-info/WHEEL +4 -0
  162. tangram_core-0.3.0.dist-info/entry_points.txt +2 -0
tangram_core/plugin.py ADDED
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import functools
5
+ import importlib.metadata
6
+ import logging
7
+ import traceback
8
+ from collections.abc import Callable, Coroutine
9
+ from contextlib import AbstractAsyncContextManager
10
+ from dataclasses import dataclass, field
11
+ from typing import TYPE_CHECKING, Any, TypeAlias
12
+
13
+ from fastapi import APIRouter
14
+
15
+ if TYPE_CHECKING:
16
+ from .backend import BackendState
17
+
18
+ ServiceAsyncFunc: TypeAlias = Callable[[BackendState], Coroutine[Any, Any, None]]
19
+ ServiceFunc: TypeAlias = ServiceAsyncFunc | Callable[[BackendState], None]
20
+ Priority: TypeAlias = int
21
+ IntoFrontendConfigFunction: TypeAlias = Callable[[dict[str, Any]], Any]
22
+ Lifespan: TypeAlias = Callable[
23
+ [BackendState], AbstractAsyncContextManager[None, bool | None]
24
+ ]
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @dataclass
30
+ class Plugin:
31
+ """Stores the metadata and registered API routes, background services and
32
+ frontend assets for a tangram plugin.
33
+
34
+ Packages should declare an entry point in the `tangram_core.plugins` group
35
+ in their `pyproject.toml` pointing to an instance of this class.
36
+ """
37
+
38
+ frontend_path: str | None = None
39
+ """Path to the compiled frontend assets, *relative* to the distribution root
40
+ (editable) or package root (wheel).
41
+ """
42
+ routers: list[APIRouter] = field(default_factory=list)
43
+ into_frontend_config_function: IntoFrontendConfigFunction | None = None
44
+ """Function to parse plugin-scoped backend configuration (within the
45
+ `tangram.toml`) into a frontend-safe configuration object.
46
+
47
+ If not specified, the backend configuration dict is passed as-is."""
48
+ lifespan: Lifespan | None = None
49
+ """Async context manager for plugin initialization and teardown."""
50
+ services: list[tuple[Priority, ServiceAsyncFunc]] = field(
51
+ default_factory=list, init=False
52
+ )
53
+ dist_name: str = field(init=False)
54
+ """Name of the distribution (package) that provided this plugin, populated
55
+ automatically during loading.
56
+ """ # we do this so plugins can know their own package name if needed
57
+
58
+ def register_service(
59
+ self, priority: Priority = 0
60
+ ) -> Callable[[ServiceFunc], ServiceFunc]:
61
+ """Decorator to register a background service function.
62
+
63
+ Services are long-running async functions that receive the BackendState
64
+ and are started when the application launches.
65
+ """
66
+
67
+ def decorator(func: ServiceFunc) -> ServiceFunc:
68
+ @functools.wraps(func)
69
+ async def async_wrapper(backend_state: BackendState) -> None:
70
+ if asyncio.iscoroutinefunction(func):
71
+ await func(backend_state)
72
+ else:
73
+ await asyncio.to_thread(func, backend_state)
74
+
75
+ self.services.append((priority, async_wrapper))
76
+ return func
77
+
78
+ return decorator
79
+
80
+
81
+ def scan_plugins() -> importlib.metadata.EntryPoints:
82
+ return importlib.metadata.entry_points(group="tangram_core.plugins")
83
+
84
+
85
+ def load_plugin(
86
+ entry_point: importlib.metadata.EntryPoint,
87
+ ) -> Plugin | None:
88
+ """Instantiates the plugin object defined in the entry point
89
+ and injects the name of the distribution into it."""
90
+ try:
91
+ plugin_instance = entry_point.load()
92
+ except Exception as e:
93
+ tb = traceback.format_exc()
94
+ logger.error(
95
+ f"failed to load plugin {entry_point.name}: {e}. {tb}"
96
+ f"\n= help: does {entry_point.value} exist?"
97
+ )
98
+ return None
99
+ if not isinstance(plugin_instance, Plugin):
100
+ logger.error(f"entry point {entry_point.name} is not an instance of `Plugin`")
101
+ return None
102
+ if entry_point.dist is None:
103
+ logger.error(f"could not determine distribution for plugin {entry_point.name}")
104
+ return None
105
+ # NOTE: we ignore `entry_point.name` for now and simply use the distribution's name
106
+ # should we raise an error if they differ? not for now
107
+
108
+ plugin_instance.dist_name = entry_point.dist.name
109
+ return plugin_instance
tangram_core/plugin.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { TangramApi } from "./api";
2
+
3
+ type PluginProgressStage = "manifest" | "plugin" | "done";
4
+ type PluginProgress = {
5
+ stage: PluginProgressStage;
6
+ pluginName?: string;
7
+ };
8
+
9
+ export type PluginConfig = unknown; // to be casted by each plugin who consume it
10
+
11
+ export async function loadPlugins(
12
+ tangramApi: TangramApi,
13
+ onProgress?: (progress: PluginProgress) => void
14
+ ) {
15
+ onProgress?.({ stage: "manifest" });
16
+ const manifest = await fetch("/manifest.json").then(res => res.json());
17
+
18
+ for (const [pluginName, meta] of Object.entries(manifest.plugins)) {
19
+ const pluginMeta = meta as {
20
+ main: string;
21
+ style?: string;
22
+ config?: PluginConfig;
23
+ };
24
+
25
+ onProgress?.({ stage: "plugin", pluginName });
26
+
27
+ if (pluginMeta.style) {
28
+ const link = document.createElement("link");
29
+ link.rel = "stylesheet";
30
+ link.href = `/plugins/${pluginName}/${pluginMeta.style}`;
31
+ document.head.appendChild(link);
32
+ }
33
+
34
+ const entryPointUrl = `/plugins/${pluginName}/${pluginMeta.main}`;
35
+
36
+ try {
37
+ const pluginModule = await import(/* @vite-ignore */ entryPointUrl);
38
+ if (pluginModule.install) {
39
+ pluginModule.install(tangramApi, pluginMeta.config);
40
+ }
41
+ } catch (e) {
42
+ console.error(`failed to load plugin "${pluginName}":`, e);
43
+ }
44
+ }
45
+
46
+ onProgress?.({ stage: "done" });
47
+ }
tangram_core/redis.py ADDED
@@ -0,0 +1,89 @@
1
+ import abc
2
+ import asyncio
3
+ import logging
4
+ from typing import Generic, List, TypeVar
5
+
6
+ from redis.asyncio import Redis
7
+ from redis.asyncio.client import PubSub
8
+ from redis.exceptions import RedisError
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+ StateT = TypeVar("StateT")
13
+
14
+
15
+ class Subscriber(abc.ABC, Generic[StateT]):
16
+ redis: Redis
17
+ task: asyncio.Task[None]
18
+ pubsub: PubSub
19
+
20
+ def __init__(
21
+ self, name: str, redis_url: str, channels: List[str], initial_state: StateT
22
+ ):
23
+ self.name = name
24
+ self.redis_url: str = redis_url
25
+ self.channels: List[str] = channels
26
+ self.state: StateT = initial_state
27
+ self._running = False
28
+
29
+ async def subscribe(self) -> None:
30
+ if self._running:
31
+ log.warning("%s already running", self.name)
32
+ return
33
+
34
+ try:
35
+ self.redis = await Redis.from_url(self.redis_url)
36
+ self.pubsub = self.redis.pubsub()
37
+ await self.pubsub.psubscribe(*self.channels)
38
+ except RedisError as e:
39
+ log.error("%s failed to connect to Redis: %s", self.name, e)
40
+ raise
41
+
42
+ async def listen() -> None:
43
+ try:
44
+ log.info("%s listening ...", self.name)
45
+ async for message in self.pubsub.listen():
46
+ log.debug("message: %s", message)
47
+ if message["type"] == "pmessage":
48
+ await self.message_handler(
49
+ message["channel"].decode("utf-8"),
50
+ message["data"].decode("utf-8"),
51
+ message["pattern"].decode("utf-8"),
52
+ self.state,
53
+ )
54
+ except asyncio.CancelledError:
55
+ log.warning("%s cancelled", self.name)
56
+
57
+ self._running = True
58
+
59
+ self.task = asyncio.create_task(listen())
60
+ log.info("%s task created, running ...", self.name)
61
+
62
+ async def cleanup(self) -> None:
63
+ if not self._running:
64
+ return
65
+
66
+ if self.task:
67
+ log.debug("%s canceling task ...", self.name)
68
+ self.task.cancel()
69
+ try:
70
+ log.debug("%s await task to finish ...", self.name)
71
+ await self.task
72
+ log.debug("%s task canceled", self.name)
73
+ except asyncio.CancelledError as exc:
74
+ log.error("%s task canceling error: %s", self.name, exc)
75
+ if self.pubsub:
76
+ await self.pubsub.unsubscribe()
77
+ if self.redis:
78
+ await self.redis.close()
79
+ self._running = False
80
+
81
+ def is_active(self) -> bool:
82
+ """Return True if the subscriber is actively listening."""
83
+ return self._running and self.task is not None and not self.task.done()
84
+
85
+ @abc.abstractmethod
86
+ async def message_handler(
87
+ self, event: str, payload: str, pattern: str, state: StateT
88
+ ) -> None:
89
+ pass
tangram_core/user.css ADDED
@@ -0,0 +1,114 @@
1
+ html,
2
+ body,
3
+ #container-map {
4
+ height: 100%;
5
+ overflow: hidden;
6
+ width: 100%;
7
+ }
8
+
9
+ #map {
10
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
11
+ height: 100%;
12
+ width: auto;
13
+ }
14
+
15
+
16
+ #chart {
17
+ border: none;
18
+ }
19
+
20
+ div.form {
21
+ float: right;
22
+ clear: right;
23
+ width: 300px;
24
+ border: 1px solid #bab0ac;
25
+ padding: 10px;
26
+ }
27
+
28
+ body {
29
+ font-family: "B612", sans-serif;
30
+ }
31
+
32
+ span.monospace {
33
+ font-family: "B612", sans-serif;
34
+ font-size: 85%;
35
+ }
36
+
37
+ .smaller td {
38
+ font-size: 95%;
39
+ }
40
+
41
+ table a:not(.btn),
42
+ .table a:not(.btn) {
43
+ text-decoration: none;
44
+ }
45
+
46
+ a.flag {
47
+ position: relative;
48
+ cursor: pointer;
49
+ color: #2c3e50;
50
+ }
51
+
52
+ a.flag:hover::after {
53
+ content: attr(data-tooltip);
54
+ position: absolute;
55
+ bottom: 1.7em;
56
+ left: 0.5em;
57
+ border: 1px #18bc9c solid;
58
+ border-radius: 5px;
59
+ padding: 4px;
60
+ color: whitesmoke;
61
+ background-color: #18bc9c;
62
+ text-align: center;
63
+ font-size: 90%;
64
+ width: max-content;
65
+ z-index: 1;
66
+ }
67
+
68
+ .w20pc {
69
+ width: 20%;
70
+ }
71
+
72
+ .center {
73
+ margin-top: 20px;
74
+ margin-left: auto;
75
+ margin-right: auto;
76
+ /* max-width: 1000px; */
77
+ }
78
+
79
+ .copyright_label {
80
+ display: inline;
81
+ padding: 0.2em 0.6em 0.3em;
82
+ font-size: 75%;
83
+ font-weight: 400;
84
+ line-height: 1;
85
+ color: #fff;
86
+ text-align: center;
87
+ white-space: nowrap;
88
+ vertical-align: baseline;
89
+ border-radius: 0.25em;
90
+ position: absolute;
91
+ bottom: 0;
92
+ z-index: 1;
93
+ }
94
+
95
+ .turb_selected {
96
+ stroke: red;
97
+ stroke-width: 15px;
98
+ z-index: -1;
99
+ }
100
+
101
+ .form-popup {
102
+ display: none;
103
+ position: fixed;
104
+ bottom: 0;
105
+ right: 15px;
106
+ border: 3px solid #f1f1f1;
107
+ z-index: 9;
108
+ }
109
+
110
+ .form-container {
111
+ max-width: 300px;
112
+ padding: 10px;
113
+ background-color: white;
114
+ }
tangram_core/utils.ts ADDED
@@ -0,0 +1,143 @@
1
+ // adapted from: https://github.com/color-js/color.js/blob/main/src/spaces/oklch.js
2
+ type Vector3 = [number, number, number];
3
+
4
+ const multiplyMatrices = (A: number[], B: Vector3): Vector3 => {
5
+ return [
6
+ A[0] * B[0] + A[1] * B[1] + A[2] * B[2],
7
+ A[3] * B[0] + A[4] * B[1] + A[5] * B[2],
8
+ A[6] * B[0] + A[7] * B[1] + A[8] * B[2]
9
+ ];
10
+ };
11
+
12
+ const oklch2oklab = ([l, c, h]: Vector3): Vector3 => [
13
+ l,
14
+ isNaN(h) ? 0 : c * Math.cos((h * Math.PI) / 180),
15
+ isNaN(h) ? 0 : c * Math.sin((h * Math.PI) / 180)
16
+ ];
17
+
18
+ const srgbLinear2rgb = (rgb: Vector3): Vector3 =>
19
+ rgb.map(c =>
20
+ Math.abs(c) > 0.0031308
21
+ ? (c < 0 ? -1 : 1) * (1.055 * Math.pow(Math.abs(c), 1 / 2.4) - 0.055)
22
+ : 12.92 * c
23
+ ) as Vector3;
24
+
25
+ const oklab2xyz = (lab: Vector3): Vector3 => {
26
+ const LMSg = multiplyMatrices(
27
+ [
28
+ 1, 0.3963377773761749, 0.2158037573099136, 1, -0.1055613458156586,
29
+ -0.0638541728258133, 1, -0.0894841775298119, -1.2914855480194092
30
+ ],
31
+ lab
32
+ );
33
+ const LMS = LMSg.map(val => val ** 3) as Vector3;
34
+ return multiplyMatrices(
35
+ [
36
+ 1.2268798758459243, -0.5578149944602171, 0.2813910456659647, -0.0405757452148008,
37
+ 1.112286803280317, -0.0717110580655164, -0.0763729366746601, -0.4214933324022432,
38
+ 1.5869240198367816
39
+ ],
40
+ LMS
41
+ );
42
+ };
43
+
44
+ const xyz2rgbLinear = (xyz: Vector3): Vector3 => {
45
+ return multiplyMatrices(
46
+ [
47
+ 3.2409699419045226, -1.537383177570094, -0.4986107602930034, -0.9692436362808796,
48
+ 1.8759675015077202, 0.04155505740717559, 0.05563007969699366,
49
+ -0.20397695888897652, 1.0569715142428786
50
+ ],
51
+ xyz
52
+ );
53
+ };
54
+
55
+ export const oklch2rgb = (lch: Vector3): Vector3 =>
56
+ srgbLinear2rgb(xyz2rgbLinear(oklab2xyz(oklch2oklab(lch))));
57
+
58
+ export function oklchToDeckGLColor(
59
+ l: number,
60
+ c: number,
61
+ h: number,
62
+ a: number = 255
63
+ ): [number, number, number, number] {
64
+ const rgb = oklch2rgb([l, c, h]);
65
+ return [
66
+ Math.max(0, Math.min(255, Math.round(rgb[0] * 255))),
67
+ Math.max(0, Math.min(255, Math.round(rgb[1] * 255))),
68
+ Math.max(0, Math.min(255, Math.round(rgb[2] * 255))),
69
+ a
70
+ ];
71
+ }
72
+
73
+ export const formatTime = (ts: string) => {
74
+ const d = new Date(ts + "Z");
75
+ return (
76
+ d.toLocaleString("en-GB", {
77
+ timeZone: "UTC",
78
+ month: "short",
79
+ day: "numeric",
80
+ hour: "2-digit",
81
+ minute: "2-digit"
82
+ }) + "Z"
83
+ );
84
+ };
85
+
86
+ export const formatDuration = (sec: number) => {
87
+ const h = Math.floor(sec / 3600);
88
+ const m = Math.floor((sec % 3600) / 60);
89
+ return `${h}h ${m}m`;
90
+ };
91
+
92
+ export type Position3D = [number, number, number];
93
+ export interface PathSegment<ColorT> {
94
+ path: Position3D[];
95
+ colors: ColorT[];
96
+ dashed: boolean;
97
+ }
98
+
99
+ export function* generateSegments<T, ColorT>(
100
+ data: Iterable<T>,
101
+ opts: {
102
+ getPosition: (d: T) => Position3D | null;
103
+ getTimestamp: (d: T) => number | null;
104
+ getColor: (d: T) => ColorT;
105
+ gapColor: ColorT;
106
+ maxGapSeconds: number;
107
+ }
108
+ ): Generator<PathSegment<ColorT>> {
109
+ let segment: PathSegment<ColorT> = { path: [], colors: [], dashed: false };
110
+ let lastT: number | null = null;
111
+ let lastPos: Position3D | null = null;
112
+
113
+ for (const d of data) {
114
+ const pos = opts.getPosition(d);
115
+ if (!pos) continue;
116
+
117
+ const t = opts.getTimestamp(d);
118
+
119
+ if (lastT !== null && t !== null && lastPos !== null) {
120
+ if (Math.abs(t - lastT) > opts.maxGapSeconds) {
121
+ if (segment.path.length > 1) {
122
+ yield segment;
123
+ }
124
+ yield {
125
+ path: [lastPos, pos],
126
+ colors: [opts.gapColor, opts.gapColor],
127
+ dashed: true
128
+ };
129
+ segment = { path: [], colors: [], dashed: false };
130
+ }
131
+ }
132
+
133
+ segment.path.push(pos);
134
+ segment.colors.push(opts.getColor(d));
135
+
136
+ lastT = t;
137
+ lastPos = pos;
138
+ }
139
+
140
+ if (segment.path.length > 1) {
141
+ yield segment;
142
+ }
143
+ }
@@ -0,0 +1,155 @@
1
+ // not using typescript because of an annoying vite bug:
2
+ // - https://github.com/vitejs/vite/issues/5370
3
+ // - https://github.com/vitejs/vite/issues/16040
4
+ // essentially, vite's on-the-fly transpilation is scoped only to the config
5
+ // file itself. therefore vite plugins in a monorepo cannot be typescript.
6
+ import vue from "@vitejs/plugin-vue";
7
+ import path from "path";
8
+ import fs from "fs/promises";
9
+
10
+ const DECKGL_PACKAGES = [
11
+ "@deck.gl/core",
12
+ "@deck.gl/layers",
13
+ "@deck.gl/aggregation-layers",
14
+ "@deck.gl/geo-layers",
15
+ "@deck.gl/mesh-layers",
16
+ "@deck.gl/json",
17
+ "@deck.gl/mapbox",
18
+ "@deck.gl/widgets",
19
+ "@deck.gl/extensions"
20
+ ];
21
+
22
+ /**
23
+ * @param {{ copyToPythonPackage?: boolean; pythonPackageDir?: string; includePackageJson?: boolean }} [options]
24
+ * If the plugin is built with maturin, `copyToPythonPackage` should be 'true' to
25
+ * avoid the built binary distribution wheel omitting the 'dist-frontend'.
26
+ * See: https://github.com/open-aviation/tangram/pull/99#issuecomment-3777038726
27
+ * @returns {import('vite').Plugin[]}
28
+ */
29
+ export function tangramPlugin(options = {}) {
30
+ const projectRoot = process.cwd();
31
+ const copyToPythonPackage = options.copyToPythonPackage ?? false;
32
+ const includePackageJson = options.includePackageJson ?? true;
33
+ /** @type {{ name: string; main: string; }} */
34
+ let pkg;
35
+ let entryFileName;
36
+
37
+ /** @type {import('vite').Plugin} */
38
+ const configInjector = {
39
+ name: "tangram-plugin-config-injector",
40
+ async config() {
41
+ const pkgPath = path.resolve(projectRoot, "package.json");
42
+ pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
43
+
44
+ if (!pkg.main) {
45
+ throw new Error(
46
+ `\`main\` field must be specified in ${pkg.name}'s package.json`
47
+ );
48
+ }
49
+ entryFileName = path.parse(pkg.main).name;
50
+
51
+ /** @type {import('vite').UserConfig} */
52
+ const tangramBuildConfig = {
53
+ build: {
54
+ sourcemap: true,
55
+ lib: {
56
+ entry: pkg.main,
57
+ fileName: entryFileName,
58
+ formats: ["es"]
59
+ },
60
+ rollupOptions: {
61
+ external: [
62
+ "vue",
63
+ "maplibre",
64
+ ...DECKGL_PACKAGES,
65
+ "lit-html",
66
+ "rs1090-wasm",
67
+ "parquet-wasm"
68
+ ]
69
+ },
70
+ outDir: "dist-frontend",
71
+ minify: true
72
+ }
73
+ };
74
+ return tangramBuildConfig;
75
+ }
76
+ };
77
+
78
+ /** @type {import('vite').Plugin} */
79
+ const manifestGenerator = {
80
+ name: "tangram-manifest-generator",
81
+ apply: "build",
82
+ async writeBundle(outputOptions, bundle) {
83
+ const outDir = outputOptions.dir;
84
+ const cssAsset = Object.values(bundle).find(
85
+ asset => asset.type === "asset" && asset.fileName.endsWith(".css")
86
+ );
87
+
88
+ const manifest = {
89
+ name: pkg.name,
90
+ main: `${entryFileName}.js`,
91
+ ...(cssAsset && { style: cssAsset.fileName })
92
+ };
93
+ await fs.writeFile(
94
+ path.resolve(outDir, "plugin.json"),
95
+ JSON.stringify(manifest, null, 2)
96
+ );
97
+ }
98
+ };
99
+
100
+ /** @type {import('vite').Plugin} */
101
+ const pythonPackageSync = {
102
+ name: "tangram-python-package-sync",
103
+ apply: "build",
104
+ async writeBundle(outputOptions) {
105
+ if (!copyToPythonPackage) return;
106
+
107
+ const outDir = outputOptions.dir
108
+ ? path.resolve(projectRoot, outputOptions.dir)
109
+ : path.resolve(projectRoot, "dist-frontend");
110
+ const pythonPackageDir = options.pythonPackageDir
111
+ ? path.resolve(projectRoot, options.pythonPackageDir)
112
+ : path.resolve(projectRoot, path.dirname(pkg.main));
113
+ const distDst = path.join(pythonPackageDir, "dist-frontend");
114
+
115
+ try {
116
+ await fs.stat(outDir);
117
+ } catch {
118
+ return;
119
+ }
120
+
121
+ await fs.rm(distDst, { recursive: true, force: true });
122
+ await copyDirRecursive(outDir, distDst);
123
+
124
+ if (includePackageJson) {
125
+ await fs.copyFile(
126
+ path.resolve(projectRoot, "package.json"),
127
+ path.join(pythonPackageDir, "package.json")
128
+ );
129
+ }
130
+ }
131
+ };
132
+
133
+ return [vue(), configInjector, manifestGenerator, pythonPackageSync];
134
+ }
135
+
136
+ /**
137
+ * @param {string} src
138
+ * @param {string} dst
139
+ */
140
+ async function copyDirRecursive(src, dst) {
141
+ await fs.mkdir(dst, { recursive: true });
142
+ const entries = await fs.readdir(src, { withFileTypes: true });
143
+
144
+ for (const entry of entries) {
145
+ const srcPath = path.join(src, entry.name);
146
+ const dstPath = path.join(dst, entry.name);
147
+
148
+ if (entry.isDirectory()) {
149
+ await copyDirRecursive(srcPath, dstPath);
150
+ } else if (entry.isFile()) {
151
+ await fs.mkdir(path.dirname(dstPath), { recursive: true });
152
+ await fs.copyFile(srcPath, dstPath);
153
+ }
154
+ }
155
+ }