@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.
Files changed (115) hide show
  1. package/README.md +45 -0
  2. package/dist/TellegenMap.svelte +63 -22
  3. package/dist/TellegenProvider.svelte +7 -4
  4. package/dist/TellegenShell.svelte +12 -1
  5. package/dist/TellegenViewer.svelte +2 -0
  6. package/dist/basemap.d.ts +14 -0
  7. package/dist/basemap.js +27 -0
  8. package/dist/colors.d.ts +6 -1
  9. package/dist/colors.js +11 -4
  10. package/dist/components/DiagnosticLogView.svelte +176 -0
  11. package/dist/components/DiagnosticLogView.svelte.d.ts +11 -0
  12. package/dist/components/FormulationSelector.svelte +1 -1
  13. package/dist/components/LocalCaseDetails.svelte +18 -3
  14. package/dist/components/LogEntryView.svelte +188 -0
  15. package/dist/components/LogEntryView.svelte.d.ts +11 -0
  16. package/dist/components/LogPanel.svelte +25 -0
  17. package/dist/components/LogPanel.svelte.d.ts +3 -0
  18. package/dist/components/McResults.svelte +18 -3
  19. package/dist/components/McWorkbench.svelte +688 -0
  20. package/dist/components/McWorkbench.svelte.d.ts +38 -0
  21. package/dist/components/MultiWire.svelte +136 -0
  22. package/dist/components/MultiWire.svelte.d.ts +7 -0
  23. package/dist/components/MulticonductorDetails.svelte +7 -0
  24. package/dist/components/NotificationCenter.svelte +37 -6
  25. package/dist/components/OmniSearch.svelte +163 -0
  26. package/dist/components/OmniSearch.svelte.d.ts +14 -0
  27. package/dist/components/ResultsTable.svelte +259 -0
  28. package/dist/components/ResultsTable.svelte.d.ts +13 -0
  29. package/dist/components/SchematicCanvas.svelte +1896 -0
  30. package/dist/components/SchematicCanvas.svelte.d.ts +13 -0
  31. package/dist/components/SolveCard.svelte +6 -0
  32. package/dist/components/schematic/PickCard.svelte +126 -0
  33. package/dist/components/schematic/PickCard.svelte.d.ts +56 -0
  34. package/dist/components/schematic/export.d.ts +21 -0
  35. package/dist/components/schematic/export.js +110 -0
  36. package/dist/components/schematic/layers.d.ts +161 -0
  37. package/dist/components/schematic/layers.js +756 -0
  38. package/dist/components/schematic/paint.d.ts +28 -0
  39. package/dist/components/schematic/paint.js +69 -0
  40. package/dist/components/schematic/palette.d.ts +34 -0
  41. package/dist/components/schematic/palette.js +54 -0
  42. package/dist/components/schematic/text.d.ts +83 -0
  43. package/dist/components/schematic/text.js +190 -0
  44. package/dist/components/schematic/view-prefs.svelte.d.ts +30 -0
  45. package/dist/components/schematic/view-prefs.svelte.js +77 -0
  46. package/dist/components.d.ts +4 -0
  47. package/dist/components.js +4 -0
  48. package/dist/context.svelte.d.ts +2 -0
  49. package/dist/context.svelte.js +2 -0
  50. package/dist/controller.svelte.d.ts +40 -5
  51. package/dist/controller.svelte.js +230 -25
  52. package/dist/diagram/basemap.d.ts +28 -0
  53. package/dist/diagram/basemap.js +39 -0
  54. package/dist/diagram/bus-stacks.d.ts +19 -0
  55. package/dist/diagram/bus-stacks.js +99 -0
  56. package/dist/diagram/colour-modes.d.ts +18 -0
  57. package/dist/diagram/colour-modes.js +33 -0
  58. package/dist/diagram/connections.d.ts +30 -0
  59. package/dist/diagram/connections.js +129 -0
  60. package/dist/diagram/display-tree.d.ts +21 -0
  61. package/dist/diagram/display-tree.js +65 -0
  62. package/dist/diagram/energisation.d.ts +10 -0
  63. package/dist/diagram/energisation.js +31 -0
  64. package/dist/diagram/feeders.d.ts +30 -0
  65. package/dist/diagram/feeders.js +57 -0
  66. package/dist/diagram/galvanic-zones.d.ts +60 -0
  67. package/dist/diagram/galvanic-zones.js +210 -0
  68. package/dist/diagram/graph-inputs.d.ts +18 -0
  69. package/dist/diagram/graph-inputs.js +103 -0
  70. package/dist/diagram/index.d.ts +28 -0
  71. package/dist/diagram/index.js +28 -0
  72. package/dist/diagram/landmarks.d.ts +50 -0
  73. package/dist/diagram/landmarks.js +153 -0
  74. package/dist/diagram/layout-morph.d.ts +15 -0
  75. package/dist/diagram/layout-morph.js +32 -0
  76. package/dist/diagram/multi-wire.d.ts +44 -0
  77. package/dist/diagram/multi-wire.js +65 -0
  78. package/dist/diagram/phase-bars.d.ts +35 -0
  79. package/dist/diagram/phase-bars.js +44 -0
  80. package/dist/diagram/results-table.d.ts +47 -0
  81. package/dist/diagram/results-table.js +173 -0
  82. package/dist/diagram/schematic-equipment.d.ts +133 -0
  83. package/dist/diagram/schematic-equipment.js +645 -0
  84. package/dist/diagram/schematic-layout.d.ts +51 -0
  85. package/dist/diagram/schematic-layout.js +240 -0
  86. package/dist/diagram/search.d.ts +36 -0
  87. package/dist/diagram/search.js +48 -0
  88. package/dist/diagram/shared-routes.d.ts +21 -0
  89. package/dist/diagram/shared-routes.js +40 -0
  90. package/dist/diagram/short-names.d.ts +11 -0
  91. package/dist/diagram/short-names.js +54 -0
  92. package/dist/diagram/snapshot.d.ts +115 -0
  93. package/dist/diagram/snapshot.js +242 -0
  94. package/dist/diagram/summary.d.ts +65 -0
  95. package/dist/diagram/summary.js +144 -0
  96. package/dist/diagram/svg-export.d.ts +80 -0
  97. package/dist/diagram/svg-export.js +108 -0
  98. package/dist/diagram/tree-walk.d.ts +13 -0
  99. package/dist/diagram/tree-walk.js +83 -0
  100. package/dist/diagram/value-labels.d.ts +24 -0
  101. package/dist/diagram/value-labels.js +69 -0
  102. package/dist/diagram/view-state.d.ts +12 -0
  103. package/dist/diagram/view-state.js +22 -0
  104. package/dist/diagram/voltage-levels.d.ts +14 -0
  105. package/dist/diagram/voltage-levels.js +20 -0
  106. package/dist/index.d.ts +3 -0
  107. package/dist/index.js +2 -0
  108. package/dist/log.svelte.d.ts +77 -0
  109. package/dist/log.svelte.js +149 -0
  110. package/dist/notices.svelte.d.ts +3 -0
  111. package/dist/state.svelte.d.ts +42 -5
  112. package/dist/state.svelte.js +56 -4
  113. package/dist/transformer-icon.d.ts +22 -13
  114. package/dist/transformer-icon.js +23 -13
  115. 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:
@@ -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 { transformerIcon } from './transformer-icon.js';
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
- if (!isOpenFreeMapStyle(style)) return;
887
- const edits = openFreeMapEdits(
888
- style,
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
- getIcon: () => transformerIcon(),
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 diagramNetwork}
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` configure the
35
- // controller this provider owns, and the links and footer are fixed copy.
36
- // `theme` and `cartoApiKey` stay live, so a host can switch them and the
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 />
@@ -5,6 +5,7 @@
5
5
 
6
6
  let {
7
7
  apiBase,
8
+ acOpfWasmUrl,
8
9
  loadDefaultCases,
9
10
  docsHref,
10
11
  orgHref,
@@ -17,6 +18,7 @@
17
18
 
18
19
  <TellegenProvider
19
20
  {apiBase}
21
+ {acOpfWasmUrl}
20
22
  {loadDefaultCases}
21
23
  {docsHref}
22
24
  {orgHref}
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
- /** Branch color by loading fraction: warm gray, amber past 0.6, red past 0.9. */
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
- /** Branch color by loading fraction: warm gray, amber past 0.6, red past 0.9. */
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 < 0.6)
191
+ if (loading < LOADING_ELEVATED)
185
192
  return [138, 131, 117, 200];
186
- if (loading < 0.9)
187
- return lerpColor([138, 131, 117, 210], [212, 116, 34, 240], (loading - 0.6) / 0.3);
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)}