@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.
- package/README.md +269 -5
- package/dist/Sparkline.svelte +2 -2
- package/dist/TellegenMap.svelte +518 -202
- package/dist/TellegenMap.svelte.d.ts +45 -0
- package/dist/TellegenProvider.svelte +40 -4
- package/dist/TellegenShell.svelte +24 -46
- package/dist/TellegenViewer.svelte +12 -2
- package/dist/api.d.ts +18 -3
- package/dist/api.js +8 -2
- package/dist/basemap.d.ts +192 -0
- package/dist/basemap.js +259 -0
- package/dist/colors.d.ts +25 -7
- package/dist/colors.js +39 -13
- package/dist/components/AppFooter.svelte +3 -2
- package/dist/components/AppHeader.svelte +44 -19
- package/dist/components/BindingLines.svelte +4 -7
- package/dist/components/BusPicker.svelte +11 -5
- package/dist/components/ControlPanel.svelte +106 -343
- package/dist/components/DemandSlider.svelte +36 -29
- package/dist/components/DiagramCanvas.svelte +486 -0
- package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
- package/dist/components/DisplayControls.svelte +8 -2
- package/dist/components/DropZone.svelte +1 -1
- package/dist/components/FormulationSelector.svelte +9 -4
- package/dist/components/LocalCaseDetails.svelte +61 -48
- package/dist/components/McResults.svelte +428 -0
- package/dist/components/McResults.svelte.d.ts +17 -0
- package/dist/components/ModelDetails.svelte +230 -0
- package/dist/components/ModelDetails.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +232 -83
- package/dist/components/NetworkStats.svelte +20 -18
- package/dist/components/NotificationCenter.svelte +245 -0
- package/dist/components/NotificationCenter.svelte.d.ts +3 -0
- package/dist/components/PanelCard.svelte +384 -0
- package/dist/components/PanelCard.svelte.d.ts +9 -0
- package/dist/components/PanelFrame.svelte +46 -0
- package/dist/components/PanelFrame.svelte.d.ts +16 -0
- package/dist/components/PanelHost.svelte +237 -0
- package/dist/components/PanelHost.svelte.d.ts +6 -0
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/PowerFlowDetails.svelte +99 -0
- package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
- package/dist/components/RatingSlider.svelte +19 -14
- package/dist/components/RestoreDefaultsButton.svelte +1 -1
- package/dist/components/SavedNetworkDetails.svelte +214 -0
- package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
- package/dist/components/SensitivityReadout.svelte +26 -18
- package/dist/components/ShellEnvironment.svelte +51 -0
- package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
- package/dist/components/SizeLegend.svelte +2 -2
- package/dist/components/SolveCard.svelte +48 -51
- package/dist/components/TopMovers.svelte +9 -4
- package/dist/components/TopMovers.svelte.d.ts +2 -17
- package/dist/components.d.ts +5 -0
- package/dist/components.js +5 -0
- package/dist/context.svelte.d.ts +8 -1
- package/dist/context.svelte.js +8 -3
- package/dist/controller.svelte.d.ts +117 -36
- package/dist/controller.svelte.js +1133 -253
- package/dist/diagram-labels.d.ts +9 -0
- package/dist/diagram-labels.js +40 -0
- package/dist/display.d.ts +3 -7
- package/dist/display.js +31 -27
- package/dist/format.d.ts +9 -0
- package/dist/format.js +17 -4
- package/dist/index.d.ts +15 -5
- package/dist/index.js +9 -2
- package/dist/map.d.ts +4 -2
- package/dist/map.js +2 -1
- package/dist/maplibre-worker.d.ts +5 -0
- package/dist/maplibre-worker.dev.d.ts +11 -0
- package/dist/maplibre-worker.dev.js +15 -0
- package/dist/maplibre-worker.js +8 -0
- package/dist/multiconductor.d.ts +11 -2
- package/dist/multiconductor.js +107 -14
- package/dist/notices.svelte.d.ts +29 -0
- package/dist/notices.svelte.js +86 -0
- package/dist/panels.svelte.d.ts +71 -0
- package/dist/panels.svelte.js +245 -0
- package/dist/slider.d.ts +12 -0
- package/dist/slider.js +31 -0
- package/dist/state.svelte.d.ts +107 -23
- package/dist/state.svelte.js +106 -20
- package/dist/styles.css +59 -6
- package/dist/synthetic-layout.js +1 -0
- package/package.json +29 -11
- package/dist/study-package.d.ts +0 -8
- 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 {
|
|
18
|
-
|
|
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, {
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
<
|
|
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 {
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
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 {};
|