@tellegen/svelte 0.2.0 → 0.3.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 +36 -5
- package/dist/TellegenMap.svelte +325 -163
- package/dist/TellegenProvider.svelte +4 -0
- package/dist/TellegenShell.svelte +19 -13
- package/dist/api.d.ts +8 -1
- package/dist/api.js +6 -2
- package/dist/colors.d.ts +7 -7
- package/dist/colors.js +13 -13
- package/dist/components/AppHeader.svelte +23 -11
- package/dist/components/BindingLines.svelte +3 -6
- package/dist/components/BusPicker.svelte +8 -2
- package/dist/components/ControlPanel.svelte +106 -343
- package/dist/components/DemandSlider.svelte +21 -23
- package/dist/components/DiagramCanvas.svelte +486 -0
- package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
- package/dist/components/DisplayControls.svelte +4 -2
- package/dist/components/FormulationSelector.svelte +8 -3
- package/dist/components/LocalCaseDetails.svelte +55 -45
- package/dist/components/McResults.svelte +365 -0
- package/dist/components/McResults.svelte.d.ts +11 -0
- package/dist/components/ModelDetails.svelte +230 -0
- package/dist/components/ModelDetails.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +227 -78
- package/dist/components/NetworkStats.svelte +20 -18
- package/dist/components/NotificationCenter.svelte +243 -0
- package/dist/components/NotificationCenter.svelte.d.ts +3 -0
- package/dist/components/PanelCard.svelte +319 -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 +212 -0
- package/dist/components/PanelHost.svelte.d.ts +3 -0
- package/dist/components/PowerFlowDetails.svelte +99 -0
- package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
- package/dist/components/RatingSlider.svelte +5 -9
- package/dist/components/SavedNetworkDetails.svelte +214 -0
- package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
- package/dist/components/SensitivityReadout.svelte +24 -16
- package/dist/components/SolveCard.svelte +46 -49
- package/dist/components/TopMovers.svelte +8 -3
- package/dist/components/TopMovers.svelte.d.ts +2 -17
- package/dist/components.d.ts +2 -0
- package/dist/components.js +2 -0
- package/dist/controller.svelte.d.ts +54 -33
- package/dist/controller.svelte.js +927 -233
- 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.js +5 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +7 -2
- package/dist/map.d.ts +1 -1
- package/dist/map.js +1 -1
- 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 +82 -0
- package/dist/panels.svelte.d.ts +65 -0
- package/dist/panels.svelte.js +215 -0
- package/dist/state.svelte.d.ts +90 -23
- package/dist/state.svelte.js +94 -20
- package/dist/synthetic-layout.js +1 -0
- package/package.json +22 -9
- package/dist/study-package.d.ts +0 -8
- package/dist/study-package.js +0 -17
package/dist/TellegenMap.svelte
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { tick } 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 {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
branchWidth,
|
|
12
12
|
busNeutral,
|
|
13
13
|
busRadius,
|
|
14
|
-
|
|
14
|
+
priceColor,
|
|
15
15
|
scalarDomain,
|
|
16
16
|
sensColor,
|
|
17
17
|
sensFlatColor,
|
|
@@ -29,13 +29,16 @@
|
|
|
29
29
|
transformerMarks,
|
|
30
30
|
type PlacedMultiBus,
|
|
31
31
|
type PlacedMultiEdge,
|
|
32
|
-
type TransformerMark
|
|
32
|
+
type TransformerMark,
|
|
33
|
+
multiDiagramNetwork
|
|
33
34
|
} from './multiconductor.js';
|
|
34
35
|
import { transformerIcon } from './transformer-icon.js';
|
|
35
36
|
import { foldMapBounds } from './map-bounds.js';
|
|
36
37
|
import { getAppState, getController } from './context.svelte.js';
|
|
37
38
|
import { displayFmt, fmt, rgbaCss } from './format.js';
|
|
39
|
+
import { getPanelLayout } from './panels.svelte.js';
|
|
38
40
|
import type { RGBA } from './colors.js';
|
|
41
|
+
import DiagramCanvas from './components/DiagramCanvas.svelte';
|
|
39
42
|
|
|
40
43
|
let {
|
|
41
44
|
onbusclick,
|
|
@@ -59,6 +62,47 @@
|
|
|
59
62
|
|
|
60
63
|
const app = getAppState();
|
|
61
64
|
const ctrl = getController();
|
|
65
|
+
const panels = getPanelLayout();
|
|
66
|
+
const diagramCase = $derived(app.studyView ? null : (app.active ?? app.activeLocal));
|
|
67
|
+
const diagramMulti = $derived(
|
|
68
|
+
!app.studyView && app.activeMulti?.coordsKind !== 'geographic' ? app.activeMulti : null
|
|
69
|
+
);
|
|
70
|
+
const multiNetwork = $derived(
|
|
71
|
+
diagramMulti?.view
|
|
72
|
+
? multiDiagramNetwork(diagramMulti.id, diagramMulti.label, diagramMulti.view)
|
|
73
|
+
: null
|
|
74
|
+
);
|
|
75
|
+
const displayedNetwork = $derived(app.studyView?.network ?? diagramCase?.network);
|
|
76
|
+
const diagramNetwork = $derived(
|
|
77
|
+
multiNetwork ?? (displayedNetwork?.coordinate_space === 'diagram' ? displayedNetwork : null)
|
|
78
|
+
);
|
|
79
|
+
const diagramCaseId = $derived(
|
|
80
|
+
app.studyView?.caseId ?? diagramMulti?.id ?? diagramCase?.id ?? 'diagram'
|
|
81
|
+
);
|
|
82
|
+
function selectDiagramBus(busId: number) {
|
|
83
|
+
if (diagramMulti?.view) {
|
|
84
|
+
const bus = diagramMulti.view.buses[busId];
|
|
85
|
+
if (bus) onmultibusclick(diagramMulti.id, bus.id);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (app.studyView) {
|
|
89
|
+
app.selectedBranch = null;
|
|
90
|
+
app.selectedBus = busId;
|
|
91
|
+
} else if (app.activeLocal) onlocalbusclick(diagramCaseId, busId);
|
|
92
|
+
else onbusclick(diagramCaseId, busId);
|
|
93
|
+
}
|
|
94
|
+
function selectDiagramBranch(branchId: number) {
|
|
95
|
+
if (diagramMulti?.view) {
|
|
96
|
+
const edge = diagramMulti.view.edges[branchId];
|
|
97
|
+
if (edge) onmultiedgeclick?.(diagramMulti.id, edge.id);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (app.studyView) {
|
|
101
|
+
app.selectedBus = null;
|
|
102
|
+
app.selectedBranch = branchId;
|
|
103
|
+
} else if (app.activeLocal) onlocalbranchclick?.(diagramCaseId, branchId);
|
|
104
|
+
else onbranchclick?.(diagramCaseId, branchId);
|
|
105
|
+
}
|
|
62
106
|
|
|
63
107
|
const STYLE = 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json';
|
|
64
108
|
const CUE_MARGIN_PX = 64;
|
|
@@ -66,7 +110,7 @@
|
|
|
66
110
|
const BRANCH_FOCUS_MAX_ZOOM = 9.5;
|
|
67
111
|
|
|
68
112
|
let map = $state.raw<MapLibreMap | null>(null);
|
|
69
|
-
let overlay = $state.raw<
|
|
113
|
+
let overlay = $state.raw<MapLibreOverlay | null>(null);
|
|
70
114
|
// Bumped to remount the map after a WebGL context is lost and cannot be
|
|
71
115
|
// repainted in place. See initMap's context-loss handling.
|
|
72
116
|
let mapGen = $state(0);
|
|
@@ -85,7 +129,7 @@
|
|
|
85
129
|
let layerCtors = $state.raw<LayerCtors | null>(null);
|
|
86
130
|
|
|
87
131
|
interface CaseDisplay {
|
|
88
|
-
|
|
132
|
+
prices: Map<number, number>;
|
|
89
133
|
scalar: Map<number, number>;
|
|
90
134
|
scalarLo: number;
|
|
91
135
|
scalarHi: number;
|
|
@@ -93,7 +137,7 @@
|
|
|
93
137
|
scalarLabel: string;
|
|
94
138
|
scalarUnit: string;
|
|
95
139
|
loading: Map<number, number>;
|
|
96
|
-
mode: '
|
|
140
|
+
mode: 'price' | 'sens' | 'preview';
|
|
97
141
|
sens: Map<number, number>;
|
|
98
142
|
sensDomain: SensitivityDomain | null;
|
|
99
143
|
preview: Map<number, number>;
|
|
@@ -106,7 +150,7 @@
|
|
|
106
150
|
type SolvableCase = CaseState | LocalCase;
|
|
107
151
|
|
|
108
152
|
// Everything the accessors need, rebuilt when any case's data moves. The
|
|
109
|
-
// LMP scale normalizes per network: each case is an islanded
|
|
153
|
+
// The LMP scale normalizes per network: each case is an islanded network, so
|
|
110
154
|
// within network structure beats cross network color comparability (the
|
|
111
155
|
// panel legend and tooltips carry the actual numbers). Preview values
|
|
112
156
|
// shift individual buses without rescaling.
|
|
@@ -114,8 +158,8 @@
|
|
|
114
158
|
const active = app.active ?? app.activeLocal;
|
|
115
159
|
const selectedBus = app.selectedBus;
|
|
116
160
|
const selectedBranch = app.selectedBranch;
|
|
117
|
-
// The active case's column for the live selection: the
|
|
118
|
-
// selected bus, or the
|
|
161
|
+
// The active case's column for the live selection: the price-demand column at the
|
|
162
|
+
// selected bus, or the price-rating column at the selected branch. Both are
|
|
119
163
|
// bus-keyed value maps, so the color pipeline below is target-agnostic.
|
|
120
164
|
const activeSensitivity =
|
|
121
165
|
active &&
|
|
@@ -125,18 +169,18 @@
|
|
|
125
169
|
: null;
|
|
126
170
|
const activeDeltas = active ? caseDeltas(active) : {};
|
|
127
171
|
const activeRatings = active ? caseRatings(active) : {};
|
|
128
|
-
// Engine first
|
|
172
|
+
// Engine first order preview (Study.preview): predicted per-bus LMP change for
|
|
129
173
|
// the live drag, scoped to this case + selection target. Preferred over the JS
|
|
130
174
|
// gradient shift when present; the gradient path stays for server and browser
|
|
131
175
|
// fallbacks.
|
|
132
176
|
const enginePreview =
|
|
133
177
|
active &&
|
|
134
|
-
app.
|
|
135
|
-
app.
|
|
136
|
-
('bus' in app.
|
|
137
|
-
? app.
|
|
138
|
-
: app.
|
|
139
|
-
? app.
|
|
178
|
+
app.previewPrices &&
|
|
179
|
+
app.previewPrices.caseId === active.id &&
|
|
180
|
+
('bus' in app.previewPrices.target
|
|
181
|
+
? app.previewPrices.target.bus === selectedBus
|
|
182
|
+
: app.previewPrices.target.branch === selectedBranch)
|
|
183
|
+
? app.previewPrices.delta
|
|
140
184
|
: null;
|
|
141
185
|
const previewStep =
|
|
142
186
|
active && activeSensitivity
|
|
@@ -153,13 +197,13 @@
|
|
|
153
197
|
const perCase = new Map<string, CaseDisplay>();
|
|
154
198
|
const addCase = (c: CaseState | LocalCase) => {
|
|
155
199
|
if (!c.network || !c.solution) return;
|
|
156
|
-
const
|
|
157
|
-
for (const e of c.solution.
|
|
200
|
+
const prices = new Map<number, number>();
|
|
201
|
+
for (const e of c.solution.prices) prices.set(e.bus, e.value);
|
|
158
202
|
// Resolve the active display mode against what this case can show, falling
|
|
159
|
-
// back to
|
|
203
|
+
// back to LMPs when the chosen mode doesn't apply (e.g. |V| under DC OPF).
|
|
160
204
|
const meta = displayMetaFor(c);
|
|
161
205
|
const activeMeta = meta.find((o) => o.mode === app.displayMode) ?? meta[0] ?? null;
|
|
162
|
-
const scalarMode = activeMeta?.mode ?? '
|
|
206
|
+
const scalarMode = activeMeta?.mode ?? 'price';
|
|
163
207
|
const seriesValues = displaySeriesFor(c, scalarMode);
|
|
164
208
|
const scalar = new Map<number, number>();
|
|
165
209
|
for (const e of seriesValues) scalar.set(e.bus, e.value);
|
|
@@ -175,10 +219,10 @@
|
|
|
175
219
|
let domain: SensitivityDomain | null = null;
|
|
176
220
|
const isActive = c === active;
|
|
177
221
|
if (isActive && enginePreview) {
|
|
178
|
-
// Engine preview owns the LMP shift
|
|
179
|
-
for (const [bus,
|
|
180
|
-
preview.set(bus,
|
|
181
|
-
|
|
222
|
+
// Engine preview owns the LMP shift.
|
|
223
|
+
for (const [bus, deltaPrice] of enginePreview) {
|
|
224
|
+
preview.set(bus, deltaPrice);
|
|
225
|
+
prices.set(bus, (prices.get(bus) ?? 0) + deltaPrice);
|
|
182
226
|
}
|
|
183
227
|
}
|
|
184
228
|
if (isActive && activeSensitivity) {
|
|
@@ -190,9 +234,9 @@
|
|
|
190
234
|
if (!enginePreview && previewStep !== 0) {
|
|
191
235
|
// Fallback first order preview: shift LMPs along the gradient.
|
|
192
236
|
for (const v of activeSensitivity.values) {
|
|
193
|
-
const
|
|
194
|
-
preview.set(v.bus,
|
|
195
|
-
|
|
237
|
+
const deltaPrice = v.value * previewStep;
|
|
238
|
+
preview.set(v.bus, deltaPrice);
|
|
239
|
+
prices.set(v.bus, (prices.get(v.bus) ?? 0) + deltaPrice);
|
|
196
240
|
}
|
|
197
241
|
}
|
|
198
242
|
}
|
|
@@ -203,16 +247,15 @@
|
|
|
203
247
|
previewDomain &&
|
|
204
248
|
(app.previewActive || c.solving || enginePreview || Math.abs(previewStep) >= 0.25)
|
|
205
249
|
);
|
|
206
|
-
// The sensitivity and demand
|
|
207
|
-
// and the ΔLMP preview). They only paint the map under the LMP display mode;
|
|
250
|
+
// The sensitivity and demand preview overlays are LMP fields. They only paint the map under the price display mode;
|
|
208
251
|
// under angle or |V| the map stays on the chosen scalar and the sensitivity
|
|
209
252
|
// shows in the side panel, so selecting a bus never silently overrides the
|
|
210
253
|
// angle/|V| coloring with the price ramp.
|
|
211
|
-
const sensEligible = scalarMode === '
|
|
254
|
+
const sensEligible = scalarMode === 'price';
|
|
212
255
|
// While a sensitivity is still loading for the selected bus, stay in sens
|
|
213
|
-
// mode (renders neutral, not the prior
|
|
256
|
+
// mode (renders neutral, not the prior price palette) so the node colors
|
|
214
257
|
// don't flash orange->purple between the old and new selection.
|
|
215
|
-
const mode: '
|
|
258
|
+
const mode: 'price' | 'sens' | 'preview' =
|
|
216
259
|
sensEligible && showPreview
|
|
217
260
|
? 'preview'
|
|
218
261
|
: sensEligible &&
|
|
@@ -221,15 +264,15 @@
|
|
|
221
264
|
(domain || app.sensitivityLoading) &&
|
|
222
265
|
!previewing
|
|
223
266
|
? 'sens'
|
|
224
|
-
: '
|
|
267
|
+
: 'price';
|
|
225
268
|
perCase.set(c.id, {
|
|
226
|
-
|
|
269
|
+
prices,
|
|
227
270
|
scalar,
|
|
228
271
|
scalarLo,
|
|
229
272
|
scalarHi,
|
|
230
273
|
scalarMode,
|
|
231
274
|
scalarLabel: activeMeta?.label ?? 'LMP',
|
|
232
|
-
scalarUnit: activeMeta?.unit ?? '
|
|
275
|
+
scalarUnit: activeMeta?.unit ?? 'objective units/MW',
|
|
233
276
|
loading,
|
|
234
277
|
mode,
|
|
235
278
|
sens,
|
|
@@ -266,7 +309,7 @@
|
|
|
266
309
|
return sensColor((d.preview.get(bus.id) ?? 0) / d.previewDomain.scale);
|
|
267
310
|
}
|
|
268
311
|
const mid = (d.scalarLo + d.scalarHi) / 2;
|
|
269
|
-
return
|
|
312
|
+
return priceColor(((d.scalar.get(bus.id) ?? mid) - d.scalarLo) / (d.scalarHi - d.scalarLo));
|
|
270
313
|
};
|
|
271
314
|
}
|
|
272
315
|
|
|
@@ -282,34 +325,38 @@
|
|
|
282
325
|
// click handler must not treat those clicks as "empty map" and clear the selection.
|
|
283
326
|
function isSelectableLayer(layerId: string | undefined): boolean {
|
|
284
327
|
return Boolean(
|
|
328
|
+
layerId?.startsWith('study-') ||
|
|
285
329
|
layerId?.startsWith('buses-') ||
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
330
|
+
layerId?.startsWith('local-buses-') ||
|
|
331
|
+
layerId?.startsWith('branches-') ||
|
|
332
|
+
layerId?.startsWith('local-branches-') ||
|
|
333
|
+
layerId?.startsWith('multi-buses-') ||
|
|
334
|
+
layerId?.startsWith('multi-edges-')
|
|
291
335
|
);
|
|
292
336
|
}
|
|
293
337
|
|
|
338
|
+
function displayedGeometry(caseId: string) {
|
|
339
|
+
if (app.studyView) return app.studyView.caseId === caseId ? app.studyView.network : null;
|
|
340
|
+
const local = app.localCases.find((c) => c.id === caseId);
|
|
341
|
+
return app.byId(caseId)?.network ?? local?.view ?? local?.network ?? null;
|
|
342
|
+
}
|
|
294
343
|
const selectedBusCue = $derived.by(() => {
|
|
295
344
|
const busId = app.selectedBus;
|
|
296
|
-
const
|
|
297
|
-
if (!
|
|
298
|
-
const buses
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
345
|
+
const caseId = app.studyView?.caseId ?? app.active?.id ?? app.activeLocal?.id;
|
|
346
|
+
if (!caseId || busId === null) return null;
|
|
347
|
+
const bus = displayedGeometry(caseId)?.buses.find((b) => b.id === busId);
|
|
348
|
+
return bus
|
|
349
|
+
? { key: `${app.studyView?.id ?? caseId}-${bus.id}`, id: bus.id, lon: bus.lon, lat: bus.lat }
|
|
350
|
+
: null;
|
|
302
351
|
});
|
|
303
352
|
const selectedBranchCue = $derived.by(() => {
|
|
304
353
|
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);
|
|
354
|
+
const caseId = app.studyView?.caseId ?? app.active?.id ?? app.activeLocal?.id;
|
|
355
|
+
if (!caseId || branchId === null) return null;
|
|
356
|
+
const branch = displayedGeometry(caseId)?.branches.find((b) => b.id === branchId);
|
|
310
357
|
const path = branch?.path.filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat));
|
|
311
358
|
return branch && path && path.length >= 2
|
|
312
|
-
? { key: `${
|
|
359
|
+
? { key: `${app.studyView?.id ?? caseId}-${branch.id}`, caseId, branchId: branch.id, path }
|
|
313
360
|
: null;
|
|
314
361
|
});
|
|
315
362
|
let selectedCueEl = $state.raw<HTMLDivElement | null>(null);
|
|
@@ -317,6 +364,7 @@
|
|
|
317
364
|
let selectedBranchHaloPath = $state.raw<SVGPathElement | null>(null);
|
|
318
365
|
let selectedBranchLinePath = $state.raw<SVGPathElement | null>(null);
|
|
319
366
|
let handledFrameSeq = 0;
|
|
367
|
+
let handledCameraSeq = 0;
|
|
320
368
|
|
|
321
369
|
// The selected multiconductor bus and its incident edges, whose per-conductor
|
|
322
370
|
// strands and terminal stack the detail overlay draws in screen space.
|
|
@@ -492,18 +540,23 @@
|
|
|
492
540
|
});
|
|
493
541
|
}
|
|
494
542
|
|
|
495
|
-
/** Camera
|
|
496
|
-
* The branch variant also clears the rating readout on the panel's lower edge. */
|
|
543
|
+
/** Camera framing reserves the dock columns and the active compact drawer. */
|
|
497
544
|
function framePadding(width: number, height: number, kind: 'case' | 'branch') {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
545
|
+
const clearance = kind === 'branch' ? 16 : 28;
|
|
546
|
+
if (panels.compact) {
|
|
547
|
+
return {
|
|
548
|
+
top: panels.top + 8,
|
|
549
|
+
left: 24,
|
|
550
|
+
right: 24,
|
|
551
|
+
bottom: Math.min(app.sheetInset + clearance, Math.round(height * 0.6))
|
|
552
|
+
};
|
|
503
553
|
}
|
|
504
|
-
return
|
|
505
|
-
|
|
506
|
-
:
|
|
554
|
+
return {
|
|
555
|
+
top: panels.top + 16,
|
|
556
|
+
left: Math.min(panels.dockWidth('left') + 44, width * 0.34),
|
|
557
|
+
right: Math.min(panels.dockWidth('right') + 44, width * 0.34),
|
|
558
|
+
bottom: 80
|
|
559
|
+
};
|
|
507
560
|
}
|
|
508
561
|
|
|
509
562
|
function branchBounds(path: [number, number][]): LngLatBoundsLike | null {
|
|
@@ -512,8 +565,7 @@
|
|
|
512
565
|
|
|
513
566
|
function prefersReducedMotion(): boolean {
|
|
514
567
|
return (
|
|
515
|
-
typeof window !== 'undefined' &&
|
|
516
|
-
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
568
|
+
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
517
569
|
);
|
|
518
570
|
}
|
|
519
571
|
|
|
@@ -521,25 +573,9 @@
|
|
|
521
573
|
return typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches;
|
|
522
574
|
}
|
|
523
575
|
|
|
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);
|
|
576
|
+
function focusGeometry(points: [number, number][], settle: () => void): boolean {
|
|
577
|
+
if (!map) return false;
|
|
578
|
+
const bounds = branchBounds(points);
|
|
543
579
|
if (!bounds) return false;
|
|
544
580
|
const { clientWidth, clientHeight } = map.getContainer();
|
|
545
581
|
const camera = map.cameraForBounds(bounds, {
|
|
@@ -547,6 +583,7 @@
|
|
|
547
583
|
maxZoom: Math.min(map.getZoom() + 2, BRANCH_FOCUS_MAX_ZOOM)
|
|
548
584
|
});
|
|
549
585
|
if (!camera?.center) return false;
|
|
586
|
+
map.once('moveend', settle);
|
|
550
587
|
map.easeTo({
|
|
551
588
|
center: camera.center,
|
|
552
589
|
zoom: camera.zoom ?? map.getZoom(),
|
|
@@ -580,6 +617,21 @@
|
|
|
580
617
|
function tooltip(info: PickingInfo): { html: string } | null {
|
|
581
618
|
const { object } = info;
|
|
582
619
|
if (!object) return null;
|
|
620
|
+
if (app.studyView && info.layer?.id.startsWith('study-')) {
|
|
621
|
+
const view = app.studyView.solution;
|
|
622
|
+
const id = Number((object as { id: number }).id);
|
|
623
|
+
if (info.layer.id === 'study-branches') {
|
|
624
|
+
const flow = view?.flows?.find((f) => f.branch === id);
|
|
625
|
+
return {
|
|
626
|
+
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)}%` : ''}`
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const p = view?.lmp?.find((v) => v.bus === id),
|
|
630
|
+
v = view?.vm?.find((v) => v.bus === id);
|
|
631
|
+
return {
|
|
632
|
+
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` : ''}`
|
|
633
|
+
};
|
|
634
|
+
}
|
|
583
635
|
if (info.layer?.id.startsWith('local-subs-')) {
|
|
584
636
|
// PowerWorld .pwd substation: display data only, position inferred from the diagram.
|
|
585
637
|
// The name is free text from a dropped file, so escape it; the number
|
|
@@ -587,7 +639,7 @@
|
|
|
587
639
|
const s = object as { number: number; name: string };
|
|
588
640
|
const named = s.name ? ` ${esc(s.name)}` : '';
|
|
589
641
|
return {
|
|
590
|
-
html: `<div class="tt"><b>substation ${Number(s.number)}</b>${named}<br><span style="opacity:0.6">.pwd diagram
|
|
642
|
+
html: `<div class="tt"><b>substation ${Number(s.number)}</b>${named}<br><span style="opacity:0.6">.pwd diagram, approximate position</span></div>`
|
|
591
643
|
};
|
|
592
644
|
}
|
|
593
645
|
const layerId = info.layer?.id;
|
|
@@ -627,7 +679,7 @@
|
|
|
627
679
|
<span style="opacity:0.6">local file</span></div>`
|
|
628
680
|
};
|
|
629
681
|
}
|
|
630
|
-
const
|
|
682
|
+
const price = d?.prices.get(bus.id);
|
|
631
683
|
const scalar = d?.scalar.get(bus.id);
|
|
632
684
|
const sens = d?.mode === 'sens' ? d.sens.get(bus.id) : undefined;
|
|
633
685
|
const preview = d?.mode === 'preview' ? d.preview.get(bus.id) : undefined;
|
|
@@ -639,19 +691,19 @@
|
|
|
639
691
|
const sensRow =
|
|
640
692
|
sens === undefined
|
|
641
693
|
? ''
|
|
642
|
-
: `<br>∂
|
|
694
|
+
: `<br>∂price/∂d ${sens >= 0 ? '+' : ''}${sens.toExponential(2)}`;
|
|
643
695
|
const previewRow =
|
|
644
696
|
preview === undefined
|
|
645
697
|
? ''
|
|
646
|
-
: `<br>ΔLMP ${preview >= 0 ? '+' : ''}${preview.toExponential(2)}
|
|
698
|
+
: `<br>Δ LMP ${preview >= 0 ? '+' : ''}${preview.toExponential(2)} objective units/MW`;
|
|
647
699
|
const localRow =
|
|
648
700
|
c instanceof CaseState ? '' : '<br><span style="opacity:0.6">local file</span>';
|
|
649
701
|
const scalarRow =
|
|
650
|
-
!d || d.scalarMode === '
|
|
651
|
-
? `LMP ${
|
|
702
|
+
!d || d.scalarMode === 'price'
|
|
703
|
+
? `LMP ${price === undefined ? '—' : fmt.format(price)} objective units/MW`
|
|
652
704
|
: `${d.scalarLabel} ${
|
|
653
705
|
scalar === undefined ? '—' : displayFmt(d.scalarMode, scalar)
|
|
654
|
-
} ${d.scalarUnit}<br>LMP ${
|
|
706
|
+
} ${d.scalarUnit}<br>LMP ${price === undefined ? '—' : fmt.format(price)} objective units/MW`;
|
|
655
707
|
return {
|
|
656
708
|
html: `<div class="tt"><b>bus ${bus.id}</b>
|
|
657
709
|
${scalarRow}<br>
|
|
@@ -660,14 +712,17 @@
|
|
|
660
712
|
}
|
|
661
713
|
|
|
662
714
|
async function loadMapModules() {
|
|
663
|
-
const [maplibre,
|
|
715
|
+
const [maplibre, deckMap, layers, worker] = await Promise.all([
|
|
664
716
|
import('maplibre-gl'),
|
|
665
|
-
import('@deck.gl/
|
|
666
|
-
import('@deck.gl/layers')
|
|
717
|
+
import('@deck.gl/maplibre'),
|
|
718
|
+
import('@deck.gl/layers'),
|
|
719
|
+
import('maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url')
|
|
667
720
|
]);
|
|
721
|
+
// Bundle the worker with its shared imports and configure it before creating a map.
|
|
722
|
+
maplibre.setWorkerUrl(worker.default);
|
|
668
723
|
return {
|
|
669
|
-
maplibregl: maplibre
|
|
670
|
-
|
|
724
|
+
maplibregl: maplibre,
|
|
725
|
+
MapLibreOverlay: deckMap.MapLibreOverlay,
|
|
671
726
|
PathLayer: layers.PathLayer,
|
|
672
727
|
ScatterplotLayer: layers.ScatterplotLayer,
|
|
673
728
|
IconLayer: layers.IconLayer
|
|
@@ -682,9 +737,9 @@
|
|
|
682
737
|
// memory and tab pressure, and one context survives that far better; the
|
|
683
738
|
// deck layers also inherit maplibre's 4096 canvas clamp. The flat positron
|
|
684
739
|
// basemap writes no depth, so the overlay always draws on top.
|
|
685
|
-
function buildOverlay(
|
|
740
|
+
function buildOverlay(MapLibreOverlay: MapModules['MapLibreOverlay']): MapLibreOverlay {
|
|
686
741
|
const touch = coarsePointer();
|
|
687
|
-
return new
|
|
742
|
+
return new MapLibreOverlay({
|
|
688
743
|
interleaved: true,
|
|
689
744
|
layers: [],
|
|
690
745
|
parameters: {
|
|
@@ -707,7 +762,7 @@
|
|
|
707
762
|
let cleanup = () => {};
|
|
708
763
|
let cancelled = false;
|
|
709
764
|
void loadMapModules()
|
|
710
|
-
.then(({ maplibregl,
|
|
765
|
+
.then(({ maplibregl, MapLibreOverlay, PathLayer, ScatterplotLayer, IconLayer }) => {
|
|
711
766
|
if (cancelled) return;
|
|
712
767
|
layerCtors = { PathLayer, ScatterplotLayer, IconLayer };
|
|
713
768
|
const m = new maplibregl.Map({
|
|
@@ -718,7 +773,7 @@
|
|
|
718
773
|
canvasContextAttributes: { antialias: true },
|
|
719
774
|
attributionControl: { compact: true }
|
|
720
775
|
});
|
|
721
|
-
const o = buildOverlay(
|
|
776
|
+
const o = buildOverlay(MapLibreOverlay);
|
|
722
777
|
m.addControl(o);
|
|
723
778
|
m.on('click', (e) => {
|
|
724
779
|
if (app.placingId) onplacecase(e.lngLat.lng, e.lngLat.lat);
|
|
@@ -743,6 +798,16 @@
|
|
|
743
798
|
if (document.visibilityState === 'visible') repaint();
|
|
744
799
|
};
|
|
745
800
|
const syncCues = () => scheduleCueSync();
|
|
801
|
+
const saveCamera = () => {
|
|
802
|
+
const center = m.getCenter();
|
|
803
|
+
app.camera = {
|
|
804
|
+
center: [center.lng, center.lat],
|
|
805
|
+
zoom: m.getZoom(),
|
|
806
|
+
bearing: m.getBearing(),
|
|
807
|
+
pitch: m.getPitch()
|
|
808
|
+
};
|
|
809
|
+
};
|
|
810
|
+
m.on('moveend', saveCamera);
|
|
746
811
|
let rebuildTimer: ReturnType<typeof setTimeout> | null = null;
|
|
747
812
|
const clearRebuild = () => {
|
|
748
813
|
if (rebuildTimer !== null) {
|
|
@@ -797,7 +862,9 @@
|
|
|
797
862
|
// (from the case load) can fire before the async map import finishes and
|
|
798
863
|
// be lost; re-issuing it here ensures the first view is the active case,
|
|
799
864
|
// not the default world view. A remount opens at the default view too.
|
|
800
|
-
|
|
865
|
+
if (mapGen > 0 && app.camera) m.jumpTo(app.camera);
|
|
866
|
+
else if (app.cameraSeq === handledCameraSeq)
|
|
867
|
+
app.requestFrame(app.activeCaseId ?? app.activeLocalId ?? 'all');
|
|
801
868
|
if (mapGen > 0) {
|
|
802
869
|
// This is a rebuilt map. Once it has held a live context without
|
|
803
870
|
// another loss, zero the budget so the cap only ever catches a tight
|
|
@@ -838,6 +905,74 @@
|
|
|
838
905
|
if (!overlay || !layerCtors) return;
|
|
839
906
|
const { PathLayer, ScatterplotLayer, IconLayer } = layerCtors;
|
|
840
907
|
const layers: Layer[] = [];
|
|
908
|
+
const study = app.studyView;
|
|
909
|
+
if (study) {
|
|
910
|
+
const s = study.solution;
|
|
911
|
+
const mode = app.displayMode;
|
|
912
|
+
const series =
|
|
913
|
+
mode === 'angle'
|
|
914
|
+
? (s?.va ?? [])
|
|
915
|
+
: mode === 'voltage'
|
|
916
|
+
? (s?.vm ??
|
|
917
|
+
(s?.w ?? []).map((v) => ({
|
|
918
|
+
...v,
|
|
919
|
+
value: Math.sqrt(Math.max(0, v.value))
|
|
920
|
+
})))
|
|
921
|
+
: (s?.lmp ?? []);
|
|
922
|
+
const values = new Map(series.map((v) => [v.bus, v.value]));
|
|
923
|
+
const domain = scalarDomain(
|
|
924
|
+
mode,
|
|
925
|
+
series.map((v) => v.value)
|
|
926
|
+
);
|
|
927
|
+
const flows = new Map((s?.flows ?? []).map((f) => [f.branch, f.loading]));
|
|
928
|
+
layers.push(
|
|
929
|
+
new PathLayer<NetworkBranch>({
|
|
930
|
+
id: 'study-branches',
|
|
931
|
+
data: study.network.branches,
|
|
932
|
+
getPath: (b) => b.path,
|
|
933
|
+
getColor: (b) => branchColor(flows.get(b.id) ?? 0, b.status === 1),
|
|
934
|
+
getWidth: (b) => branchWidth(flows.get(b.id) ?? 0),
|
|
935
|
+
widthUnits: 'pixels',
|
|
936
|
+
widthMinPixels: 1.5,
|
|
937
|
+
pickable: true,
|
|
938
|
+
onClick: (info: PickingInfo) => {
|
|
939
|
+
const branch = info.object as NetworkBranch | undefined;
|
|
940
|
+
if (!branch) return false;
|
|
941
|
+
app.selectedBus = null;
|
|
942
|
+
app.selectedBranch = branch.id;
|
|
943
|
+
return true;
|
|
944
|
+
},
|
|
945
|
+
updateTriggers: { getColor: [study], getWidth: [study] }
|
|
946
|
+
}),
|
|
947
|
+
new ScatterplotLayer<NetworkBus>({
|
|
948
|
+
id: 'study-buses',
|
|
949
|
+
data: study.network.buses,
|
|
950
|
+
getPosition: (b) => [b.lon, b.lat],
|
|
951
|
+
getRadius: (b) => busRadius(Math.max(b.demand_mw, b.gen_mw)),
|
|
952
|
+
radiusUnits: 'pixels',
|
|
953
|
+
getFillColor: (b) => {
|
|
954
|
+
const value = values.get(b.id);
|
|
955
|
+
return value === undefined
|
|
956
|
+
? [110, 115, 120, 200]
|
|
957
|
+
: priceColor((value - domain.lo) / Math.max(domain.hi - domain.lo, 1e-12));
|
|
958
|
+
},
|
|
959
|
+
pickable: true,
|
|
960
|
+
stroked: true,
|
|
961
|
+
getLineColor: [46, 42, 34, 110],
|
|
962
|
+
lineWidthUnits: 'pixels',
|
|
963
|
+
onClick: (info: PickingInfo) => {
|
|
964
|
+
const bus = info.object as NetworkBus | undefined;
|
|
965
|
+
if (!bus) return false;
|
|
966
|
+
app.selectedBranch = null;
|
|
967
|
+
app.selectedBus = bus.id;
|
|
968
|
+
return true;
|
|
969
|
+
},
|
|
970
|
+
updateTriggers: { getFillColor: [study, mode] }
|
|
971
|
+
})
|
|
972
|
+
);
|
|
973
|
+
overlay.setProps({ layers });
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
841
976
|
for (const c of app.cases) {
|
|
842
977
|
if (!c.network) continue;
|
|
843
978
|
const d = display.get(c.id);
|
|
@@ -847,7 +982,7 @@
|
|
|
847
982
|
data: c.network.branches,
|
|
848
983
|
getPath: (b) => b.path,
|
|
849
984
|
// The selected branch draws in the selection blue of the bus halo cue,
|
|
850
|
-
// wider, so the
|
|
985
|
+
// wider, so the price-rating source line reads at a glance.
|
|
851
986
|
getColor: (b) =>
|
|
852
987
|
c.id === app.activeCaseId && b.id === app.selectedBranch
|
|
853
988
|
? [47, 111, 187, 255]
|
|
@@ -983,7 +1118,12 @@
|
|
|
983
1118
|
}
|
|
984
1119
|
if (c.substations) {
|
|
985
1120
|
layers.push(
|
|
986
|
-
new ScatterplotLayer<{
|
|
1121
|
+
new ScatterplotLayer<{
|
|
1122
|
+
number: number;
|
|
1123
|
+
name: string;
|
|
1124
|
+
lon: number;
|
|
1125
|
+
lat: number;
|
|
1126
|
+
}>({
|
|
987
1127
|
id: `local-subs-${c.id}`,
|
|
988
1128
|
data: c.substations.points,
|
|
989
1129
|
getPosition: (s) => [s.lon, s.lat],
|
|
@@ -1007,7 +1147,7 @@
|
|
|
1007
1147
|
// kind and buses badged by attachment role. Selecting a bus fans its
|
|
1008
1148
|
// incident conductors and expands its terminal stack in the SVG overlay.
|
|
1009
1149
|
for (const c of app.multiCases) {
|
|
1010
|
-
if (!c.view) continue;
|
|
1150
|
+
if (c.coordsKind !== 'geographic' || !c.view) continue;
|
|
1011
1151
|
const selectedId = c.id === app.activeMultiId ? c.selectedBusId : null;
|
|
1012
1152
|
const selectedEdgeId = c.id === app.activeMultiId ? c.selectedEdgeId : null;
|
|
1013
1153
|
layers.push(
|
|
@@ -1114,7 +1254,7 @@
|
|
|
1114
1254
|
// does not re-run (and re-pan) on every frame of a sheet drag.
|
|
1115
1255
|
$effect(() => {
|
|
1116
1256
|
const cue = selectedBusCue;
|
|
1117
|
-
if (!map || !cue || !app.compactLayout) return;
|
|
1257
|
+
if (!map || !cue || !app.compactLayout || app.studyView) return;
|
|
1118
1258
|
void cue.key;
|
|
1119
1259
|
const m = map;
|
|
1120
1260
|
const timer = setTimeout(() => {
|
|
@@ -1129,6 +1269,7 @@
|
|
|
1129
1269
|
});
|
|
1130
1270
|
|
|
1131
1271
|
function boundsFor(target: string): LngLatBoundsLike | null {
|
|
1272
|
+
if (app.studyView) return foldMapBounds(app.studyView.network.buses.map((b) => [b.lon, b.lat]));
|
|
1132
1273
|
const points: [number, number][] = [];
|
|
1133
1274
|
const fold = (pts: { lon: number; lat: number }[]) => {
|
|
1134
1275
|
for (const b of pts) points.push([b.lon, b.lat]);
|
|
@@ -1143,28 +1284,41 @@
|
|
|
1143
1284
|
if (c.substations) fold(c.substations.points);
|
|
1144
1285
|
}
|
|
1145
1286
|
for (const c of app.multiCases) {
|
|
1287
|
+
if (c.coordsKind !== 'geographic') continue;
|
|
1146
1288
|
if (target !== 'all' && c.id !== target) continue;
|
|
1147
1289
|
if (c.view) fold(c.view.buses);
|
|
1148
1290
|
}
|
|
1149
1291
|
return foldMapBounds(points);
|
|
1150
1292
|
}
|
|
1151
1293
|
|
|
1294
|
+
$effect(() => {
|
|
1295
|
+
const seq = app.cameraSeq;
|
|
1296
|
+
const snapshot = app.cameraRequest;
|
|
1297
|
+
if (diagramNetwork || !map || !snapshot || seq === handledCameraSeq) return;
|
|
1298
|
+
handledCameraSeq = seq;
|
|
1299
|
+
handledFrameSeq = app.frameSeq;
|
|
1300
|
+
map.jumpTo(snapshot);
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1152
1303
|
// Fly to whatever the header, the initial load, or a branch selection asked
|
|
1153
1304
|
// for, and settle the request's promise once the camera lands so callers can
|
|
1154
1305
|
// defer heavy work past the animation. A branch request waits (unhandled)
|
|
1155
1306
|
// until its cue exists; anything unframeable settles immediately.
|
|
1156
1307
|
$effect(() => {
|
|
1157
1308
|
const seq = app.frameSeq;
|
|
1158
|
-
if (!map || seq === handledFrameSeq) return;
|
|
1309
|
+
if (diagramNetwork || !map || seq === handledFrameSeq) return;
|
|
1159
1310
|
const m = map;
|
|
1160
1311
|
const target = app.frameTarget;
|
|
1161
1312
|
const settle = () => app.settleFrame();
|
|
1162
1313
|
if (typeof target === 'object') {
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1314
|
+
const geometry = displayedGeometry(target.caseId);
|
|
1315
|
+
const bus =
|
|
1316
|
+
'busId' in target ? geometry?.buses.find((b) => b.id === target.busId) : undefined;
|
|
1317
|
+
const branch =
|
|
1318
|
+
'branchId' in target ? geometry?.branches.find((b) => b.id === target.branchId) : undefined;
|
|
1165
1319
|
handledFrameSeq = seq;
|
|
1166
|
-
|
|
1167
|
-
|
|
1320
|
+
const points: [number, number][] = bus ? [[bus.lon, bus.lat]] : (branch?.path ?? []);
|
|
1321
|
+
if (!focusGeometry(points, settle)) settle();
|
|
1168
1322
|
return;
|
|
1169
1323
|
}
|
|
1170
1324
|
handledFrameSeq = seq;
|
|
@@ -1174,47 +1328,53 @@
|
|
|
1174
1328
|
return;
|
|
1175
1329
|
}
|
|
1176
1330
|
const { clientWidth: w, clientHeight: h } = m.getContainer();
|
|
1331
|
+
m.once('moveend', settle);
|
|
1177
1332
|
m.fitBounds(bounds, {
|
|
1178
1333
|
padding: framePadding(w, h, 'case'),
|
|
1179
1334
|
duration: prefersReducedMotion() ? 0 : 1400
|
|
1180
1335
|
});
|
|
1181
|
-
m.once('moveend', settle);
|
|
1182
1336
|
});
|
|
1183
1337
|
</script>
|
|
1184
1338
|
|
|
1185
|
-
{#
|
|
1186
|
-
<
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
{
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
{#
|
|
1199
|
-
|
|
1200
|
-
<
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
{
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1339
|
+
{#if diagramNetwork}
|
|
1340
|
+
<DiagramCanvas
|
|
1341
|
+
network={diagramNetwork}
|
|
1342
|
+
multiconductor={diagramMulti}
|
|
1343
|
+
caseId={diagramCaseId}
|
|
1344
|
+
onbusclick={selectDiagramBus}
|
|
1345
|
+
onbranchclick={selectDiagramBranch}
|
|
1346
|
+
onclear={onmapclick}
|
|
1347
|
+
/>
|
|
1348
|
+
{:else}
|
|
1349
|
+
{#key mapGen}
|
|
1350
|
+
<div class="map-stage">
|
|
1351
|
+
<div class="map" {@attach initMap}></div>
|
|
1352
|
+
{#if selectedBusCue}
|
|
1353
|
+
{#key selectedBusCue.key}
|
|
1354
|
+
<div class="selected-bus-cue" bind:this={selectedCueEl} aria-hidden="true"></div>
|
|
1355
|
+
{/key}
|
|
1356
|
+
{/if}
|
|
1357
|
+
{#if selectedBranchCue}
|
|
1358
|
+
{#key selectedBranchCue.key}
|
|
1359
|
+
<svg class="selected-branch-cue" bind:this={selectedBranchCueEl} aria-hidden="true">
|
|
1360
|
+
<path class="selected-branch-halo" bind:this={selectedBranchHaloPath}></path>
|
|
1361
|
+
<path class="selected-branch-line" bind:this={selectedBranchLinePath}></path>
|
|
1362
|
+
</svg>
|
|
1363
|
+
{/key}
|
|
1364
|
+
{/if}
|
|
1365
|
+
{#if selectedMultiDetail}
|
|
1366
|
+
<!-- Content is built imperatively in syncMultiDetail (screen-space
|
|
1207
1367
|
projection); file-derived text is escaped there. -->
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
{/key}
|
|
1368
|
+
<svg class="multi-detail" bind:this={multiDetailEl} aria-hidden="true"></svg>
|
|
1369
|
+
{/if}
|
|
1370
|
+
</div>
|
|
1371
|
+
{/key}
|
|
1372
|
+
{/if}
|
|
1212
1373
|
|
|
1213
1374
|
{#if app.placingId && map}
|
|
1214
1375
|
<button
|
|
1215
1376
|
type="button"
|
|
1216
1377
|
class="place-at-center mono"
|
|
1217
|
-
style="--chrome-inset: {chromeInset}px"
|
|
1218
1378
|
onclick={() => {
|
|
1219
1379
|
if (!map) return;
|
|
1220
1380
|
const center = map.getCenter();
|
|
@@ -1398,7 +1558,7 @@
|
|
|
1398
1558
|
.place-at-center {
|
|
1399
1559
|
position: absolute;
|
|
1400
1560
|
left: 50%;
|
|
1401
|
-
bottom:
|
|
1561
|
+
bottom: 92px;
|
|
1402
1562
|
z-index: 15;
|
|
1403
1563
|
transform: translateX(-50%);
|
|
1404
1564
|
padding: 7px 14px;
|
|
@@ -1413,38 +1573,36 @@
|
|
|
1413
1573
|
|
|
1414
1574
|
@media (max-width: 420px) {
|
|
1415
1575
|
.place-at-center {
|
|
1416
|
-
bottom:
|
|
1576
|
+
bottom: 78px;
|
|
1417
1577
|
}
|
|
1418
1578
|
}
|
|
1419
1579
|
|
|
1420
1580
|
.map :global(.maplibregl-ctrl-bottom-right) {
|
|
1421
1581
|
right: 20px;
|
|
1422
|
-
|
|
1423
|
-
bottom: calc(18px + var(--chrome-inset, 0px));
|
|
1582
|
+
bottom: 12px;
|
|
1424
1583
|
display: flex;
|
|
1425
|
-
flex-direction:
|
|
1584
|
+
flex-direction: row;
|
|
1426
1585
|
align-items: flex-end;
|
|
1427
|
-
gap:
|
|
1586
|
+
gap: 16px;
|
|
1428
1587
|
transition:
|
|
1429
1588
|
bottom var(--dur-med) var(--ease-out),
|
|
1430
1589
|
opacity var(--dur-fast) var(--ease-out);
|
|
1431
1590
|
}
|
|
1432
1591
|
|
|
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
1592
|
.map :global(.maplibregl-ctrl-bottom-right .maplibregl-ctrl) {
|
|
1440
1593
|
float: none;
|
|
1441
1594
|
margin: 0;
|
|
1442
1595
|
}
|
|
1443
1596
|
|
|
1597
|
+
.map :global(.maplibregl-ctrl-group) {
|
|
1598
|
+
order: 2;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1444
1601
|
.map :global(.maplibregl-ctrl-attrib) {
|
|
1445
|
-
order:
|
|
1602
|
+
order: 1;
|
|
1446
1603
|
position: relative;
|
|
1447
|
-
|
|
1604
|
+
bottom: 0;
|
|
1605
|
+
right: 0;
|
|
1448
1606
|
width: auto;
|
|
1449
1607
|
min-width: 0;
|
|
1450
1608
|
max-width: calc(100vw - 96px);
|
|
@@ -1494,31 +1652,35 @@
|
|
|
1494
1652
|
}
|
|
1495
1653
|
|
|
1496
1654
|
@media (max-width: 760px) {
|
|
1655
|
+
.map :global(.maplibregl-ctrl-bottom-right) {
|
|
1656
|
+
bottom: 12px;
|
|
1657
|
+
right: 12px;
|
|
1658
|
+
gap: 12px;
|
|
1659
|
+
}
|
|
1497
1660
|
.map :global(.maplibregl-ctrl-attrib) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
order: 1;
|
|
1661
|
+
position: relative;
|
|
1662
|
+
bottom: auto;
|
|
1501
1663
|
width: auto;
|
|
1502
1664
|
min-width: 0;
|
|
1503
1665
|
right: 0;
|
|
1504
1666
|
white-space: normal;
|
|
1505
1667
|
}
|
|
1506
1668
|
|
|
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
1669
|
.map :global(.maplibregl-ctrl-bottom-right .maplibregl-ctrl-group) {
|
|
1511
|
-
display:
|
|
1670
|
+
display: flex;
|
|
1671
|
+
}
|
|
1672
|
+
.map :global(.maplibregl-ctrl-group button + button) {
|
|
1673
|
+
border-top: 0;
|
|
1674
|
+
border-left: 1px solid var(--line);
|
|
1512
1675
|
}
|
|
1513
1676
|
}
|
|
1514
1677
|
|
|
1515
|
-
/*
|
|
1516
|
-
sitting over the network. */
|
|
1678
|
+
/* Attribution shares the bottom row with touch-sized zoom buttons. */
|
|
1517
1679
|
@media (max-width: 460px) {
|
|
1518
1680
|
.map :global(.maplibregl-ctrl-attrib) {
|
|
1519
|
-
max-width: calc(100vw -
|
|
1681
|
+
max-width: calc(100vw - 140px);
|
|
1520
1682
|
font-size: 9px;
|
|
1521
|
-
white-space:
|
|
1683
|
+
white-space: normal;
|
|
1522
1684
|
}
|
|
1523
1685
|
}
|
|
1524
1686
|
|