@tellegen/svelte 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -5
- package/dist/Sparkline.svelte +2 -2
- package/dist/TellegenMap.svelte +518 -202
- package/dist/TellegenMap.svelte.d.ts +45 -0
- package/dist/TellegenProvider.svelte +40 -4
- package/dist/TellegenShell.svelte +24 -46
- package/dist/TellegenViewer.svelte +12 -2
- package/dist/api.d.ts +18 -3
- package/dist/api.js +8 -2
- package/dist/basemap.d.ts +192 -0
- package/dist/basemap.js +259 -0
- package/dist/colors.d.ts +25 -7
- package/dist/colors.js +39 -13
- package/dist/components/AppFooter.svelte +3 -2
- package/dist/components/AppHeader.svelte +44 -19
- package/dist/components/BindingLines.svelte +4 -7
- package/dist/components/BusPicker.svelte +11 -5
- package/dist/components/ControlPanel.svelte +106 -343
- package/dist/components/DemandSlider.svelte +36 -29
- package/dist/components/DiagramCanvas.svelte +486 -0
- package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
- package/dist/components/DisplayControls.svelte +8 -2
- package/dist/components/DropZone.svelte +1 -1
- package/dist/components/FormulationSelector.svelte +9 -4
- package/dist/components/LocalCaseDetails.svelte +61 -48
- package/dist/components/McResults.svelte +428 -0
- package/dist/components/McResults.svelte.d.ts +17 -0
- package/dist/components/ModelDetails.svelte +230 -0
- package/dist/components/ModelDetails.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +232 -83
- package/dist/components/NetworkStats.svelte +20 -18
- package/dist/components/NotificationCenter.svelte +245 -0
- package/dist/components/NotificationCenter.svelte.d.ts +3 -0
- package/dist/components/PanelCard.svelte +384 -0
- package/dist/components/PanelCard.svelte.d.ts +9 -0
- package/dist/components/PanelFrame.svelte +46 -0
- package/dist/components/PanelFrame.svelte.d.ts +16 -0
- package/dist/components/PanelHost.svelte +237 -0
- package/dist/components/PanelHost.svelte.d.ts +6 -0
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/PowerFlowDetails.svelte +99 -0
- package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
- package/dist/components/RatingSlider.svelte +19 -14
- package/dist/components/RestoreDefaultsButton.svelte +1 -1
- package/dist/components/SavedNetworkDetails.svelte +214 -0
- package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
- package/dist/components/SensitivityReadout.svelte +26 -18
- package/dist/components/ShellEnvironment.svelte +51 -0
- package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
- package/dist/components/SizeLegend.svelte +2 -2
- package/dist/components/SolveCard.svelte +48 -51
- package/dist/components/TopMovers.svelte +9 -4
- package/dist/components/TopMovers.svelte.d.ts +2 -17
- package/dist/components.d.ts +5 -0
- package/dist/components.js +5 -0
- package/dist/context.svelte.d.ts +8 -1
- package/dist/context.svelte.js +8 -3
- package/dist/controller.svelte.d.ts +117 -36
- package/dist/controller.svelte.js +1133 -253
- package/dist/diagram-labels.d.ts +9 -0
- package/dist/diagram-labels.js +40 -0
- package/dist/display.d.ts +3 -7
- package/dist/display.js +31 -27
- package/dist/format.d.ts +9 -0
- package/dist/format.js +17 -4
- package/dist/index.d.ts +15 -5
- package/dist/index.js +9 -2
- package/dist/map.d.ts +4 -2
- package/dist/map.js +2 -1
- package/dist/maplibre-worker.d.ts +5 -0
- package/dist/maplibre-worker.dev.d.ts +11 -0
- package/dist/maplibre-worker.dev.js +15 -0
- package/dist/maplibre-worker.js +8 -0
- package/dist/multiconductor.d.ts +11 -2
- package/dist/multiconductor.js +107 -14
- package/dist/notices.svelte.d.ts +29 -0
- package/dist/notices.svelte.js +86 -0
- package/dist/panels.svelte.d.ts +71 -0
- package/dist/panels.svelte.js +245 -0
- package/dist/slider.d.ts +12 -0
- package/dist/slider.js +31 -0
- package/dist/state.svelte.d.ts +107 -23
- package/dist/state.svelte.js +106 -20
- package/dist/styles.css +59 -6
- package/dist/synthetic-layout.js +1 -0
- package/package.json +29 -11
- package/dist/study-package.d.ts +0 -8
- package/dist/study-package.js +0 -17
package/dist/TellegenMap.svelte
CHANGED
|
@@ -1,25 +1,41 @@
|
|
|
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
|
-
import type {
|
|
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,
|
|
13
14
|
busRadius,
|
|
14
|
-
|
|
15
|
+
priceColor,
|
|
15
16
|
scalarDomain,
|
|
16
17
|
sensColor,
|
|
17
18
|
sensFlatColor,
|
|
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,
|
|
@@ -29,13 +45,18 @@
|
|
|
29
45
|
transformerMarks,
|
|
30
46
|
type PlacedMultiBus,
|
|
31
47
|
type PlacedMultiEdge,
|
|
32
|
-
type TransformerMark
|
|
48
|
+
type TransformerMark,
|
|
49
|
+
multiDiagramNetwork
|
|
33
50
|
} from './multiconductor.js';
|
|
34
51
|
import { transformerIcon } from './transformer-icon.js';
|
|
35
52
|
import { foldMapBounds } from './map-bounds.js';
|
|
36
53
|
import { getAppState, getController } from './context.svelte.js';
|
|
37
54
|
import { displayFmt, fmt, rgbaCss } from './format.js';
|
|
55
|
+
import { getPanelLayout } from './panels.svelte.js';
|
|
38
56
|
import type { RGBA } from './colors.js';
|
|
57
|
+
import DiagramCanvas from './components/DiagramCanvas.svelte';
|
|
58
|
+
|
|
59
|
+
const DEFAULT_CAMERA: CameraSnapshot = { center: [-85, 36], zoom: 4.5, bearing: 0, pitch: 0 };
|
|
39
60
|
|
|
40
61
|
let {
|
|
41
62
|
onbusclick,
|
|
@@ -45,7 +66,19 @@
|
|
|
45
66
|
onmultibusclick,
|
|
46
67
|
onmultiedgeclick,
|
|
47
68
|
onplacecase,
|
|
48
|
-
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
|
|
49
82
|
}: {
|
|
50
83
|
onbusclick: (caseId: string, busId: number) => void;
|
|
51
84
|
onlocalbusclick: (caseId: string, busId: number) => void;
|
|
@@ -55,18 +88,112 @@
|
|
|
55
88
|
onmultiedgeclick?: (caseId: string, edgeId: string) => void;
|
|
56
89
|
onplacecase: (lon: number, lat: number) => void;
|
|
57
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;
|
|
58
131
|
} = $props();
|
|
59
132
|
|
|
60
133
|
const app = getAppState();
|
|
61
134
|
const ctrl = getController();
|
|
135
|
+
const panels = getPanelLayout();
|
|
136
|
+
const diagramCase = $derived(app.studyView ? null : (app.active ?? app.activeLocal));
|
|
137
|
+
const diagramMulti = $derived(
|
|
138
|
+
!app.studyView && app.activeMulti?.coordsKind !== 'geographic' ? app.activeMulti : null
|
|
139
|
+
);
|
|
140
|
+
const multiNetwork = $derived(
|
|
141
|
+
diagramMulti?.view
|
|
142
|
+
? multiDiagramNetwork(diagramMulti.id, diagramMulti.label, diagramMulti.view)
|
|
143
|
+
: null
|
|
144
|
+
);
|
|
145
|
+
const displayedNetwork = $derived(app.studyView?.network ?? diagramCase?.network);
|
|
146
|
+
const diagramNetwork = $derived(
|
|
147
|
+
multiNetwork ?? (displayedNetwork?.coordinate_space === 'diagram' ? displayedNetwork : null)
|
|
148
|
+
);
|
|
149
|
+
const diagramCaseId = $derived(
|
|
150
|
+
app.studyView?.caseId ?? diagramMulti?.id ?? diagramCase?.id ?? 'diagram'
|
|
151
|
+
);
|
|
152
|
+
function selectDiagramBus(busId: number) {
|
|
153
|
+
if (diagramMulti?.view) {
|
|
154
|
+
const bus = diagramMulti.view.buses[busId];
|
|
155
|
+
if (bus) onmultibusclick(diagramMulti.id, bus.id);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (app.studyView) {
|
|
159
|
+
app.selectedBranch = null;
|
|
160
|
+
app.selectedBus = busId;
|
|
161
|
+
} else if (app.activeLocal) onlocalbusclick(diagramCaseId, busId);
|
|
162
|
+
else onbusclick(diagramCaseId, busId);
|
|
163
|
+
}
|
|
164
|
+
function selectDiagramBranch(branchId: number) {
|
|
165
|
+
if (diagramMulti?.view) {
|
|
166
|
+
const edge = diagramMulti.view.edges[branchId];
|
|
167
|
+
if (edge) onmultiedgeclick?.(diagramMulti.id, edge.id);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (app.studyView) {
|
|
171
|
+
app.selectedBus = null;
|
|
172
|
+
app.selectedBranch = branchId;
|
|
173
|
+
} else if (app.activeLocal) onlocalbranchclick?.(diagramCaseId, branchId);
|
|
174
|
+
else onbranchclick?.(diagramCaseId, branchId);
|
|
175
|
+
}
|
|
62
176
|
|
|
63
|
-
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;
|
|
64
191
|
const CUE_MARGIN_PX = 64;
|
|
65
192
|
const BRANCH_FOCUS_MS = 850;
|
|
66
193
|
const BRANCH_FOCUS_MAX_ZOOM = 9.5;
|
|
67
194
|
|
|
68
195
|
let map = $state.raw<MapLibreMap | null>(null);
|
|
69
|
-
let overlay = $state.raw<
|
|
196
|
+
let overlay = $state.raw<MapLibreOverlay | null>(null);
|
|
70
197
|
// Bumped to remount the map after a WebGL context is lost and cannot be
|
|
71
198
|
// repainted in place. See initMap's context-loss handling.
|
|
72
199
|
let mapGen = $state(0);
|
|
@@ -85,7 +212,7 @@
|
|
|
85
212
|
let layerCtors = $state.raw<LayerCtors | null>(null);
|
|
86
213
|
|
|
87
214
|
interface CaseDisplay {
|
|
88
|
-
|
|
215
|
+
prices: Map<number, number>;
|
|
89
216
|
scalar: Map<number, number>;
|
|
90
217
|
scalarLo: number;
|
|
91
218
|
scalarHi: number;
|
|
@@ -93,7 +220,7 @@
|
|
|
93
220
|
scalarLabel: string;
|
|
94
221
|
scalarUnit: string;
|
|
95
222
|
loading: Map<number, number>;
|
|
96
|
-
mode: '
|
|
223
|
+
mode: 'price' | 'sens' | 'preview';
|
|
97
224
|
sens: Map<number, number>;
|
|
98
225
|
sensDomain: SensitivityDomain | null;
|
|
99
226
|
preview: Map<number, number>;
|
|
@@ -106,7 +233,7 @@
|
|
|
106
233
|
type SolvableCase = CaseState | LocalCase;
|
|
107
234
|
|
|
108
235
|
// Everything the accessors need, rebuilt when any case's data moves. The
|
|
109
|
-
// LMP scale normalizes per network: each case is an islanded
|
|
236
|
+
// The LMP scale normalizes per network: each case is an islanded network, so
|
|
110
237
|
// within network structure beats cross network color comparability (the
|
|
111
238
|
// panel legend and tooltips carry the actual numbers). Preview values
|
|
112
239
|
// shift individual buses without rescaling.
|
|
@@ -114,8 +241,8 @@
|
|
|
114
241
|
const active = app.active ?? app.activeLocal;
|
|
115
242
|
const selectedBus = app.selectedBus;
|
|
116
243
|
const selectedBranch = app.selectedBranch;
|
|
117
|
-
// The active case's column for the live selection: the
|
|
118
|
-
// selected bus, or the
|
|
244
|
+
// The active case's column for the live selection: the price-demand column at the
|
|
245
|
+
// selected bus, or the price-rating column at the selected branch. Both are
|
|
119
246
|
// bus-keyed value maps, so the color pipeline below is target-agnostic.
|
|
120
247
|
const activeSensitivity =
|
|
121
248
|
active &&
|
|
@@ -125,18 +252,18 @@
|
|
|
125
252
|
: null;
|
|
126
253
|
const activeDeltas = active ? caseDeltas(active) : {};
|
|
127
254
|
const activeRatings = active ? caseRatings(active) : {};
|
|
128
|
-
// Engine first
|
|
255
|
+
// Engine first order preview (Study.preview): predicted per-bus LMP change for
|
|
129
256
|
// the live drag, scoped to this case + selection target. Preferred over the JS
|
|
130
257
|
// gradient shift when present; the gradient path stays for server and browser
|
|
131
258
|
// fallbacks.
|
|
132
259
|
const enginePreview =
|
|
133
260
|
active &&
|
|
134
|
-
app.
|
|
135
|
-
app.
|
|
136
|
-
('bus' in app.
|
|
137
|
-
? app.
|
|
138
|
-
: app.
|
|
139
|
-
? app.
|
|
261
|
+
app.previewPrices &&
|
|
262
|
+
app.previewPrices.caseId === active.id &&
|
|
263
|
+
('bus' in app.previewPrices.target
|
|
264
|
+
? app.previewPrices.target.bus === selectedBus
|
|
265
|
+
: app.previewPrices.target.branch === selectedBranch)
|
|
266
|
+
? app.previewPrices.delta
|
|
140
267
|
: null;
|
|
141
268
|
const previewStep =
|
|
142
269
|
active && activeSensitivity
|
|
@@ -153,13 +280,13 @@
|
|
|
153
280
|
const perCase = new Map<string, CaseDisplay>();
|
|
154
281
|
const addCase = (c: CaseState | LocalCase) => {
|
|
155
282
|
if (!c.network || !c.solution) return;
|
|
156
|
-
const
|
|
157
|
-
for (const e of c.solution.
|
|
283
|
+
const prices = new Map<number, number>();
|
|
284
|
+
for (const e of c.solution.prices) prices.set(e.bus, e.value);
|
|
158
285
|
// Resolve the active display mode against what this case can show, falling
|
|
159
|
-
// back to
|
|
286
|
+
// back to LMPs when the chosen mode doesn't apply (e.g. |V| under DC OPF).
|
|
160
287
|
const meta = displayMetaFor(c);
|
|
161
288
|
const activeMeta = meta.find((o) => o.mode === app.displayMode) ?? meta[0] ?? null;
|
|
162
|
-
const scalarMode = activeMeta?.mode ?? '
|
|
289
|
+
const scalarMode = activeMeta?.mode ?? 'price';
|
|
163
290
|
const seriesValues = displaySeriesFor(c, scalarMode);
|
|
164
291
|
const scalar = new Map<number, number>();
|
|
165
292
|
for (const e of seriesValues) scalar.set(e.bus, e.value);
|
|
@@ -175,10 +302,10 @@
|
|
|
175
302
|
let domain: SensitivityDomain | null = null;
|
|
176
303
|
const isActive = c === active;
|
|
177
304
|
if (isActive && enginePreview) {
|
|
178
|
-
// Engine preview owns the LMP shift
|
|
179
|
-
for (const [bus,
|
|
180
|
-
preview.set(bus,
|
|
181
|
-
|
|
305
|
+
// Engine preview owns the LMP shift.
|
|
306
|
+
for (const [bus, deltaPrice] of enginePreview) {
|
|
307
|
+
preview.set(bus, deltaPrice);
|
|
308
|
+
prices.set(bus, (prices.get(bus) ?? 0) + deltaPrice);
|
|
182
309
|
}
|
|
183
310
|
}
|
|
184
311
|
if (isActive && activeSensitivity) {
|
|
@@ -190,9 +317,9 @@
|
|
|
190
317
|
if (!enginePreview && previewStep !== 0) {
|
|
191
318
|
// Fallback first order preview: shift LMPs along the gradient.
|
|
192
319
|
for (const v of activeSensitivity.values) {
|
|
193
|
-
const
|
|
194
|
-
preview.set(v.bus,
|
|
195
|
-
|
|
320
|
+
const deltaPrice = v.value * previewStep;
|
|
321
|
+
preview.set(v.bus, deltaPrice);
|
|
322
|
+
prices.set(v.bus, (prices.get(v.bus) ?? 0) + deltaPrice);
|
|
196
323
|
}
|
|
197
324
|
}
|
|
198
325
|
}
|
|
@@ -203,16 +330,15 @@
|
|
|
203
330
|
previewDomain &&
|
|
204
331
|
(app.previewActive || c.solving || enginePreview || Math.abs(previewStep) >= 0.25)
|
|
205
332
|
);
|
|
206
|
-
// The sensitivity and demand
|
|
207
|
-
// and the ΔLMP preview). They only paint the map under the LMP display mode;
|
|
333
|
+
// The sensitivity and demand preview overlays are LMP fields. They only paint the map under the price display mode;
|
|
208
334
|
// under angle or |V| the map stays on the chosen scalar and the sensitivity
|
|
209
335
|
// shows in the side panel, so selecting a bus never silently overrides the
|
|
210
336
|
// angle/|V| coloring with the price ramp.
|
|
211
|
-
const sensEligible = scalarMode === '
|
|
337
|
+
const sensEligible = scalarMode === 'price';
|
|
212
338
|
// While a sensitivity is still loading for the selected bus, stay in sens
|
|
213
|
-
// mode (renders neutral, not the prior
|
|
339
|
+
// mode (renders neutral, not the prior price palette) so the node colors
|
|
214
340
|
// don't flash orange->purple between the old and new selection.
|
|
215
|
-
const mode: '
|
|
341
|
+
const mode: 'price' | 'sens' | 'preview' =
|
|
216
342
|
sensEligible && showPreview
|
|
217
343
|
? 'preview'
|
|
218
344
|
: sensEligible &&
|
|
@@ -221,15 +347,15 @@
|
|
|
221
347
|
(domain || app.sensitivityLoading) &&
|
|
222
348
|
!previewing
|
|
223
349
|
? 'sens'
|
|
224
|
-
: '
|
|
350
|
+
: 'price';
|
|
225
351
|
perCase.set(c.id, {
|
|
226
|
-
|
|
352
|
+
prices,
|
|
227
353
|
scalar,
|
|
228
354
|
scalarLo,
|
|
229
355
|
scalarHi,
|
|
230
356
|
scalarMode,
|
|
231
357
|
scalarLabel: activeMeta?.label ?? 'LMP',
|
|
232
|
-
scalarUnit: activeMeta?.unit ?? '
|
|
358
|
+
scalarUnit: activeMeta?.unit ?? 'objective units/MW',
|
|
233
359
|
loading,
|
|
234
360
|
mode,
|
|
235
361
|
sens,
|
|
@@ -266,7 +392,7 @@
|
|
|
266
392
|
return sensColor((d.preview.get(bus.id) ?? 0) / d.previewDomain.scale);
|
|
267
393
|
}
|
|
268
394
|
const mid = (d.scalarLo + d.scalarHi) / 2;
|
|
269
|
-
return
|
|
395
|
+
return priceColor(((d.scalar.get(bus.id) ?? mid) - d.scalarLo) / (d.scalarHi - d.scalarLo));
|
|
270
396
|
};
|
|
271
397
|
}
|
|
272
398
|
|
|
@@ -282,34 +408,38 @@
|
|
|
282
408
|
// click handler must not treat those clicks as "empty map" and clear the selection.
|
|
283
409
|
function isSelectableLayer(layerId: string | undefined): boolean {
|
|
284
410
|
return Boolean(
|
|
411
|
+
layerId?.startsWith('study-') ||
|
|
285
412
|
layerId?.startsWith('buses-') ||
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
413
|
+
layerId?.startsWith('local-buses-') ||
|
|
414
|
+
layerId?.startsWith('branches-') ||
|
|
415
|
+
layerId?.startsWith('local-branches-') ||
|
|
416
|
+
layerId?.startsWith('multi-buses-') ||
|
|
417
|
+
layerId?.startsWith('multi-edges-')
|
|
291
418
|
);
|
|
292
419
|
}
|
|
293
420
|
|
|
421
|
+
function displayedGeometry(caseId: string) {
|
|
422
|
+
if (app.studyView) return app.studyView.caseId === caseId ? app.studyView.network : null;
|
|
423
|
+
const local = app.localCases.find((c) => c.id === caseId);
|
|
424
|
+
return app.byId(caseId)?.network ?? local?.view ?? local?.network ?? null;
|
|
425
|
+
}
|
|
294
426
|
const selectedBusCue = $derived.by(() => {
|
|
295
427
|
const busId = app.selectedBus;
|
|
296
|
-
const
|
|
297
|
-
if (!
|
|
298
|
-
const buses
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
428
|
+
const caseId = app.studyView?.caseId ?? app.active?.id ?? app.activeLocal?.id;
|
|
429
|
+
if (!caseId || busId === null) return null;
|
|
430
|
+
const bus = displayedGeometry(caseId)?.buses.find((b) => b.id === busId);
|
|
431
|
+
return bus
|
|
432
|
+
? { key: `${app.studyView?.id ?? caseId}-${bus.id}`, id: bus.id, lon: bus.lon, lat: bus.lat }
|
|
433
|
+
: null;
|
|
302
434
|
});
|
|
303
435
|
const selectedBranchCue = $derived.by(() => {
|
|
304
436
|
const branchId = app.selectedBranch;
|
|
305
|
-
const
|
|
306
|
-
if (!
|
|
307
|
-
const branches
|
|
308
|
-
c instanceof CaseState ? c.network?.branches : (c.view?.branches ?? c.network?.branches);
|
|
309
|
-
const branch = branches?.find((b) => b.id === branchId);
|
|
437
|
+
const caseId = app.studyView?.caseId ?? app.active?.id ?? app.activeLocal?.id;
|
|
438
|
+
if (!caseId || branchId === null) return null;
|
|
439
|
+
const branch = displayedGeometry(caseId)?.branches.find((b) => b.id === branchId);
|
|
310
440
|
const path = branch?.path.filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat));
|
|
311
441
|
return branch && path && path.length >= 2
|
|
312
|
-
? { key: `${
|
|
442
|
+
? { key: `${app.studyView?.id ?? caseId}-${branch.id}`, caseId, branchId: branch.id, path }
|
|
313
443
|
: null;
|
|
314
444
|
});
|
|
315
445
|
let selectedCueEl = $state.raw<HTMLDivElement | null>(null);
|
|
@@ -317,6 +447,7 @@
|
|
|
317
447
|
let selectedBranchHaloPath = $state.raw<SVGPathElement | null>(null);
|
|
318
448
|
let selectedBranchLinePath = $state.raw<SVGPathElement | null>(null);
|
|
319
449
|
let handledFrameSeq = 0;
|
|
450
|
+
let handledCameraSeq = 0;
|
|
320
451
|
|
|
321
452
|
// The selected multiconductor bus and its incident edges, whose per-conductor
|
|
322
453
|
// strands and terminal stack the detail overlay draws in screen space.
|
|
@@ -460,9 +591,9 @@
|
|
|
460
591
|
terminals.forEach((t, k) => {
|
|
461
592
|
const cy = top + k * STACK_DY;
|
|
462
593
|
const grounded = bus.grounded.includes(t);
|
|
463
|
-
const fill = isPhaseTerminal(t) ? col(phaseColor(t)) : col(
|
|
594
|
+
const fill = isPhaseTerminal(t) ? col(phaseColor(t)) : col(palette.neutral);
|
|
464
595
|
if (grounded) stack += groundGlyph(sx - 14, cy);
|
|
465
|
-
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}"/>`;
|
|
466
597
|
stack += `<text class="mc-term" x="${(sx + 11).toFixed(1)}" y="${(cy + 3.5).toFixed(1)}">${esc(t)}</text>`;
|
|
467
598
|
// Attachment squares for elements landing on this terminal.
|
|
468
599
|
const kinds = [...new Set((bus.terminalAttachments[t] ?? []).map((a) => a.kind))];
|
|
@@ -492,18 +623,23 @@
|
|
|
492
623
|
});
|
|
493
624
|
}
|
|
494
625
|
|
|
495
|
-
/** Camera
|
|
496
|
-
* The branch variant also clears the rating readout on the panel's lower edge. */
|
|
626
|
+
/** Camera framing reserves the dock columns and the active compact drawer. */
|
|
497
627
|
function framePadding(width: number, height: number, kind: 'case' | 'branch') {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
628
|
+
const clearance = kind === 'branch' ? 16 : 28;
|
|
629
|
+
if (panels.compact) {
|
|
630
|
+
return {
|
|
631
|
+
top: panels.top + 8,
|
|
632
|
+
left: 24,
|
|
633
|
+
right: 24,
|
|
634
|
+
bottom: Math.min(app.sheetInset + clearance, Math.round(height * 0.6))
|
|
635
|
+
};
|
|
503
636
|
}
|
|
504
|
-
return
|
|
505
|
-
|
|
506
|
-
:
|
|
637
|
+
return {
|
|
638
|
+
top: panels.top + 16,
|
|
639
|
+
left: Math.min(panels.dockWidth('left') + 44, width * 0.34),
|
|
640
|
+
right: Math.min(panels.dockWidth('right') + 44, width * 0.34),
|
|
641
|
+
bottom: 80
|
|
642
|
+
};
|
|
507
643
|
}
|
|
508
644
|
|
|
509
645
|
function branchBounds(path: [number, number][]): LngLatBoundsLike | null {
|
|
@@ -512,8 +648,7 @@
|
|
|
512
648
|
|
|
513
649
|
function prefersReducedMotion(): boolean {
|
|
514
650
|
return (
|
|
515
|
-
typeof window !== 'undefined' &&
|
|
516
|
-
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
651
|
+
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
517
652
|
);
|
|
518
653
|
}
|
|
519
654
|
|
|
@@ -521,25 +656,9 @@
|
|
|
521
656
|
return typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches;
|
|
522
657
|
}
|
|
523
658
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
// header — at that point the sheet covers what they annotate.
|
|
528
|
-
const CHROME_MIN_BAND_PX = 132;
|
|
529
|
-
|
|
530
|
-
/** How far the sheet can push the chrome up before it would reach the header. */
|
|
531
|
-
const chromeLiftCap = $derived(
|
|
532
|
-
Math.max(0, app.viewportHeight - app.headerInset - CHROME_MIN_BAND_PX)
|
|
533
|
-
);
|
|
534
|
-
const chromeInset = $derived(
|
|
535
|
-
app.compactLayout ? Math.min(app.sheetInset, chromeLiftCap) : 0
|
|
536
|
-
);
|
|
537
|
-
const chromeVisible = $derived(!app.compactLayout || app.sheetInset <= chromeLiftCap);
|
|
538
|
-
|
|
539
|
-
/** Fly to the selected branch; true when a camera move started. */
|
|
540
|
-
function focusSelectedBranch(): boolean {
|
|
541
|
-
if (!map || !selectedBranchCue) return false;
|
|
542
|
-
const bounds = branchBounds(selectedBranchCue.path);
|
|
659
|
+
function focusGeometry(points: [number, number][], settle: () => void): boolean {
|
|
660
|
+
if (!map) return false;
|
|
661
|
+
const bounds = branchBounds(points);
|
|
543
662
|
if (!bounds) return false;
|
|
544
663
|
const { clientWidth, clientHeight } = map.getContainer();
|
|
545
664
|
const camera = map.cameraForBounds(bounds, {
|
|
@@ -547,6 +666,7 @@
|
|
|
547
666
|
maxZoom: Math.min(map.getZoom() + 2, BRANCH_FOCUS_MAX_ZOOM)
|
|
548
667
|
});
|
|
549
668
|
if (!camera?.center) return false;
|
|
669
|
+
map.once('moveend', settle);
|
|
550
670
|
map.easeTo({
|
|
551
671
|
center: camera.center,
|
|
552
672
|
zoom: camera.zoom ?? map.getZoom(),
|
|
@@ -580,6 +700,21 @@
|
|
|
580
700
|
function tooltip(info: PickingInfo): { html: string } | null {
|
|
581
701
|
const { object } = info;
|
|
582
702
|
if (!object) return null;
|
|
703
|
+
if (app.studyView && info.layer?.id.startsWith('study-')) {
|
|
704
|
+
const view = app.studyView.solution;
|
|
705
|
+
const id = Number((object as { id: number }).id);
|
|
706
|
+
if (info.layer.id === 'study-branches') {
|
|
707
|
+
const flow = view?.flows?.find((f) => f.branch === id);
|
|
708
|
+
return {
|
|
709
|
+
html: `<b>branch ${id}</b><br>saved state: ${esc(app.studyView.label)}${flow ? `<br>P from ${flow.pf.toFixed(3)} MW, loading ${(100 * flow.loading).toFixed(1)}%` : ''}`
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
const p = view?.lmp?.find((v) => v.bus === id),
|
|
713
|
+
v = view?.vm?.find((v) => v.bus === id);
|
|
714
|
+
return {
|
|
715
|
+
html: `<b>bus ${id}</b><br>saved state: ${esc(app.studyView.label)}${p ? `<br>LMP ${p.value.toFixed(4)}` : ''}${v ? `<br>voltage ${v.value.toFixed(5)} pu` : ''}`
|
|
716
|
+
};
|
|
717
|
+
}
|
|
583
718
|
if (info.layer?.id.startsWith('local-subs-')) {
|
|
584
719
|
// PowerWorld .pwd substation: display data only, position inferred from the diagram.
|
|
585
720
|
// The name is free text from a dropped file, so escape it; the number
|
|
@@ -587,7 +722,7 @@
|
|
|
587
722
|
const s = object as { number: number; name: string };
|
|
588
723
|
const named = s.name ? ` ${esc(s.name)}` : '';
|
|
589
724
|
return {
|
|
590
|
-
html: `<div class="tt"><b>substation ${Number(s.number)}</b>${named}<br><span style="opacity:0.6">.pwd diagram
|
|
725
|
+
html: `<div class="tt"><b>substation ${Number(s.number)}</b>${named}<br><span style="opacity:0.6">.pwd diagram, approximate position</span></div>`
|
|
591
726
|
};
|
|
592
727
|
}
|
|
593
728
|
const layerId = info.layer?.id;
|
|
@@ -627,7 +762,7 @@
|
|
|
627
762
|
<span style="opacity:0.6">local file</span></div>`
|
|
628
763
|
};
|
|
629
764
|
}
|
|
630
|
-
const
|
|
765
|
+
const price = d?.prices.get(bus.id);
|
|
631
766
|
const scalar = d?.scalar.get(bus.id);
|
|
632
767
|
const sens = d?.mode === 'sens' ? d.sens.get(bus.id) : undefined;
|
|
633
768
|
const preview = d?.mode === 'preview' ? d.preview.get(bus.id) : undefined;
|
|
@@ -639,19 +774,19 @@
|
|
|
639
774
|
const sensRow =
|
|
640
775
|
sens === undefined
|
|
641
776
|
? ''
|
|
642
|
-
: `<br>∂
|
|
777
|
+
: `<br>∂price/∂d ${sens >= 0 ? '+' : ''}${sens.toExponential(2)}`;
|
|
643
778
|
const previewRow =
|
|
644
779
|
preview === undefined
|
|
645
780
|
? ''
|
|
646
|
-
: `<br>ΔLMP ${preview >= 0 ? '+' : ''}${preview.toExponential(2)}
|
|
781
|
+
: `<br>Δ LMP ${preview >= 0 ? '+' : ''}${preview.toExponential(2)} objective units/MW`;
|
|
647
782
|
const localRow =
|
|
648
783
|
c instanceof CaseState ? '' : '<br><span style="opacity:0.6">local file</span>';
|
|
649
784
|
const scalarRow =
|
|
650
|
-
!d || d.scalarMode === '
|
|
651
|
-
? `LMP ${
|
|
785
|
+
!d || d.scalarMode === 'price'
|
|
786
|
+
? `LMP ${price === undefined ? '—' : fmt.format(price)} objective units/MW`
|
|
652
787
|
: `${d.scalarLabel} ${
|
|
653
788
|
scalar === undefined ? '—' : displayFmt(d.scalarMode, scalar)
|
|
654
|
-
} ${d.scalarUnit}<br>LMP ${
|
|
789
|
+
} ${d.scalarUnit}<br>LMP ${price === undefined ? '—' : fmt.format(price)} objective units/MW`;
|
|
655
790
|
return {
|
|
656
791
|
html: `<div class="tt"><b>bus ${bus.id}</b>
|
|
657
792
|
${scalarRow}<br>
|
|
@@ -660,14 +795,20 @@
|
|
|
660
795
|
}
|
|
661
796
|
|
|
662
797
|
async function loadMapModules() {
|
|
663
|
-
const [maplibre,
|
|
798
|
+
const [maplibre, deckMap, layers, workerUrl] = await Promise.all([
|
|
664
799
|
import('maplibre-gl'),
|
|
665
|
-
import('@deck.gl/
|
|
666
|
-
import('@deck.gl/layers')
|
|
800
|
+
import('@deck.gl/maplibre'),
|
|
801
|
+
import('@deck.gl/layers'),
|
|
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())
|
|
667
806
|
]);
|
|
807
|
+
// Bundle the worker with its shared imports and configure it before creating a map.
|
|
808
|
+
maplibre.setWorkerUrl(workerUrl);
|
|
668
809
|
return {
|
|
669
|
-
maplibregl: maplibre
|
|
670
|
-
|
|
810
|
+
maplibregl: maplibre,
|
|
811
|
+
MapLibreOverlay: deckMap.MapLibreOverlay,
|
|
671
812
|
PathLayer: layers.PathLayer,
|
|
672
813
|
ScatterplotLayer: layers.ScatterplotLayer,
|
|
673
814
|
IconLayer: layers.IconLayer
|
|
@@ -682,9 +823,9 @@
|
|
|
682
823
|
// memory and tab pressure, and one context survives that far better; the
|
|
683
824
|
// deck layers also inherit maplibre's 4096 canvas clamp. The flat positron
|
|
684
825
|
// basemap writes no depth, so the overlay always draws on top.
|
|
685
|
-
function buildOverlay(
|
|
826
|
+
function buildOverlay(MapLibreOverlay: MapModules['MapLibreOverlay']): MapLibreOverlay {
|
|
686
827
|
const touch = coarsePointer();
|
|
687
|
-
return new
|
|
828
|
+
return new MapLibreOverlay({
|
|
688
829
|
interleaved: true,
|
|
689
830
|
layers: [],
|
|
690
831
|
parameters: {
|
|
@@ -704,21 +845,64 @@
|
|
|
704
845
|
}
|
|
705
846
|
|
|
706
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;
|
|
707
862
|
let cleanup = () => {};
|
|
708
863
|
let cancelled = false;
|
|
709
864
|
void loadMapModules()
|
|
710
|
-
.then(({ maplibregl,
|
|
865
|
+
.then(({ maplibregl, MapLibreOverlay, PathLayer, ScatterplotLayer, IconLayer }) => {
|
|
711
866
|
if (cancelled) return;
|
|
712
867
|
layerCtors = { PathLayer, ScatterplotLayer, IconLayer };
|
|
713
868
|
const m = new maplibregl.Map({
|
|
714
869
|
container,
|
|
715
|
-
style:
|
|
716
|
-
center:
|
|
717
|
-
zoom:
|
|
870
|
+
style: spec.primary,
|
|
871
|
+
center: camera.center,
|
|
872
|
+
zoom: camera.zoom,
|
|
873
|
+
bearing: camera.bearing,
|
|
874
|
+
pitch: camera.pitch,
|
|
718
875
|
canvasContextAttributes: { antialias: true },
|
|
719
|
-
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)
|
|
720
904
|
});
|
|
721
|
-
const o = buildOverlay(
|
|
905
|
+
const o = buildOverlay(MapLibreOverlay);
|
|
722
906
|
m.addControl(o);
|
|
723
907
|
m.on('click', (e) => {
|
|
724
908
|
if (app.placingId) onplacecase(e.lngLat.lng, e.lngLat.lat);
|
|
@@ -743,6 +927,16 @@
|
|
|
743
927
|
if (document.visibilityState === 'visible') repaint();
|
|
744
928
|
};
|
|
745
929
|
const syncCues = () => scheduleCueSync();
|
|
930
|
+
const saveCamera = () => {
|
|
931
|
+
const center = m.getCenter();
|
|
932
|
+
app.camera = {
|
|
933
|
+
center: [center.lng, center.lat],
|
|
934
|
+
zoom: m.getZoom(),
|
|
935
|
+
bearing: m.getBearing(),
|
|
936
|
+
pitch: m.getPitch()
|
|
937
|
+
};
|
|
938
|
+
};
|
|
939
|
+
m.on('moveend', saveCamera);
|
|
746
940
|
let rebuildTimer: ReturnType<typeof setTimeout> | null = null;
|
|
747
941
|
const clearRebuild = () => {
|
|
748
942
|
if (rebuildTimer !== null) {
|
|
@@ -793,11 +987,15 @@
|
|
|
793
987
|
|
|
794
988
|
map = m;
|
|
795
989
|
overlay = o;
|
|
990
|
+
const detachReady = onMapReady?.(m, o);
|
|
796
991
|
// Frame the active grid once the map is ready. The initial frame request
|
|
797
992
|
// (from the case load) can fire before the async map import finishes and
|
|
798
993
|
// be lost; re-issuing it here ensures the first view is the active case,
|
|
799
|
-
// not the default world view. A
|
|
800
|
-
|
|
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);
|
|
997
|
+
else if (app.cameraSeq === handledCameraSeq)
|
|
998
|
+
app.requestFrame(app.activeCaseId ?? app.activeLocalId ?? 'all');
|
|
801
999
|
if (mapGen > 0) {
|
|
802
1000
|
// This is a rebuilt map. Once it has held a live context without
|
|
803
1001
|
// another loss, zero the budget so the cap only ever catches a tight
|
|
@@ -817,6 +1015,9 @@
|
|
|
817
1015
|
m.off('load', syncCues);
|
|
818
1016
|
m.off('move', syncCues);
|
|
819
1017
|
m.off('resize', syncCues);
|
|
1018
|
+
m.off('style.load', applyStyleExtras);
|
|
1019
|
+
detachFallback();
|
|
1020
|
+
detachReady?.();
|
|
820
1021
|
app.settleFrame();
|
|
821
1022
|
m.remove();
|
|
822
1023
|
map = null;
|
|
@@ -832,12 +1033,95 @@
|
|
|
832
1033
|
};
|
|
833
1034
|
}
|
|
834
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
|
+
|
|
835
1050
|
// Sync deck.gl layers with app state. New layer instances diff cheaply;
|
|
836
|
-
// 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.
|
|
837
1053
|
$effect(() => {
|
|
838
1054
|
if (!overlay || !layerCtors) return;
|
|
839
1055
|
const { PathLayer, ScatterplotLayer, IconLayer } = layerCtors;
|
|
840
1056
|
const layers: Layer[] = [];
|
|
1057
|
+
const study = app.studyView;
|
|
1058
|
+
if (study) {
|
|
1059
|
+
const s = study.solution;
|
|
1060
|
+
const mode = app.displayMode;
|
|
1061
|
+
const series =
|
|
1062
|
+
mode === 'angle'
|
|
1063
|
+
? (s?.va ?? [])
|
|
1064
|
+
: mode === 'voltage'
|
|
1065
|
+
? (s?.vm ??
|
|
1066
|
+
(s?.w ?? []).map((v) => ({
|
|
1067
|
+
...v,
|
|
1068
|
+
value: Math.sqrt(Math.max(0, v.value))
|
|
1069
|
+
})))
|
|
1070
|
+
: (s?.lmp ?? []);
|
|
1071
|
+
const values = new Map(series.map((v) => [v.bus, v.value]));
|
|
1072
|
+
const domain = scalarDomain(
|
|
1073
|
+
mode,
|
|
1074
|
+
series.map((v) => v.value)
|
|
1075
|
+
);
|
|
1076
|
+
const flows = new Map((s?.flows ?? []).map((f) => [f.branch, f.loading]));
|
|
1077
|
+
layers.push(
|
|
1078
|
+
new PathLayer<NetworkBranch>({
|
|
1079
|
+
id: 'study-branches',
|
|
1080
|
+
data: study.network.branches,
|
|
1081
|
+
getPath: (b) => b.path,
|
|
1082
|
+
getColor: (b) => branchColor(flows.get(b.id) ?? 0, b.status === 1),
|
|
1083
|
+
getWidth: (b) => branchWidth(flows.get(b.id) ?? 0),
|
|
1084
|
+
widthUnits: 'pixels',
|
|
1085
|
+
widthMinPixels: 1.5,
|
|
1086
|
+
pickable: true,
|
|
1087
|
+
onClick: (info: PickingInfo) => {
|
|
1088
|
+
const branch = info.object as NetworkBranch | undefined;
|
|
1089
|
+
if (!branch) return false;
|
|
1090
|
+
app.selectedBus = null;
|
|
1091
|
+
app.selectedBranch = branch.id;
|
|
1092
|
+
return true;
|
|
1093
|
+
},
|
|
1094
|
+
updateTriggers: { getColor: [study], getWidth: [study] }
|
|
1095
|
+
}),
|
|
1096
|
+
new ScatterplotLayer<NetworkBus>({
|
|
1097
|
+
id: 'study-buses',
|
|
1098
|
+
data: study.network.buses,
|
|
1099
|
+
getPosition: (b) => [b.lon, b.lat],
|
|
1100
|
+
getRadius: (b) => busRadius(Math.max(b.demand_mw, b.gen_mw)),
|
|
1101
|
+
radiusUnits: 'pixels',
|
|
1102
|
+
getFillColor: (b) => {
|
|
1103
|
+
const value = values.get(b.id);
|
|
1104
|
+
return value === undefined
|
|
1105
|
+
? palette.grey
|
|
1106
|
+
: priceColor((value - domain.lo) / Math.max(domain.hi - domain.lo, 1e-12));
|
|
1107
|
+
},
|
|
1108
|
+
pickable: true,
|
|
1109
|
+
stroked: true,
|
|
1110
|
+
getLineColor: palette.outline,
|
|
1111
|
+
lineWidthUnits: 'pixels',
|
|
1112
|
+
onClick: (info: PickingInfo) => {
|
|
1113
|
+
const bus = info.object as NetworkBus | undefined;
|
|
1114
|
+
if (!bus) return false;
|
|
1115
|
+
app.selectedBranch = null;
|
|
1116
|
+
app.selectedBus = bus.id;
|
|
1117
|
+
return true;
|
|
1118
|
+
},
|
|
1119
|
+
updateTriggers: { getFillColor: [study, mode, theme] }
|
|
1120
|
+
})
|
|
1121
|
+
);
|
|
1122
|
+
overlay.setProps({ layers: [...layers, ...extraLayers] });
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
841
1125
|
for (const c of app.cases) {
|
|
842
1126
|
if (!c.network) continue;
|
|
843
1127
|
const d = display.get(c.id);
|
|
@@ -847,7 +1131,7 @@
|
|
|
847
1131
|
data: c.network.branches,
|
|
848
1132
|
getPath: (b) => b.path,
|
|
849
1133
|
// The selected branch draws in the selection blue of the bus halo cue,
|
|
850
|
-
// wider, so the
|
|
1134
|
+
// wider, so the price-rating source line reads at a glance.
|
|
851
1135
|
getColor: (b) =>
|
|
852
1136
|
c.id === app.activeCaseId && b.id === app.selectedBranch
|
|
853
1137
|
? [47, 111, 187, 255]
|
|
@@ -863,7 +1147,7 @@
|
|
|
863
1147
|
miterLimit: 2,
|
|
864
1148
|
pickable: true,
|
|
865
1149
|
autoHighlight: true,
|
|
866
|
-
highlightColor:
|
|
1150
|
+
highlightColor: palette.highlight,
|
|
867
1151
|
onClick: (info: PickingInfo) => {
|
|
868
1152
|
const branch = info.object as NetworkBranch | undefined;
|
|
869
1153
|
if (!branch || !onbranchclick) return false;
|
|
@@ -886,13 +1170,13 @@
|
|
|
886
1170
|
billboard: true,
|
|
887
1171
|
getLineColor: (b) =>
|
|
888
1172
|
c.id === app.activeCaseId && b.id === app.selectedBus
|
|
889
|
-
?
|
|
890
|
-
:
|
|
1173
|
+
? palette.selected
|
|
1174
|
+
: palette.outline,
|
|
891
1175
|
getLineWidth: (b) => (c.id === app.activeCaseId && b.id === app.selectedBus ? 2.5 : 1),
|
|
892
1176
|
lineWidthUnits: 'pixels',
|
|
893
1177
|
pickable: true,
|
|
894
1178
|
autoHighlight: true,
|
|
895
|
-
highlightColor:
|
|
1179
|
+
highlightColor: palette.highlightSoft,
|
|
896
1180
|
onClick: (info: PickingInfo) => {
|
|
897
1181
|
const bus = info.object as NetworkBus | undefined;
|
|
898
1182
|
if (!bus) return false;
|
|
@@ -901,7 +1185,7 @@
|
|
|
901
1185
|
},
|
|
902
1186
|
updateTriggers: {
|
|
903
1187
|
getFillColor: [display],
|
|
904
|
-
getLineColor: [app.selectedBus, app.activeCaseId],
|
|
1188
|
+
getLineColor: [app.selectedBus, app.activeCaseId, theme],
|
|
905
1189
|
getLineWidth: [app.selectedBus, app.activeCaseId]
|
|
906
1190
|
}
|
|
907
1191
|
})
|
|
@@ -923,7 +1207,7 @@
|
|
|
923
1207
|
? [47, 111, 187, 255]
|
|
924
1208
|
: d
|
|
925
1209
|
? branchColor(d.loading.get(b.id) ?? 0, b.status === 1)
|
|
926
|
-
:
|
|
1210
|
+
: palette.greyLine,
|
|
927
1211
|
getWidth: (b) =>
|
|
928
1212
|
c.id === app.activeLocalId && b.id === app.selectedBranch
|
|
929
1213
|
? Math.max((d ? branchWidth(d.loading.get(b.id) ?? 0) : 1.5) + 2, 4.5)
|
|
@@ -937,7 +1221,7 @@
|
|
|
937
1221
|
miterLimit: 2,
|
|
938
1222
|
pickable: true,
|
|
939
1223
|
autoHighlight: true,
|
|
940
|
-
highlightColor:
|
|
1224
|
+
highlightColor: palette.highlight,
|
|
941
1225
|
onClick: (info: PickingInfo) => {
|
|
942
1226
|
const branch = info.object as NetworkBranch | undefined;
|
|
943
1227
|
if (!branch || !onlocalbranchclick) return false;
|
|
@@ -945,7 +1229,7 @@
|
|
|
945
1229
|
return true;
|
|
946
1230
|
},
|
|
947
1231
|
updateTriggers: {
|
|
948
|
-
getColor: [display, app.selectedBranch, app.activeLocalId],
|
|
1232
|
+
getColor: [display, app.selectedBranch, app.activeLocalId, theme],
|
|
949
1233
|
getWidth: [display, app.selectedBranch, app.activeLocalId]
|
|
950
1234
|
}
|
|
951
1235
|
}),
|
|
@@ -955,18 +1239,18 @@
|
|
|
955
1239
|
getPosition: (b) => [b.lon, b.lat],
|
|
956
1240
|
getRadius: (b) => busRadius(Math.max(b.demand_mw, b.gen_mw)),
|
|
957
1241
|
radiusUnits: 'pixels',
|
|
958
|
-
getFillColor: d ? busFill(c.id) :
|
|
1242
|
+
getFillColor: d ? busFill(c.id) : palette.grey,
|
|
959
1243
|
stroked: true,
|
|
960
1244
|
billboard: true,
|
|
961
1245
|
getLineColor: (b) =>
|
|
962
1246
|
c.id === app.activeLocalId && b.id === app.selectedBus
|
|
963
|
-
?
|
|
964
|
-
:
|
|
1247
|
+
? palette.selected
|
|
1248
|
+
: palette.outline,
|
|
965
1249
|
getLineWidth: (b) => (c.id === app.activeLocalId && b.id === app.selectedBus ? 2.5 : 1),
|
|
966
1250
|
lineWidthUnits: 'pixels',
|
|
967
1251
|
pickable: true,
|
|
968
1252
|
autoHighlight: true,
|
|
969
|
-
highlightColor:
|
|
1253
|
+
highlightColor: palette.highlightSoft,
|
|
970
1254
|
onClick: (info: PickingInfo) => {
|
|
971
1255
|
const bus = info.object as NetworkBus | undefined;
|
|
972
1256
|
if (!bus) return false;
|
|
@@ -975,7 +1259,7 @@
|
|
|
975
1259
|
},
|
|
976
1260
|
updateTriggers: {
|
|
977
1261
|
getFillColor: [display],
|
|
978
|
-
getLineColor: [app.selectedBus, app.activeLocalId],
|
|
1262
|
+
getLineColor: [app.selectedBus, app.activeLocalId, theme],
|
|
979
1263
|
getLineWidth: [app.selectedBus, app.activeLocalId]
|
|
980
1264
|
}
|
|
981
1265
|
})
|
|
@@ -983,22 +1267,27 @@
|
|
|
983
1267
|
}
|
|
984
1268
|
if (c.substations) {
|
|
985
1269
|
layers.push(
|
|
986
|
-
new ScatterplotLayer<{
|
|
1270
|
+
new ScatterplotLayer<{
|
|
1271
|
+
number: number;
|
|
1272
|
+
name: string;
|
|
1273
|
+
lon: number;
|
|
1274
|
+
lat: number;
|
|
1275
|
+
}>({
|
|
987
1276
|
id: `local-subs-${c.id}`,
|
|
988
1277
|
data: c.substations.points,
|
|
989
1278
|
getPosition: (s) => [s.lon, s.lat],
|
|
990
1279
|
getRadius: 5,
|
|
991
1280
|
radiusUnits: 'pixels',
|
|
992
1281
|
radiusMinPixels: 3,
|
|
993
|
-
getFillColor:
|
|
1282
|
+
getFillColor: palette.substation,
|
|
994
1283
|
stroked: true,
|
|
995
1284
|
billboard: true,
|
|
996
|
-
getLineColor:
|
|
1285
|
+
getLineColor: palette.substationLine,
|
|
997
1286
|
getLineWidth: 1,
|
|
998
1287
|
lineWidthUnits: 'pixels',
|
|
999
1288
|
pickable: true,
|
|
1000
1289
|
autoHighlight: true,
|
|
1001
|
-
highlightColor:
|
|
1290
|
+
highlightColor: palette.highlightSoft
|
|
1002
1291
|
})
|
|
1003
1292
|
);
|
|
1004
1293
|
}
|
|
@@ -1007,7 +1296,7 @@
|
|
|
1007
1296
|
// kind and buses badged by attachment role. Selecting a bus fans its
|
|
1008
1297
|
// incident conductors and expands its terminal stack in the SVG overlay.
|
|
1009
1298
|
for (const c of app.multiCases) {
|
|
1010
|
-
if (!c.view) continue;
|
|
1299
|
+
if (c.coordsKind !== 'geographic' || !c.view) continue;
|
|
1011
1300
|
const selectedId = c.id === app.activeMultiId ? c.selectedBusId : null;
|
|
1012
1301
|
const selectedEdgeId = c.id === app.activeMultiId ? c.selectedEdgeId : null;
|
|
1013
1302
|
layers.push(
|
|
@@ -1027,7 +1316,7 @@
|
|
|
1027
1316
|
miterLimit: 2,
|
|
1028
1317
|
pickable: true,
|
|
1029
1318
|
autoHighlight: true,
|
|
1030
|
-
highlightColor:
|
|
1319
|
+
highlightColor: palette.highlight,
|
|
1031
1320
|
onClick: (info: PickingInfo) => {
|
|
1032
1321
|
const e = info.object as PlacedMultiEdge | undefined;
|
|
1033
1322
|
if (!e || !onmultiedgeclick) return false;
|
|
@@ -1070,15 +1359,15 @@
|
|
|
1070
1359
|
billboard: true,
|
|
1071
1360
|
getLineColor: (b) =>
|
|
1072
1361
|
selectedId === b.id
|
|
1073
|
-
?
|
|
1362
|
+
? palette.selected
|
|
1074
1363
|
: b.has_source
|
|
1075
1364
|
? [63, 111, 187, 220]
|
|
1076
|
-
:
|
|
1365
|
+
: palette.outlineStrong,
|
|
1077
1366
|
getLineWidth: (b) => (selectedId === b.id ? 2.5 : b.has_source ? 2 : 1),
|
|
1078
1367
|
lineWidthUnits: 'pixels',
|
|
1079
1368
|
pickable: true,
|
|
1080
1369
|
autoHighlight: true,
|
|
1081
|
-
highlightColor:
|
|
1370
|
+
highlightColor: palette.highlightSoft,
|
|
1082
1371
|
onClick: (info: PickingInfo) => {
|
|
1083
1372
|
const bus = info.object as PlacedMultiBus | undefined;
|
|
1084
1373
|
if (!bus) return false;
|
|
@@ -1086,13 +1375,13 @@
|
|
|
1086
1375
|
return true;
|
|
1087
1376
|
},
|
|
1088
1377
|
updateTriggers: {
|
|
1089
|
-
getLineColor: [selectedId],
|
|
1378
|
+
getLineColor: [selectedId, theme],
|
|
1090
1379
|
getLineWidth: [selectedId]
|
|
1091
1380
|
}
|
|
1092
1381
|
})
|
|
1093
1382
|
);
|
|
1094
1383
|
}
|
|
1095
|
-
overlay.setProps({ layers });
|
|
1384
|
+
overlay.setProps({ layers: [...layers, ...extraLayers] });
|
|
1096
1385
|
});
|
|
1097
1386
|
|
|
1098
1387
|
$effect(() => {
|
|
@@ -1114,7 +1403,7 @@
|
|
|
1114
1403
|
// does not re-run (and re-pan) on every frame of a sheet drag.
|
|
1115
1404
|
$effect(() => {
|
|
1116
1405
|
const cue = selectedBusCue;
|
|
1117
|
-
if (!map || !cue || !app.compactLayout) return;
|
|
1406
|
+
if (!map || !cue || !app.compactLayout || app.studyView) return;
|
|
1118
1407
|
void cue.key;
|
|
1119
1408
|
const m = map;
|
|
1120
1409
|
const timer = setTimeout(() => {
|
|
@@ -1129,6 +1418,7 @@
|
|
|
1129
1418
|
});
|
|
1130
1419
|
|
|
1131
1420
|
function boundsFor(target: string): LngLatBoundsLike | null {
|
|
1421
|
+
if (app.studyView) return foldMapBounds(app.studyView.network.buses.map((b) => [b.lon, b.lat]));
|
|
1132
1422
|
const points: [number, number][] = [];
|
|
1133
1423
|
const fold = (pts: { lon: number; lat: number }[]) => {
|
|
1134
1424
|
for (const b of pts) points.push([b.lon, b.lat]);
|
|
@@ -1143,28 +1433,41 @@
|
|
|
1143
1433
|
if (c.substations) fold(c.substations.points);
|
|
1144
1434
|
}
|
|
1145
1435
|
for (const c of app.multiCases) {
|
|
1436
|
+
if (c.coordsKind !== 'geographic') continue;
|
|
1146
1437
|
if (target !== 'all' && c.id !== target) continue;
|
|
1147
1438
|
if (c.view) fold(c.view.buses);
|
|
1148
1439
|
}
|
|
1149
1440
|
return foldMapBounds(points);
|
|
1150
1441
|
}
|
|
1151
1442
|
|
|
1443
|
+
$effect(() => {
|
|
1444
|
+
const seq = app.cameraSeq;
|
|
1445
|
+
const snapshot = app.cameraRequest;
|
|
1446
|
+
if (diagramNetwork || !map || !snapshot || seq === handledCameraSeq) return;
|
|
1447
|
+
handledCameraSeq = seq;
|
|
1448
|
+
handledFrameSeq = app.frameSeq;
|
|
1449
|
+
map.jumpTo(snapshot);
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1152
1452
|
// Fly to whatever the header, the initial load, or a branch selection asked
|
|
1153
1453
|
// for, and settle the request's promise once the camera lands so callers can
|
|
1154
1454
|
// defer heavy work past the animation. A branch request waits (unhandled)
|
|
1155
1455
|
// until its cue exists; anything unframeable settles immediately.
|
|
1156
1456
|
$effect(() => {
|
|
1157
1457
|
const seq = app.frameSeq;
|
|
1158
|
-
if (!map || seq === handledFrameSeq) return;
|
|
1458
|
+
if (diagramNetwork || !map || seq === handledFrameSeq) return;
|
|
1159
1459
|
const m = map;
|
|
1160
1460
|
const target = app.frameTarget;
|
|
1161
1461
|
const settle = () => app.settleFrame();
|
|
1162
1462
|
if (typeof target === 'object') {
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1463
|
+
const geometry = displayedGeometry(target.caseId);
|
|
1464
|
+
const bus =
|
|
1465
|
+
'busId' in target ? geometry?.buses.find((b) => b.id === target.busId) : undefined;
|
|
1466
|
+
const branch =
|
|
1467
|
+
'branchId' in target ? geometry?.branches.find((b) => b.id === target.branchId) : undefined;
|
|
1165
1468
|
handledFrameSeq = seq;
|
|
1166
|
-
|
|
1167
|
-
|
|
1469
|
+
const points: [number, number][] = bus ? [[bus.lon, bus.lat]] : (branch?.path ?? []);
|
|
1470
|
+
if (!focusGeometry(points, settle)) settle();
|
|
1168
1471
|
return;
|
|
1169
1472
|
}
|
|
1170
1473
|
handledFrameSeq = seq;
|
|
@@ -1174,47 +1477,53 @@
|
|
|
1174
1477
|
return;
|
|
1175
1478
|
}
|
|
1176
1479
|
const { clientWidth: w, clientHeight: h } = m.getContainer();
|
|
1480
|
+
m.once('moveend', settle);
|
|
1177
1481
|
m.fitBounds(bounds, {
|
|
1178
1482
|
padding: framePadding(w, h, 'case'),
|
|
1179
1483
|
duration: prefersReducedMotion() ? 0 : 1400
|
|
1180
1484
|
});
|
|
1181
|
-
m.once('moveend', settle);
|
|
1182
1485
|
});
|
|
1183
1486
|
</script>
|
|
1184
1487
|
|
|
1185
|
-
{#
|
|
1186
|
-
<
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
{
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
{#
|
|
1199
|
-
|
|
1200
|
-
<
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
{
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1488
|
+
{#if diagramNetwork}
|
|
1489
|
+
<DiagramCanvas
|
|
1490
|
+
network={diagramNetwork}
|
|
1491
|
+
multiconductor={diagramMulti}
|
|
1492
|
+
caseId={diagramCaseId}
|
|
1493
|
+
onbusclick={selectDiagramBus}
|
|
1494
|
+
onbranchclick={selectDiagramBranch}
|
|
1495
|
+
onclear={onmapclick}
|
|
1496
|
+
/>
|
|
1497
|
+
{:else}
|
|
1498
|
+
{#key mapGen}
|
|
1499
|
+
<div class="map-stage">
|
|
1500
|
+
<div class="map" {@attach initMap}></div>
|
|
1501
|
+
{#if selectedBusCue}
|
|
1502
|
+
{#key selectedBusCue.key}
|
|
1503
|
+
<div class="selected-bus-cue" bind:this={selectedCueEl} aria-hidden="true"></div>
|
|
1504
|
+
{/key}
|
|
1505
|
+
{/if}
|
|
1506
|
+
{#if selectedBranchCue}
|
|
1507
|
+
{#key selectedBranchCue.key}
|
|
1508
|
+
<svg class="selected-branch-cue" bind:this={selectedBranchCueEl} aria-hidden="true">
|
|
1509
|
+
<path class="selected-branch-halo" bind:this={selectedBranchHaloPath}></path>
|
|
1510
|
+
<path class="selected-branch-line" bind:this={selectedBranchLinePath}></path>
|
|
1511
|
+
</svg>
|
|
1512
|
+
{/key}
|
|
1513
|
+
{/if}
|
|
1514
|
+
{#if selectedMultiDetail}
|
|
1515
|
+
<!-- Content is built imperatively in syncMultiDetail (screen-space
|
|
1207
1516
|
projection); file-derived text is escaped there. -->
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
{/key}
|
|
1517
|
+
<svg class="multi-detail" bind:this={multiDetailEl} aria-hidden="true"></svg>
|
|
1518
|
+
{/if}
|
|
1519
|
+
</div>
|
|
1520
|
+
{/key}
|
|
1521
|
+
{/if}
|
|
1212
1522
|
|
|
1213
1523
|
{#if app.placingId && map}
|
|
1214
1524
|
<button
|
|
1215
1525
|
type="button"
|
|
1216
1526
|
class="place-at-center mono"
|
|
1217
|
-
style="--chrome-inset: {chromeInset}px"
|
|
1218
1527
|
onclick={() => {
|
|
1219
1528
|
if (!map) return;
|
|
1220
1529
|
const center = map.getCenter();
|
|
@@ -1278,16 +1587,21 @@
|
|
|
1278
1587
|
}
|
|
1279
1588
|
|
|
1280
1589
|
.multi-detail :global(.mc-leader) {
|
|
1281
|
-
stroke:
|
|
1590
|
+
stroke: rgb(var(--ink-rgb) / 0.35);
|
|
1282
1591
|
stroke-width: 1;
|
|
1283
1592
|
stroke-dasharray: 2 2;
|
|
1284
1593
|
}
|
|
1285
1594
|
|
|
1286
1595
|
.multi-detail :global(.mc-ground line) {
|
|
1287
|
-
stroke:
|
|
1596
|
+
stroke: var(--mc-ground-stroke);
|
|
1288
1597
|
stroke-width: 1.3;
|
|
1289
1598
|
}
|
|
1290
1599
|
|
|
1600
|
+
.multi-detail :global(.mc-terminal) {
|
|
1601
|
+
stroke: var(--ink);
|
|
1602
|
+
stroke-width: 1.2;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1291
1605
|
.multi-detail :global(.mc-term) {
|
|
1292
1606
|
fill: var(--ink, #20242b);
|
|
1293
1607
|
font-family: var(--font-mono);
|
|
@@ -1398,7 +1712,7 @@
|
|
|
1398
1712
|
.place-at-center {
|
|
1399
1713
|
position: absolute;
|
|
1400
1714
|
left: 50%;
|
|
1401
|
-
bottom:
|
|
1715
|
+
bottom: 92px;
|
|
1402
1716
|
z-index: 15;
|
|
1403
1717
|
transform: translateX(-50%);
|
|
1404
1718
|
padding: 7px 14px;
|
|
@@ -1408,48 +1722,46 @@
|
|
|
1408
1722
|
color: var(--text-accent);
|
|
1409
1723
|
font-size: 11px;
|
|
1410
1724
|
cursor: pointer;
|
|
1411
|
-
box-shadow:
|
|
1725
|
+
box-shadow: var(--elev-cue);
|
|
1412
1726
|
}
|
|
1413
1727
|
|
|
1414
1728
|
@media (max-width: 420px) {
|
|
1415
1729
|
.place-at-center {
|
|
1416
|
-
bottom:
|
|
1730
|
+
bottom: 78px;
|
|
1417
1731
|
}
|
|
1418
1732
|
}
|
|
1419
1733
|
|
|
1420
1734
|
.map :global(.maplibregl-ctrl-bottom-right) {
|
|
1421
1735
|
right: 20px;
|
|
1422
|
-
|
|
1423
|
-
bottom: calc(18px + var(--chrome-inset, 0px));
|
|
1736
|
+
bottom: 12px;
|
|
1424
1737
|
display: flex;
|
|
1425
|
-
flex-direction:
|
|
1738
|
+
flex-direction: row;
|
|
1426
1739
|
align-items: flex-end;
|
|
1427
|
-
gap:
|
|
1740
|
+
gap: 16px;
|
|
1428
1741
|
transition:
|
|
1429
1742
|
bottom var(--dur-med) var(--ease-out),
|
|
1430
1743
|
opacity var(--dur-fast) var(--ease-out);
|
|
1431
1744
|
}
|
|
1432
1745
|
|
|
1433
|
-
/* The sheet has taken the map; nothing left down there to attribute. */
|
|
1434
|
-
.map-stage.chrome-clear :global(.maplibregl-ctrl-bottom-right) {
|
|
1435
|
-
opacity: 0;
|
|
1436
|
-
pointer-events: none;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
1746
|
.map :global(.maplibregl-ctrl-bottom-right .maplibregl-ctrl) {
|
|
1440
1747
|
float: none;
|
|
1441
1748
|
margin: 0;
|
|
1442
1749
|
}
|
|
1443
1750
|
|
|
1751
|
+
.map :global(.maplibregl-ctrl-group) {
|
|
1752
|
+
order: 2;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1444
1755
|
.map :global(.maplibregl-ctrl-attrib) {
|
|
1445
|
-
order:
|
|
1756
|
+
order: 1;
|
|
1446
1757
|
position: relative;
|
|
1447
|
-
|
|
1758
|
+
bottom: 0;
|
|
1759
|
+
right: 0;
|
|
1448
1760
|
width: auto;
|
|
1449
1761
|
min-width: 0;
|
|
1450
1762
|
max-width: calc(100vw - 96px);
|
|
1451
1763
|
box-sizing: border-box;
|
|
1452
|
-
background:
|
|
1764
|
+
background: rgb(var(--paper-rgb) / 0.75);
|
|
1453
1765
|
font-family: var(--font-mono);
|
|
1454
1766
|
font-size: 10px;
|
|
1455
1767
|
white-space: nowrap;
|
|
@@ -1490,35 +1802,39 @@
|
|
|
1490
1802
|
line-height: 1.5;
|
|
1491
1803
|
padding: 8px 10px !important;
|
|
1492
1804
|
border-radius: 2px;
|
|
1493
|
-
box-shadow:
|
|
1805
|
+
box-shadow: var(--elev-tooltip);
|
|
1494
1806
|
}
|
|
1495
1807
|
|
|
1496
1808
|
@media (max-width: 760px) {
|
|
1809
|
+
.map :global(.maplibregl-ctrl-bottom-right) {
|
|
1810
|
+
bottom: 12px;
|
|
1811
|
+
right: 12px;
|
|
1812
|
+
gap: 12px;
|
|
1813
|
+
}
|
|
1497
1814
|
.map :global(.maplibregl-ctrl-attrib) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
order: 1;
|
|
1815
|
+
position: relative;
|
|
1816
|
+
bottom: auto;
|
|
1501
1817
|
width: auto;
|
|
1502
1818
|
min-width: 0;
|
|
1503
1819
|
right: 0;
|
|
1504
1820
|
white-space: normal;
|
|
1505
1821
|
}
|
|
1506
1822
|
|
|
1507
|
-
/* The band of map a phone has left between the header and the sheet is
|
|
1508
|
-
about the height of the solve card, which covers the buttons wherever
|
|
1509
|
-
they land. Pinch and double tap zoom; the attribution stays. */
|
|
1510
1823
|
.map :global(.maplibregl-ctrl-bottom-right .maplibregl-ctrl-group) {
|
|
1511
|
-
display:
|
|
1824
|
+
display: flex;
|
|
1825
|
+
}
|
|
1826
|
+
.map :global(.maplibregl-ctrl-group button + button) {
|
|
1827
|
+
border-top: 0;
|
|
1828
|
+
border-left: 1px solid var(--line);
|
|
1512
1829
|
}
|
|
1513
1830
|
}
|
|
1514
1831
|
|
|
1515
|
-
/*
|
|
1516
|
-
sitting over the network. */
|
|
1832
|
+
/* Attribution shares the bottom row with touch-sized zoom buttons. */
|
|
1517
1833
|
@media (max-width: 460px) {
|
|
1518
1834
|
.map :global(.maplibregl-ctrl-attrib) {
|
|
1519
|
-
max-width: calc(100vw -
|
|
1835
|
+
max-width: calc(100vw - 140px);
|
|
1520
1836
|
font-size: 9px;
|
|
1521
|
-
white-space:
|
|
1837
|
+
white-space: normal;
|
|
1522
1838
|
}
|
|
1523
1839
|
}
|
|
1524
1840
|
|