@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.
- package/README.md +233 -0
- package/dist/Sparkline.svelte +2 -2
- package/dist/TellegenMap.svelte +239 -66
- 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 +55 -1
- package/dist/colors.js +89 -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/ControlPanel.svelte +4 -2
- package/dist/components/DemandSlider.svelte +16 -7
- package/dist/components/DiagramCanvas.svelte +17 -5
- 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 +266 -78
- package/dist/display.d.ts +3 -2
- package/dist/display.js +22 -4
- package/dist/format.d.ts +9 -0
- package/dist/format.js +13 -1
- package/dist/index.d.ts +6 -2
- 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 +25 -3
- package/dist/state.svelte.js +13 -0
- package/dist/styles.css +59 -6
- package/dist/types.d.ts +1 -1
- package/package.json +13 -8
package/dist/TellegenMap.svelte
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
|
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(
|
|
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}"
|
|
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>Δ 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 ? '—' : fmt.format(price)} objective units/MW`
|
|
704
792
|
: `${d.scalarLabel} ${
|
|
705
793
|
scalar === undefined ? '—' : displayFmt(d.scalarMode, scalar)
|
|
@@ -712,14 +800,17 @@
|
|
|
712
800
|
}
|
|
713
801
|
|
|
714
802
|
async function loadMapModules() {
|
|
715
|
-
const [maplibre, deckMap, layers,
|
|
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
|
|
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(
|
|
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:
|
|
771
|
-
center:
|
|
772
|
-
zoom:
|
|
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:
|
|
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
|
|
865
|
-
|
|
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 === '
|
|
914
|
-
? (
|
|
915
|
-
: mode === '
|
|
916
|
-
? (s?.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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) =>
|
|
952
|
-
radiusUnits:
|
|
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
|
-
?
|
|
957
|
-
:
|
|
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:
|
|
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:
|
|
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) =>
|
|
1018
|
-
radiusUnits:
|
|
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
|
-
?
|
|
1025
|
-
:
|
|
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:
|
|
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
|
-
:
|
|
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:
|
|
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) =>
|
|
1092
|
-
radiusUnits:
|
|
1093
|
-
|
|
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
|
-
?
|
|
1099
|
-
:
|
|
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:
|
|
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:
|
|
1300
|
+
getFillColor: palette.substation,
|
|
1134
1301
|
stroked: true,
|
|
1135
1302
|
billboard: true,
|
|
1136
|
-
getLineColor:
|
|
1303
|
+
getLineColor: palette.substationLine,
|
|
1137
1304
|
getLineWidth: 1,
|
|
1138
1305
|
lineWidthUnits: 'pixels',
|
|
1139
1306
|
pickable: true,
|
|
1140
1307
|
autoHighlight: true,
|
|
1141
|
-
highlightColor:
|
|
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:
|
|
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) =>
|
|
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
|
-
?
|
|
1380
|
+
? palette.selected
|
|
1214
1381
|
: b.has_source
|
|
1215
1382
|
? [63, 111, 187, 220]
|
|
1216
|
-
:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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>;
|