gtfs-garage-web 0.0.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 ADDED
@@ -0,0 +1,86 @@
1
+ # gtfs-garage-web
2
+
3
+ Browse a GTFS feed: a table with filters and foreign-key navigation, and a map
4
+ of its stops, shapes and zones. The viewer of
5
+ [GTFS Garage](https://github.com/MobilityData/gtfs-garage), packaged so it can
6
+ be mounted in another application.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npm install gtfs-garage-web
12
+ ```
13
+
14
+ The map is a separate entry point, and MapLibre an **optional** peer. Install
15
+ it only if you want one:
16
+
17
+ ```sh
18
+ npm install maplibre-gl # only with the map
19
+ ```
20
+
21
+ Nothing in the main entry mentions MapLibre, so a viewer without a map neither
22
+ downloads it nor needs it installed to build. That is why the map arrives as an
23
+ import rather than a flag: a bundler resolves `import()` specifiers whether or
24
+ not the branch runs, so a flag would still have made MapLibre mandatory.
25
+
26
+ ## React
27
+
28
+ ```tsx
29
+ "use client";
30
+
31
+ import { GtfsGarage } from "gtfs-garage-web/react";
32
+ import { mapPart } from "gtfs-garage-web/map";
33
+ import "gtfs-garage-web/style.css";
34
+ import "maplibre-gl/dist/maplibre-gl.css";
35
+
36
+ export default function Feed() {
37
+ return <GtfsGarage map={mapPart} baseUrl="https://example.org" style={{ height: "80vh" }} />;
38
+ }```
39
+
40
+ Drop the two `map` imports and the MapLibre stylesheet for a table-only viewer:
41
+
42
+ ```tsx
43
+ <GtfsGarage baseUrl="https://example.org" style={{ height: "80vh" }} />
44
+ ```
45
+
46
+ `"use client"` is required: the viewer builds DOM and cannot render on a server.
47
+
48
+ **Give the container a height.** The viewer fills whatever box it is given, and
49
+ a box with no height renders an invisible map.
50
+
51
+ ## Without React
52
+
53
+ ```ts
54
+ import { mount } from "gtfs-garage-web";
55
+ import "gtfs-garage-web/style.css";
56
+
57
+ const viewer = await mount(document.querySelector("#feed")!, {
58
+ baseUrl: "https://example.org",
59
+ // map: mapPart, from "gtfs-garage-web/map"
60
+ });
61
+
62
+ viewer.destroy(); // releases the map, the history subscription and the markup
63
+ ```
64
+
65
+ ## Where the data comes from
66
+
67
+ By default the viewer talks to a GTFS Garage server at `baseUrl`. To drive it
68
+ from your own backend instead, pass a `source` implementing `ViewerSource` —
69
+ `GtfsSource` for the table, plus `geojsonStream` for the map, which draws a
70
+ large feed's shapes as they arrive rather than all at the end.
71
+
72
+ ## Styling
73
+
74
+ Every rule is scoped under `.gtfs-viewer`, so the stylesheet cannot reach your
75
+ page. The design tokens are declared on that class and are the theming API:
76
+
77
+ ```css
78
+ .gtfs-viewer {
79
+ --accent: #2563eb;
80
+ --font: "Inter", sans-serif;
81
+ }
82
+ ```
83
+
84
+ ## Licence
85
+
86
+ Apache-2.0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Where a dataset stands, as the host sees it.
3
+ *
4
+ * A host knows whether a dataset has been prepared and how far along it is; the
5
+ * viewer knows how to say so. This interface is the whole of what a host
6
+ * supplies - facts, never markup - and `mountDataset` renders every state from
7
+ * it, so no integration writes its own progress bar or its own error box.
8
+ */
9
+ import type { LoadProgress, ViewerSource } from "./sources/types";
10
+ export type DatasetState =
11
+ /** Nothing has been prepared yet. `prepare` is offered the chance to start it. */
12
+ {
13
+ state: "absent";
14
+ }
15
+ /**
16
+ * Being prepared. `progress` uses the same phases a feed load reports -
17
+ * download, extract, convert, summarise - so the wording is already written.
18
+ */
19
+ | {
20
+ state: "preparing";
21
+ progress?: LoadProgress;
22
+ }
23
+ /** Ready to browse. The host supplies the source, so it decides how to read it. */
24
+ | {
25
+ state: "ready";
26
+ source: ViewerSource;
27
+ } | {
28
+ state: "failed";
29
+ message: string;
30
+ };
31
+ export interface DatasetProvider {
32
+ /**
33
+ * Where the dataset stands. Polled while it is being prepared, and passed an
34
+ * `AbortSignal` that fires when the viewer goes away mid-request.
35
+ */
36
+ status(signal: AbortSignal): Promise<DatasetState>;
37
+ /**
38
+ * Start preparing the dataset, for a host that offers that. Called at most
39
+ * once, on the first `absent` - calling it per poll would queue a conversion
40
+ * every second.
41
+ */
42
+ prepare?(signal: AbortSignal): Promise<void>;
43
+ }
44
+ /**
45
+ * What to do about a reported state.
46
+ *
47
+ * Separated from the mounting so it can be tested without a document, which is
48
+ * how the rest of this package is arranged: `values.ts` decides and `table.ts`
49
+ * draws. Every rule that matters - when to stop polling, when to ask for
50
+ * preparation, what to say - is here.
51
+ */
52
+ export interface DatasetDecision {
53
+ /** What to show. Absent when the viewer itself is taking the root over. */
54
+ line?: string;
55
+ tone?: "error";
56
+ /** Stop polling: there is nothing further to wait for. */
57
+ settled?: boolean;
58
+ /** Ask the host to begin preparing. Never more than once. */
59
+ prepare?: boolean;
60
+ /** Open the viewer on this. */
61
+ source?: ViewerSource;
62
+ }
63
+ export interface DecisionContext {
64
+ /** Named in the lines, so a host can say which dataset is meant. */
65
+ description: string;
66
+ /** Whether the host offers to start preparation at all. */
67
+ canPrepare: boolean;
68
+ /** Whether it has already been asked. */
69
+ asked: boolean;
70
+ /** How a progress reading is worded; the viewer's own load phrasing. */
71
+ phrase: (progress: LoadProgress | null, description: string) => string;
72
+ }
73
+ export declare function decideDataset(status: DatasetState, context: DecisionContext): DatasetDecision;
@@ -0,0 +1,64 @@
1
+ import { m as h, p as L } from "./mount-Bpr3Nw4_.js";
2
+ function P(e, s) {
3
+ const { description: a, canPrepare: n, asked: c, phrase: f } = s;
4
+ switch (e.state) {
5
+ case "ready":
6
+ return { settled: !0, source: e.source };
7
+ case "failed":
8
+ return { settled: !0, line: e.message, tone: "error" };
9
+ case "absent":
10
+ return n ? c ? { line: `Preparing ${a}…` } : { prepare: !0, line: `Preparing ${a}…` } : { settled: !0, line: `${a} has not been prepared yet.` };
11
+ case "preparing":
12
+ return { line: f(e.progress ?? null, a) };
13
+ }
14
+ }
15
+ const b = 500, o = /* @__PURE__ */ new WeakMap();
16
+ function M(e) {
17
+ e.classList.add("gtfs-viewer"), e.innerHTML = '<div data-el="dataset-status"><div data-el="empty-state"></div></div>';
18
+ const s = e.querySelector('[data-el="empty-state"]');
19
+ return (a, n = "normal") => {
20
+ s.textContent = a, s.classList.toggle("is-error", n === "error");
21
+ };
22
+ }
23
+ async function C(e, s) {
24
+ const { dataset: a, pollMs: n = b, description: c = "the dataset", ...f } = s, g = M(e), d = {};
25
+ o.set(e, d);
26
+ const w = new AbortController(), m = (t) => new Promise((r) => setTimeout(r, t));
27
+ let l = !1, p, i, y = !1;
28
+ const v = async (t) => {
29
+ var u;
30
+ if (l || o.get(e) !== d) return void ((u = t.close) == null ? void 0 : u.call(t));
31
+ i = t;
32
+ const r = await h(e, { ...f, source: t });
33
+ l || o.get(e) !== d ? r.destroy() : p = r;
34
+ };
35
+ return (async () => {
36
+ for (g(`Loading ${c}…`); !l; ) {
37
+ let t;
38
+ try {
39
+ t = await a.status(w.signal);
40
+ } catch {
41
+ await m(n);
42
+ continue;
43
+ }
44
+ if (l) return;
45
+ const r = P(t, {
46
+ description: c,
47
+ canPrepare: !!a.prepare,
48
+ asked: y,
49
+ phrase: L
50
+ });
51
+ if (r.line && g(r.line, r.tone === "error" ? "error" : "normal"), r.prepare && a.prepare && (y = !0, a.prepare(w.signal).catch((u) => g(u.message, "error"))), r.source) return v(r.source);
52
+ if (r.settled) return;
53
+ await m(n);
54
+ }
55
+ })(), {
56
+ destroy() {
57
+ var t;
58
+ l = !0, w.abort(), p == null || p.destroy(), (t = i == null ? void 0 : i.close) == null || t.call(i), o.get(e) === d && (o.delete(e), e.replaceChildren(), e.classList.remove("gtfs-viewer"));
59
+ }
60
+ };
61
+ }
62
+ export {
63
+ C as mountDataset
64
+ };
package/dist/dom.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /** Small DOM helpers shared by the UI modules. */
2
+ /**
3
+ * Element lookup scoped to one viewer.
4
+ *
5
+ * Every lookup goes through a root rather than `document`, so two viewers can
6
+ * exist on one page without reaching into each other's markup. That is what a
7
+ * host embedding this needs: the UI used to resolve 37 fixed ids against the
8
+ * whole document, which silently assumed it was the only thing on the page.
9
+ */
10
+ export interface Dom {
11
+ el<T extends HTMLElement = HTMLElement>(name: string): T;
12
+ }
13
+ export declare function createDom(root: ParentNode): Dom;
14
+ /**
15
+ * The whole document, for the application that owns the page.
16
+ *
17
+ * Embedded viewers build their own with `createDom(root)`; this is the one case
18
+ * where taking the document is correct rather than an assumption. A function
19
+ * rather than a constant so that merely importing this module does not require
20
+ * a document - the tests run without one, which is what keeps the UI's pure
21
+ * logic testable.
22
+ */
23
+ export declare function documentDom(): Dom;
24
+ export declare function button(label: string, title: string, onClick: () => void): HTMLButtonElement;
@@ -0,0 +1 @@
1
+ .gtfs-viewer{--bg: #f7f7f8;--panel: #ffffff;--border: #dcdde0;--text: #1c1e21;--muted: #6b7280;--accent: #2563eb;--accent-bg: #eef2ff;--chip-bg: #eef1f5;--font: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-family:var(--font);color:var(--text);background:var(--bg);display:flex;flex-direction:column;height:100%;min-height:0;position:relative}.gtfs-viewer,.gtfs-viewer *{box-sizing:border-box}.gtfs-viewer header{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--panel);border-bottom:1px solid var(--border);flex-shrink:0}.gtfs-viewer header h1{font-size:16px;margin:0}.gtfs-viewer [data-el=source-label]{color:var(--muted);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gtfs-viewer button{font:inherit;cursor:pointer;border:1px solid var(--border);background:var(--panel);border-radius:6px;padding:6px 10px}.gtfs-viewer button.primary{background:var(--accent);color:#fff;border-color:var(--accent)}.gtfs-viewer button:disabled{opacity:.45;cursor:default}.gtfs-viewer button:hover{filter:brightness(.97)}.gtfs-viewer [data-el=layout]{flex:1;display:flex;min-height:0}.gtfs-viewer [data-el=sidebar]{width:210px;flex-shrink:0;background:var(--panel);border-right:1px solid var(--border);overflow-y:auto;padding:8px}.gtfs-viewer .table-tab{display:flex;justify-content:space-between;padding:7px 10px;border-radius:6px;cursor:pointer;font-size:13px}.gtfs-viewer .table-tab:hover{background:var(--bg)}.gtfs-viewer .table-tab.active{background:var(--accent-bg);color:var(--accent);font-weight:600}.gtfs-viewer .table-tab .count{color:var(--muted);font-weight:400}.gtfs-viewer .table-tab.active .count{color:var(--accent)}.gtfs-viewer .table-tab.missing{color:var(--muted);cursor:default;font-style:italic}.gtfs-viewer .table-tab.missing:hover{background:none}.gtfs-viewer .table-tab.forbidden .count{font-style:italic}.gtfs-viewer [data-el=content]{flex:1;display:flex;min-width:0;min-height:0}.gtfs-viewer [data-el=table-pane]{flex:1.3;display:flex;flex-direction:column;min-width:0;border-right:1px solid var(--border)}.gtfs-viewer [data-el=map-pane]{flex:1;min-width:0;position:relative}.gtfs-viewer [data-el=map]{position:absolute;top:0;right:0;bottom:0;left:0}.gtfs-viewer [data-el=filter-bar]{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);background:var(--panel)}.gtfs-viewer .raw-toggle{display:flex;align-items:center;gap:5px;margin-left:auto;padding:0 4px;font-size:12px;color:var(--muted);cursor:pointer;-webkit-user-select:none;user-select:none}.gtfs-viewer .raw-toggle input{cursor:pointer}.gtfs-viewer .chip{display:flex;align-items:center;gap:6px;background:var(--chip-bg);border-radius:999px;padding:4px 10px;font-size:12px}.gtfs-viewer .chip button{border:none;background:none;padding:0;color:var(--muted);font-size:13px;line-height:1}.gtfs-viewer [data-el=add-filter-form]{display:none;gap:6px;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);background:var(--bg);font-size:13px}.gtfs-viewer [data-el=add-filter-form].open{display:flex}.gtfs-viewer [data-el=add-filter-form] select,.gtfs-viewer [data-el=add-filter-form] input{font:inherit;padding:4px 6px;border:1px solid var(--border);border-radius:4px}.gtfs-viewer [data-el=dataset-status]{flex:1;display:flex;align-items:center;justify-content:center;padding:24px}.gtfs-viewer [data-el=dataset-status] .is-error{color:#b42318}.gtfs-viewer [data-el=table-scroll]{flex:1;overflow:auto}.gtfs-viewer table{border-collapse:collapse;width:100%;font-size:12.5px}.gtfs-viewer th,.gtfs-viewer td{border-bottom:1px solid var(--border);padding:5px 10px;text-align:left;white-space:nowrap}.gtfs-viewer th{position:sticky;top:0;background:var(--panel);z-index:1}.gtfs-viewer tr:hover td{background:#fafafa}.gtfs-viewer a.fk-link{color:var(--accent);text-decoration:none}.gtfs-viewer a.fk-link:hover{text-decoration:underline}.gtfs-viewer .related-row{display:flex;gap:4px;flex-wrap:wrap;margin-top:2px}.gtfs-viewer .related-btn{font-size:10.5px;padding:1px 6px;border-radius:999px;background:var(--chip-bg);border:none;color:var(--muted)}.gtfs-viewer .related-btn:hover{background:var(--accent-bg);color:var(--accent)}.gtfs-viewer [data-el=pager]{display:flex;align-items:center;gap:8px;padding:6px 10px;border-top:1px solid var(--border);background:var(--panel);font-size:12.5px}.gtfs-viewer [data-el=pager] .spacer{flex:1}.gtfs-viewer [data-el=load-overlay]{position:fixed;top:0;right:0;bottom:0;left:0;background:#14141973;display:flex;align-items:center;justify-content:center;z-index:50}.gtfs-viewer [data-el=load-overlay].hidden{display:none}.gtfs-viewer [data-el=load-panel]{background:var(--panel);border-radius:10px;padding:24px;width:420px;max-width:90vw}.gtfs-viewer [data-el=load-panel] h2{margin-top:0;font-size:16px}.gtfs-viewer [data-el=dropzone]{border:2px dashed var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--muted);font-size:13px;margin-bottom:12px}.gtfs-viewer [data-el=dropzone].drag{border-color:var(--accent);color:var(--accent)}.gtfs-viewer [data-el=load-panel] .or{text-align:center;color:var(--muted);font-size:12px;margin:8px 0}.gtfs-viewer [data-el=load-panel] input[type=text]{width:100%;padding:6px 8px;border:1px solid var(--border);border-radius:6px;font:inherit;margin-bottom:10px}.gtfs-viewer [data-el=load-panel] input[type=text]:focus{outline:2px solid var(--accent);outline-offset:-1px;border-color:var(--accent)}.gtfs-viewer .field-label{display:block;font-size:12px;color:var(--muted);margin-bottom:4px}.gtfs-viewer [data-el=pick-row]{display:flex;gap:8px;margin-bottom:4px}.gtfs-viewer [data-el=pick-row] button{flex:1}.gtfs-viewer [data-el=load-actions]{display:flex;gap:8px;align-items:center}.gtfs-viewer [data-el=load-status]{color:var(--muted);font-size:12.5px;margin-top:8px}.gtfs-viewer [data-el=load-error]{color:[data-el="b91c1c"];font-size:12.5px;margin-top:8px}.gtfs-viewer [data-el=empty-state]{padding:40px;color:var(--muted);text-align:center}.gtfs-viewer [data-el=metrics-btn]{color:var(--muted);font-size:12.5px;padding:4px 8px;flex-shrink:0;white-space:nowrap}.gtfs-viewer [data-el=metrics-panel]{position:absolute;top:46px;right:16px;z-index:5;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:12px 14px;font-size:12.5px;max-width:90vw;max-height:70vh;overflow:auto;box-shadow:0 6px 20px #0000001f}.gtfs-viewer [data-el=metrics-panel].hidden{display:none}.gtfs-viewer [data-el=metrics-header]{display:flex;align-items:center;gap:12px;margin-bottom:8px}.gtfs-viewer [data-el=metrics-header] strong{flex:1}.gtfs-viewer [data-el=metrics-header] button{padding:0 6px;line-height:1.4}.gtfs-viewer [data-el=metrics-phases]{display:grid;grid-template-columns:auto auto;gap:2px 14px;margin:0 0 12px}.gtfs-viewer [data-el=metrics-phases] dt{color:var(--muted)}.gtfs-viewer [data-el=metrics-phases] dd{margin:0;text-align:right;font-variant-numeric:tabular-nums}.gtfs-viewer [data-el=metrics-body] table{border-collapse:collapse;white-space:nowrap}.gtfs-viewer [data-el=metrics-body] th{text-align:left;color:var(--muted);font-weight:500;border-bottom:1px solid var(--border)}.gtfs-viewer [data-el=metrics-body] th,.gtfs-viewer [data-el=metrics-body] td{padding:3px 10px 3px 0}.gtfs-viewer [data-el=metrics-body] td:not(:first-child){text-align:right;font-variant-numeric:tabular-nums}.gtfs-viewer [data-el=map-progress]{position:absolute;left:12px;bottom:12px;z-index:2;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:5px 10px;color:var(--muted);font-size:12.5px;font-variant-numeric:tabular-nums;box-shadow:0 2px 8px #0000001a;pointer-events:none}.gtfs-viewer [data-el=table-scroll] td.value-numeric{text-align:right;font-variant-numeric:tabular-nums}.gtfs-viewer [data-el=table-scroll] td.value-implied{color:var(--muted);font-style:italic;cursor:help}.gtfs-viewer [data-el=table-scroll] td.value-malformed{cursor:help}.gtfs-viewer .value-swatch{display:inline-block;width:.75em;height:.75em;margin-right:6px;border:1px solid var(--border);border-radius:2px;vertical-align:baseline}.gtfs-viewer .value-link{color:var(--accent)}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The backdrop under the feed's geometry.
3
+ *
4
+ * Presets are keyless, except CARTO, which watermarks its tiles unless an API
5
+ * key is appended to the URL.
6
+ *
7
+ * Attribution is a licence obligation, not decoration: OpenStreetMap data is
8
+ * ODbL and the styles are CC BY, both of which require credit. It is declared
9
+ * here rather than taken from the upstream style, which may not carry any.
10
+ */
11
+ export interface BasemapSpec {
12
+ kind: "vector" | "raster" | "none";
13
+ /** Style URL for vector, tile template for raster. */
14
+ url?: string;
15
+ attribution?: string;
16
+ }
17
+ export declare const BASEMAPS: Record<string, BasemapSpec>;
18
+ export declare const DEFAULT_BASEMAP = "openfreemap";
19
+ /**
20
+ * Turn a configured value into a spec. Accepts a preset name, a raster tile
21
+ * template (recognised by its {z}/{x}/{y} placeholders), or a vector style URL.
22
+ */
23
+ export declare function resolveBasemap(value?: string | null): BasemapSpec;
@@ -0,0 +1,4 @@
1
+ import type { FeatureCollection } from "../sources/types";
2
+ export type Bbox = [number, number, number, number];
3
+ /** Bounding box of every coordinate in a collection, or null if it has none. */
4
+ export declare function bboxOf(collection: FeatureCollection): Bbox | null;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The feed's own layers, and how they are combined with a basemap.
3
+ *
4
+ * Sources and layers are declared up front rather than added in a `load`
5
+ * handler: `load` only fires after the first paint, so in a background tab
6
+ * (where requestAnimationFrame never runs) the layers would never exist and the
7
+ * feed's data would silently never reach the map.
8
+ *
9
+ * Route colouring follows the conventions used by mobilitydatabase.org - colour
10
+ * from `route_color`, width by `route_type`, a white casing underneath - so the
11
+ * two read alike. The geometry itself is generated locally from the feed's own
12
+ * shapes.txt; nothing here depends on that service.
13
+ */
14
+ import type { BasemapSpec } from "./basemap";
15
+ export declare const EMPTY_FEATURE_COLLECTION: {
16
+ type: "FeatureCollection";
17
+ features: never[];
18
+ };
19
+ export declare const SOURCE_IDS: readonly ["locations", "routes", "shapes", "stops", "highlight"];
20
+ export type SourceId = (typeof SOURCE_IDS)[number];
21
+ /**
22
+ * Build the complete style: the basemap underneath, the feed's layers on top.
23
+ *
24
+ * A vector basemap is fetched and merged rather than referenced by URL, so the
25
+ * map is constructed from one complete style object and the feed's layers exist
26
+ * from the first frame.
27
+ */
28
+ export declare function buildStyle(basemap: BasemapSpec): Promise<Record<string, unknown>>;
@@ -0,0 +1,60 @@
1
+ import type { Dom } from "../dom";
2
+ import type { FeatureCollection, ViewerSource } from "../sources/types";
3
+ import type { AppState } from "../state";
4
+ export declare class MapController {
5
+ private readonly maplibregl;
6
+ private readonly dom;
7
+ private readonly state;
8
+ private readonly source;
9
+ private readonly map;
10
+ private readonly ready;
11
+ /**
12
+ * A vector basemap has to be fetched before the map is constructed, and
13
+ * MapLibre itself is fetched here rather than imported at the top of the
14
+ * file.
15
+ *
16
+ * That is what keeps it optional. A host that mounts the viewer without the
17
+ * map part never reaches this method, so its bundler never follows the
18
+ * import and none of MapLibre's ~1 MB is downloaded. A static import would
19
+ * make it a cost every consumer pays whether or not they show a map.
20
+ */
21
+ static create(dom: Dom, state: AppState, source: ViewerSource, basemap?: string | null): Promise<MapController>;
22
+ /** Cancels an in-flight refresh when a new feed arrives mid-draw. */
23
+ private drawing;
24
+ /** The first full draw, while it is still running. See `highlight`. */
25
+ private pendingRefresh;
26
+ /** Layers the server thinned to fit, and by how much. */
27
+ private readonly simplifiedLayers;
28
+ private constructor();
29
+ isVisible(): boolean;
30
+ /** Restore the saved preference; defaults to shown. */
31
+ restoreVisibility(): void;
32
+ setVisible(visible: boolean): void;
33
+ private setSourceData;
34
+ private fitTo;
35
+ highlight(collection: FeatureCollection, options?: {
36
+ point?: boolean;
37
+ }): Promise<void>;
38
+ private showProgress;
39
+ /**
40
+ * Draw one layer as it arrives.
41
+ *
42
+ * The features are handed over repeatedly rather than once at the end, so a
43
+ * large feed paints steadily instead of freezing the tab and then jumping.
44
+ * Returns what was collected, for the caller to frame the view with.
45
+ */
46
+ private drawLayer;
47
+ /** Names the layers the server had to reduce, once everything is drawn. */
48
+ private simplificationNote;
49
+ /** Fetch and draw the whole feed. Skipped entirely while the map is hidden. */
50
+ refresh(): Promise<void>;
51
+ clear(): Promise<void>;
52
+ /**
53
+ * Give up the map and everything holding it.
54
+ *
55
+ * An embedded viewer is unmounted and remounted by its host - React does it
56
+ * twice on every mount in development - and a MapLibre instance left behind
57
+ * keeps a WebGL context, which browsers allow only a handful of.
58
+ */
59
+ destroy(): void;
60
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * When to hand a growing layer to MapLibre while it is still arriving.
3
+ *
4
+ * Its own module because vitest runs with no DOM, so anything inside
5
+ * `MapController` is untestable - the same constraint `Navigator.parseUrl` is
6
+ * written around. The schedule is the part worth testing: get it wrong and a
7
+ * large feed takes minutes to draw.
8
+ */
9
+ /**
10
+ * Redraw once the layer has grown by this factor, rather than on a timer.
11
+ *
12
+ * `setData` re-serialises and re-indexes the whole collection every call, so
13
+ * the cost of a redraw grows with the layer. Redrawing at a fixed interval - or
14
+ * worse, per batch - therefore does work proportional to the square of the
15
+ * feature count, which is what made a quarter of a million shapes take minutes.
16
+ * Growth-based redrawing keeps the total proportional to the layer itself, and
17
+ * widens the gaps in a way that still reads as filling in.
18
+ *
19
+ * `GeoJSONSource.updateData` would remove the need for any of this, but in
20
+ * MapLibre 5.7 it accepts a diff and silently discards it.
21
+ */
22
+ export declare const REDRAW_GROWTH_FACTOR = 2;
23
+ /** Features worth drawing before waiting for the layer to double. */
24
+ export declare const FIRST_REDRAW_AT = 5000;
25
+ /**
26
+ * Returns a predicate: given how many features have arrived, should the layer
27
+ * be redrawn now? Stateful across calls, one per layer being drawn.
28
+ */
29
+ export declare function createRedrawSchedule(firstAt?: number, factor?: number): (featureCount: number) => boolean;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The map, as a separately imported part.
3
+ *
4
+ * This is the only module that reaches MapLibre. A host that imports it wants a
5
+ * map and installs the peer dependency; a host that does not never mentions
6
+ * either, so nothing about MapLibre appears in its build.
7
+ */
8
+ import type { MapPart } from "./mount";
9
+ export declare const mapPart: MapPart;
10
+ export type { MapPart, MapHandle } from "./mount";