@tellegen/svelte 0.4.7 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/dist/TellegenMap.svelte +47 -13
- package/dist/TellegenMap.svelte.d.ts +3 -1
- package/dist/antimeridian.d.ts +21 -0
- package/dist/antimeridian.js +40 -0
- package/dist/basemap.d.ts +10 -0
- package/dist/basemap.js +20 -0
- package/dist/bus-kinds.d.ts +9 -0
- package/dist/bus-kinds.js +33 -0
- package/dist/components/DiagramCanvas.svelte +5 -1
- package/dist/components/McModelQualification.svelte +67 -0
- package/dist/components/McModelQualification.svelte.d.ts +8 -0
- package/dist/components/McNetworkScope.svelte +90 -0
- package/dist/components/McNetworkScope.svelte.d.ts +9 -0
- package/dist/components/McPfSettings.svelte +158 -0
- package/dist/components/McPfSettings.svelte.d.ts +9 -0
- package/dist/components/McResults.svelte +16 -1
- package/dist/components/McResults.svelte.d.ts +1 -0
- package/dist/components/McWorkbench.svelte +2 -0
- package/dist/components/MulticonductorDetails.svelte +25 -6
- package/dist/components.d.ts +3 -0
- package/dist/components.js +3 -0
- package/dist/controller.svelte.d.ts +9 -5
- package/dist/controller.svelte.js +83 -18
- package/dist/diagram/schematic-equipment.d.ts +4 -3
- package/dist/diagram/schematic-equipment.js +13 -1
- package/dist/diagram/snapshot.d.ts +6 -3
- package/dist/diagram/snapshot.js +56 -35
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/map-bounds.d.ts +8 -0
- package/dist/map-bounds.js +21 -8
- package/dist/multiconductor.js +38 -4
- package/dist/state.svelte.d.ts +33 -8
- package/dist/state.svelte.js +35 -5
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -231,6 +231,9 @@ The map switches to the fallback in three cases:
|
|
|
231
231
|
- its tiles answer 401, 403 or 429;
|
|
232
232
|
- tile fetches keep failing with nothing arriving.
|
|
233
233
|
|
|
234
|
+
Only the basemap style's own sources count. A source the host adds to the map
|
|
235
|
+
never triggers the switch, and its errors are logged to the console.
|
|
236
|
+
|
|
234
237
|
Whenever an OpenFreeMap style renders, as primary or as fallback,
|
|
235
238
|
`openFreeMapEdits` tunes it towards CARTO's look. Road lines start at zoom 8.
|
|
236
239
|
In the dark theme, water and waterways take Dark Matter's colours and land
|
|
@@ -260,13 +263,34 @@ besides its click callbacks:
|
|
|
260
263
|
- `onMapReady(map, overlay)`: called with the MapLibre map and the deck
|
|
261
264
|
overlay once they exist. It may return a cleanup, and it fires again after
|
|
262
265
|
a context-loss or basemap rebuild. If it throws, the map and its window
|
|
263
|
-
and document listeners are still released when the map is torn down.
|
|
266
|
+
and document listeners are still released when the map is torn down. A
|
|
267
|
+
switch to the fallback does not call it again (see below).
|
|
264
268
|
- `onBasemapFallback(reason)`: the switch to the fallback happened.
|
|
265
269
|
`reason.from` and `reason.to` name the two providers.
|
|
266
270
|
- `onBasemapError(reason)`: no basemap can be shown. The network layers
|
|
267
271
|
still draw. `reason.from` names the provider that failed last, and
|
|
268
272
|
`reason.to` is `null`.
|
|
269
273
|
|
|
274
|
+
The switch to the fallback replaces the whole MapLibre style, so sources and
|
|
275
|
+
layers the host added to the map are gone after it. The network,
|
|
276
|
+
`extraLayers` and `projection` carry over. A host that adds its own sources
|
|
277
|
+
and layers adds them on every `style.load`, not once in `onMapReady`, which
|
|
278
|
+
runs before the first style loads, so one listener covers the primary and the
|
|
279
|
+
fallback:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import type { Map as MapLibreMap } from "maplibre-gl";
|
|
283
|
+
|
|
284
|
+
function onMapReady(map: MapLibreMap) {
|
|
285
|
+
const addFeeders = () => {
|
|
286
|
+
map.addSource("feeders", { type: "geojson", data: "/feeders.geojson" });
|
|
287
|
+
map.addLayer({ id: "feeders", type: "line", source: "feeders" });
|
|
288
|
+
};
|
|
289
|
+
map.on("style.load", addFeeders);
|
|
290
|
+
return () => map.off("style.load", addFeeders);
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
270
294
|
`TellegenProvider` and `TellegenViewer` also take `cartoApiKey`, and pass it,
|
|
271
295
|
together with `theme`, to the default shell's map. Both follow their props:
|
|
272
296
|
changing either rebuilds that map in place. A SvelteKit 3 host can
|
package/dist/TellegenMap.svelte
CHANGED
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
} from './transformer-icon.js';
|
|
57
57
|
import { createGlobeHorizon, withGlobeHorizon } from './globe-horizon.js';
|
|
58
58
|
import { foldMapBounds } from './map-bounds.js';
|
|
59
|
+
import { nearestLongitude, routeBounds, unwrapLongitudePath } from './antimeridian.js';
|
|
59
60
|
import { getAppState, getController } from './context.svelte.js';
|
|
60
61
|
import { displayFmt, fmt, rgbaCss } from './format.js';
|
|
61
62
|
import { getPanelLayout } from './panels.svelte.js';
|
|
@@ -126,7 +127,9 @@
|
|
|
126
127
|
attribution?: 'compact' | 'full';
|
|
127
128
|
/** Called once the map and overlay exist, with an optional cleanup. It
|
|
128
129
|
* fires again after a context-loss or basemap remount, after the previous
|
|
129
|
-
* cleanup ran.
|
|
130
|
+
* cleanup ran. It runs before the first style loads, and a switch to the
|
|
131
|
+
* fallback basemap does not call it again: the new style drops every
|
|
132
|
+
* source and layer the host added, so add those on each `style.load`. */
|
|
130
133
|
onMapReady?: (map: MapLibreMap, overlay: MapLibreOverlay) => void | (() => void);
|
|
131
134
|
/** The primary basemap failed and the fallback was requested. The
|
|
132
135
|
* reason's `from` and `to` name the providers (`'carto'`,
|
|
@@ -473,7 +476,12 @@
|
|
|
473
476
|
const branch = displayedGeometry(caseId)?.branches.find((b) => b.id === branchId);
|
|
474
477
|
const path = branch?.path.filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat));
|
|
475
478
|
return branch && path && path.length >= 2
|
|
476
|
-
? {
|
|
479
|
+
? {
|
|
480
|
+
key: `${app.studyView?.id ?? caseId}-${branch.id}`,
|
|
481
|
+
caseId,
|
|
482
|
+
branchId: branch.id,
|
|
483
|
+
path: unwrapLongitudePath(path)
|
|
484
|
+
}
|
|
477
485
|
: null;
|
|
478
486
|
});
|
|
479
487
|
let selectedCueEl = $state.raw<HTMLDivElement | null>(null);
|
|
@@ -496,9 +504,16 @@
|
|
|
496
504
|
});
|
|
497
505
|
let multiDetailEl = $state.raw<SVGSVGElement | null>(null);
|
|
498
506
|
|
|
507
|
+
/** The screen point of a coordinate in the world copy nearest the camera.
|
|
508
|
+
* MapLibre projects a longitude in the copy it names, so a point across the
|
|
509
|
+
* antimeridian from the camera would land a whole world width away. */
|
|
510
|
+
function projectNear(m: MapLibreMap, [lon, lat]: [number, number]) {
|
|
511
|
+
return m.project([nearestLongitude(lon, m.getCenter().lng), lat]);
|
|
512
|
+
}
|
|
513
|
+
|
|
499
514
|
function syncSelectedCue() {
|
|
500
515
|
if (!map || !selectedCueEl || !selectedBusCue) return;
|
|
501
|
-
const point = map
|
|
516
|
+
const point = projectNear(map, [selectedBusCue.lon, selectedBusCue.lat]);
|
|
502
517
|
const { clientWidth, clientHeight } = map.getContainer();
|
|
503
518
|
const inRange =
|
|
504
519
|
point.x >= -40 &&
|
|
@@ -522,8 +537,12 @@
|
|
|
522
537
|
}
|
|
523
538
|
const activeMap = map;
|
|
524
539
|
const { clientWidth, clientHeight } = activeMap.getContainer();
|
|
540
|
+
// The path is unwrapped, so one shift by whole turns moves all of it to
|
|
541
|
+
// the world copy nearest the camera without breaking it at the seam.
|
|
542
|
+
const [lon0] = selectedBranchCue.path[0];
|
|
543
|
+
const shift = nearestLongitude(lon0, activeMap.getCenter().lng) - lon0;
|
|
525
544
|
const points = selectedBranchCue.path
|
|
526
|
-
.map(([lon, lat]) => activeMap.project([lon, lat]))
|
|
545
|
+
.map(([lon, lat]) => activeMap.project([lon + shift, lat]))
|
|
527
546
|
.filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y));
|
|
528
547
|
if (clientWidth === 0 || clientHeight === 0 || points.length < 2) {
|
|
529
548
|
selectedBranchCueEl.style.display = 'none';
|
|
@@ -589,14 +608,14 @@
|
|
|
589
608
|
return;
|
|
590
609
|
}
|
|
591
610
|
const { bus, edges } = detail;
|
|
592
|
-
const busPt = m
|
|
611
|
+
const busPt = projectNear(m, [bus.lon, bus.lat]);
|
|
593
612
|
|
|
594
613
|
const STRAND_SPACING = 5;
|
|
595
614
|
let strands = '';
|
|
596
615
|
for (const e of edges) {
|
|
597
616
|
const selectedIsFrom = e.from === bus.id;
|
|
598
617
|
const other = selectedIsFrom ? e.path[e.path.length - 1] : e.path[0];
|
|
599
|
-
const b = m
|
|
618
|
+
const b = projectNear(m, other);
|
|
600
619
|
const dx = b.x - busPt.x;
|
|
601
620
|
const dy = b.y - busPt.y;
|
|
602
621
|
const len = Math.hypot(dx, dy) || 1;
|
|
@@ -677,7 +696,7 @@
|
|
|
677
696
|
}
|
|
678
697
|
|
|
679
698
|
function branchBounds(path: [number, number][]): LngLatBoundsLike | null {
|
|
680
|
-
return
|
|
699
|
+
return routeBounds(path);
|
|
681
700
|
}
|
|
682
701
|
|
|
683
702
|
function prefersReducedMotion(): boolean {
|
|
@@ -1077,6 +1096,12 @@
|
|
|
1077
1096
|
// Sync deck.gl layers with app state. New layer instances diff cheaply;
|
|
1078
1097
|
// updateTriggers tell deck.gl when accessor outputs changed. Host layers
|
|
1079
1098
|
// (`extraLayers`) draw after tellegen's own at both exits.
|
|
1099
|
+
//
|
|
1100
|
+
// Stored routes keep longitudes in [-180, 180], so a route across the
|
|
1101
|
+
// antimeridian is unwrapped before drawing and each segment takes the
|
|
1102
|
+
// short way. On the globe deck.gl ignores `wrapLongitude` and needs the
|
|
1103
|
+
// unwrapped path itself; on the flat map `wrapLongitude` cuts the path at
|
|
1104
|
+
// ±180 so both halves draw in every world copy.
|
|
1080
1105
|
$effect(() => {
|
|
1081
1106
|
if (!overlay || !layerCtors) return;
|
|
1082
1107
|
const { PathLayer, ScatterplotLayer, IconLayer } = layerCtors;
|
|
@@ -1108,7 +1133,8 @@
|
|
|
1108
1133
|
new PathLayer<NetworkBranch>({
|
|
1109
1134
|
id: 'study-branches',
|
|
1110
1135
|
data: study.network.branches,
|
|
1111
|
-
getPath: (b) => b.path,
|
|
1136
|
+
getPath: (b) => unwrapLongitudePath(b.path),
|
|
1137
|
+
wrapLongitude: true,
|
|
1112
1138
|
getColor: (b) => branchColor(flows.get(b.id) ?? 0, b.status === 1),
|
|
1113
1139
|
getWidth: (b) => branchWidth(flows.get(b.id) ?? 0),
|
|
1114
1140
|
widthUnits: 'pixels',
|
|
@@ -1163,7 +1189,8 @@
|
|
|
1163
1189
|
new PathLayer<NetworkBranch>({
|
|
1164
1190
|
id: `branches-${c.id}`,
|
|
1165
1191
|
data: c.network.branches,
|
|
1166
|
-
getPath: (b) => b.path,
|
|
1192
|
+
getPath: (b) => unwrapLongitudePath(b.path),
|
|
1193
|
+
wrapLongitude: true,
|
|
1167
1194
|
// The selected branch draws in the selection blue of the bus halo cue,
|
|
1168
1195
|
// wider, so the price-rating source line reads at a glance.
|
|
1169
1196
|
getColor: (b) =>
|
|
@@ -1230,15 +1257,21 @@
|
|
|
1230
1257
|
}
|
|
1231
1258
|
// Local cases are grey until the browser solve returns. A .pwd display
|
|
1232
1259
|
// file contributes substation points only, in a cooler slate so they
|
|
1233
|
-
// read as diagram derived positions.
|
|
1260
|
+
// read as diagram derived positions. A generated grid's line taps draw
|
|
1261
|
+
// no marker, as their lines show them, unless one is the selected bus.
|
|
1234
1262
|
for (const c of app.localCases) {
|
|
1235
1263
|
if (c.view) {
|
|
1236
1264
|
const d = display.get(c.id);
|
|
1265
|
+
const selectedTap =
|
|
1266
|
+
c.id === app.activeLocalId && app.selectedBus !== null && c.tapBuses.has(app.selectedBus)
|
|
1267
|
+
? c.view.buses.find((b) => b.id === app.selectedBus)
|
|
1268
|
+
: undefined;
|
|
1237
1269
|
layers.push(
|
|
1238
1270
|
new PathLayer<NetworkBranch>({
|
|
1239
1271
|
id: `local-branches-${c.id}`,
|
|
1240
1272
|
data: c.view.branches,
|
|
1241
|
-
getPath: (b) => b.path,
|
|
1273
|
+
getPath: (b) => unwrapLongitudePath(b.path),
|
|
1274
|
+
wrapLongitude: true,
|
|
1242
1275
|
getColor: (b) =>
|
|
1243
1276
|
c.id === app.activeLocalId && b.id === app.selectedBranch
|
|
1244
1277
|
? [47, 111, 187, 255]
|
|
@@ -1272,7 +1305,7 @@
|
|
|
1272
1305
|
}),
|
|
1273
1306
|
new ScatterplotLayer<NetworkBus>({
|
|
1274
1307
|
id: `local-buses-${c.id}`,
|
|
1275
|
-
data: c.
|
|
1308
|
+
data: selectedTap ? [...c.markerBuses, selectedTap] : c.markerBuses,
|
|
1276
1309
|
getPosition: (b) => [b.lon, b.lat],
|
|
1277
1310
|
getRadius: (b) => marker.radius(Math.max(b.demand_mw, b.gen_mw)),
|
|
1278
1311
|
radiusUnits: marker.radiusUnits,
|
|
@@ -1343,7 +1376,8 @@
|
|
|
1343
1376
|
new PathLayer<PlacedMultiEdge>({
|
|
1344
1377
|
id: `multi-edges-${c.id}`,
|
|
1345
1378
|
data: c.view.edges,
|
|
1346
|
-
getPath: (e) => e.path,
|
|
1379
|
+
getPath: (e) => unwrapLongitudePath(e.path),
|
|
1380
|
+
wrapLongitude: true,
|
|
1347
1381
|
// The selected edge draws in the selection blue of the branch cue,
|
|
1348
1382
|
// wider, so its panel detail has an obvious source on the map.
|
|
1349
1383
|
getColor: (e) =>
|
|
@@ -44,7 +44,9 @@ type $$ComponentProps = {
|
|
|
44
44
|
attribution?: 'compact' | 'full';
|
|
45
45
|
/** Called once the map and overlay exist, with an optional cleanup. It
|
|
46
46
|
* fires again after a context-loss or basemap remount, after the previous
|
|
47
|
-
* cleanup ran.
|
|
47
|
+
* cleanup ran. It runs before the first style loads, and a switch to the
|
|
48
|
+
* fallback basemap does not call it again: the new style drops every
|
|
49
|
+
* source and layer the host added, so add those on each `style.load`. */
|
|
48
50
|
onMapReady?: (map: MapLibreMap, overlay: MapLibreOverlay) => void | (() => void);
|
|
49
51
|
/** The primary basemap failed and the fallback was requested. The
|
|
50
52
|
* reason's `from` and `to` name the providers (`'carto'`,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Coordinate helpers for geographic paths drawn across the antimeridian.
|
|
2
|
+
*
|
|
3
|
+
* Stored longitudes stay in [-180, 180]. A drawn path is unwrapped so each
|
|
4
|
+
* vertex lies within 180° of the one before, and every segment takes the
|
|
5
|
+
* short way across ±180 instead of crossing the whole map.
|
|
6
|
+
*/
|
|
7
|
+
import { type MapBounds } from './map-bounds.js';
|
|
8
|
+
export type LngLat = readonly [number, number];
|
|
9
|
+
/** `lon` moved by whole turns of 360° to lie within 180° of `reference`.
|
|
10
|
+
* Returned unchanged when either value is not finite. */
|
|
11
|
+
export declare function nearestLongitude(lon: number, reference: number): number;
|
|
12
|
+
/** Return a display path whose consecutive longitudes differ by at most 180°.
|
|
13
|
+
* The returned longitudes may lie outside [-180, 180]; use only for drawing,
|
|
14
|
+
* never for serialization or exported GeoJSON. A vertex whose longitude is
|
|
15
|
+
* not finite is copied as it is and does not anchor the next one. */
|
|
16
|
+
export declare function unwrapLongitudePath(path: readonly LngLat[]): [number, number][];
|
|
17
|
+
/** Bounds of a stored route around its short arc, so a seam-crossing route
|
|
18
|
+
* frames the few degrees it spans rather than the whole world. West may lie
|
|
19
|
+
* below -180 or east above 180. Vertices `foldMapBounds` ignores are ignored
|
|
20
|
+
* here too. */
|
|
21
|
+
export declare function routeBounds(path: readonly LngLat[]): MapBounds | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Coordinate helpers for geographic paths drawn across the antimeridian.
|
|
2
|
+
*
|
|
3
|
+
* Stored longitudes stay in [-180, 180]. A drawn path is unwrapped so each
|
|
4
|
+
* vertex lies within 180° of the one before, and every segment takes the
|
|
5
|
+
* short way across ±180 instead of crossing the whole map.
|
|
6
|
+
*/
|
|
7
|
+
import { isMapPoint, spanBounds } from './map-bounds.js';
|
|
8
|
+
/** `lon` moved by whole turns of 360° to lie within 180° of `reference`.
|
|
9
|
+
* Returned unchanged when either value is not finite. */
|
|
10
|
+
export function nearestLongitude(lon, reference) {
|
|
11
|
+
const delta = lon - reference;
|
|
12
|
+
if (!Number.isFinite(delta) || Math.abs(delta) <= 180)
|
|
13
|
+
return lon;
|
|
14
|
+
return lon - 360 * Math.round(delta / 360);
|
|
15
|
+
}
|
|
16
|
+
/** Return a display path whose consecutive longitudes differ by at most 180°.
|
|
17
|
+
* The returned longitudes may lie outside [-180, 180]; use only for drawing,
|
|
18
|
+
* never for serialization or exported GeoJSON. A vertex whose longitude is
|
|
19
|
+
* not finite is copied as it is and does not anchor the next one. */
|
|
20
|
+
export function unwrapLongitudePath(path) {
|
|
21
|
+
const out = [];
|
|
22
|
+
let previous = null;
|
|
23
|
+
for (const [rawLongitude, latitude] of path) {
|
|
24
|
+
if (!Number.isFinite(rawLongitude)) {
|
|
25
|
+
out.push([rawLongitude, latitude]);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const longitude = previous === null ? rawLongitude : nearestLongitude(rawLongitude, previous);
|
|
29
|
+
out.push([longitude, latitude]);
|
|
30
|
+
previous = longitude;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** Bounds of a stored route around its short arc, so a seam-crossing route
|
|
35
|
+
* frames the few degrees it spans rather than the whole world. West may lie
|
|
36
|
+
* below -180 or east above 180. Vertices `foldMapBounds` ignores are ignored
|
|
37
|
+
* here too. */
|
|
38
|
+
export function routeBounds(path) {
|
|
39
|
+
return spanBounds(unwrapLongitudePath(path.filter(isMapPoint)));
|
|
40
|
+
}
|
package/dist/basemap.d.ts
CHANGED
|
@@ -157,6 +157,10 @@ export interface BasemapMapLike {
|
|
|
157
157
|
}): unknown;
|
|
158
158
|
/** MapLibre types this `boolean | void`; only `true` counts as loaded. */
|
|
159
159
|
isStyleLoaded(): boolean | void;
|
|
160
|
+
/** Read at every `style.load` to learn which sources the basemap style
|
|
161
|
+
* brings, before a host adds its own. Without it, every source counts as
|
|
162
|
+
* the basemap's. */
|
|
163
|
+
getStyle?(): StyleSpecification | undefined;
|
|
160
164
|
}
|
|
161
165
|
export interface BasemapFallbackHandlers {
|
|
162
166
|
/** The primary failed and the fallback style was requested. `from` is the
|
|
@@ -185,6 +189,12 @@ export interface BasemapFallbackOptions {
|
|
|
185
189
|
* 5. everything else is logged the way MapLibre logs an unhandled error,
|
|
186
190
|
* because registering this listener silences that default.
|
|
187
191
|
*
|
|
192
|
+
* Rules 2 and 3 only count errors from the basemap style's own sources: the
|
|
193
|
+
* sources `map.getStyle()` lists when that style's `style.load` reaches this
|
|
194
|
+
* listener, so attach it before any listener that adds sources. A source the
|
|
195
|
+
* host adds later, through `onMapReady` for instance, falls under rule 5 and
|
|
196
|
+
* never switches the basemap.
|
|
197
|
+
*
|
|
188
198
|
* After the switch the same rules report `onFailure` instead of switching
|
|
189
199
|
* again, as they do when `spec.fallback` is `null`. Returns a detach
|
|
190
200
|
* function. */
|
package/dist/basemap.js
CHANGED
|
@@ -169,6 +169,12 @@ function errorStatus(error) {
|
|
|
169
169
|
* 5. everything else is logged the way MapLibre logs an unhandled error,
|
|
170
170
|
* because registering this listener silences that default.
|
|
171
171
|
*
|
|
172
|
+
* Rules 2 and 3 only count errors from the basemap style's own sources: the
|
|
173
|
+
* sources `map.getStyle()` lists when that style's `style.load` reaches this
|
|
174
|
+
* listener, so attach it before any listener that adds sources. A source the
|
|
175
|
+
* host adds later, through `onMapReady` for instance, falls under rule 5 and
|
|
176
|
+
* never switches the basemap.
|
|
177
|
+
*
|
|
172
178
|
* After the switch the same rules report `onFailure` instead of switching
|
|
173
179
|
* again, as they do when `spec.fallback` is `null`. Returns a detach
|
|
174
180
|
* function. */
|
|
@@ -176,7 +182,14 @@ export function attachBasemapFallback(map, spec, handlers = {}, options = {}) {
|
|
|
176
182
|
const now = options.now ?? (() => Date.now());
|
|
177
183
|
const primaryProvider = spec.primaryProvider ?? 'custom';
|
|
178
184
|
const fallbackProvider = spec.fallback === null ? null : (spec.fallbackProvider ?? 'custom');
|
|
185
|
+
/** The source ids of the style on screen, or `null` while unknown, when
|
|
186
|
+
* every source counts as the basemap's. */
|
|
187
|
+
const styleSources = () => {
|
|
188
|
+
const style = map.getStyle?.();
|
|
189
|
+
return style ? new Set(Object.keys(style.sources ?? {})) : null;
|
|
190
|
+
};
|
|
179
191
|
let styleLoaded = map.isStyleLoaded() === true;
|
|
192
|
+
let basemapSources = styleLoaded ? styleSources() : null;
|
|
180
193
|
let fellBack = false;
|
|
181
194
|
let failed = false;
|
|
182
195
|
let tileFailures = 0;
|
|
@@ -196,6 +209,7 @@ export function attachBasemapFallback(map, spec, handlers = {}, options = {}) {
|
|
|
196
209
|
}
|
|
197
210
|
fellBack = true;
|
|
198
211
|
styleLoaded = false;
|
|
212
|
+
basemapSources = null;
|
|
199
213
|
tileFailures = 0;
|
|
200
214
|
tileArrived = false;
|
|
201
215
|
handlers.onFallback?.({ ...reason, from: primaryProvider, to: fallbackProvider });
|
|
@@ -206,6 +220,7 @@ export function attachBasemapFallback(map, spec, handlers = {}, options = {}) {
|
|
|
206
220
|
};
|
|
207
221
|
const onStyleLoad = () => {
|
|
208
222
|
styleLoaded = true;
|
|
223
|
+
basemapSources = styleSources();
|
|
209
224
|
};
|
|
210
225
|
const onSourceData = (event) => {
|
|
211
226
|
if (event?.tile || event?.isSourceLoaded === true)
|
|
@@ -227,6 +242,11 @@ export function attachBasemapFallback(map, spec, handlers = {}, options = {}) {
|
|
|
227
242
|
passThrough(event);
|
|
228
243
|
return;
|
|
229
244
|
}
|
|
245
|
+
// A host's own source failing says nothing about the basemap.
|
|
246
|
+
if (basemapSources !== null && !basemapSources.has(sourceId)) {
|
|
247
|
+
passThrough(event);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
230
250
|
if ((status !== undefined && FORBIDDEN_STATUSES.has(status)) ||
|
|
231
251
|
FORBIDDEN_MESSAGE.test(message)) {
|
|
232
252
|
fallBack({ kind: 'source-forbidden', message, status: status ?? 0, sourceId });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NetworkBus } from './api.js';
|
|
2
|
+
/** Ids of the buses a generated grid marks as line taps: points where a line
|
|
3
|
+
* is tapped, with no load or machine. tellegen-synth records the kind in
|
|
4
|
+
* each bus row's `extras.tellegen_synth.kind`; the map view does not carry
|
|
5
|
+
* it. Empty for any other module, and for text that does not parse. */
|
|
6
|
+
export declare function tapBusIds(moduleJson: string | null | undefined): ReadonlySet<number>;
|
|
7
|
+
/** `buses` less the taps, the ones the map draws as markers. The same array
|
|
8
|
+
* when there is no tap to drop, so the map layer keeps its data. */
|
|
9
|
+
export declare function withoutTaps(buses: NetworkBus[], taps: ReadonlySet<number>): NetworkBus[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The `extras` key under which tellegen-synth records each element's
|
|
2
|
+
* annotation in the modules it writes. */
|
|
3
|
+
const SYNTH_EXTRAS = 'tellegen_synth';
|
|
4
|
+
/** Ids of the buses a generated grid marks as line taps: points where a line
|
|
5
|
+
* is tapped, with no load or machine. tellegen-synth records the kind in
|
|
6
|
+
* each bus row's `extras.tellegen_synth.kind`; the map view does not carry
|
|
7
|
+
* it. Empty for any other module, and for text that does not parse. */
|
|
8
|
+
export function tapBusIds(moduleJson) {
|
|
9
|
+
const ids = new Set();
|
|
10
|
+
// Only a module tellegen-synth wrote is parsed a second time; a branch's
|
|
11
|
+
// `"tap"` ratio is in every transmission case.
|
|
12
|
+
if (!moduleJson?.includes(`"${SYNTH_EXTRAS}"`) || !moduleJson.includes('"tap"'))
|
|
13
|
+
return ids;
|
|
14
|
+
let buses;
|
|
15
|
+
try {
|
|
16
|
+
buses = JSON.parse(moduleJson)?.value?.data?.buses;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return ids;
|
|
20
|
+
}
|
|
21
|
+
if (!Array.isArray(buses))
|
|
22
|
+
return ids;
|
|
23
|
+
for (const bus of buses) {
|
|
24
|
+
if (typeof bus?.id === 'number' && bus.extras?.[SYNTH_EXTRAS]?.kind === 'tap')
|
|
25
|
+
ids.add(bus.id);
|
|
26
|
+
}
|
|
27
|
+
return ids;
|
|
28
|
+
}
|
|
29
|
+
/** `buses` less the taps, the ones the map draws as markers. The same array
|
|
30
|
+
* when there is no tap to drop, so the map layer keeps its data. */
|
|
31
|
+
export function withoutTaps(buses, taps) {
|
|
32
|
+
return taps.size === 0 ? buses : buses.filter((b) => !taps.has(b.id));
|
|
33
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { mcNetworkScopeLabel, mcLoadModelLabel } from '@tellegen/engine';
|
|
2
3
|
import { untrack } from 'svelte';
|
|
3
4
|
import { on } from 'svelte/events';
|
|
4
5
|
import type { Network, NetworkBus } from '../api.js';
|
|
@@ -395,7 +396,10 @@
|
|
|
395
396
|
>
|
|
396
397
|
</div>
|
|
397
398
|
</div>
|
|
398
|
-
<p class="coordinate-label">
|
|
399
|
+
<p class="coordinate-label">
|
|
400
|
+
Diagram coordinates{#if multiconductor?.result}
|
|
401
|
+
· {mcLoadModelLabel(multiconductor.result.load_model)} · {mcNetworkScopeLabel(multiconductor.result.network_scope)}{/if}
|
|
402
|
+
</p>
|
|
399
403
|
</div>
|
|
400
404
|
|
|
401
405
|
<style>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { mcLoadModelLabel, type McLoadModelAssessment } from '@tellegen/engine';
|
|
3
|
+
let { model, compact = false }: { model?: McLoadModelAssessment | null; compact?: boolean } =
|
|
4
|
+
$props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<aside class:warning={model?.status !== 'original'} aria-label="Load-model qualification">
|
|
8
|
+
<strong>{mcLoadModelLabel(model)}</strong>
|
|
9
|
+
{#if model?.status === 'modified'}
|
|
10
|
+
<p>
|
|
11
|
+
The calculation converged using modified load equations on {model.modified_branch_count}
|
|
12
|
+
load {model.modified_branch_count === 1 ? 'branch' : 'branches'}. These values are not a
|
|
13
|
+
verified solution of the original load model. The qualification is retained when saving or
|
|
14
|
+
exporting.
|
|
15
|
+
</p>
|
|
16
|
+
{:else if model?.status !== 'original'}
|
|
17
|
+
<p>
|
|
18
|
+
These results have no verified solution of the original load equations. Rerun to assess the
|
|
19
|
+
load model.
|
|
20
|
+
</p>
|
|
21
|
+
{/if}
|
|
22
|
+
{#if model && !compact}
|
|
23
|
+
<details>
|
|
24
|
+
<summary>Model assessment and settings used</summary>
|
|
25
|
+
<p>
|
|
26
|
+
Original-model KCL residual: {model.original_kcl_residual === null
|
|
27
|
+
? 'undefined at this voltage'
|
|
28
|
+
: `${model.original_kcl_residual.toExponential(3)} A`}. Scaled residual: {model.original_scaled_kcl_residual ===
|
|
29
|
+
null
|
|
30
|
+
? 'unavailable'
|
|
31
|
+
: model.original_scaled_kcl_residual.toPrecision(4)} (at most 1 passes).
|
|
32
|
+
</p>
|
|
33
|
+
<p>
|
|
34
|
+
Load-model envelope {model.options.voltage_envelope ? 'enabled' : 'disabled'}; Vlow {model
|
|
35
|
+
.options.v_low_pu}, Vmin {model.options.v_min_pu}, Vmax {model.options.v_max_pu} pu. These thresholds
|
|
36
|
+
change load equations; they are not operating voltage limits.
|
|
37
|
+
</p>
|
|
38
|
+
<p>
|
|
39
|
+
Maximum iterations {model.options.max_iterations}; damping {model.options.damping};
|
|
40
|
+
voltage-change tolerance {model.options.tolerance} V; zero-voltage tolerance {model.options
|
|
41
|
+
.zero_voltage_tolerance} V; KCL tolerances {model.options.absolute_kcl_tolerance} A absolute
|
|
42
|
+
and {model.options.relative_kcl_tolerance} relative.
|
|
43
|
+
</p>
|
|
44
|
+
</details>
|
|
45
|
+
{/if}
|
|
46
|
+
</aside>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
aside {
|
|
50
|
+
margin: 0.75rem 0;
|
|
51
|
+
padding: 0.65rem;
|
|
52
|
+
border: 1px solid var(--line);
|
|
53
|
+
border-radius: 4px;
|
|
54
|
+
font-size: 12px;
|
|
55
|
+
}
|
|
56
|
+
.warning {
|
|
57
|
+
border-left: 4px solid #b7791f;
|
|
58
|
+
}
|
|
59
|
+
p {
|
|
60
|
+
margin: 0.4rem 0;
|
|
61
|
+
line-height: 1.5;
|
|
62
|
+
}
|
|
63
|
+
summary {
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
margin-top: 0.4rem;
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type McLoadModelAssessment } from '@tellegen/engine';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
model?: McLoadModelAssessment | null;
|
|
4
|
+
compact?: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const McModelQualification: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type McModelQualification = ReturnType<typeof McModelQualification>;
|
|
8
|
+
export default McModelQualification;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
mcNetworkScopeLabel,
|
|
4
|
+
type McNetworkScope as NetworkScope,
|
|
5
|
+
type McExcludedIsland
|
|
6
|
+
} from '@tellegen/engine';
|
|
7
|
+
let {
|
|
8
|
+
scope,
|
|
9
|
+
islands = [],
|
|
10
|
+
preview = false
|
|
11
|
+
}: {
|
|
12
|
+
scope?: NetworkScope | null;
|
|
13
|
+
islands?: McExcludedIsland[];
|
|
14
|
+
preview?: boolean;
|
|
15
|
+
} = $props();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#if scope}
|
|
19
|
+
<aside aria-label="Network calculation scope">
|
|
20
|
+
<strong>{preview ? 'Unpowered islands detected' : mcNetworkScopeLabel(scope)}</strong>
|
|
21
|
+
{#if preview}<p>
|
|
22
|
+
{scope.excluded_island_count}
|
|
23
|
+
{scope.excluded_island_count === 1 ? 'island has' : 'islands have'} no voltage source.
|
|
24
|
+
</p>{/if}
|
|
25
|
+
<p>
|
|
26
|
+
{scope.excluded_bus_count}
|
|
27
|
+
{scope.excluded_bus_count === 1 ? 'bus' : 'buses'} and {scope.excluded_equipment_count}
|
|
28
|
+
equipment
|
|
29
|
+
{scope.excluded_equipment_count === 1 ? 'record' : 'records'}
|
|
30
|
+
{preview
|
|
31
|
+
? 'will be excluded when you solve the energized network'
|
|
32
|
+
: 'are excluded from the calculation'}
|
|
33
|
+
because their {scope.excluded_island_count === 1 ? 'island has' : 'islands have'} no voltage source.
|
|
34
|
+
The full network remains available for inspection. Excluded equipment has no calculated result;
|
|
35
|
+
its voltage and power are not reported as zero.
|
|
36
|
+
</p>
|
|
37
|
+
{#if scope.excluded_load_count}<p>
|
|
38
|
+
{scope.excluded_load_count}
|
|
39
|
+
{scope.excluded_load_count === 1 ? 'load has' : 'loads have'} no voltage source and {preview
|
|
40
|
+
? 'will be omitted'
|
|
41
|
+
: scope.excluded_load_count === 1
|
|
42
|
+
? 'is absent'
|
|
43
|
+
: 'are absent'} from solved totals.
|
|
44
|
+
</p>{/if}
|
|
45
|
+
{#if islands.length}
|
|
46
|
+
<details>
|
|
47
|
+
<summary
|
|
48
|
+
>{preview
|
|
49
|
+
? 'Islands and equipment to exclude'
|
|
50
|
+
: 'Excluded islands and equipment'}</summary
|
|
51
|
+
>
|
|
52
|
+
{#each islands as island, i}
|
|
53
|
+
<h4>Island {i + 1}: no voltage source</h4>
|
|
54
|
+
<p>Buses: {island.buses.join(', ')}</p>
|
|
55
|
+
<ul>
|
|
56
|
+
{#each island.equipment as item}<li>
|
|
57
|
+
{item.kind}: {item.name}
|
|
58
|
+
</li>{/each}
|
|
59
|
+
</ul>
|
|
60
|
+
{/each}
|
|
61
|
+
</details>
|
|
62
|
+
{/if}
|
|
63
|
+
</aside>
|
|
64
|
+
{/if}
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
aside {
|
|
68
|
+
margin: 0.75rem 0;
|
|
69
|
+
padding: 0.65rem;
|
|
70
|
+
border: 1px solid var(--line);
|
|
71
|
+
border-left: 4px solid #b7791f;
|
|
72
|
+
border-radius: 4px;
|
|
73
|
+
font-size: 12px;
|
|
74
|
+
overflow-wrap: anywhere;
|
|
75
|
+
}
|
|
76
|
+
p {
|
|
77
|
+
margin: 0.4rem 0;
|
|
78
|
+
line-height: 1.5;
|
|
79
|
+
}
|
|
80
|
+
summary {
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
}
|
|
83
|
+
details {
|
|
84
|
+
max-height: 20rem;
|
|
85
|
+
overflow: auto;
|
|
86
|
+
}
|
|
87
|
+
h4 {
|
|
88
|
+
margin: 0.75rem 0 0.25rem;
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type McNetworkScope as NetworkScope, type McExcludedIsland } from '@tellegen/engine';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
scope?: NetworkScope | null;
|
|
4
|
+
islands?: McExcludedIsland[];
|
|
5
|
+
preview?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare const McNetworkScope: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type McNetworkScope = ReturnType<typeof McNetworkScope>;
|
|
9
|
+
export default McNetworkScope;
|