@tellegen/svelte 0.3.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 +233 -0
- package/dist/Sparkline.svelte +2 -2
- package/dist/TellegenMap.svelte +201 -47
- package/dist/TellegenMap.svelte.d.ts +45 -0
- package/dist/TellegenProvider.svelte +36 -4
- package/dist/TellegenShell.svelte +14 -42
- package/dist/TellegenViewer.svelte +12 -2
- package/dist/api.d.ts +10 -2
- package/dist/api.js +2 -0
- package/dist/basemap.d.ts +192 -0
- package/dist/basemap.js +259 -0
- package/dist/colors.d.ts +18 -0
- package/dist/colors.js +26 -0
- package/dist/components/AppFooter.svelte +3 -2
- package/dist/components/AppHeader.svelte +21 -8
- package/dist/components/BindingLines.svelte +1 -1
- package/dist/components/BusPicker.svelte +3 -3
- package/dist/components/DemandSlider.svelte +16 -7
- package/dist/components/DiagramCanvas.svelte +1 -1
- package/dist/components/DisplayControls.svelte +5 -1
- package/dist/components/DropZone.svelte +1 -1
- package/dist/components/FormulationSelector.svelte +1 -1
- package/dist/components/LocalCaseDetails.svelte +6 -3
- package/dist/components/McResults.svelte +98 -35
- package/dist/components/McResults.svelte.d.ts +8 -2
- package/dist/components/ModelDetails.svelte +5 -5
- package/dist/components/MulticonductorDetails.svelte +9 -9
- package/dist/components/NotificationCenter.svelte +4 -2
- package/dist/components/PanelCard.svelte +72 -7
- package/dist/components/PanelHost.svelte +59 -34
- package/dist/components/PanelHost.svelte.d.ts +4 -1
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/PowerFlowDetails.svelte +1 -1
- package/dist/components/RatingSlider.svelte +14 -5
- package/dist/components/RestoreDefaultsButton.svelte +1 -1
- package/dist/components/SavedNetworkDetails.svelte +3 -3
- package/dist/components/SensitivityReadout.svelte +2 -2
- 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 +3 -3
- package/dist/components/TopMovers.svelte +1 -1
- package/dist/components.d.ts +3 -0
- package/dist/components.js +3 -0
- package/dist/context.svelte.d.ts +8 -1
- package/dist/context.svelte.js +8 -3
- package/dist/controller.svelte.d.ts +65 -5
- package/dist/controller.svelte.js +262 -76
- package/dist/format.d.ts +9 -0
- package/dist/format.js +12 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- 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/notices.svelte.js +5 -1
- package/dist/panels.svelte.d.ts +6 -0
- package/dist/panels.svelte.js +31 -1
- package/dist/slider.d.ts +12 -0
- package/dist/slider.js +31 -0
- package/dist/state.svelte.d.ts +19 -2
- package/dist/state.svelte.js +12 -0
- package/dist/styles.css +59 -6
- package/package.json +13 -8
package/dist/TellegenMap.svelte
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
3
|
-
import { tick } from 'svelte';
|
|
3
|
+
import { tick, untrack } from 'svelte';
|
|
4
4
|
import type { Layer, PickingInfo } from '@deck.gl/core';
|
|
5
5
|
import type { IconLayer, PathLayer, ScatterplotLayer } from '@deck.gl/layers';
|
|
6
6
|
import type { MapLibreOverlay } from '@deck.gl/maplibre';
|
|
7
7
|
import type { LngLatBoundsLike, Map as MapLibreMap } from 'maplibre-gl';
|
|
8
8
|
import type { NetworkBranch, NetworkBus } from './api.js';
|
|
9
9
|
import {
|
|
10
|
+
MAP_PALETTE,
|
|
10
11
|
branchColor,
|
|
11
12
|
branchWidth,
|
|
12
13
|
busNeutral,
|
|
@@ -18,8 +19,23 @@
|
|
|
18
19
|
sensitivityDomain,
|
|
19
20
|
type SensitivityDomain
|
|
20
21
|
} from './colors.js';
|
|
22
|
+
import {
|
|
23
|
+
attachBasemapFallback,
|
|
24
|
+
isOpenFreeMapStyle,
|
|
25
|
+
openFreeMapEdits,
|
|
26
|
+
resolveBasemap,
|
|
27
|
+
type BasemapFallbackReason,
|
|
28
|
+
type BasemapStyle,
|
|
29
|
+
type MapProjection,
|
|
30
|
+
type MapTheme
|
|
31
|
+
} from './basemap.js';
|
|
21
32
|
import { caseDeltas, caseRatings, displayMetaFor, displaySeriesFor } from './display.js';
|
|
22
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
CaseState,
|
|
35
|
+
type CameraSnapshot,
|
|
36
|
+
type DisplayMode,
|
|
37
|
+
type LocalCase
|
|
38
|
+
} from './state.svelte.js';
|
|
23
39
|
import {
|
|
24
40
|
attachmentColor,
|
|
25
41
|
edgeColor,
|
|
@@ -40,6 +56,8 @@
|
|
|
40
56
|
import type { RGBA } from './colors.js';
|
|
41
57
|
import DiagramCanvas from './components/DiagramCanvas.svelte';
|
|
42
58
|
|
|
59
|
+
const DEFAULT_CAMERA: CameraSnapshot = { center: [-85, 36], zoom: 4.5, bearing: 0, pitch: 0 };
|
|
60
|
+
|
|
43
61
|
let {
|
|
44
62
|
onbusclick,
|
|
45
63
|
onlocalbusclick,
|
|
@@ -48,7 +66,19 @@
|
|
|
48
66
|
onmultibusclick,
|
|
49
67
|
onmultiedgeclick,
|
|
50
68
|
onplacecase,
|
|
51
|
-
onmapclick
|
|
69
|
+
onmapclick,
|
|
70
|
+
mapStyle,
|
|
71
|
+
fallbackMapStyle,
|
|
72
|
+
theme = 'light',
|
|
73
|
+
projection = 'mercator',
|
|
74
|
+
initialCamera = DEFAULT_CAMERA,
|
|
75
|
+
hideBasemapLabels = true,
|
|
76
|
+
cartoApiKey,
|
|
77
|
+
extraLayers = [],
|
|
78
|
+
attribution = 'compact',
|
|
79
|
+
onMapReady,
|
|
80
|
+
onBasemapFallback,
|
|
81
|
+
onBasemapError
|
|
52
82
|
}: {
|
|
53
83
|
onbusclick: (caseId: string, busId: number) => void;
|
|
54
84
|
onlocalbusclick: (caseId: string, busId: number) => void;
|
|
@@ -58,6 +88,46 @@
|
|
|
58
88
|
onmultiedgeclick?: (caseId: string, edgeId: string) => void;
|
|
59
89
|
onplacecase: (lon: number, lat: number) => void;
|
|
60
90
|
onmapclick: () => void;
|
|
91
|
+
/** Primary basemap: a style URL or inline style. Default: the CARTO style
|
|
92
|
+
* for `theme` when `cartoApiKey` is set, else the OpenFreeMap style.
|
|
93
|
+
* Changing it (or `fallbackMapStyle`, `theme`, `hideBasemapLabels`,
|
|
94
|
+
* `cartoApiKey`, `attribution`) rebuilds the map in place and keeps the
|
|
95
|
+
* camera. */
|
|
96
|
+
mapStyle?: BasemapStyle;
|
|
97
|
+
/** Basemap used when the primary cannot be shown; `null` disables the
|
|
98
|
+
* switch. Default: whichever of the CARTO and OpenFreeMap styles is not
|
|
99
|
+
* the default primary. */
|
|
100
|
+
fallbackMapStyle?: BasemapStyle | null;
|
|
101
|
+
/** Picks the default basemaps and the overlay palette (bus outlines,
|
|
102
|
+
* hover highlight, unsolved grey). */
|
|
103
|
+
theme?: MapTheme;
|
|
104
|
+
/** Re-applied after every `style.load`, so a basemap switch keeps it. */
|
|
105
|
+
projection?: MapProjection;
|
|
106
|
+
/** Camera for the first paint; read once per mount. */
|
|
107
|
+
initialCamera?: CameraSnapshot;
|
|
108
|
+
/** Hide the basemap's `symbol` layers after every `style.load`. */
|
|
109
|
+
hideBasemapLabels?: boolean;
|
|
110
|
+
/** CARTO basemap key (free for non-commercial use from
|
|
111
|
+
* carto.com/basemaps/apikey; restrict it to your hosts). With one, the
|
|
112
|
+
* CARTO style leads and OpenFreeMap is the fallback; without one, the
|
|
113
|
+
* order flips. Trimmed; a blank key counts as none. */
|
|
114
|
+
cartoApiKey?: string;
|
|
115
|
+
/** deck.gl layers drawn after tellegen's own, in the same overlay. */
|
|
116
|
+
extraLayers?: Layer[];
|
|
117
|
+
/** Attribution control mode. There is no off switch: tile licences
|
|
118
|
+
* require the credit on screen. */
|
|
119
|
+
attribution?: 'compact' | 'full';
|
|
120
|
+
/** Called once the map and overlay exist, with an optional cleanup. It
|
|
121
|
+
* fires again after a context-loss or basemap remount, after the previous
|
|
122
|
+
* cleanup ran. */
|
|
123
|
+
onMapReady?: (map: MapLibreMap, overlay: MapLibreOverlay) => void | (() => void);
|
|
124
|
+
/** The primary basemap failed and the fallback was requested. The
|
|
125
|
+
* reason's `from` and `to` name the providers (`'carto'`,
|
|
126
|
+
* `'openfreemap'` or `'custom'`). */
|
|
127
|
+
onBasemapFallback?: (reason: BasemapFallbackReason) => void;
|
|
128
|
+
/** No basemap can be shown; the network layers still draw. The reason's
|
|
129
|
+
* `from` names the provider that failed last, and `to` is `null`. */
|
|
130
|
+
onBasemapError?: (reason: BasemapFallbackReason) => void;
|
|
61
131
|
} = $props();
|
|
62
132
|
|
|
63
133
|
const app = getAppState();
|
|
@@ -104,7 +174,20 @@
|
|
|
104
174
|
else onbranchclick?.(diagramCaseId, branchId);
|
|
105
175
|
}
|
|
106
176
|
|
|
107
|
-
const
|
|
177
|
+
const basemap = $derived(
|
|
178
|
+
resolveBasemap({ mapStyle, fallbackMapStyle, theme, hideBasemapLabels, cartoApiKey })
|
|
179
|
+
);
|
|
180
|
+
// Read as separate values, not one object, so a theme change that resolves
|
|
181
|
+
// to the same styles does not rebuild the map.
|
|
182
|
+
const basemapPrimary = $derived(basemap.primary);
|
|
183
|
+
const basemapFallback = $derived(basemap.fallback);
|
|
184
|
+
const basemapHideLabels = $derived(basemap.hideLabels);
|
|
185
|
+
const basemapPrimaryProvider = $derived(basemap.primaryProvider);
|
|
186
|
+
const basemapFallbackProvider = $derived(basemap.fallbackProvider);
|
|
187
|
+
const palette = $derived(MAP_PALETTE[theme]);
|
|
188
|
+
// Set once a map has existed in this component instance: a rebuild (context
|
|
189
|
+
// loss or basemap change) restores the camera instead of reframing.
|
|
190
|
+
let hadMap = false;
|
|
108
191
|
const CUE_MARGIN_PX = 64;
|
|
109
192
|
const BRANCH_FOCUS_MS = 850;
|
|
110
193
|
const BRANCH_FOCUS_MAX_ZOOM = 9.5;
|
|
@@ -508,9 +591,9 @@
|
|
|
508
591
|
terminals.forEach((t, k) => {
|
|
509
592
|
const cy = top + k * STACK_DY;
|
|
510
593
|
const grounded = bus.grounded.includes(t);
|
|
511
|
-
const fill = isPhaseTerminal(t) ? col(phaseColor(t)) : col(
|
|
594
|
+
const fill = isPhaseTerminal(t) ? col(phaseColor(t)) : col(palette.neutral);
|
|
512
595
|
if (grounded) stack += groundGlyph(sx - 14, cy);
|
|
513
|
-
stack += `<circle cx="${sx.toFixed(1)}" cy="${cy.toFixed(1)}" r="6" fill="${fill}"
|
|
596
|
+
stack += `<circle class="mc-terminal" cx="${sx.toFixed(1)}" cy="${cy.toFixed(1)}" r="6" fill="${fill}"/>`;
|
|
514
597
|
stack += `<text class="mc-term" x="${(sx + 11).toFixed(1)}" y="${(cy + 3.5).toFixed(1)}">${esc(t)}</text>`;
|
|
515
598
|
// Attachment squares for elements landing on this terminal.
|
|
516
599
|
const kinds = [...new Set((bus.terminalAttachments[t] ?? []).map((a) => a.kind))];
|
|
@@ -712,14 +795,17 @@
|
|
|
712
795
|
}
|
|
713
796
|
|
|
714
797
|
async function loadMapModules() {
|
|
715
|
-
const [maplibre, deckMap, layers,
|
|
798
|
+
const [maplibre, deckMap, layers, workerUrl] = await Promise.all([
|
|
716
799
|
import('maplibre-gl'),
|
|
717
800
|
import('@deck.gl/maplibre'),
|
|
718
801
|
import('@deck.gl/layers'),
|
|
719
|
-
import
|
|
802
|
+
// A package export, not a relative import: its `development`
|
|
803
|
+
// condition keeps the worker's `?worker&url` import out of the dev
|
|
804
|
+
// server's pre-bundle (see maplibre-worker.dev.ts).
|
|
805
|
+
import('@tellegen/svelte/maplibre-worker').then((m) => m.maplibreWorkerUrl())
|
|
720
806
|
]);
|
|
721
807
|
// Bundle the worker with its shared imports and configure it before creating a map.
|
|
722
|
-
maplibre.setWorkerUrl(
|
|
808
|
+
maplibre.setWorkerUrl(workerUrl);
|
|
723
809
|
return {
|
|
724
810
|
maplibregl: maplibre,
|
|
725
811
|
MapLibreOverlay: deckMap.MapLibreOverlay,
|
|
@@ -759,6 +845,20 @@
|
|
|
759
845
|
}
|
|
760
846
|
|
|
761
847
|
function initMap(container: HTMLDivElement) {
|
|
848
|
+
// Attachments re-run when state they read changes, so reading the
|
|
849
|
+
// resolved basemap and attribution mode here is what rebuilds the map
|
|
850
|
+
// when those props change. The camera is read once: it is initial.
|
|
851
|
+
const spec = {
|
|
852
|
+
primary: basemapPrimary,
|
|
853
|
+
fallback: basemapFallback,
|
|
854
|
+
hideLabels: basemapHideLabels,
|
|
855
|
+
primaryProvider: basemapPrimaryProvider,
|
|
856
|
+
fallbackProvider: basemapFallbackProvider
|
|
857
|
+
};
|
|
858
|
+
const compactAttribution = attribution === 'compact';
|
|
859
|
+
const camera = untrack(() => initialCamera);
|
|
860
|
+
const restoreCamera = hadMap;
|
|
861
|
+
hadMap = true;
|
|
762
862
|
let cleanup = () => {};
|
|
763
863
|
let cancelled = false;
|
|
764
864
|
void loadMapModules()
|
|
@@ -767,11 +867,40 @@
|
|
|
767
867
|
layerCtors = { PathLayer, ScatterplotLayer, IconLayer };
|
|
768
868
|
const m = new maplibregl.Map({
|
|
769
869
|
container,
|
|
770
|
-
style:
|
|
771
|
-
center:
|
|
772
|
-
zoom:
|
|
870
|
+
style: spec.primary,
|
|
871
|
+
center: camera.center,
|
|
872
|
+
zoom: camera.zoom,
|
|
873
|
+
bearing: camera.bearing,
|
|
874
|
+
pitch: camera.pitch,
|
|
773
875
|
canvasContextAttributes: { antialias: true },
|
|
774
|
-
attributionControl: { compact:
|
|
876
|
+
attributionControl: { compact: compactAttribution }
|
|
877
|
+
});
|
|
878
|
+
// `setStyle` (the fallback path included) drops the projection and
|
|
879
|
+
// shows every layer again, so both are re-applied on each load.
|
|
880
|
+
const applyStyleExtras = () => {
|
|
881
|
+
m.setProjection({ type: untrack(() => projection) });
|
|
882
|
+
const style = m.getStyle();
|
|
883
|
+
if (spec.hideLabels) {
|
|
884
|
+
for (const layer of style.layers) {
|
|
885
|
+
if (layer.type === 'symbol') m.setLayoutProperty(layer.id, 'visibility', 'none');
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// OpenFreeMap renders as the primary or as the fallback; either
|
|
889
|
+
// way it is tuned towards CARTO's quieter look.
|
|
890
|
+
if (!isOpenFreeMapStyle(style)) return;
|
|
891
|
+
const edits = openFreeMapEdits(
|
|
892
|
+
style,
|
|
893
|
+
untrack(() => theme)
|
|
894
|
+
);
|
|
895
|
+
for (const edit of edits) {
|
|
896
|
+
if (edit.kind === 'paint') m.setPaintProperty(edit.layer, edit.property, edit.value);
|
|
897
|
+
else m.setLayerZoomRange(edit.layer, edit.minzoom, edit.maxzoom);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
m.on('style.load', applyStyleExtras);
|
|
901
|
+
const detachFallback = attachBasemapFallback(m, spec, {
|
|
902
|
+
onFallback: (reason) => onBasemapFallback?.(reason),
|
|
903
|
+
onFailure: (reason) => onBasemapError?.(reason)
|
|
775
904
|
});
|
|
776
905
|
const o = buildOverlay(MapLibreOverlay);
|
|
777
906
|
m.addControl(o);
|
|
@@ -858,11 +987,13 @@
|
|
|
858
987
|
|
|
859
988
|
map = m;
|
|
860
989
|
overlay = o;
|
|
990
|
+
const detachReady = onMapReady?.(m, o);
|
|
861
991
|
// Frame the active grid once the map is ready. The initial frame request
|
|
862
992
|
// (from the case load) can fire before the async map import finishes and
|
|
863
993
|
// be lost; re-issuing it here ensures the first view is the active case,
|
|
864
|
-
// not the default world view. A
|
|
865
|
-
|
|
994
|
+
// not the default world view. A rebuilt map opens at the default view
|
|
995
|
+
// too, so it restores the camera the last map saved.
|
|
996
|
+
if (restoreCamera && app.camera) m.jumpTo(app.camera);
|
|
866
997
|
else if (app.cameraSeq === handledCameraSeq)
|
|
867
998
|
app.requestFrame(app.activeCaseId ?? app.activeLocalId ?? 'all');
|
|
868
999
|
if (mapGen > 0) {
|
|
@@ -884,6 +1015,9 @@
|
|
|
884
1015
|
m.off('load', syncCues);
|
|
885
1016
|
m.off('move', syncCues);
|
|
886
1017
|
m.off('resize', syncCues);
|
|
1018
|
+
m.off('style.load', applyStyleExtras);
|
|
1019
|
+
detachFallback();
|
|
1020
|
+
detachReady?.();
|
|
887
1021
|
app.settleFrame();
|
|
888
1022
|
m.remove();
|
|
889
1023
|
map = null;
|
|
@@ -899,8 +1033,23 @@
|
|
|
899
1033
|
};
|
|
900
1034
|
}
|
|
901
1035
|
|
|
1036
|
+
// A projection change applies at once when the style is ready. While a
|
|
1037
|
+
// style is still loading (the first paint, or just after a basemap switch)
|
|
1038
|
+
// MapLibre throws, and the `style.load` handler above applies it instead.
|
|
1039
|
+
$effect(() => {
|
|
1040
|
+
const type = projection;
|
|
1041
|
+
const m = map;
|
|
1042
|
+
if (!m) return;
|
|
1043
|
+
try {
|
|
1044
|
+
m.setProjection({ type });
|
|
1045
|
+
} catch {
|
|
1046
|
+
// style not loaded yet; applyStyleExtras runs on style.load
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
|
|
902
1050
|
// Sync deck.gl layers with app state. New layer instances diff cheaply;
|
|
903
|
-
// updateTriggers tell deck.gl when accessor outputs changed.
|
|
1051
|
+
// updateTriggers tell deck.gl when accessor outputs changed. Host layers
|
|
1052
|
+
// (`extraLayers`) draw after tellegen's own at both exits.
|
|
904
1053
|
$effect(() => {
|
|
905
1054
|
if (!overlay || !layerCtors) return;
|
|
906
1055
|
const { PathLayer, ScatterplotLayer, IconLayer } = layerCtors;
|
|
@@ -953,12 +1102,12 @@
|
|
|
953
1102
|
getFillColor: (b) => {
|
|
954
1103
|
const value = values.get(b.id);
|
|
955
1104
|
return value === undefined
|
|
956
|
-
?
|
|
1105
|
+
? palette.grey
|
|
957
1106
|
: priceColor((value - domain.lo) / Math.max(domain.hi - domain.lo, 1e-12));
|
|
958
1107
|
},
|
|
959
1108
|
pickable: true,
|
|
960
1109
|
stroked: true,
|
|
961
|
-
getLineColor:
|
|
1110
|
+
getLineColor: palette.outline,
|
|
962
1111
|
lineWidthUnits: 'pixels',
|
|
963
1112
|
onClick: (info: PickingInfo) => {
|
|
964
1113
|
const bus = info.object as NetworkBus | undefined;
|
|
@@ -967,10 +1116,10 @@
|
|
|
967
1116
|
app.selectedBus = bus.id;
|
|
968
1117
|
return true;
|
|
969
1118
|
},
|
|
970
|
-
updateTriggers: { getFillColor: [study, mode] }
|
|
1119
|
+
updateTriggers: { getFillColor: [study, mode, theme] }
|
|
971
1120
|
})
|
|
972
1121
|
);
|
|
973
|
-
overlay.setProps({ layers });
|
|
1122
|
+
overlay.setProps({ layers: [...layers, ...extraLayers] });
|
|
974
1123
|
return;
|
|
975
1124
|
}
|
|
976
1125
|
for (const c of app.cases) {
|
|
@@ -998,7 +1147,7 @@
|
|
|
998
1147
|
miterLimit: 2,
|
|
999
1148
|
pickable: true,
|
|
1000
1149
|
autoHighlight: true,
|
|
1001
|
-
highlightColor:
|
|
1150
|
+
highlightColor: palette.highlight,
|
|
1002
1151
|
onClick: (info: PickingInfo) => {
|
|
1003
1152
|
const branch = info.object as NetworkBranch | undefined;
|
|
1004
1153
|
if (!branch || !onbranchclick) return false;
|
|
@@ -1021,13 +1170,13 @@
|
|
|
1021
1170
|
billboard: true,
|
|
1022
1171
|
getLineColor: (b) =>
|
|
1023
1172
|
c.id === app.activeCaseId && b.id === app.selectedBus
|
|
1024
|
-
?
|
|
1025
|
-
:
|
|
1173
|
+
? palette.selected
|
|
1174
|
+
: palette.outline,
|
|
1026
1175
|
getLineWidth: (b) => (c.id === app.activeCaseId && b.id === app.selectedBus ? 2.5 : 1),
|
|
1027
1176
|
lineWidthUnits: 'pixels',
|
|
1028
1177
|
pickable: true,
|
|
1029
1178
|
autoHighlight: true,
|
|
1030
|
-
highlightColor:
|
|
1179
|
+
highlightColor: palette.highlightSoft,
|
|
1031
1180
|
onClick: (info: PickingInfo) => {
|
|
1032
1181
|
const bus = info.object as NetworkBus | undefined;
|
|
1033
1182
|
if (!bus) return false;
|
|
@@ -1036,7 +1185,7 @@
|
|
|
1036
1185
|
},
|
|
1037
1186
|
updateTriggers: {
|
|
1038
1187
|
getFillColor: [display],
|
|
1039
|
-
getLineColor: [app.selectedBus, app.activeCaseId],
|
|
1188
|
+
getLineColor: [app.selectedBus, app.activeCaseId, theme],
|
|
1040
1189
|
getLineWidth: [app.selectedBus, app.activeCaseId]
|
|
1041
1190
|
}
|
|
1042
1191
|
})
|
|
@@ -1058,7 +1207,7 @@
|
|
|
1058
1207
|
? [47, 111, 187, 255]
|
|
1059
1208
|
: d
|
|
1060
1209
|
? branchColor(d.loading.get(b.id) ?? 0, b.status === 1)
|
|
1061
|
-
:
|
|
1210
|
+
: palette.greyLine,
|
|
1062
1211
|
getWidth: (b) =>
|
|
1063
1212
|
c.id === app.activeLocalId && b.id === app.selectedBranch
|
|
1064
1213
|
? Math.max((d ? branchWidth(d.loading.get(b.id) ?? 0) : 1.5) + 2, 4.5)
|
|
@@ -1072,7 +1221,7 @@
|
|
|
1072
1221
|
miterLimit: 2,
|
|
1073
1222
|
pickable: true,
|
|
1074
1223
|
autoHighlight: true,
|
|
1075
|
-
highlightColor:
|
|
1224
|
+
highlightColor: palette.highlight,
|
|
1076
1225
|
onClick: (info: PickingInfo) => {
|
|
1077
1226
|
const branch = info.object as NetworkBranch | undefined;
|
|
1078
1227
|
if (!branch || !onlocalbranchclick) return false;
|
|
@@ -1080,7 +1229,7 @@
|
|
|
1080
1229
|
return true;
|
|
1081
1230
|
},
|
|
1082
1231
|
updateTriggers: {
|
|
1083
|
-
getColor: [display, app.selectedBranch, app.activeLocalId],
|
|
1232
|
+
getColor: [display, app.selectedBranch, app.activeLocalId, theme],
|
|
1084
1233
|
getWidth: [display, app.selectedBranch, app.activeLocalId]
|
|
1085
1234
|
}
|
|
1086
1235
|
}),
|
|
@@ -1090,18 +1239,18 @@
|
|
|
1090
1239
|
getPosition: (b) => [b.lon, b.lat],
|
|
1091
1240
|
getRadius: (b) => busRadius(Math.max(b.demand_mw, b.gen_mw)),
|
|
1092
1241
|
radiusUnits: 'pixels',
|
|
1093
|
-
getFillColor: d ? busFill(c.id) :
|
|
1242
|
+
getFillColor: d ? busFill(c.id) : palette.grey,
|
|
1094
1243
|
stroked: true,
|
|
1095
1244
|
billboard: true,
|
|
1096
1245
|
getLineColor: (b) =>
|
|
1097
1246
|
c.id === app.activeLocalId && b.id === app.selectedBus
|
|
1098
|
-
?
|
|
1099
|
-
:
|
|
1247
|
+
? palette.selected
|
|
1248
|
+
: palette.outline,
|
|
1100
1249
|
getLineWidth: (b) => (c.id === app.activeLocalId && b.id === app.selectedBus ? 2.5 : 1),
|
|
1101
1250
|
lineWidthUnits: 'pixels',
|
|
1102
1251
|
pickable: true,
|
|
1103
1252
|
autoHighlight: true,
|
|
1104
|
-
highlightColor:
|
|
1253
|
+
highlightColor: palette.highlightSoft,
|
|
1105
1254
|
onClick: (info: PickingInfo) => {
|
|
1106
1255
|
const bus = info.object as NetworkBus | undefined;
|
|
1107
1256
|
if (!bus) return false;
|
|
@@ -1110,7 +1259,7 @@
|
|
|
1110
1259
|
},
|
|
1111
1260
|
updateTriggers: {
|
|
1112
1261
|
getFillColor: [display],
|
|
1113
|
-
getLineColor: [app.selectedBus, app.activeLocalId],
|
|
1262
|
+
getLineColor: [app.selectedBus, app.activeLocalId, theme],
|
|
1114
1263
|
getLineWidth: [app.selectedBus, app.activeLocalId]
|
|
1115
1264
|
}
|
|
1116
1265
|
})
|
|
@@ -1130,15 +1279,15 @@
|
|
|
1130
1279
|
getRadius: 5,
|
|
1131
1280
|
radiusUnits: 'pixels',
|
|
1132
1281
|
radiusMinPixels: 3,
|
|
1133
|
-
getFillColor:
|
|
1282
|
+
getFillColor: palette.substation,
|
|
1134
1283
|
stroked: true,
|
|
1135
1284
|
billboard: true,
|
|
1136
|
-
getLineColor:
|
|
1285
|
+
getLineColor: palette.substationLine,
|
|
1137
1286
|
getLineWidth: 1,
|
|
1138
1287
|
lineWidthUnits: 'pixels',
|
|
1139
1288
|
pickable: true,
|
|
1140
1289
|
autoHighlight: true,
|
|
1141
|
-
highlightColor:
|
|
1290
|
+
highlightColor: palette.highlightSoft
|
|
1142
1291
|
})
|
|
1143
1292
|
);
|
|
1144
1293
|
}
|
|
@@ -1167,7 +1316,7 @@
|
|
|
1167
1316
|
miterLimit: 2,
|
|
1168
1317
|
pickable: true,
|
|
1169
1318
|
autoHighlight: true,
|
|
1170
|
-
highlightColor:
|
|
1319
|
+
highlightColor: palette.highlight,
|
|
1171
1320
|
onClick: (info: PickingInfo) => {
|
|
1172
1321
|
const e = info.object as PlacedMultiEdge | undefined;
|
|
1173
1322
|
if (!e || !onmultiedgeclick) return false;
|
|
@@ -1210,15 +1359,15 @@
|
|
|
1210
1359
|
billboard: true,
|
|
1211
1360
|
getLineColor: (b) =>
|
|
1212
1361
|
selectedId === b.id
|
|
1213
|
-
?
|
|
1362
|
+
? palette.selected
|
|
1214
1363
|
: b.has_source
|
|
1215
1364
|
? [63, 111, 187, 220]
|
|
1216
|
-
:
|
|
1365
|
+
: palette.outlineStrong,
|
|
1217
1366
|
getLineWidth: (b) => (selectedId === b.id ? 2.5 : b.has_source ? 2 : 1),
|
|
1218
1367
|
lineWidthUnits: 'pixels',
|
|
1219
1368
|
pickable: true,
|
|
1220
1369
|
autoHighlight: true,
|
|
1221
|
-
highlightColor:
|
|
1370
|
+
highlightColor: palette.highlightSoft,
|
|
1222
1371
|
onClick: (info: PickingInfo) => {
|
|
1223
1372
|
const bus = info.object as PlacedMultiBus | undefined;
|
|
1224
1373
|
if (!bus) return false;
|
|
@@ -1226,13 +1375,13 @@
|
|
|
1226
1375
|
return true;
|
|
1227
1376
|
},
|
|
1228
1377
|
updateTriggers: {
|
|
1229
|
-
getLineColor: [selectedId],
|
|
1378
|
+
getLineColor: [selectedId, theme],
|
|
1230
1379
|
getLineWidth: [selectedId]
|
|
1231
1380
|
}
|
|
1232
1381
|
})
|
|
1233
1382
|
);
|
|
1234
1383
|
}
|
|
1235
|
-
overlay.setProps({ layers });
|
|
1384
|
+
overlay.setProps({ layers: [...layers, ...extraLayers] });
|
|
1236
1385
|
});
|
|
1237
1386
|
|
|
1238
1387
|
$effect(() => {
|
|
@@ -1438,16 +1587,21 @@
|
|
|
1438
1587
|
}
|
|
1439
1588
|
|
|
1440
1589
|
.multi-detail :global(.mc-leader) {
|
|
1441
|
-
stroke:
|
|
1590
|
+
stroke: rgb(var(--ink-rgb) / 0.35);
|
|
1442
1591
|
stroke-width: 1;
|
|
1443
1592
|
stroke-dasharray: 2 2;
|
|
1444
1593
|
}
|
|
1445
1594
|
|
|
1446
1595
|
.multi-detail :global(.mc-ground line) {
|
|
1447
|
-
stroke:
|
|
1596
|
+
stroke: var(--mc-ground-stroke);
|
|
1448
1597
|
stroke-width: 1.3;
|
|
1449
1598
|
}
|
|
1450
1599
|
|
|
1600
|
+
.multi-detail :global(.mc-terminal) {
|
|
1601
|
+
stroke: var(--ink);
|
|
1602
|
+
stroke-width: 1.2;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1451
1605
|
.multi-detail :global(.mc-term) {
|
|
1452
1606
|
fill: var(--ink, #20242b);
|
|
1453
1607
|
font-family: var(--font-mono);
|
|
@@ -1568,7 +1722,7 @@
|
|
|
1568
1722
|
color: var(--text-accent);
|
|
1569
1723
|
font-size: 11px;
|
|
1570
1724
|
cursor: pointer;
|
|
1571
|
-
box-shadow:
|
|
1725
|
+
box-shadow: var(--elev-cue);
|
|
1572
1726
|
}
|
|
1573
1727
|
|
|
1574
1728
|
@media (max-width: 420px) {
|
|
@@ -1607,7 +1761,7 @@
|
|
|
1607
1761
|
min-width: 0;
|
|
1608
1762
|
max-width: calc(100vw - 96px);
|
|
1609
1763
|
box-sizing: border-box;
|
|
1610
|
-
background:
|
|
1764
|
+
background: rgb(var(--paper-rgb) / 0.75);
|
|
1611
1765
|
font-family: var(--font-mono);
|
|
1612
1766
|
font-size: 10px;
|
|
1613
1767
|
white-space: nowrap;
|
|
@@ -1648,7 +1802,7 @@
|
|
|
1648
1802
|
line-height: 1.5;
|
|
1649
1803
|
padding: 8px 10px !important;
|
|
1650
1804
|
border-radius: 2px;
|
|
1651
|
-
box-shadow:
|
|
1805
|
+
box-shadow: var(--elev-tooltip);
|
|
1652
1806
|
}
|
|
1653
1807
|
|
|
1654
1808
|
@media (max-width: 760px) {
|
|
@@ -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>;
|
|
@@ -16,8 +16,17 @@
|
|
|
16
16
|
children?: Snippet;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
let {
|
|
20
|
-
|
|
19
|
+
let {
|
|
20
|
+
apiBase,
|
|
21
|
+
loadDefaultCases,
|
|
22
|
+
docsHref,
|
|
23
|
+
orgHref,
|
|
24
|
+
orgLabel,
|
|
25
|
+
showFooter,
|
|
26
|
+
theme,
|
|
27
|
+
cartoApiKey,
|
|
28
|
+
children
|
|
29
|
+
}: Props = $props();
|
|
21
30
|
|
|
22
31
|
const config = untrack(() =>
|
|
23
32
|
resolveTellegenUiConfig({
|
|
@@ -26,17 +35,40 @@
|
|
|
26
35
|
docsHref,
|
|
27
36
|
orgHref,
|
|
28
37
|
orgLabel,
|
|
29
|
-
showFooter
|
|
38
|
+
showFooter,
|
|
39
|
+
theme,
|
|
40
|
+
cartoApiKey
|
|
30
41
|
})
|
|
31
42
|
);
|
|
32
43
|
const app = createAppState();
|
|
33
|
-
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;
|
|
34
51
|
|
|
35
52
|
setPanelLayout(new PanelLayout());
|
|
36
53
|
setNoticeCenter(new NoticeCenter());
|
|
37
54
|
setUiConfig(config);
|
|
38
55
|
setAppState(app);
|
|
39
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
|
+
});
|
|
40
72
|
</script>
|
|
41
73
|
|
|
42
74
|
{@render children?.()}
|