@tellegen/svelte 0.4.3 → 0.4.5
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 +45 -0
- package/dist/TellegenMap.svelte +63 -22
- package/dist/TellegenProvider.svelte +7 -4
- package/dist/TellegenShell.svelte +12 -1
- package/dist/TellegenViewer.svelte +2 -0
- package/dist/basemap.d.ts +14 -0
- package/dist/basemap.js +27 -0
- package/dist/colors.d.ts +6 -1
- package/dist/colors.js +11 -4
- package/dist/components/DiagnosticLogView.svelte +176 -0
- package/dist/components/DiagnosticLogView.svelte.d.ts +11 -0
- package/dist/components/FormulationSelector.svelte +1 -1
- package/dist/components/LocalCaseDetails.svelte +18 -3
- package/dist/components/LogEntryView.svelte +188 -0
- package/dist/components/LogEntryView.svelte.d.ts +11 -0
- package/dist/components/LogPanel.svelte +25 -0
- package/dist/components/LogPanel.svelte.d.ts +3 -0
- package/dist/components/McResults.svelte +18 -3
- package/dist/components/McWorkbench.svelte +688 -0
- package/dist/components/McWorkbench.svelte.d.ts +38 -0
- package/dist/components/MultiWire.svelte +136 -0
- package/dist/components/MultiWire.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +7 -0
- package/dist/components/NotificationCenter.svelte +37 -6
- package/dist/components/OmniSearch.svelte +163 -0
- package/dist/components/OmniSearch.svelte.d.ts +14 -0
- package/dist/components/ResultsTable.svelte +259 -0
- package/dist/components/ResultsTable.svelte.d.ts +13 -0
- package/dist/components/SchematicCanvas.svelte +1896 -0
- package/dist/components/SchematicCanvas.svelte.d.ts +13 -0
- package/dist/components/SolveCard.svelte +6 -0
- package/dist/components/schematic/PickCard.svelte +126 -0
- package/dist/components/schematic/PickCard.svelte.d.ts +56 -0
- package/dist/components/schematic/export.d.ts +21 -0
- package/dist/components/schematic/export.js +110 -0
- package/dist/components/schematic/layers.d.ts +161 -0
- package/dist/components/schematic/layers.js +756 -0
- package/dist/components/schematic/paint.d.ts +28 -0
- package/dist/components/schematic/paint.js +69 -0
- package/dist/components/schematic/palette.d.ts +34 -0
- package/dist/components/schematic/palette.js +54 -0
- package/dist/components/schematic/text.d.ts +83 -0
- package/dist/components/schematic/text.js +190 -0
- package/dist/components/schematic/view-prefs.svelte.d.ts +30 -0
- package/dist/components/schematic/view-prefs.svelte.js +77 -0
- package/dist/components.d.ts +4 -0
- package/dist/components.js +4 -0
- package/dist/context.svelte.d.ts +2 -0
- package/dist/context.svelte.js +2 -0
- package/dist/controller.svelte.d.ts +40 -5
- package/dist/controller.svelte.js +230 -25
- package/dist/diagram/basemap.d.ts +28 -0
- package/dist/diagram/basemap.js +39 -0
- package/dist/diagram/bus-stacks.d.ts +19 -0
- package/dist/diagram/bus-stacks.js +99 -0
- package/dist/diagram/colour-modes.d.ts +18 -0
- package/dist/diagram/colour-modes.js +33 -0
- package/dist/diagram/connections.d.ts +30 -0
- package/dist/diagram/connections.js +129 -0
- package/dist/diagram/display-tree.d.ts +21 -0
- package/dist/diagram/display-tree.js +65 -0
- package/dist/diagram/energisation.d.ts +10 -0
- package/dist/diagram/energisation.js +31 -0
- package/dist/diagram/feeders.d.ts +30 -0
- package/dist/diagram/feeders.js +57 -0
- package/dist/diagram/galvanic-zones.d.ts +60 -0
- package/dist/diagram/galvanic-zones.js +210 -0
- package/dist/diagram/graph-inputs.d.ts +18 -0
- package/dist/diagram/graph-inputs.js +103 -0
- package/dist/diagram/index.d.ts +28 -0
- package/dist/diagram/index.js +28 -0
- package/dist/diagram/landmarks.d.ts +50 -0
- package/dist/diagram/landmarks.js +153 -0
- package/dist/diagram/layout-morph.d.ts +15 -0
- package/dist/diagram/layout-morph.js +32 -0
- package/dist/diagram/multi-wire.d.ts +44 -0
- package/dist/diagram/multi-wire.js +65 -0
- package/dist/diagram/phase-bars.d.ts +35 -0
- package/dist/diagram/phase-bars.js +44 -0
- package/dist/diagram/results-table.d.ts +47 -0
- package/dist/diagram/results-table.js +173 -0
- package/dist/diagram/schematic-equipment.d.ts +133 -0
- package/dist/diagram/schematic-equipment.js +645 -0
- package/dist/diagram/schematic-layout.d.ts +51 -0
- package/dist/diagram/schematic-layout.js +240 -0
- package/dist/diagram/search.d.ts +36 -0
- package/dist/diagram/search.js +48 -0
- package/dist/diagram/shared-routes.d.ts +21 -0
- package/dist/diagram/shared-routes.js +40 -0
- package/dist/diagram/short-names.d.ts +11 -0
- package/dist/diagram/short-names.js +54 -0
- package/dist/diagram/snapshot.d.ts +115 -0
- package/dist/diagram/snapshot.js +242 -0
- package/dist/diagram/summary.d.ts +65 -0
- package/dist/diagram/summary.js +144 -0
- package/dist/diagram/svg-export.d.ts +80 -0
- package/dist/diagram/svg-export.js +108 -0
- package/dist/diagram/tree-walk.d.ts +13 -0
- package/dist/diagram/tree-walk.js +83 -0
- package/dist/diagram/value-labels.d.ts +24 -0
- package/dist/diagram/value-labels.js +69 -0
- package/dist/diagram/view-state.d.ts +12 -0
- package/dist/diagram/view-state.js +22 -0
- package/dist/diagram/voltage-levels.d.ts +14 -0
- package/dist/diagram/voltage-levels.js +20 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/log.svelte.d.ts +77 -0
- package/dist/log.svelte.js +149 -0
- package/dist/notices.svelte.d.ts +3 -0
- package/dist/state.svelte.d.ts +42 -5
- package/dist/state.svelte.js +56 -4
- package/dist/transformer-icon.d.ts +22 -13
- package/dist/transformer-icon.js +23 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ remain in the browser on this path.
|
|
|
39
39
|
`TellegenViewer` accepts:
|
|
40
40
|
|
|
41
41
|
- `apiBase`, default `/api`
|
|
42
|
+
- `acOpfWasmUrl`, default `null` (optional experimental AC OPF asset)
|
|
42
43
|
- `loadDefaultCases`, default `true`
|
|
43
44
|
- `docsHref`
|
|
44
45
|
- `orgHref`
|
|
@@ -68,6 +69,20 @@ controller, which frees every case's browser Study and multiconductor session
|
|
|
68
69
|
in the engine worker, so a provider that should keep its cases across pages
|
|
69
70
|
belongs in a layout that stays mounted.
|
|
70
71
|
|
|
72
|
+
## Experimental AC OPF
|
|
73
|
+
|
|
74
|
+
A development host can pass `acOpfWasmUrl="/experimental-acopf/tellegen_acopf_wasi.wasm"`
|
|
75
|
+
to `TellegenViewer` or `TellegenProvider`. The asset must be built and served
|
|
76
|
+
separately; the selector enables AC OPF only after a worker probe succeeds.
|
|
77
|
+
See the [local setup and release gates](../../docs/src/acopf-pounce-decision.md#experimental-browser-abi).
|
|
78
|
+
|
|
79
|
+
Balanced networks and stored canonical AC OPF instances are supported at the
|
|
80
|
+
base operating point. Imported instances retain their objective and constraints.
|
|
81
|
+
Demand/rating editing, LMPs, sensitivities, and saving/exporting the AC OPF case
|
|
82
|
+
are unavailable. Geographic sidecars and layout downloads remain available.
|
|
83
|
+
Without the optional worker, imported AC OPF instances remain viewable but
|
|
84
|
+
cannot be solved. No server solve is substituted for AC OPF.
|
|
85
|
+
|
|
71
86
|
## Panel layout
|
|
72
87
|
|
|
73
88
|
The shell docks Network and Studies on the left, and Solver and Agent on the
|
|
@@ -165,6 +180,31 @@ it is set, `app.error` is a message written for people, and the detail holds
|
|
|
165
180
|
the technical text behind it. `NotificationCenter` shows that text in a
|
|
166
181
|
collapsed disclosure.
|
|
167
182
|
|
|
183
|
+
### Log
|
|
184
|
+
|
|
185
|
+
`app.log` keeps every error and engine finding of the session as structured
|
|
186
|
+
entries: severity (error, warning or note), the step (`Open`, `Coordinates`,
|
|
187
|
+
`Solve`, `Export`), who reported it (`PowerIO` or `tellegen`), the code, the
|
|
188
|
+
message, the file or case it belongs to, the element and buses it names, and
|
|
189
|
+
PowerIO's suggested action and details. The controller records what PowerIO
|
|
190
|
+
reported while reading a case or a geographic layer, what each solve reported
|
|
191
|
+
(islands, HVDC transfers), what each export's writer reported, and every
|
|
192
|
+
`app.error`. A finding reported again counts up in place.
|
|
193
|
+
|
|
194
|
+
`DiagnosticLogView` (from `@tellegen/svelte/components`) shows the log grouped
|
|
195
|
+
by step and file, with severity filters, Copy, Download (`tellegen.log` JSON)
|
|
196
|
+
and Clear; pass `onbus` to make the named buses selectable. The default shell
|
|
197
|
+
shows it in a "Log" panel (`LogPanel`). A host can add its own findings:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
getAppState().log.record({
|
|
201
|
+
severity: 'warning',
|
|
202
|
+
stage: 'Fetch',
|
|
203
|
+
source: 'Overpass',
|
|
204
|
+
message: 'overpass-api.de answered 504; trying the next mirror'
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
168
208
|
## Basemap
|
|
169
209
|
|
|
170
210
|
CARTO's vector styles suit a network overlay best: quiet roads, flat land and
|
|
@@ -366,6 +406,11 @@ package metadata. The packed smoke test installs the generated tarballs into a
|
|
|
366
406
|
temporary Svelte consumer and builds it so missing exports, styles, or wasm
|
|
367
407
|
assets fail before publish.
|
|
368
408
|
|
|
409
|
+
Unmounting `TellegenProvider` (including through `TellegenViewer`) disposes its
|
|
410
|
+
controller and terminates pending AC OPF probes and solves, including background
|
|
411
|
+
cases. Custom hosts that construct a `Controller` directly must call
|
|
412
|
+
`controller.dispose()` when their owning view is destroyed.
|
|
413
|
+
|
|
369
414
|
## Citing
|
|
370
415
|
|
|
371
416
|
If you use this package in published work, please cite the tellegen paper:
|
package/dist/TellegenMap.svelte
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
3
3
|
import { tick, untrack } from 'svelte';
|
|
4
4
|
import type { Layer, PickingInfo } from '@deck.gl/core';
|
|
5
|
-
import type { IconLayer, PathLayer, ScatterplotLayer } from '@deck.gl/layers';
|
|
5
|
+
import type { IconLayer, IconLayerProps, PathLayer, ScatterplotLayer } from '@deck.gl/layers';
|
|
6
6
|
import type { MapLibreOverlay } from '@deck.gl/maplibre';
|
|
7
7
|
import type { LngLatBoundsLike, Map as MapLibreMap } from 'maplibre-gl';
|
|
8
8
|
import type { NetworkBranch, NetworkBus } from './api.js';
|
|
@@ -22,9 +22,8 @@
|
|
|
22
22
|
type SensitivityDomain
|
|
23
23
|
} from './colors.js';
|
|
24
24
|
import {
|
|
25
|
+
applyBasemapTuning,
|
|
25
26
|
attachBasemapFallback,
|
|
26
|
-
isOpenFreeMapStyle,
|
|
27
|
-
openFreeMapEdits,
|
|
28
27
|
resolveBasemap,
|
|
29
28
|
type BasemapFallbackReason,
|
|
30
29
|
type BasemapStyle,
|
|
@@ -50,13 +49,18 @@
|
|
|
50
49
|
type TransformerMark,
|
|
51
50
|
multiDiagramNetwork
|
|
52
51
|
} from './multiconductor.js';
|
|
53
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
TRANSFORMER_ICON,
|
|
54
|
+
transformerIconAtlas,
|
|
55
|
+
transformerIconMapping
|
|
56
|
+
} from './transformer-icon.js';
|
|
54
57
|
import { foldMapBounds } from './map-bounds.js';
|
|
55
58
|
import { getAppState, getController } from './context.svelte.js';
|
|
56
59
|
import { displayFmt, fmt, rgbaCss } from './format.js';
|
|
57
60
|
import { getPanelLayout } from './panels.svelte.js';
|
|
58
61
|
import type { RGBA } from './colors.js';
|
|
59
62
|
import DiagramCanvas from './components/DiagramCanvas.svelte';
|
|
63
|
+
import SchematicCanvas from './components/SchematicCanvas.svelte';
|
|
60
64
|
|
|
61
65
|
const DEFAULT_CAMERA: CameraSnapshot = { center: [-85, 36], zoom: 4.5, bearing: 0, pitch: 0 };
|
|
62
66
|
|
|
@@ -148,6 +152,24 @@
|
|
|
148
152
|
const diagramNetwork = $derived(
|
|
149
153
|
multiNetwork ?? (displayedNetwork?.coordinate_space === 'diagram' ? displayedNetwork : null)
|
|
150
154
|
);
|
|
155
|
+
/** A multiconductor case shown as a schematic replaces both the map and the diagram canvas. */
|
|
156
|
+
const schematicCase = $derived(
|
|
157
|
+
!app.studyView && app.activeMulti?.schematic && app.activeMulti.graph ? app.activeMulti : null
|
|
158
|
+
);
|
|
159
|
+
// When a case closes its own diagram (Close, Show map), the map continues from the diagram's
|
|
160
|
+
// geographic view. Switching to another case does not: the map frames the new case instead.
|
|
161
|
+
let shownSchematic: typeof schematicCase = null;
|
|
162
|
+
$effect(() => {
|
|
163
|
+
const now = schematicCase;
|
|
164
|
+
const before = shownSchematic;
|
|
165
|
+
shownSchematic = now;
|
|
166
|
+
untrack(() => {
|
|
167
|
+
if (before && !now && app.activeMulti === before && !before.schematic && before.lastGeoView) {
|
|
168
|
+
app.camera = before.lastGeoView;
|
|
169
|
+
app.cameraHandoff = true;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
151
173
|
const diagramCaseId = $derived(
|
|
152
174
|
app.studyView?.caseId ?? diagramMulti?.id ?? diagramCase?.id ?? 'diagram'
|
|
153
175
|
);
|
|
@@ -187,6 +209,11 @@
|
|
|
187
209
|
const basemapPrimaryProvider = $derived(basemap.primaryProvider);
|
|
188
210
|
const basemapFallbackProvider = $derived(basemap.fallbackProvider);
|
|
189
211
|
const palette = $derived(MAP_PALETTE[theme]);
|
|
212
|
+
/** The diagram draws on light paper, so its background map is the light basemap whatever the
|
|
213
|
+
* map's theme; a host's own style is used as given. */
|
|
214
|
+
const diagramBasemap = $derived(
|
|
215
|
+
resolveBasemap({ mapStyle, fallbackMapStyle, theme: 'light', hideBasemapLabels, cartoApiKey })
|
|
216
|
+
);
|
|
190
217
|
// Set once a map has existed in this component instance: a rebuild (context
|
|
191
218
|
// loss or basemap change) restores the camera instead of reframing.
|
|
192
219
|
let hadMap = false;
|
|
@@ -875,23 +902,13 @@
|
|
|
875
902
|
// shows every layer again, so both are re-applied on each load.
|
|
876
903
|
const applyStyleExtras = () => {
|
|
877
904
|
m.setProjection({ type: untrack(() => projection) });
|
|
878
|
-
const style = m.getStyle();
|
|
879
|
-
if (spec.hideLabels) {
|
|
880
|
-
for (const layer of style.layers) {
|
|
881
|
-
if (layer.type === 'symbol') m.setLayoutProperty(layer.id, 'visibility', 'none');
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
905
|
// OpenFreeMap renders as the primary or as the fallback; either
|
|
885
906
|
// way it is tuned towards CARTO's quieter look.
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
907
|
+
applyBasemapTuning(
|
|
908
|
+
m,
|
|
909
|
+
spec,
|
|
889
910
|
untrack(() => theme)
|
|
890
911
|
);
|
|
891
|
-
for (const edit of edits) {
|
|
892
|
-
if (edit.kind === 'paint') m.setPaintProperty(edit.layer, edit.property, edit.value);
|
|
893
|
-
else m.setLayerZoomRange(edit.layer, edit.minzoom, edit.maxzoom);
|
|
894
|
-
}
|
|
895
912
|
};
|
|
896
913
|
m.on('style.load', applyStyleExtras);
|
|
897
914
|
const detachFallback = attachBasemapFallback(m, spec, {
|
|
@@ -1004,12 +1021,16 @@
|
|
|
1004
1021
|
overlay = null;
|
|
1005
1022
|
};
|
|
1006
1023
|
detachReady = onMapReady?.(m, o);
|
|
1024
|
+
// A view closing onto this map (the diagram, from geographic positions) hands it its
|
|
1025
|
+
// camera to continue from.
|
|
1026
|
+
const handedOff = app.cameraHandoff;
|
|
1027
|
+
app.cameraHandoff = false;
|
|
1007
1028
|
// Frame the active grid once the map is ready. The initial frame request
|
|
1008
1029
|
// (from the case load) can fire before the async map import finishes and
|
|
1009
1030
|
// be lost; re-issuing it here ensures the first view is the active case,
|
|
1010
1031
|
// not the default world view. A rebuilt map opens at the default view
|
|
1011
1032
|
// too, so it restores the camera the last map saved.
|
|
1012
|
-
if (restoreCamera && app.camera) m.jumpTo(app.camera);
|
|
1033
|
+
if ((restoreCamera || handedOff) && app.camera) m.jumpTo(app.camera);
|
|
1013
1034
|
else if (app.cameraSeq === handledCameraSeq)
|
|
1014
1035
|
app.requestFrame(app.activeCaseId ?? app.activeLocalId ?? 'all');
|
|
1015
1036
|
if (mapGen > 0) {
|
|
@@ -1345,7 +1366,11 @@
|
|
|
1345
1366
|
new IconLayer<TransformerMark>({
|
|
1346
1367
|
id: `multi-xfmr-${c.id}`,
|
|
1347
1368
|
data: transformerMarks(c.view.edges),
|
|
1348
|
-
|
|
1369
|
+
// deck.gl makes a texture from any image, canvas included, but types
|
|
1370
|
+
// iconAtlas as a URL or a Texture.
|
|
1371
|
+
iconAtlas: transformerIconAtlas() as unknown as IconLayerProps['iconAtlas'],
|
|
1372
|
+
iconMapping: transformerIconMapping,
|
|
1373
|
+
getIcon: () => TRANSFORMER_ICON,
|
|
1349
1374
|
getPosition: (m) => m.position,
|
|
1350
1375
|
getAngle: (m) => m.angle,
|
|
1351
1376
|
billboard: false,
|
|
@@ -1455,7 +1480,7 @@
|
|
|
1455
1480
|
$effect(() => {
|
|
1456
1481
|
const seq = app.cameraSeq;
|
|
1457
1482
|
const snapshot = app.cameraRequest;
|
|
1458
|
-
if (diagramNetwork || !map || !snapshot || seq === handledCameraSeq) return;
|
|
1483
|
+
if (diagramNetwork || schematicCase || !map || !snapshot || seq === handledCameraSeq) return;
|
|
1459
1484
|
handledCameraSeq = seq;
|
|
1460
1485
|
handledFrameSeq = app.frameSeq;
|
|
1461
1486
|
map.jumpTo(snapshot);
|
|
@@ -1467,7 +1492,7 @@
|
|
|
1467
1492
|
// until its cue exists; anything unframeable settles immediately.
|
|
1468
1493
|
$effect(() => {
|
|
1469
1494
|
const seq = app.frameSeq;
|
|
1470
|
-
if (diagramNetwork || !map || seq === handledFrameSeq) return;
|
|
1495
|
+
if (diagramNetwork || schematicCase || !map || seq === handledFrameSeq) return;
|
|
1471
1496
|
const m = map;
|
|
1472
1497
|
const target = app.frameTarget;
|
|
1473
1498
|
const settle = () => app.settleFrame();
|
|
@@ -1497,7 +1522,23 @@
|
|
|
1497
1522
|
});
|
|
1498
1523
|
</script>
|
|
1499
1524
|
|
|
1500
|
-
{#if
|
|
1525
|
+
{#if schematicCase}
|
|
1526
|
+
<!-- One canvas per graph: its layouts, camera and geometry belong to that graph, so another
|
|
1527
|
+
case, or the same case with coordinates attached, starts afresh. -->
|
|
1528
|
+
{#key schematicCase.graph}
|
|
1529
|
+
<SchematicCanvas
|
|
1530
|
+
mc={schematicCase}
|
|
1531
|
+
onselectbus={(id) => {
|
|
1532
|
+
if (id !== schematicCase.selectedBusId) onmultibusclick(schematicCase.id, id);
|
|
1533
|
+
}}
|
|
1534
|
+
onselectedge={(id) => {
|
|
1535
|
+
if (id !== schematicCase.selectedEdgeId) onmultiedgeclick?.(schematicCase.id, id);
|
|
1536
|
+
}}
|
|
1537
|
+
onclose={() => (schematicCase.schematic = false)}
|
|
1538
|
+
basemap={diagramBasemap}
|
|
1539
|
+
/>
|
|
1540
|
+
{/key}
|
|
1541
|
+
{:else if diagramNetwork}
|
|
1501
1542
|
<DiagramCanvas
|
|
1502
1543
|
network={diagramNetwork}
|
|
1503
1544
|
multiconductor={diagramMulti}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
let {
|
|
23
23
|
apiBase,
|
|
24
|
+
acOpfWasmUrl,
|
|
24
25
|
loadDefaultCases,
|
|
25
26
|
docsHref,
|
|
26
27
|
orgHref,
|
|
@@ -31,14 +32,15 @@
|
|
|
31
32
|
children
|
|
32
33
|
}: Props = $props();
|
|
33
34
|
|
|
34
|
-
// Read once, at mount: `apiBase` and `loadDefaultCases`
|
|
35
|
-
// controller this provider owns, and the links and footer are
|
|
36
|
-
// `theme` and `cartoApiKey` stay live, so a host can switch them
|
|
37
|
-
// root attribute and the default shell's map follow.
|
|
35
|
+
// Read once, at mount: `apiBase`, `acOpfWasmUrl` and `loadDefaultCases`
|
|
36
|
+
// configure the controller this provider owns, and the links and footer are
|
|
37
|
+
// fixed copy. `theme` and `cartoApiKey` stay live, so a host can switch them
|
|
38
|
+
// and the root attribute and the default shell's map follow.
|
|
38
39
|
const config: TellegenUiConfig = {
|
|
39
40
|
...untrack(() =>
|
|
40
41
|
resolveTellegenUiConfig({
|
|
41
42
|
apiBase,
|
|
43
|
+
acOpfWasmUrl,
|
|
42
44
|
loadDefaultCases,
|
|
43
45
|
docsHref,
|
|
44
46
|
orgHref,
|
|
@@ -56,6 +58,7 @@
|
|
|
56
58
|
const app = createAppState();
|
|
57
59
|
const ctrl = createController(app, {
|
|
58
60
|
apiBase: config.apiBase,
|
|
61
|
+
acOpfWasmUrl: config.acOpfWasmUrl ?? undefined,
|
|
59
62
|
loadDefaultCases: config.loadDefaultCases
|
|
60
63
|
});
|
|
61
64
|
// A host that opens its own cases never calls load(), so the shell must not
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
2
|
+
import { onMount, untrack } from 'svelte';
|
|
3
3
|
import AppFooter from './components/AppFooter.svelte';
|
|
4
4
|
import NotificationCenter from './components/NotificationCenter.svelte';
|
|
5
5
|
import AppHeader from './components/AppHeader.svelte';
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import RestoreDefaultsButton from './components/RestoreDefaultsButton.svelte';
|
|
10
10
|
import SolveCard from './components/SolveCard.svelte';
|
|
11
11
|
import PanelHost from './components/PanelHost.svelte';
|
|
12
|
+
import LogPanel from './components/LogPanel.svelte';
|
|
12
13
|
import ShellEnvironment from './components/ShellEnvironment.svelte';
|
|
13
14
|
import { getAppState, getController, getUiConfig } from './context.svelte.js';
|
|
14
15
|
import TellegenMap from './TellegenMap.svelte';
|
|
@@ -19,6 +20,14 @@
|
|
|
19
20
|
|
|
20
21
|
let dragDepth = 0;
|
|
21
22
|
|
|
23
|
+
// The overview and the diagram read the active case's whole operating point once per solve.
|
|
24
|
+
$effect(() => {
|
|
25
|
+
const c = app.activeMulti;
|
|
26
|
+
void c?.result?.solve_count;
|
|
27
|
+
void c?.mcFullResult;
|
|
28
|
+
return untrack(() => ctrl.followOverview(c));
|
|
29
|
+
});
|
|
30
|
+
|
|
22
31
|
// ShellEnvironment drops the affordance when no fine pointer is attached;
|
|
23
32
|
// forget any drag that was in flight so the next one counts from zero.
|
|
24
33
|
$effect(() => {
|
|
@@ -40,6 +49,7 @@
|
|
|
40
49
|
});
|
|
41
50
|
|
|
42
51
|
onMount(() => {
|
|
52
|
+
ctrl.probeAcOpf();
|
|
43
53
|
// The provider already marked the shell loaded when default cases are
|
|
44
54
|
// off, and load() itself is gated; this only frames whatever is there.
|
|
45
55
|
if (config.loadDefaultCases) {
|
|
@@ -111,6 +121,7 @@
|
|
|
111
121
|
<NotificationCenter />
|
|
112
122
|
<ControlPanel />
|
|
113
123
|
<SolveCard />
|
|
124
|
+
<LogPanel />
|
|
114
125
|
{#if !app.studyView}
|
|
115
126
|
<DropZone />
|
|
116
127
|
<PlacementCue />
|
package/dist/basemap.d.ts
CHANGED
|
@@ -189,4 +189,18 @@ export interface BasemapFallbackOptions {
|
|
|
189
189
|
* again, as they do when `spec.fallback` is `null`. Returns a detach
|
|
190
190
|
* function. */
|
|
191
191
|
export declare function attachBasemapFallback(map: BasemapMapLike, spec: BasemapSpec, handlers?: BasemapFallbackHandlers, options?: BasemapFallbackOptions): () => void;
|
|
192
|
+
/** The parts of a MapLibre map `applyBasemapTuning` touches. */
|
|
193
|
+
export interface TunableMap {
|
|
194
|
+
getStyle(): StyleSpecification;
|
|
195
|
+
setLayoutProperty(layer: string, name: string, value: unknown): unknown;
|
|
196
|
+
setPaintProperty(layer: string, name: string, value: unknown): unknown;
|
|
197
|
+
setLayerZoomRange(layer: string, minzoom: number, maxzoom: number): unknown;
|
|
198
|
+
}
|
|
199
|
+
/** After a style loads: hide its labels when the spec asks, and tune an OpenFreeMap style (as the
|
|
200
|
+
* primary or the fallback) towards CARTO's quieter look. `setStyle` undoes both, so run this on
|
|
201
|
+
* every `style.load`. */
|
|
202
|
+
export declare function applyBasemapTuning(map: TunableMap, spec: BasemapSpec, theme: BasemapTheme): void;
|
|
203
|
+
/** A credit line for a basemap provider, for views that draw their own instead of MapLibre's
|
|
204
|
+
* attribution control. */
|
|
205
|
+
export declare function basemapCredit(provider: BasemapProvider): string;
|
|
192
206
|
export {};
|
package/dist/basemap.js
CHANGED
|
@@ -257,3 +257,30 @@ export function attachBasemapFallback(map, spec, handlers = {}, options = {}) {
|
|
|
257
257
|
map.off('error', onError);
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
|
+
/** After a style loads: hide its labels when the spec asks, and tune an OpenFreeMap style (as the
|
|
261
|
+
* primary or the fallback) towards CARTO's quieter look. `setStyle` undoes both, so run this on
|
|
262
|
+
* every `style.load`. */
|
|
263
|
+
export function applyBasemapTuning(map, spec, theme) {
|
|
264
|
+
const style = map.getStyle();
|
|
265
|
+
if (spec.hideLabels)
|
|
266
|
+
for (const layer of style.layers)
|
|
267
|
+
if (layer.type === 'symbol')
|
|
268
|
+
map.setLayoutProperty(layer.id, 'visibility', 'none');
|
|
269
|
+
if (!isOpenFreeMapStyle(style))
|
|
270
|
+
return;
|
|
271
|
+
for (const edit of openFreeMapEdits(style, theme)) {
|
|
272
|
+
if (edit.kind === 'paint')
|
|
273
|
+
map.setPaintProperty(edit.layer, edit.property, edit.value);
|
|
274
|
+
else
|
|
275
|
+
map.setLayerZoomRange(edit.layer, edit.minzoom, edit.maxzoom);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/** A credit line for a basemap provider, for views that draw their own instead of MapLibre's
|
|
279
|
+
* attribution control. */
|
|
280
|
+
export function basemapCredit(provider) {
|
|
281
|
+
return provider === 'carto'
|
|
282
|
+
? '© CARTO © OpenStreetMap contributors'
|
|
283
|
+
: provider === 'openfreemap'
|
|
284
|
+
? '© OpenFreeMap © OpenMapTiles © OpenStreetMap contributors'
|
|
285
|
+
: '© OpenStreetMap contributors';
|
|
286
|
+
}
|
package/dist/colors.d.ts
CHANGED
|
@@ -75,7 +75,12 @@ export declare function sensFlatColor(domain: Pick<SensitivityDomain, 'mean'>):
|
|
|
75
75
|
export declare function previewScaleFor(domain: Pick<SensitivityDomain, 'scale' | 'flat'>, maxAbsStep: number): number | null;
|
|
76
76
|
/** Zero anchored sensitivity domain with a flat column guard. */
|
|
77
77
|
export declare function sensitivityDomain(values: number[]): SensitivityDomain;
|
|
78
|
-
/**
|
|
78
|
+
/** Loading, as a fraction of rating, from which a branch needs attention. Both loading scales
|
|
79
|
+
* share it. An optimal power flow enforces ratings, so its map draws congestion: red from here,
|
|
80
|
+
* because it cannot exceed 1. A multiconductor power flow does not enforce them, so its overview
|
|
81
|
+
* warns from here and calls a violation only above 1 (`LOADING_LIMIT` in `diagram/summary.ts`). */
|
|
82
|
+
export declare const LOADING_ATTENTION = 0.9;
|
|
83
|
+
/** Branch color by loading fraction: warm gray, amber past 0.6, red from `LOADING_ATTENTION`. */
|
|
79
84
|
export declare function branchColor(loading: number, inService: boolean): RGBA;
|
|
80
85
|
export declare function branchWidth(loading: number): number;
|
|
81
86
|
/** Bus radius in px; area tracks max(load, gen) so big plants and big loads
|
package/dist/colors.js
CHANGED
|
@@ -177,14 +177,21 @@ export function sensitivityDomain(values) {
|
|
|
177
177
|
flat
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
|
-
/**
|
|
180
|
+
/** Loading, as a fraction of rating, from which a branch needs attention. Both loading scales
|
|
181
|
+
* share it. An optimal power flow enforces ratings, so its map draws congestion: red from here,
|
|
182
|
+
* because it cannot exceed 1. A multiconductor power flow does not enforce them, so its overview
|
|
183
|
+
* warns from here and calls a violation only above 1 (`LOADING_LIMIT` in `diagram/summary.ts`). */
|
|
184
|
+
export const LOADING_ATTENTION = 0.9;
|
|
185
|
+
/** Where the optimal power flow map starts shading a branch towards congestion. */
|
|
186
|
+
const LOADING_ELEVATED = 0.6;
|
|
187
|
+
/** Branch color by loading fraction: warm gray, amber past 0.6, red from `LOADING_ATTENTION`. */
|
|
181
188
|
export function branchColor(loading, inService) {
|
|
182
189
|
if (!inService)
|
|
183
190
|
return [197, 191, 178, 110];
|
|
184
|
-
if (loading <
|
|
191
|
+
if (loading < LOADING_ELEVATED)
|
|
185
192
|
return [138, 131, 117, 200];
|
|
186
|
-
if (loading <
|
|
187
|
-
return lerpColor([138, 131, 117, 210], [212, 116, 34, 240], (loading -
|
|
193
|
+
if (loading < LOADING_ATTENTION)
|
|
194
|
+
return lerpColor([138, 131, 117, 210], [212, 116, 34, 240], (loading - LOADING_ELEVATED) / (LOADING_ATTENTION - LOADING_ELEVATED));
|
|
188
195
|
return [179, 38, 30, 250];
|
|
189
196
|
}
|
|
190
197
|
export function branchWidth(loading) {
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getAppState } from '../context.svelte.js';
|
|
3
|
+
import { downloadText } from '../controller.svelte.js';
|
|
4
|
+
import {
|
|
5
|
+
LOG_SEVERITIES,
|
|
6
|
+
type DiagnosticLog,
|
|
7
|
+
type LogEntry,
|
|
8
|
+
type LogEntryInput,
|
|
9
|
+
type LogSeverity
|
|
10
|
+
} from '../log.svelte.js';
|
|
11
|
+
import LogEntryView from './LogEntryView.svelte';
|
|
12
|
+
let {
|
|
13
|
+
log = getAppState().log,
|
|
14
|
+
onbus,
|
|
15
|
+
filename = 'tellegen-log.json',
|
|
16
|
+
empty = 'Nothing to report. Findings from opening, solving and exporting a case appear here.'
|
|
17
|
+
}: {
|
|
18
|
+
log?: DiagnosticLog;
|
|
19
|
+
/** Makes each named bus a button, for a host that can select it. */
|
|
20
|
+
onbus?: (bus: number, entry: LogEntryInput) => void;
|
|
21
|
+
filename?: string;
|
|
22
|
+
empty?: string;
|
|
23
|
+
} = $props();
|
|
24
|
+
const PLURAL = { error: 'Errors', warning: 'Warnings', note: 'Notes' } as const;
|
|
25
|
+
let filter = $state<LogSeverity | 'all'>('all');
|
|
26
|
+
let copied = $state(false);
|
|
27
|
+
const counts = $derived(log.counts);
|
|
28
|
+
const shown = $derived(
|
|
29
|
+
filter === 'all' ? log.entries : log.entries.filter((e) => e.severity === filter)
|
|
30
|
+
);
|
|
31
|
+
// One group per step and subject, in the order each first appeared.
|
|
32
|
+
const groups = $derived.by(() => {
|
|
33
|
+
const out = new Map<string, { stage: string; subject?: string; entries: LogEntry[] }>();
|
|
34
|
+
for (const entry of shown) {
|
|
35
|
+
const key = `${entry.stage}\u0000${entry.subject ?? ''}`;
|
|
36
|
+
let group = out.get(key);
|
|
37
|
+
if (!group)
|
|
38
|
+
out.set(key, (group = { stage: entry.stage, subject: entry.subject, entries: [] }));
|
|
39
|
+
group.entries.push(entry);
|
|
40
|
+
}
|
|
41
|
+
return [...out.values()];
|
|
42
|
+
});
|
|
43
|
+
$effect(() => {
|
|
44
|
+
if (filter !== 'all' && counts[filter] === 0) filter = 'all';
|
|
45
|
+
});
|
|
46
|
+
const text = () => JSON.stringify(log.toJSON(), null, 2);
|
|
47
|
+
async function copy() {
|
|
48
|
+
try {
|
|
49
|
+
await navigator.clipboard.writeText(text());
|
|
50
|
+
copied = true;
|
|
51
|
+
setTimeout(() => (copied = false), 1600);
|
|
52
|
+
} catch {
|
|
53
|
+
copied = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<section class="log" aria-label="Log" data-testid="diagnostic-log">
|
|
59
|
+
{#if log.entries.length}
|
|
60
|
+
<div class="toolbar">
|
|
61
|
+
<div class="filters" role="group" aria-label="Show">
|
|
62
|
+
<button type="button" aria-pressed={filter === 'all'} onclick={() => (filter = 'all')}
|
|
63
|
+
>All <span class="n">{log.entries.length}</span></button
|
|
64
|
+
>
|
|
65
|
+
{#each LOG_SEVERITIES as severity (severity)}
|
|
66
|
+
{#if counts[severity]}
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
data-severity={severity}
|
|
70
|
+
aria-pressed={filter === severity}
|
|
71
|
+
onclick={() => (filter = severity)}
|
|
72
|
+
>{PLURAL[severity]} <span class="n">{counts[severity]}</span></button
|
|
73
|
+
>
|
|
74
|
+
{/if}
|
|
75
|
+
{/each}
|
|
76
|
+
</div>
|
|
77
|
+
<div class="actions">
|
|
78
|
+
<button type="button" onclick={copy}>{copied ? 'Copied' : 'Copy'}</button>
|
|
79
|
+
<button type="button" onclick={() => downloadText(text(), filename, 'application/json')}
|
|
80
|
+
>Download</button
|
|
81
|
+
>
|
|
82
|
+
<button type="button" onclick={() => log.clear()}>Clear</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
{#each groups as group (`${group.stage}\u0000${group.subject ?? ''}`)}
|
|
86
|
+
<section class="group" data-stage={group.stage}>
|
|
87
|
+
<h4>
|
|
88
|
+
<span class="stage">{group.stage}</span>{#if group.subject}<span class="subject"
|
|
89
|
+
>{group.subject}</span
|
|
90
|
+
>{/if}
|
|
91
|
+
</h4>
|
|
92
|
+
<ol>
|
|
93
|
+
{#each group.entries as entry (entry.id)}
|
|
94
|
+
<li><LogEntryView {entry} showSubject={false} {onbus} /></li>
|
|
95
|
+
{/each}
|
|
96
|
+
</ol>
|
|
97
|
+
</section>
|
|
98
|
+
{/each}
|
|
99
|
+
{:else}
|
|
100
|
+
<p class="empty">{empty}</p>
|
|
101
|
+
{/if}
|
|
102
|
+
</section>
|
|
103
|
+
|
|
104
|
+
<style>
|
|
105
|
+
.log {
|
|
106
|
+
font: var(--fs-meta, 12px) / 1.45 var(--font-display);
|
|
107
|
+
color: var(--text-primary, var(--ink));
|
|
108
|
+
}
|
|
109
|
+
.toolbar {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-wrap: wrap;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: space-between;
|
|
114
|
+
gap: 6px;
|
|
115
|
+
margin-bottom: 8px;
|
|
116
|
+
}
|
|
117
|
+
.filters,
|
|
118
|
+
.actions {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-wrap: wrap;
|
|
121
|
+
gap: 4px;
|
|
122
|
+
}
|
|
123
|
+
button {
|
|
124
|
+
font: inherit;
|
|
125
|
+
color: var(--ink);
|
|
126
|
+
border: 1px solid var(--line);
|
|
127
|
+
background: transparent;
|
|
128
|
+
padding: 3px 8px;
|
|
129
|
+
border-radius: 3px;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
}
|
|
132
|
+
button:hover {
|
|
133
|
+
background: var(--accent-soft);
|
|
134
|
+
}
|
|
135
|
+
button[aria-pressed='true'] {
|
|
136
|
+
background: var(--accent-soft);
|
|
137
|
+
border-color: var(--border-strong, var(--line));
|
|
138
|
+
}
|
|
139
|
+
button:focus-visible {
|
|
140
|
+
outline: 2px solid var(--focus-ring);
|
|
141
|
+
outline-offset: 2px;
|
|
142
|
+
}
|
|
143
|
+
.n {
|
|
144
|
+
font: 10px var(--font-mono);
|
|
145
|
+
color: var(--text-secondary);
|
|
146
|
+
}
|
|
147
|
+
.group + .group {
|
|
148
|
+
margin-top: 10px;
|
|
149
|
+
}
|
|
150
|
+
h4 {
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-wrap: wrap;
|
|
153
|
+
gap: 6px;
|
|
154
|
+
align-items: baseline;
|
|
155
|
+
margin: 0 0 4px;
|
|
156
|
+
font: 600 var(--fs-meta, 12px) / 1.4 var(--font-display);
|
|
157
|
+
}
|
|
158
|
+
.subject {
|
|
159
|
+
font-weight: 400;
|
|
160
|
+
color: var(--text-secondary);
|
|
161
|
+
overflow-wrap: anywhere;
|
|
162
|
+
}
|
|
163
|
+
ol {
|
|
164
|
+
list-style: none;
|
|
165
|
+
margin: 0;
|
|
166
|
+
padding: 0;
|
|
167
|
+
}
|
|
168
|
+
li {
|
|
169
|
+
padding: 6px 0;
|
|
170
|
+
border-top: 1px solid var(--line);
|
|
171
|
+
}
|
|
172
|
+
.empty {
|
|
173
|
+
margin: 0;
|
|
174
|
+
color: var(--text-secondary);
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type DiagnosticLog, type LogEntryInput } from '../log.svelte.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
log?: DiagnosticLog;
|
|
4
|
+
/** Makes each named bus a button, for a host that can select it. */
|
|
5
|
+
onbus?: (bus: number, entry: LogEntryInput) => void;
|
|
6
|
+
filename?: string;
|
|
7
|
+
empty?: string;
|
|
8
|
+
};
|
|
9
|
+
declare const DiagnosticLogView: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type DiagnosticLogView = ReturnType<typeof DiagnosticLogView>;
|
|
11
|
+
export default DiagnosticLogView;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
>
|
|
20
20
|
{#each FORMULATIONS as f (f.id)}
|
|
21
21
|
{@const unavailable =
|
|
22
|
-
f.disabled ||
|
|
22
|
+
(f.disabled && !(f.id === 'acopf' && ctrl.acOpfAvailable && !c.perturbed)) ||
|
|
23
23
|
('declaredFormulation' in c &&
|
|
24
24
|
c.declaredFormulation != null &&
|
|
25
25
|
c.declaredFormulation !== f.id)}
|