@tellegen/svelte 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +269 -5
  2. package/dist/Sparkline.svelte +2 -2
  3. package/dist/TellegenMap.svelte +518 -202
  4. package/dist/TellegenMap.svelte.d.ts +45 -0
  5. package/dist/TellegenProvider.svelte +40 -4
  6. package/dist/TellegenShell.svelte +24 -46
  7. package/dist/TellegenViewer.svelte +12 -2
  8. package/dist/api.d.ts +18 -3
  9. package/dist/api.js +8 -2
  10. package/dist/basemap.d.ts +192 -0
  11. package/dist/basemap.js +259 -0
  12. package/dist/colors.d.ts +25 -7
  13. package/dist/colors.js +39 -13
  14. package/dist/components/AppFooter.svelte +3 -2
  15. package/dist/components/AppHeader.svelte +44 -19
  16. package/dist/components/BindingLines.svelte +4 -7
  17. package/dist/components/BusPicker.svelte +11 -5
  18. package/dist/components/ControlPanel.svelte +106 -343
  19. package/dist/components/DemandSlider.svelte +36 -29
  20. package/dist/components/DiagramCanvas.svelte +486 -0
  21. package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
  22. package/dist/components/DisplayControls.svelte +8 -2
  23. package/dist/components/DropZone.svelte +1 -1
  24. package/dist/components/FormulationSelector.svelte +9 -4
  25. package/dist/components/LocalCaseDetails.svelte +61 -48
  26. package/dist/components/McResults.svelte +428 -0
  27. package/dist/components/McResults.svelte.d.ts +17 -0
  28. package/dist/components/ModelDetails.svelte +230 -0
  29. package/dist/components/ModelDetails.svelte.d.ts +7 -0
  30. package/dist/components/MulticonductorDetails.svelte +232 -83
  31. package/dist/components/NetworkStats.svelte +20 -18
  32. package/dist/components/NotificationCenter.svelte +245 -0
  33. package/dist/components/NotificationCenter.svelte.d.ts +3 -0
  34. package/dist/components/PanelCard.svelte +384 -0
  35. package/dist/components/PanelCard.svelte.d.ts +9 -0
  36. package/dist/components/PanelFrame.svelte +46 -0
  37. package/dist/components/PanelFrame.svelte.d.ts +16 -0
  38. package/dist/components/PanelHost.svelte +237 -0
  39. package/dist/components/PanelHost.svelte.d.ts +6 -0
  40. package/dist/components/PlacementCue.svelte +1 -1
  41. package/dist/components/PowerFlowDetails.svelte +99 -0
  42. package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
  43. package/dist/components/RatingSlider.svelte +19 -14
  44. package/dist/components/RestoreDefaultsButton.svelte +1 -1
  45. package/dist/components/SavedNetworkDetails.svelte +214 -0
  46. package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
  47. package/dist/components/SensitivityReadout.svelte +26 -18
  48. package/dist/components/ShellEnvironment.svelte +51 -0
  49. package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
  50. package/dist/components/SizeLegend.svelte +2 -2
  51. package/dist/components/SolveCard.svelte +48 -51
  52. package/dist/components/TopMovers.svelte +9 -4
  53. package/dist/components/TopMovers.svelte.d.ts +2 -17
  54. package/dist/components.d.ts +5 -0
  55. package/dist/components.js +5 -0
  56. package/dist/context.svelte.d.ts +8 -1
  57. package/dist/context.svelte.js +8 -3
  58. package/dist/controller.svelte.d.ts +117 -36
  59. package/dist/controller.svelte.js +1133 -253
  60. package/dist/diagram-labels.d.ts +9 -0
  61. package/dist/diagram-labels.js +40 -0
  62. package/dist/display.d.ts +3 -7
  63. package/dist/display.js +31 -27
  64. package/dist/format.d.ts +9 -0
  65. package/dist/format.js +17 -4
  66. package/dist/index.d.ts +15 -5
  67. package/dist/index.js +9 -2
  68. package/dist/map.d.ts +4 -2
  69. package/dist/map.js +2 -1
  70. package/dist/maplibre-worker.d.ts +5 -0
  71. package/dist/maplibre-worker.dev.d.ts +11 -0
  72. package/dist/maplibre-worker.dev.js +15 -0
  73. package/dist/maplibre-worker.js +8 -0
  74. package/dist/multiconductor.d.ts +11 -2
  75. package/dist/multiconductor.js +107 -14
  76. package/dist/notices.svelte.d.ts +29 -0
  77. package/dist/notices.svelte.js +86 -0
  78. package/dist/panels.svelte.d.ts +71 -0
  79. package/dist/panels.svelte.js +245 -0
  80. package/dist/slider.d.ts +12 -0
  81. package/dist/slider.js +31 -0
  82. package/dist/state.svelte.d.ts +107 -23
  83. package/dist/state.svelte.js +106 -20
  84. package/dist/styles.css +59 -6
  85. package/dist/synthetic-layout.js +1 -0
  86. package/package.json +29 -11
  87. package/dist/study-package.d.ts +0 -8
  88. package/dist/study-package.js +0 -17
@@ -1,4 +1,9 @@
1
1
  import 'maplibre-gl/dist/maplibre-gl.css';
2
+ import type { Layer } from '@deck.gl/core';
3
+ import type { MapLibreOverlay } from '@deck.gl/maplibre';
4
+ import type { Map as MapLibreMap } from 'maplibre-gl';
5
+ import { type BasemapFallbackReason, type BasemapStyle, type MapProjection, type MapTheme } from './basemap.js';
6
+ import { type CameraSnapshot } from './state.svelte.js';
2
7
  type $$ComponentProps = {
3
8
  onbusclick: (caseId: string, busId: number) => void;
4
9
  onlocalbusclick: (caseId: string, busId: number) => void;
@@ -8,6 +13,46 @@ type $$ComponentProps = {
8
13
  onmultiedgeclick?: (caseId: string, edgeId: string) => void;
9
14
  onplacecase: (lon: number, lat: number) => void;
10
15
  onmapclick: () => void;
16
+ /** Primary basemap: a style URL or inline style. Default: the CARTO style
17
+ * for `theme` when `cartoApiKey` is set, else the OpenFreeMap style.
18
+ * Changing it (or `fallbackMapStyle`, `theme`, `hideBasemapLabels`,
19
+ * `cartoApiKey`, `attribution`) rebuilds the map in place and keeps the
20
+ * camera. */
21
+ mapStyle?: BasemapStyle;
22
+ /** Basemap used when the primary cannot be shown; `null` disables the
23
+ * switch. Default: whichever of the CARTO and OpenFreeMap styles is not
24
+ * the default primary. */
25
+ fallbackMapStyle?: BasemapStyle | null;
26
+ /** Picks the default basemaps and the overlay palette (bus outlines,
27
+ * hover highlight, unsolved grey). */
28
+ theme?: MapTheme;
29
+ /** Re-applied after every `style.load`, so a basemap switch keeps it. */
30
+ projection?: MapProjection;
31
+ /** Camera for the first paint; read once per mount. */
32
+ initialCamera?: CameraSnapshot;
33
+ /** Hide the basemap's `symbol` layers after every `style.load`. */
34
+ hideBasemapLabels?: boolean;
35
+ /** CARTO basemap key (free for non-commercial use from
36
+ * carto.com/basemaps/apikey; restrict it to your hosts). With one, the
37
+ * CARTO style leads and OpenFreeMap is the fallback; without one, the
38
+ * order flips. Trimmed; a blank key counts as none. */
39
+ cartoApiKey?: string;
40
+ /** deck.gl layers drawn after tellegen's own, in the same overlay. */
41
+ extraLayers?: Layer[];
42
+ /** Attribution control mode. There is no off switch: tile licences
43
+ * require the credit on screen. */
44
+ attribution?: 'compact' | 'full';
45
+ /** Called once the map and overlay exist, with an optional cleanup. It
46
+ * fires again after a context-loss or basemap remount, after the previous
47
+ * cleanup ran. */
48
+ onMapReady?: (map: MapLibreMap, overlay: MapLibreOverlay) => void | (() => void);
49
+ /** The primary basemap failed and the fallback was requested. The
50
+ * reason's `from` and `to` name the providers (`'carto'`,
51
+ * `'openfreemap'` or `'custom'`). */
52
+ onBasemapFallback?: (reason: BasemapFallbackReason) => void;
53
+ /** No basemap can be shown; the network layers still draw. The reason's
54
+ * `from` names the provider that failed last, and `to` is `null`. */
55
+ onBasemapError?: (reason: BasemapFallbackReason) => void;
11
56
  };
12
57
  declare const TellegenMap: import("svelte").Component<$$ComponentProps, {}, "">;
13
58
  type TellegenMap = ReturnType<typeof TellegenMap>;
@@ -9,13 +9,24 @@
9
9
  } from './context.svelte.js';
10
10
  import { createController } from './controller.svelte.js';
11
11
  import { createAppState } from './state.svelte.js';
12
+ import { PanelLayout, setPanelLayout } from './panels.svelte.js';
13
+ import { NoticeCenter, setNoticeCenter } from './notices.svelte.js';
12
14
 
13
15
  interface Props extends TellegenUiOptions {
14
16
  children?: Snippet;
15
17
  }
16
18
 
17
- let { apiBase, loadDefaultCases, docsHref, orgHref, orgLabel, showFooter, children }: Props =
18
- $props();
19
+ let {
20
+ apiBase,
21
+ loadDefaultCases,
22
+ docsHref,
23
+ orgHref,
24
+ orgLabel,
25
+ showFooter,
26
+ theme,
27
+ cartoApiKey,
28
+ children
29
+ }: Props = $props();
19
30
 
20
31
  const config = untrack(() =>
21
32
  resolveTellegenUiConfig({
@@ -24,15 +35,40 @@
24
35
  docsHref,
25
36
  orgHref,
26
37
  orgLabel,
27
- showFooter
38
+ showFooter,
39
+ theme,
40
+ cartoApiKey
28
41
  })
29
42
  );
30
43
  const app = createAppState();
31
- const ctrl = createController(app, { apiBase: config.apiBase });
44
+ const ctrl = createController(app, {
45
+ apiBase: config.apiBase,
46
+ loadDefaultCases: config.loadDefaultCases
47
+ });
48
+ // A host that opens its own cases never calls load(), so the shell must not
49
+ // sit in its loading state waiting for a fetch that will not come.
50
+ if (!config.loadDefaultCases) ctrl.casesLoaded = true;
32
51
 
52
+ setPanelLayout(new PanelLayout());
53
+ setNoticeCenter(new NoticeCenter());
33
54
  setUiConfig(config);
34
55
  setAppState(app);
35
56
  setController(ctrl);
57
+
58
+ // The dark tokens in styles.css hang off `:root[data-theme='dark']`. Set it
59
+ // only for dark, and only while mounted, so a light host is untouched. A
60
+ // server-rendered host that wants no light flash can also put the
61
+ // attribute in its app template.
62
+ $effect(() => {
63
+ if (config.theme !== 'dark') return;
64
+ const root = document.documentElement;
65
+ const previous = root.dataset.theme;
66
+ root.dataset.theme = 'dark';
67
+ return () => {
68
+ if (previous === undefined) delete root.dataset.theme;
69
+ else root.dataset.theme = previous;
70
+ };
71
+ });
36
72
  </script>
37
73
 
38
74
  {@render children?.()}
@@ -1,26 +1,32 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
3
  import AppFooter from './components/AppFooter.svelte';
4
+ import NotificationCenter from './components/NotificationCenter.svelte';
4
5
  import AppHeader from './components/AppHeader.svelte';
5
- import BusPicker from './components/BusPicker.svelte';
6
6
  import ControlPanel from './components/ControlPanel.svelte';
7
7
  import DropZone from './components/DropZone.svelte';
8
8
  import PlacementCue from './components/PlacementCue.svelte';
9
9
  import RestoreDefaultsButton from './components/RestoreDefaultsButton.svelte';
10
10
  import SolveCard from './components/SolveCard.svelte';
11
+ import PanelHost from './components/PanelHost.svelte';
12
+ import ShellEnvironment from './components/ShellEnvironment.svelte';
11
13
  import { getAppState, getController, getUiConfig } from './context.svelte.js';
12
14
  import TellegenMap from './TellegenMap.svelte';
13
15
 
14
- const FILE_DROP_QUERY = '(hover: hover) and (pointer: fine) and (min-width: 761px)';
15
- const COMPACT_QUERY = '(max-width: 760px)';
16
-
17
16
  const app = getAppState();
18
17
  const ctrl = getController();
19
18
  const config = getUiConfig();
20
19
 
21
20
  let dragDepth = 0;
22
21
 
22
+ // ShellEnvironment drops the affordance when no fine pointer is attached;
23
+ // forget any drag that was in flight so the next one counts from zero.
23
24
  $effect(() => {
25
+ if (!ctrl.showFileDropUi) dragDepth = 0;
26
+ });
27
+
28
+ $effect(() => {
29
+ if (app.studyView) return;
24
30
  let hasDisplayMode = false;
25
31
  for (const option of ctrl.displayOptions) {
26
32
  if (option.mode === app.displayMode) {
@@ -29,50 +35,18 @@
29
35
  }
30
36
  }
31
37
  if (ctrl.displayOptions.length > 0 && !hasDisplayMode) {
32
- app.displayMode = 'lmp';
38
+ app.displayMode = 'price';
33
39
  }
34
40
  });
35
41
 
36
42
  onMount(() => {
43
+ // The provider already marked the shell loaded when default cases are
44
+ // off, and load() itself is gated; this only frames whatever is there.
37
45
  if (config.loadDefaultCases) {
38
46
  if (!ctrl.casesLoaded) void ctrl.load();
39
47
  } else {
40
- ctrl.casesLoaded = true;
41
- app.requestFrame('all');
48
+ void app.requestFrame('all');
42
49
  }
43
-
44
- const query = window.matchMedia(FILE_DROP_QUERY);
45
- const syncFileDropUi = () => {
46
- ctrl.showFileDropUi = query.matches;
47
- if (!ctrl.showFileDropUi) {
48
- dragDepth = 0;
49
- app.dragOver = false;
50
- }
51
- };
52
- syncFileDropUi();
53
- query.addEventListener('change', syncFileDropUi);
54
-
55
- const compact = window.matchMedia(COMPACT_QUERY);
56
- const syncCompact = () => {
57
- app.compactLayout = compact.matches;
58
- if (!compact.matches) app.sheetInset = 0;
59
- };
60
- syncCompact();
61
- compact.addEventListener('change', syncCompact);
62
-
63
- // The sheet sizes its snaps against the viewport and the map caps how far
64
- // it lifts the basemap chrome, so both read one measurement taken here.
65
- const syncViewport = () => (app.viewportHeight = window.innerHeight);
66
- syncViewport();
67
- window.addEventListener('resize', syncViewport);
68
- window.addEventListener('orientationchange', syncViewport);
69
-
70
- return () => {
71
- query.removeEventListener('change', syncFileDropUi);
72
- compact.removeEventListener('change', syncCompact);
73
- window.removeEventListener('resize', syncViewport);
74
- window.removeEventListener('orientationchange', syncViewport);
75
- };
76
50
  });
77
51
 
78
52
  function dragHasFiles(e: DragEvent): boolean {
@@ -116,6 +90,8 @@
116
90
  ondrop={onDrop}
117
91
  />
118
92
 
93
+ <ShellEnvironment />
94
+
119
95
  <main>
120
96
  <TellegenMap
121
97
  onbusclick={ctrl.selectBus}
@@ -126,18 +102,20 @@
126
102
  onmultiedgeclick={ctrl.selectMultiEdge}
127
103
  onplacecase={ctrl.placeCase}
128
104
  onmapclick={ctrl.clearSelection}
105
+ theme={config.theme}
106
+ cartoApiKey={config.cartoApiKey || undefined}
129
107
  />
130
108
 
131
109
  <AppHeader />
110
+ <PanelHost />
111
+ <NotificationCenter />
132
112
  <ControlPanel />
133
113
  <SolveCard />
134
- <!-- ControlPanel mounts the lookup and the footer inline when compact. -->
135
- {#if !app.compactLayout}
136
- <BusPicker />
114
+ {#if !app.studyView}
115
+ <DropZone />
116
+ <PlacementCue />
117
+ <RestoreDefaultsButton />
137
118
  {/if}
138
- <DropZone />
139
- <PlacementCue />
140
- <RestoreDefaultsButton />
141
119
  {#if config.showFooter && !app.compactLayout}
142
120
  <AppFooter />
143
121
  {/if}
@@ -3,8 +3,16 @@
3
3
  import TellegenShell from './TellegenShell.svelte';
4
4
  import type { TellegenUiOptions } from './context.svelte.js';
5
5
 
6
- let { apiBase, loadDefaultCases, docsHref, orgHref, orgLabel, showFooter }: TellegenUiOptions =
7
- $props();
6
+ let {
7
+ apiBase,
8
+ loadDefaultCases,
9
+ docsHref,
10
+ orgHref,
11
+ orgLabel,
12
+ showFooter,
13
+ theme,
14
+ cartoApiKey
15
+ }: TellegenUiOptions = $props();
8
16
  </script>
9
17
 
10
18
  <TellegenProvider
@@ -14,6 +22,8 @@
14
22
  {orgHref}
15
23
  {orgLabel}
16
24
  {showFooter}
25
+ {theme}
26
+ {cartoApiKey}
17
27
  >
18
28
  <TellegenShell />
19
29
  </TellegenProvider>
package/dist/api.d.ts CHANGED
@@ -2,15 +2,24 @@ import type { CaseSummary, DemandDeltas, Network, SensitivityColumn, Solution, S
2
2
  export type { BranchRatingDeltas, CaseSummary, DemandDeltas, Network, NetworkBranch, NetworkBus, SensitivityColumn, Solution, SolveIteration } from '@tellegen/engine';
3
3
  type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
4
4
  export interface TellegenApiClientOptions {
5
- apiBase?: string;
5
+ /** Backend base path, default `/api`. `null` means no backend: the
6
+ * factory returns `null` instead of a client. */
7
+ apiBase?: string | null;
6
8
  fetch?: FetchLike;
7
9
  EventSource?: typeof EventSource;
8
10
  }
9
11
  export interface TellegenApiClient {
10
12
  getCases(): Promise<CaseSummary[]>;
11
13
  getNetwork(caseId: string): Promise<Network>;
12
- getCaseNetworkJson(caseId: string, signal?: AbortSignal): Promise<string>;
14
+ getCaseModuleJson(caseId: string, signal?: AbortSignal): Promise<string>;
13
15
  getSolution(caseId: string): Promise<Solution>;
16
+ getSavedCase?(caseId: string): Promise<{
17
+ base_input: string;
18
+ input: string;
19
+ solution: string;
20
+ view: import('@tellegen/engine').StudyView;
21
+ display_solution: Solution;
22
+ }>;
14
23
  getSensitivity(caseId: string, bus: number, deltas?: DemandDeltas, signal?: AbortSignal): Promise<SensitivityColumn>;
15
24
  getBranchSensitivity(caseId: string, branch: number, deltas?: DemandDeltas, signal?: AbortSignal): Promise<SensitivityColumn>;
16
25
  /** Whether the server's compute endpoints (solve stream, sensitivities) are
@@ -40,4 +49,10 @@ export interface SolveStreamHandlers {
40
49
  onfail?: (message: string) => void;
41
50
  ondone?: () => void;
42
51
  }
43
- export declare function createApiClient(options?: TellegenApiClientOptions): TellegenApiClient;
52
+ export declare function createApiClient(options: TellegenApiClientOptions & {
53
+ apiBase: null;
54
+ }): null;
55
+ export declare function createApiClient(options?: TellegenApiClientOptions & {
56
+ apiBase?: string;
57
+ }): TellegenApiClient;
58
+ export declare function createApiClient(options?: TellegenApiClientOptions): TellegenApiClient | null;
package/dist/api.js CHANGED
@@ -72,16 +72,19 @@ async function getJson(fetchImpl, url, signal) {
72
72
  return res.json();
73
73
  }
74
74
  export function createApiClient(options = {}) {
75
+ if (options.apiBase === null)
76
+ return null;
75
77
  const apiBase = options.apiBase ?? '/api';
76
78
  const fetchImpl = options.fetch ?? defaultFetch();
77
79
  return {
78
80
  getCases: () => getJson(fetchImpl, apiPath(apiBase, '/cases')),
79
81
  getNetwork: (caseId) => getJson(fetchImpl, apiPath(apiBase, `/cases/${caseId}/network`)),
80
- async getCaseNetworkJson(caseId, signal) {
82
+ async getCaseModuleJson(caseId, signal) {
81
83
  const url = apiPath(apiBase, `/cases/${caseId}/case`);
82
84
  const res = await checkedFetch(fetchImpl, url, signal);
83
85
  return res.text();
84
86
  },
87
+ getSavedCase: (caseId) => getJson(fetchImpl, apiPath(apiBase, `/cases/${caseId}/snapshot`)),
85
88
  getSolution: (caseId) => getJson(fetchImpl, apiPath(apiBase, `/cases/${caseId}/solution`)),
86
89
  getSensitivity(caseId, bus, deltas = {}, signal) {
87
90
  const d = encodeDeltas(deltas);
@@ -111,7 +114,10 @@ export function createApiClient(options = {}) {
111
114
  finished = true;
112
115
  es.close();
113
116
  };
114
- es.addEventListener('solution', (e) => handlers.onsolution?.(JSON.parse(e.data)));
117
+ es.addEventListener('solution', (e) => {
118
+ const solution = JSON.parse(e.data);
119
+ handlers.onsolution?.(solution);
120
+ });
115
121
  es.addEventListener('sensitivity', (e) => handlers.onsensitivity?.(JSON.parse(e.data)));
116
122
  es.addEventListener('fail', (e) => {
117
123
  finish();
@@ -0,0 +1,192 @@
1
+ import type { StyleSpecification } from 'maplibre-gl';
2
+ /** Basemap selection, fallback and tuning for `TellegenMap`.
3
+ *
4
+ * CARTO's vector styles sit quietly under a network overlay, and CARTO's
5
+ * basemap terms ask for an API key (free for non-commercial use, from
6
+ * carto.com/basemaps/apikey). With a key, the CARTO style leads and
7
+ * OpenFreeMap (no key, CORS open) is the fallback. Without one, OpenFreeMap
8
+ * leads and the keyless CARTO style is the fallback. Whenever an OpenFreeMap
9
+ * style renders, `openFreeMapEdits` tunes it towards CARTO's look. A host can
10
+ * replace either style with its own URL or inline `StyleSpecification`, or
11
+ * disable the fallback with `null`. */
12
+ export type BasemapTheme = 'light' | 'dark';
13
+ export type BasemapStyle = string | StyleSpecification;
14
+ /** `TellegenMap`'s `theme` prop: the basemap theme, which also picks the
15
+ * overlay palette. */
16
+ export type MapTheme = BasemapTheme;
17
+ /** `TellegenMap`'s `projection` prop. MapLibre's globe falls back to
18
+ * Mercator at high zoom on its own. */
19
+ export type MapProjection = 'mercator' | 'globe';
20
+ /** Who serves a basemap style: one of the two default providers, or
21
+ * `'custom'` for a style the host supplied. */
22
+ export type BasemapProvider = 'carto' | 'openfreemap' | 'custom';
23
+ export interface BasemapSpec {
24
+ primary: BasemapStyle;
25
+ fallback: BasemapStyle | null;
26
+ /** Hide the style's `symbol` layers after every `style.load`. */
27
+ hideLabels: boolean;
28
+ /** Who serves `primary`. Absent counts as `'custom'`. */
29
+ primaryProvider?: BasemapProvider;
30
+ /** Who serves `fallback`, `null` with no fallback. Absent counts as
31
+ * `'custom'`. */
32
+ fallbackProvider?: BasemapProvider | null;
33
+ }
34
+ /** A `BasemapSpec` that names both providers, as `defaultBasemap` and
35
+ * `resolveBasemap` return it. A host words its credits and fallback notice
36
+ * from these instead of reading style URLs. */
37
+ export interface ResolvedBasemap extends BasemapSpec {
38
+ primaryProvider: BasemapProvider;
39
+ fallbackProvider: BasemapProvider | null;
40
+ }
41
+ /** What `defaultBasemap` returns: one CARTO and one OpenFreeMap style URL,
42
+ * in the order the key decides. Never `'custom'`, and never without a
43
+ * fallback. */
44
+ export interface DefaultBasemap extends ResolvedBasemap {
45
+ primary: string;
46
+ fallback: string;
47
+ primaryProvider: 'carto' | 'openfreemap';
48
+ fallbackProvider: 'carto' | 'openfreemap';
49
+ }
50
+ export declare const OPENFREEMAP_STYLES: {
51
+ readonly light: "https://tiles.openfreemap.org/styles/positron";
52
+ readonly dark: "https://tiles.openfreemap.org/styles/dark";
53
+ };
54
+ export declare const CARTO_STYLES: {
55
+ readonly light: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json";
56
+ readonly 'light-nolabels': "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json";
57
+ readonly dark: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json";
58
+ readonly 'dark-nolabels': "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json";
59
+ };
60
+ /** A CARTO key with surrounding whitespace removed, or `undefined` when
61
+ * nothing is left. A variable saved with a stray newline or space must not
62
+ * count as a key. */
63
+ export declare function normalizeCartoApiKey(apiKey: string | undefined): string | undefined;
64
+ /** A CARTO style URL with the optional API key appended as `key=`. A blank
65
+ * key appends nothing. */
66
+ export declare function cartoStyleUrl(style: string, apiKey?: string): string;
67
+ export interface DefaultBasemapOptions {
68
+ /** CARTO basemap key, sent as `key=`. With one, CARTO leads and
69
+ * OpenFreeMap is the fallback; without one, the order flips. The key is
70
+ * trimmed, and a blank one counts as none. Restrict the key to the hosts
71
+ * that serve your app. */
72
+ cartoApiKey?: string;
73
+ /** Default `true`: the CARTO style uses its `-nolabels` variant, and label
74
+ * layers are hidden on load whichever style renders. */
75
+ hideLabels?: boolean;
76
+ }
77
+ /** The default primary and fallback for a theme, ordered by whether a CARTO
78
+ * key is at hand. */
79
+ export declare function defaultBasemap(theme: BasemapTheme, options?: DefaultBasemapOptions): DefaultBasemap;
80
+ export interface ResolveBasemapInput {
81
+ /** Overrides the default primary. */
82
+ mapStyle?: BasemapStyle;
83
+ /** Overrides the default fallback; `null` disables the fallback. */
84
+ fallbackMapStyle?: BasemapStyle | null;
85
+ theme: BasemapTheme;
86
+ hideBasemapLabels: boolean;
87
+ cartoApiKey?: string;
88
+ }
89
+ /** The spec `TellegenMap` renders from its props: explicit styles win, the
90
+ * theme fills in whatever is left, and `null` turns the fallback off. An
91
+ * explicit style is `'custom'`, whichever host serves it. */
92
+ export declare function resolveBasemap(input: ResolveBasemapInput): ResolvedBasemap;
93
+ /** Whether a style draws on OpenFreeMap's tiles, whichever URL loaded it. */
94
+ export declare function isOpenFreeMapStyle(style: StyleSpecification): boolean;
95
+ /** One change to a loaded style, applied through the map's own API. */
96
+ export type StyleEdit = {
97
+ kind: 'zoom-range';
98
+ layer: string;
99
+ minzoom: number;
100
+ maxzoom: number;
101
+ } | {
102
+ kind: 'paint';
103
+ layer: string;
104
+ property: 'fill-color' | 'line-color';
105
+ value: string;
106
+ };
107
+ /** Edits that let an OpenFreeMap style sit under a network overlay the way
108
+ * CARTO's styles do.
109
+ *
110
+ * - Road lines (`transportation`) start at z8.
111
+ * - In the dark theme, water and waterways take Dark Matter's colours, and
112
+ * plain land fills (`landuse`, `landcover`, `park`) take the style's
113
+ * background colour, so the water stands out against one tone of land.
114
+ *
115
+ * Layers are matched by `source-layer`, not by id, so the edits still apply
116
+ * when OpenFreeMap renames its layers. */
117
+ export declare function openFreeMapEdits(style: StyleSpecification, theme: BasemapTheme): StyleEdit[];
118
+ /** The providers a fallback event concerns. */
119
+ export interface BasemapSwitch {
120
+ /** The provider whose style failed. */
121
+ from: BasemapProvider;
122
+ /** The provider switched to; `null` when no basemap is left to show. */
123
+ to: BasemapProvider | null;
124
+ }
125
+ type BasemapFailure = {
126
+ kind: 'style-load';
127
+ message: string;
128
+ status?: number;
129
+ } | {
130
+ kind: 'source-forbidden';
131
+ message: string;
132
+ status: number;
133
+ sourceId: string;
134
+ } | {
135
+ kind: 'tiles-unavailable';
136
+ message: string;
137
+ sourceId: string;
138
+ } | {
139
+ kind: 'webgl';
140
+ message: string;
141
+ };
142
+ export type BasemapFallbackReason = BasemapFailure & BasemapSwitch;
143
+ /** The fields of a MapLibre `error` or `sourcedata` event the fallback reads. */
144
+ export interface BasemapMapEvent {
145
+ error?: unknown;
146
+ sourceId?: string;
147
+ tile?: unknown;
148
+ dataType?: string;
149
+ isSourceLoaded?: boolean;
150
+ }
151
+ /** The slice of `maplibregl.Map` the fallback needs; a test drives a fake. */
152
+ export interface BasemapMapLike {
153
+ on(type: string, listener: (event: BasemapMapEvent) => void): unknown;
154
+ off(type: string, listener: (event: BasemapMapEvent) => void): unknown;
155
+ setStyle(style: BasemapStyle, options?: {
156
+ diff?: boolean;
157
+ }): unknown;
158
+ /** MapLibre types this `boolean | void`; only `true` counts as loaded. */
159
+ isStyleLoaded(): boolean | void;
160
+ }
161
+ export interface BasemapFallbackHandlers {
162
+ /** The primary failed and the fallback style was requested. `from` is the
163
+ * spec's primary provider and `to` its fallback provider. */
164
+ onFallback?(reason: BasemapFallbackReason): void;
165
+ /** No basemap can be shown: the fallback failed too, none was configured,
166
+ * or WebGL is unavailable. `from` is the provider on screen, `to` is
167
+ * `null`. */
168
+ onFailure?(reason: BasemapFallbackReason): void;
169
+ }
170
+ export interface BasemapFallbackOptions {
171
+ /** Clock for the tile failure window; tests inject one. */
172
+ now?: () => number;
173
+ }
174
+ /** Watch a map's `error` stream and switch to `spec.fallback` when the
175
+ * primary basemap cannot be shown. Rules, in order:
176
+ *
177
+ * 1. an error with no `sourceId` before the first `style.load` is a style
178
+ * fetch failure: switch to the fallback once;
179
+ * 2. a source or tile error with status 401, 403 or 429, or a message naming
180
+ * an API key, forbidden or unauthorized access: switch;
181
+ * 3. more than six tile fetch failures within ten seconds while no tile of
182
+ * any source has arrived: switch. A failure is a network error with no
183
+ * status, or an HTTP 5xx answer;
184
+ * 4. a WebGL error: report failure, no style can help;
185
+ * 5. everything else is logged the way MapLibre logs an unhandled error,
186
+ * because registering this listener silences that default.
187
+ *
188
+ * After the switch the same rules report `onFailure` instead of switching
189
+ * again, as they do when `spec.fallback` is `null`. Returns a detach
190
+ * function. */
191
+ export declare function attachBasemapFallback(map: BasemapMapLike, spec: BasemapSpec, handlers?: BasemapFallbackHandlers, options?: BasemapFallbackOptions): () => void;
192
+ export {};