@tellegen/svelte 0.3.0 → 0.4.1

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 (71) hide show
  1. package/README.md +233 -0
  2. package/dist/Sparkline.svelte +2 -2
  3. package/dist/TellegenMap.svelte +239 -66
  4. package/dist/TellegenMap.svelte.d.ts +45 -0
  5. package/dist/TellegenProvider.svelte +36 -4
  6. package/dist/TellegenShell.svelte +14 -42
  7. package/dist/TellegenViewer.svelte +12 -2
  8. package/dist/api.d.ts +10 -2
  9. package/dist/api.js +2 -0
  10. package/dist/basemap.d.ts +192 -0
  11. package/dist/basemap.js +259 -0
  12. package/dist/colors.d.ts +55 -1
  13. package/dist/colors.js +89 -0
  14. package/dist/components/AppFooter.svelte +3 -2
  15. package/dist/components/AppHeader.svelte +21 -8
  16. package/dist/components/BindingLines.svelte +1 -1
  17. package/dist/components/BusPicker.svelte +3 -3
  18. package/dist/components/ControlPanel.svelte +4 -2
  19. package/dist/components/DemandSlider.svelte +16 -7
  20. package/dist/components/DiagramCanvas.svelte +17 -5
  21. package/dist/components/DisplayControls.svelte +5 -1
  22. package/dist/components/DropZone.svelte +1 -1
  23. package/dist/components/FormulationSelector.svelte +1 -1
  24. package/dist/components/LocalCaseDetails.svelte +6 -3
  25. package/dist/components/McResults.svelte +98 -35
  26. package/dist/components/McResults.svelte.d.ts +8 -2
  27. package/dist/components/ModelDetails.svelte +5 -5
  28. package/dist/components/MulticonductorDetails.svelte +9 -9
  29. package/dist/components/NotificationCenter.svelte +4 -2
  30. package/dist/components/PanelCard.svelte +72 -7
  31. package/dist/components/PanelHost.svelte +59 -34
  32. package/dist/components/PanelHost.svelte.d.ts +4 -1
  33. package/dist/components/PlacementCue.svelte +1 -1
  34. package/dist/components/PowerFlowDetails.svelte +1 -1
  35. package/dist/components/RatingSlider.svelte +14 -5
  36. package/dist/components/RestoreDefaultsButton.svelte +1 -1
  37. package/dist/components/SavedNetworkDetails.svelte +3 -3
  38. package/dist/components/SensitivityReadout.svelte +2 -2
  39. package/dist/components/ShellEnvironment.svelte +51 -0
  40. package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
  41. package/dist/components/SizeLegend.svelte +2 -2
  42. package/dist/components/SolveCard.svelte +3 -3
  43. package/dist/components/TopMovers.svelte +1 -1
  44. package/dist/components.d.ts +3 -0
  45. package/dist/components.js +3 -0
  46. package/dist/context.svelte.d.ts +8 -1
  47. package/dist/context.svelte.js +8 -3
  48. package/dist/controller.svelte.d.ts +65 -5
  49. package/dist/controller.svelte.js +266 -78
  50. package/dist/display.d.ts +3 -2
  51. package/dist/display.js +22 -4
  52. package/dist/format.d.ts +9 -0
  53. package/dist/format.js +13 -1
  54. package/dist/index.d.ts +6 -2
  55. package/dist/index.js +2 -0
  56. package/dist/map.d.ts +4 -2
  57. package/dist/map.js +2 -1
  58. package/dist/maplibre-worker.d.ts +5 -0
  59. package/dist/maplibre-worker.dev.d.ts +11 -0
  60. package/dist/maplibre-worker.dev.js +15 -0
  61. package/dist/maplibre-worker.js +8 -0
  62. package/dist/notices.svelte.js +5 -1
  63. package/dist/panels.svelte.d.ts +6 -0
  64. package/dist/panels.svelte.js +31 -1
  65. package/dist/slider.d.ts +12 -0
  66. package/dist/slider.js +31 -0
  67. package/dist/state.svelte.d.ts +25 -3
  68. package/dist/state.svelte.js +13 -0
  69. package/dist/styles.css +59 -6
  70. package/dist/types.d.ts +1 -1
  71. package/package.json +13 -8
@@ -1,16 +1,19 @@
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,
13
+ busMarkerRadius,
12
14
  busNeutral,
13
- busRadius,
15
+ busRadiusFor,
16
+ demandColor,
14
17
  priceColor,
15
18
  scalarDomain,
16
19
  sensColor,
@@ -18,8 +21,23 @@
18
21
  sensitivityDomain,
19
22
  type SensitivityDomain
20
23
  } from './colors.js';
24
+ import {
25
+ attachBasemapFallback,
26
+ isOpenFreeMapStyle,
27
+ openFreeMapEdits,
28
+ resolveBasemap,
29
+ type BasemapFallbackReason,
30
+ type BasemapStyle,
31
+ type MapProjection,
32
+ type MapTheme
33
+ } from './basemap.js';
21
34
  import { caseDeltas, caseRatings, displayMetaFor, displaySeriesFor } from './display.js';
22
- import { CaseState, type DisplayMode, type LocalCase } from './state.svelte.js';
35
+ import {
36
+ CaseState,
37
+ type CameraSnapshot,
38
+ type DisplayMode,
39
+ type LocalCase
40
+ } from './state.svelte.js';
23
41
  import {
24
42
  attachmentColor,
25
43
  edgeColor,
@@ -40,6 +58,8 @@
40
58
  import type { RGBA } from './colors.js';
41
59
  import DiagramCanvas from './components/DiagramCanvas.svelte';
42
60
 
61
+ const DEFAULT_CAMERA: CameraSnapshot = { center: [-85, 36], zoom: 4.5, bearing: 0, pitch: 0 };
62
+
43
63
  let {
44
64
  onbusclick,
45
65
  onlocalbusclick,
@@ -48,7 +68,19 @@
48
68
  onmultibusclick,
49
69
  onmultiedgeclick,
50
70
  onplacecase,
51
- onmapclick
71
+ onmapclick,
72
+ mapStyle,
73
+ fallbackMapStyle,
74
+ theme = 'light',
75
+ projection = 'mercator',
76
+ initialCamera = DEFAULT_CAMERA,
77
+ hideBasemapLabels = true,
78
+ cartoApiKey,
79
+ extraLayers = [],
80
+ attribution = 'compact',
81
+ onMapReady,
82
+ onBasemapFallback,
83
+ onBasemapError
52
84
  }: {
53
85
  onbusclick: (caseId: string, busId: number) => void;
54
86
  onlocalbusclick: (caseId: string, busId: number) => void;
@@ -58,6 +90,46 @@
58
90
  onmultiedgeclick?: (caseId: string, edgeId: string) => void;
59
91
  onplacecase: (lon: number, lat: number) => void;
60
92
  onmapclick: () => void;
93
+ /** Primary basemap: a style URL or inline style. Default: the CARTO style
94
+ * for `theme` when `cartoApiKey` is set, else the OpenFreeMap style.
95
+ * Changing it (or `fallbackMapStyle`, `theme`, `hideBasemapLabels`,
96
+ * `cartoApiKey`, `attribution`) rebuilds the map in place and keeps the
97
+ * camera. */
98
+ mapStyle?: BasemapStyle;
99
+ /** Basemap used when the primary cannot be shown; `null` disables the
100
+ * switch. Default: whichever of the CARTO and OpenFreeMap styles is not
101
+ * the default primary. */
102
+ fallbackMapStyle?: BasemapStyle | null;
103
+ /** Picks the default basemaps and the overlay palette (bus outlines,
104
+ * hover highlight, unsolved grey). */
105
+ theme?: MapTheme;
106
+ /** Re-applied after every `style.load`, so a basemap switch keeps it. */
107
+ projection?: MapProjection;
108
+ /** Camera for the first paint; read once per mount. */
109
+ initialCamera?: CameraSnapshot;
110
+ /** Hide the basemap's `symbol` layers after every `style.load`. */
111
+ hideBasemapLabels?: boolean;
112
+ /** CARTO basemap key (free for non-commercial use from
113
+ * carto.com/basemaps/apikey; restrict it to your hosts). With one, the
114
+ * CARTO style leads and OpenFreeMap is the fallback; without one, the
115
+ * order flips. Trimmed; a blank key counts as none. */
116
+ cartoApiKey?: string;
117
+ /** deck.gl layers drawn after tellegen's own, in the same overlay. */
118
+ extraLayers?: Layer[];
119
+ /** Attribution control mode. There is no off switch: tile licences
120
+ * require the credit on screen. */
121
+ attribution?: 'compact' | 'full';
122
+ /** Called once the map and overlay exist, with an optional cleanup. It
123
+ * fires again after a context-loss or basemap remount, after the previous
124
+ * cleanup ran. */
125
+ onMapReady?: (map: MapLibreMap, overlay: MapLibreOverlay) => void | (() => void);
126
+ /** The primary basemap failed and the fallback was requested. The
127
+ * reason's `from` and `to` name the providers (`'carto'`,
128
+ * `'openfreemap'` or `'custom'`). */
129
+ onBasemapFallback?: (reason: BasemapFallbackReason) => void;
130
+ /** No basemap can be shown; the network layers still draw. The reason's
131
+ * `from` names the provider that failed last, and `to` is `null`. */
132
+ onBasemapError?: (reason: BasemapFallbackReason) => void;
61
133
  } = $props();
62
134
 
63
135
  const app = getAppState();
@@ -104,7 +176,20 @@
104
176
  else onbranchclick?.(diagramCaseId, branchId);
105
177
  }
106
178
 
107
- const STYLE = 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json';
179
+ const basemap = $derived(
180
+ resolveBasemap({ mapStyle, fallbackMapStyle, theme, hideBasemapLabels, cartoApiKey })
181
+ );
182
+ // Read as separate values, not one object, so a theme change that resolves
183
+ // to the same styles does not rebuild the map.
184
+ const basemapPrimary = $derived(basemap.primary);
185
+ const basemapFallback = $derived(basemap.fallback);
186
+ const basemapHideLabels = $derived(basemap.hideLabels);
187
+ const basemapPrimaryProvider = $derived(basemap.primaryProvider);
188
+ const basemapFallbackProvider = $derived(basemap.fallbackProvider);
189
+ const palette = $derived(MAP_PALETTE[theme]);
190
+ // Set once a map has existed in this component instance: a rebuild (context
191
+ // loss or basemap change) restores the camera instead of reframing.
192
+ let hadMap = false;
108
193
  const CUE_MARGIN_PX = 64;
109
194
  const BRANCH_FOCUS_MS = 850;
110
195
  const BRANCH_FOCUS_MAX_ZOOM = 9.5;
@@ -308,6 +393,8 @@
308
393
  if (d.previewDomain.flat) return sensFlatColor(d.previewDomain);
309
394
  return sensColor((d.preview.get(bus.id) ?? 0) / d.previewDomain.scale);
310
395
  }
396
+ if (d.scalarMode === 'demand')
397
+ return demandColor(((d.scalar.get(bus.id) ?? 0) - d.scalarLo) / (d.scalarHi - d.scalarLo));
311
398
  const mid = (d.scalarLo + d.scalarHi) / 2;
312
399
  return priceColor(((d.scalar.get(bus.id) ?? mid) - d.scalarLo) / (d.scalarHi - d.scalarLo));
313
400
  };
@@ -508,9 +595,9 @@
508
595
  terminals.forEach((t, k) => {
509
596
  const cy = top + k * STACK_DY;
510
597
  const grounded = bus.grounded.includes(t);
511
- const fill = isPhaseTerminal(t) ? col(phaseColor(t)) : col([120, 114, 102, 255]);
598
+ const fill = isPhaseTerminal(t) ? col(phaseColor(t)) : col(palette.neutral);
512
599
  if (grounded) stack += groundGlyph(sx - 14, cy);
513
- stack += `<circle cx="${sx.toFixed(1)}" cy="${cy.toFixed(1)}" r="6" fill="${fill}" stroke="#20242b" stroke-width="1.2"/>`;
600
+ stack += `<circle class="mc-terminal" cx="${sx.toFixed(1)}" cy="${cy.toFixed(1)}" r="6" fill="${fill}"/>`;
514
601
  stack += `<text class="mc-term" x="${(sx + 11).toFixed(1)}" y="${(cy + 3.5).toFixed(1)}">${esc(t)}</text>`;
515
602
  // Attachment squares for elements landing on this terminal.
516
603
  const kinds = [...new Set((bus.terminalAttachments[t] ?? []).map((a) => a.kind))];
@@ -698,8 +785,9 @@
698
785
  : `<br>&Delta; LMP ${preview >= 0 ? '+' : ''}${preview.toExponential(2)} objective units/MW`;
699
786
  const localRow =
700
787
  c instanceof CaseState ? '' : '<br><span style="opacity:0.6">local file</span>';
788
+ // Demand already has its own row below, so the demand colouring shows LMP here alone.
701
789
  const scalarRow =
702
- !d || d.scalarMode === 'price'
790
+ !d || d.scalarMode === 'price' || d.scalarMode === 'demand'
703
791
  ? `LMP ${price === undefined ? '&mdash;' : fmt.format(price)} objective units/MW`
704
792
  : `${d.scalarLabel} ${
705
793
  scalar === undefined ? '&mdash;' : displayFmt(d.scalarMode, scalar)
@@ -712,14 +800,17 @@
712
800
  }
713
801
 
714
802
  async function loadMapModules() {
715
- const [maplibre, deckMap, layers, worker] = await Promise.all([
803
+ const [maplibre, deckMap, layers, workerUrl] = await Promise.all([
716
804
  import('maplibre-gl'),
717
805
  import('@deck.gl/maplibre'),
718
806
  import('@deck.gl/layers'),
719
- import('maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url')
807
+ // A package export, not a relative import: its `development`
808
+ // condition keeps the worker's `?worker&url` import out of the dev
809
+ // server's pre-bundle (see maplibre-worker.dev.ts).
810
+ import('@tellegen/svelte/maplibre-worker').then((m) => m.maplibreWorkerUrl())
720
811
  ]);
721
812
  // Bundle the worker with its shared imports and configure it before creating a map.
722
- maplibre.setWorkerUrl(worker.default);
813
+ maplibre.setWorkerUrl(workerUrl);
723
814
  return {
724
815
  maplibregl: maplibre,
725
816
  MapLibreOverlay: deckMap.MapLibreOverlay,
@@ -759,6 +850,20 @@
759
850
  }
760
851
 
761
852
  function initMap(container: HTMLDivElement) {
853
+ // Attachments re-run when state they read changes, so reading the
854
+ // resolved basemap and attribution mode here is what rebuilds the map
855
+ // when those props change. The camera is read once: it is initial.
856
+ const spec = {
857
+ primary: basemapPrimary,
858
+ fallback: basemapFallback,
859
+ hideLabels: basemapHideLabels,
860
+ primaryProvider: basemapPrimaryProvider,
861
+ fallbackProvider: basemapFallbackProvider
862
+ };
863
+ const compactAttribution = attribution === 'compact';
864
+ const camera = untrack(() => initialCamera);
865
+ const restoreCamera = hadMap;
866
+ hadMap = true;
762
867
  let cleanup = () => {};
763
868
  let cancelled = false;
764
869
  void loadMapModules()
@@ -767,11 +872,40 @@
767
872
  layerCtors = { PathLayer, ScatterplotLayer, IconLayer };
768
873
  const m = new maplibregl.Map({
769
874
  container,
770
- style: STYLE,
771
- center: [-85, 36],
772
- zoom: 4.5,
875
+ style: spec.primary,
876
+ center: camera.center,
877
+ zoom: camera.zoom,
878
+ bearing: camera.bearing,
879
+ pitch: camera.pitch,
773
880
  canvasContextAttributes: { antialias: true },
774
- attributionControl: { compact: true }
881
+ attributionControl: { compact: compactAttribution }
882
+ });
883
+ // `setStyle` (the fallback path included) drops the projection and
884
+ // shows every layer again, so both are re-applied on each load.
885
+ const applyStyleExtras = () => {
886
+ m.setProjection({ type: untrack(() => projection) });
887
+ const style = m.getStyle();
888
+ if (spec.hideLabels) {
889
+ for (const layer of style.layers) {
890
+ if (layer.type === 'symbol') m.setLayoutProperty(layer.id, 'visibility', 'none');
891
+ }
892
+ }
893
+ // OpenFreeMap renders as the primary or as the fallback; either
894
+ // way it is tuned towards CARTO's quieter look.
895
+ if (!isOpenFreeMapStyle(style)) return;
896
+ const edits = openFreeMapEdits(
897
+ style,
898
+ untrack(() => theme)
899
+ );
900
+ for (const edit of edits) {
901
+ if (edit.kind === 'paint') m.setPaintProperty(edit.layer, edit.property, edit.value);
902
+ else m.setLayerZoomRange(edit.layer, edit.minzoom, edit.maxzoom);
903
+ }
904
+ };
905
+ m.on('style.load', applyStyleExtras);
906
+ const detachFallback = attachBasemapFallback(m, spec, {
907
+ onFallback: (reason) => onBasemapFallback?.(reason),
908
+ onFailure: (reason) => onBasemapError?.(reason)
775
909
  });
776
910
  const o = buildOverlay(MapLibreOverlay);
777
911
  m.addControl(o);
@@ -858,11 +992,13 @@
858
992
 
859
993
  map = m;
860
994
  overlay = o;
995
+ const detachReady = onMapReady?.(m, o);
861
996
  // Frame the active grid once the map is ready. The initial frame request
862
997
  // (from the case load) can fire before the async map import finishes and
863
998
  // be lost; re-issuing it here ensures the first view is the active case,
864
- // not the default world view. A remount opens at the default view too.
865
- if (mapGen > 0 && app.camera) m.jumpTo(app.camera);
999
+ // not the default world view. A rebuilt map opens at the default view
1000
+ // too, so it restores the camera the last map saved.
1001
+ if (restoreCamera && app.camera) m.jumpTo(app.camera);
866
1002
  else if (app.cameraSeq === handledCameraSeq)
867
1003
  app.requestFrame(app.activeCaseId ?? app.activeLocalId ?? 'all');
868
1004
  if (mapGen > 0) {
@@ -884,6 +1020,9 @@
884
1020
  m.off('load', syncCues);
885
1021
  m.off('move', syncCues);
886
1022
  m.off('resize', syncCues);
1023
+ m.off('style.load', applyStyleExtras);
1024
+ detachFallback();
1025
+ detachReady?.();
887
1026
  app.settleFrame();
888
1027
  m.remove();
889
1028
  map = null;
@@ -899,26 +1038,44 @@
899
1038
  };
900
1039
  }
901
1040
 
1041
+ // A projection change applies at once when the style is ready. While a
1042
+ // style is still loading (the first paint, or just after a basemap switch)
1043
+ // MapLibre throws, and the `style.load` handler above applies it instead.
1044
+ $effect(() => {
1045
+ const type = projection;
1046
+ const m = map;
1047
+ if (!m) return;
1048
+ try {
1049
+ m.setProjection({ type });
1050
+ } catch {
1051
+ // style not loaded yet; applyStyleExtras runs on style.load
1052
+ }
1053
+ });
1054
+
902
1055
  // Sync deck.gl layers with app state. New layer instances diff cheaply;
903
- // updateTriggers tell deck.gl when accessor outputs changed.
1056
+ // updateTriggers tell deck.gl when accessor outputs changed. Host layers
1057
+ // (`extraLayers`) draw after tellegen's own at both exits.
904
1058
  $effect(() => {
905
1059
  if (!overlay || !layerCtors) return;
906
1060
  const { PathLayer, ScatterplotLayer, IconLayer } = layerCtors;
907
1061
  const layers: Layer[] = [];
1062
+ const marker = busMarkerRadius(app.busSizing);
908
1063
  const study = app.studyView;
909
1064
  if (study) {
910
1065
  const s = study.solution;
911
1066
  const mode = app.displayMode;
912
1067
  const series =
913
- mode === 'angle'
914
- ? (s?.va ?? [])
915
- : mode === 'voltage'
916
- ? (s?.vm ??
917
- (s?.w ?? []).map((v) => ({
918
- ...v,
919
- value: Math.sqrt(Math.max(0, v.value))
920
- })))
921
- : (s?.lmp ?? []);
1068
+ mode === 'demand'
1069
+ ? study.network.buses.map((b) => ({ bus: b.id, value: b.demand_mw }))
1070
+ : mode === 'angle'
1071
+ ? (s?.va ?? [])
1072
+ : mode === 'voltage'
1073
+ ? (s?.vm ??
1074
+ (s?.w ?? []).map((v) => ({
1075
+ ...v,
1076
+ value: Math.sqrt(Math.max(0, v.value))
1077
+ })))
1078
+ : (s?.lmp ?? []);
922
1079
  const values = new Map(series.map((v) => [v.bus, v.value]));
923
1080
  const domain = scalarDomain(
924
1081
  mode,
@@ -948,17 +1105,21 @@
948
1105
  id: 'study-buses',
949
1106
  data: study.network.buses,
950
1107
  getPosition: (b) => [b.lon, b.lat],
951
- getRadius: (b) => busRadius(Math.max(b.demand_mw, b.gen_mw)),
952
- radiusUnits: 'pixels',
1108
+ getRadius: (b) => marker.radius(Math.max(b.demand_mw, b.gen_mw)),
1109
+ radiusUnits: marker.radiusUnits,
1110
+ radiusMinPixels: marker.radiusMinPixels,
1111
+ radiusMaxPixels: marker.radiusMaxPixels,
953
1112
  getFillColor: (b) => {
954
1113
  const value = values.get(b.id);
955
1114
  return value === undefined
956
- ? [110, 115, 120, 200]
957
- : priceColor((value - domain.lo) / Math.max(domain.hi - domain.lo, 1e-12));
1115
+ ? palette.grey
1116
+ : (mode === 'demand' ? demandColor : priceColor)(
1117
+ (value - domain.lo) / Math.max(domain.hi - domain.lo, 1e-12)
1118
+ );
958
1119
  },
959
1120
  pickable: true,
960
1121
  stroked: true,
961
- getLineColor: [46, 42, 34, 110],
1122
+ getLineColor: palette.outline,
962
1123
  lineWidthUnits: 'pixels',
963
1124
  onClick: (info: PickingInfo) => {
964
1125
  const bus = info.object as NetworkBus | undefined;
@@ -967,10 +1128,10 @@
967
1128
  app.selectedBus = bus.id;
968
1129
  return true;
969
1130
  },
970
- updateTriggers: { getFillColor: [study, mode] }
1131
+ updateTriggers: { getFillColor: [study, mode, theme], getRadius: [app.busSizing] }
971
1132
  })
972
1133
  );
973
- overlay.setProps({ layers });
1134
+ overlay.setProps({ layers: [...layers, ...extraLayers] });
974
1135
  return;
975
1136
  }
976
1137
  for (const c of app.cases) {
@@ -998,7 +1159,7 @@
998
1159
  miterLimit: 2,
999
1160
  pickable: true,
1000
1161
  autoHighlight: true,
1001
- highlightColor: [32, 36, 43, 90],
1162
+ highlightColor: palette.highlight,
1002
1163
  onClick: (info: PickingInfo) => {
1003
1164
  const branch = info.object as NetworkBranch | undefined;
1004
1165
  if (!branch || !onbranchclick) return false;
@@ -1014,20 +1175,22 @@
1014
1175
  id: `buses-${c.id}`,
1015
1176
  data: c.network.buses,
1016
1177
  getPosition: (b) => [b.lon, b.lat],
1017
- getRadius: (b) => busRadius(Math.max(b.demand_mw, b.gen_mw)),
1018
- radiusUnits: 'pixels',
1178
+ getRadius: (b) => marker.radius(Math.max(b.demand_mw, b.gen_mw)),
1179
+ radiusUnits: marker.radiusUnits,
1180
+ radiusMinPixels: marker.radiusMinPixels,
1181
+ radiusMaxPixels: marker.radiusMaxPixels,
1019
1182
  getFillColor: busFill(c.id),
1020
1183
  stroked: true,
1021
1184
  billboard: true,
1022
1185
  getLineColor: (b) =>
1023
1186
  c.id === app.activeCaseId && b.id === app.selectedBus
1024
- ? [32, 36, 43, 255]
1025
- : [46, 42, 34, 110],
1187
+ ? palette.selected
1188
+ : palette.outline,
1026
1189
  getLineWidth: (b) => (c.id === app.activeCaseId && b.id === app.selectedBus ? 2.5 : 1),
1027
1190
  lineWidthUnits: 'pixels',
1028
1191
  pickable: true,
1029
1192
  autoHighlight: true,
1030
- highlightColor: [32, 36, 43, 70],
1193
+ highlightColor: palette.highlightSoft,
1031
1194
  onClick: (info: PickingInfo) => {
1032
1195
  const bus = info.object as NetworkBus | undefined;
1033
1196
  if (!bus) return false;
@@ -1035,8 +1198,9 @@
1035
1198
  return true;
1036
1199
  },
1037
1200
  updateTriggers: {
1201
+ getRadius: [app.busSizing],
1038
1202
  getFillColor: [display],
1039
- getLineColor: [app.selectedBus, app.activeCaseId],
1203
+ getLineColor: [app.selectedBus, app.activeCaseId, theme],
1040
1204
  getLineWidth: [app.selectedBus, app.activeCaseId]
1041
1205
  }
1042
1206
  })
@@ -1058,7 +1222,7 @@
1058
1222
  ? [47, 111, 187, 255]
1059
1223
  : d
1060
1224
  ? branchColor(d.loading.get(b.id) ?? 0, b.status === 1)
1061
- : [138, 131, 117, 150],
1225
+ : palette.greyLine,
1062
1226
  getWidth: (b) =>
1063
1227
  c.id === app.activeLocalId && b.id === app.selectedBranch
1064
1228
  ? Math.max((d ? branchWidth(d.loading.get(b.id) ?? 0) : 1.5) + 2, 4.5)
@@ -1072,7 +1236,7 @@
1072
1236
  miterLimit: 2,
1073
1237
  pickable: true,
1074
1238
  autoHighlight: true,
1075
- highlightColor: [32, 36, 43, 90],
1239
+ highlightColor: palette.highlight,
1076
1240
  onClick: (info: PickingInfo) => {
1077
1241
  const branch = info.object as NetworkBranch | undefined;
1078
1242
  if (!branch || !onlocalbranchclick) return false;
@@ -1080,7 +1244,7 @@
1080
1244
  return true;
1081
1245
  },
1082
1246
  updateTriggers: {
1083
- getColor: [display, app.selectedBranch, app.activeLocalId],
1247
+ getColor: [display, app.selectedBranch, app.activeLocalId, theme],
1084
1248
  getWidth: [display, app.selectedBranch, app.activeLocalId]
1085
1249
  }
1086
1250
  }),
@@ -1088,20 +1252,22 @@
1088
1252
  id: `local-buses-${c.id}`,
1089
1253
  data: c.view.buses,
1090
1254
  getPosition: (b) => [b.lon, b.lat],
1091
- getRadius: (b) => busRadius(Math.max(b.demand_mw, b.gen_mw)),
1092
- radiusUnits: 'pixels',
1093
- getFillColor: d ? busFill(c.id) : [110, 115, 120, 200],
1255
+ getRadius: (b) => marker.radius(Math.max(b.demand_mw, b.gen_mw)),
1256
+ radiusUnits: marker.radiusUnits,
1257
+ radiusMinPixels: marker.radiusMinPixels,
1258
+ radiusMaxPixels: marker.radiusMaxPixels,
1259
+ getFillColor: d ? busFill(c.id) : palette.grey,
1094
1260
  stroked: true,
1095
1261
  billboard: true,
1096
1262
  getLineColor: (b) =>
1097
1263
  c.id === app.activeLocalId && b.id === app.selectedBus
1098
- ? [32, 36, 43, 255]
1099
- : [46, 42, 34, 110],
1264
+ ? palette.selected
1265
+ : palette.outline,
1100
1266
  getLineWidth: (b) => (c.id === app.activeLocalId && b.id === app.selectedBus ? 2.5 : 1),
1101
1267
  lineWidthUnits: 'pixels',
1102
1268
  pickable: true,
1103
1269
  autoHighlight: true,
1104
- highlightColor: [32, 36, 43, 70],
1270
+ highlightColor: palette.highlightSoft,
1105
1271
  onClick: (info: PickingInfo) => {
1106
1272
  const bus = info.object as NetworkBus | undefined;
1107
1273
  if (!bus) return false;
@@ -1109,8 +1275,9 @@
1109
1275
  return true;
1110
1276
  },
1111
1277
  updateTriggers: {
1278
+ getRadius: [app.busSizing],
1112
1279
  getFillColor: [display],
1113
- getLineColor: [app.selectedBus, app.activeLocalId],
1280
+ getLineColor: [app.selectedBus, app.activeLocalId, theme],
1114
1281
  getLineWidth: [app.selectedBus, app.activeLocalId]
1115
1282
  }
1116
1283
  })
@@ -1130,15 +1297,15 @@
1130
1297
  getRadius: 5,
1131
1298
  radiusUnits: 'pixels',
1132
1299
  radiusMinPixels: 3,
1133
- getFillColor: [70, 92, 124, 190],
1300
+ getFillColor: palette.substation,
1134
1301
  stroked: true,
1135
1302
  billboard: true,
1136
- getLineColor: [38, 52, 78, 220],
1303
+ getLineColor: palette.substationLine,
1137
1304
  getLineWidth: 1,
1138
1305
  lineWidthUnits: 'pixels',
1139
1306
  pickable: true,
1140
1307
  autoHighlight: true,
1141
- highlightColor: [32, 36, 43, 70]
1308
+ highlightColor: palette.highlightSoft
1142
1309
  })
1143
1310
  );
1144
1311
  }
@@ -1167,7 +1334,7 @@
1167
1334
  miterLimit: 2,
1168
1335
  pickable: true,
1169
1336
  autoHighlight: true,
1170
- highlightColor: [32, 36, 43, 90],
1337
+ highlightColor: palette.highlight,
1171
1338
  onClick: (info: PickingInfo) => {
1172
1339
  const e = info.object as PlacedMultiEdge | undefined;
1173
1340
  if (!e || !onmultiedgeclick) return false;
@@ -1202,7 +1369,7 @@
1202
1369
  id: `multi-buses-${c.id}`,
1203
1370
  data: c.view.buses,
1204
1371
  getPosition: (b) => [b.lon, b.lat],
1205
- getRadius: (b) => busRadius(Math.max(b.load_kw, b.gen_kw)),
1372
+ getRadius: (b) => busRadiusFor(app.busSizing, Math.max(b.load_kw, b.gen_kw)),
1206
1373
  radiusUnits: 'pixels',
1207
1374
  radiusMinPixels: 3,
1208
1375
  getFillColor: (b) => multiBusFill(b),
@@ -1210,15 +1377,15 @@
1210
1377
  billboard: true,
1211
1378
  getLineColor: (b) =>
1212
1379
  selectedId === b.id
1213
- ? [32, 36, 43, 255]
1380
+ ? palette.selected
1214
1381
  : b.has_source
1215
1382
  ? [63, 111, 187, 220]
1216
- : [46, 42, 34, 120],
1383
+ : palette.outlineStrong,
1217
1384
  getLineWidth: (b) => (selectedId === b.id ? 2.5 : b.has_source ? 2 : 1),
1218
1385
  lineWidthUnits: 'pixels',
1219
1386
  pickable: true,
1220
1387
  autoHighlight: true,
1221
- highlightColor: [32, 36, 43, 70],
1388
+ highlightColor: palette.highlightSoft,
1222
1389
  onClick: (info: PickingInfo) => {
1223
1390
  const bus = info.object as PlacedMultiBus | undefined;
1224
1391
  if (!bus) return false;
@@ -1226,13 +1393,14 @@
1226
1393
  return true;
1227
1394
  },
1228
1395
  updateTriggers: {
1229
- getLineColor: [selectedId],
1396
+ getRadius: [app.busSizing],
1397
+ getLineColor: [selectedId, theme],
1230
1398
  getLineWidth: [selectedId]
1231
1399
  }
1232
1400
  })
1233
1401
  );
1234
1402
  }
1235
- overlay.setProps({ layers });
1403
+ overlay.setProps({ layers: [...layers, ...extraLayers] });
1236
1404
  });
1237
1405
 
1238
1406
  $effect(() => {
@@ -1438,16 +1606,21 @@
1438
1606
  }
1439
1607
 
1440
1608
  .multi-detail :global(.mc-leader) {
1441
- stroke: rgba(32, 36, 43, 0.35);
1609
+ stroke: rgb(var(--ink-rgb) / 0.35);
1442
1610
  stroke-width: 1;
1443
1611
  stroke-dasharray: 2 2;
1444
1612
  }
1445
1613
 
1446
1614
  .multi-detail :global(.mc-ground line) {
1447
- stroke: #3a3a3a;
1615
+ stroke: var(--mc-ground-stroke);
1448
1616
  stroke-width: 1.3;
1449
1617
  }
1450
1618
 
1619
+ .multi-detail :global(.mc-terminal) {
1620
+ stroke: var(--ink);
1621
+ stroke-width: 1.2;
1622
+ }
1623
+
1451
1624
  .multi-detail :global(.mc-term) {
1452
1625
  fill: var(--ink, #20242b);
1453
1626
  font-family: var(--font-mono);
@@ -1568,7 +1741,7 @@
1568
1741
  color: var(--text-accent);
1569
1742
  font-size: 11px;
1570
1743
  cursor: pointer;
1571
- box-shadow: 0 4px 18px rgba(32, 36, 43, 0.1);
1744
+ box-shadow: var(--elev-cue);
1572
1745
  }
1573
1746
 
1574
1747
  @media (max-width: 420px) {
@@ -1607,7 +1780,7 @@
1607
1780
  min-width: 0;
1608
1781
  max-width: calc(100vw - 96px);
1609
1782
  box-sizing: border-box;
1610
- background: rgba(252, 251, 247, 0.75);
1783
+ background: rgb(var(--paper-rgb) / 0.75);
1611
1784
  font-family: var(--font-mono);
1612
1785
  font-size: 10px;
1613
1786
  white-space: nowrap;
@@ -1648,7 +1821,7 @@
1648
1821
  line-height: 1.5;
1649
1822
  padding: 8px 10px !important;
1650
1823
  border-radius: 2px;
1651
- box-shadow: 0 2px 10px rgba(32, 36, 43, 0.12);
1824
+ box-shadow: var(--elev-tooltip);
1652
1825
  }
1653
1826
 
1654
1827
  @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>;