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.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Mounting a dataset, including the part before there is one.
3
+ *
4
+ * `mount` browses a dataset that exists. A dataset behind an API may not exist
5
+ * yet: it is converted once, and that takes seconds after a download. This
6
+ * waits for it and renders being-prepared, not-prepared and failed in the
7
+ * viewer's own design, so no host writes those three states again.
8
+ *
9
+ * The host supplies facts through `DatasetProvider` and nothing else.
10
+ */
11
+ import { type DatasetProvider } from "./dataset";
12
+ import { type Viewer, type ViewerOptions } from "./mount";
13
+ export type { DatasetProvider, DatasetState } from "./dataset";
14
+ export interface DatasetOptions extends ViewerOptions {
15
+ dataset: DatasetProvider;
16
+ /** How often to ask while the dataset is being prepared. */
17
+ pollMs?: number;
18
+ /** Named in the waiting line - "Loading mdb-1210…". */
19
+ description?: string;
20
+ }
21
+ /**
22
+ * Returns as soon as there is something to tear down - not when the dataset is
23
+ * ready.
24
+ *
25
+ * A host must be able to abandon a viewer that is still waiting: React unmounts
26
+ * every effect once on mount in development, and a dataset that never becomes
27
+ * ready would otherwise leave a poll running against a viewer nobody can reach.
28
+ */
29
+ export declare function mountDataset(root: Element, options: DatasetOptions): Promise<Viewer>;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Putting a viewer into a page.
3
+ *
4
+ * The standalone application and an embedded one differ only in what they pass
5
+ * here: the application owns its page, so it takes the load dialog, the load
6
+ * report and the browser's own history; a host embedding the viewer takes none
7
+ * of those and keeps its address bar to itself.
8
+ */
9
+ import "./style.css";
10
+ import { type ViewerParts } from "./markup";
11
+ import type { Dom } from "./dom";
12
+ import type { FeatureCollection, ViewerSource } from "./sources/types";
13
+ import { type AppState } from "./state";
14
+ import { type HistoryPort } from "./ui/history";
15
+ export type { ViewerParts } from "./markup";
16
+ export type { GtfsSource, ViewerSource } from "./sources/types";
17
+ export { RestSource } from "./sources/rest";
18
+ export { browserHistory, memoryHistory, type HistoryPort } from "./ui/history";
19
+ export interface ViewerOptions {
20
+ /** Where feed data comes from. Defaults to a REST source against `baseUrl`. */
21
+ source?: ViewerSource;
22
+ /** Base URL for the default REST source. Defaults to the current origin. */
23
+ baseUrl?: string;
24
+ /**
25
+ * Preset name, raster tile template or vector style URL. Falls back to the
26
+ * server's own setting when the source can report one.
27
+ */
28
+ basemap?: string;
29
+ /**
30
+ * The map implementation, from this package's `./map` entry. Omitted means no
31
+ * map pane: there is nothing to draw one with.
32
+ */
33
+ map?: MapPart;
34
+ /** Which of the remaining pieces to build - see `ViewerParts`. */
35
+ parts?: Omit<ViewerParts, "map">;
36
+ /**
37
+ * Defaults to a back stack the viewer keeps to itself. The application passes
38
+ * `browserHistory()` so that a view is a shareable link; an embedded viewer
39
+ * leaves its host's address bar alone.
40
+ */
41
+ history?: HistoryPort;
42
+ }
43
+ /**
44
+ * The map, supplied by whoever wants one.
45
+ *
46
+ * MapLibre is a megabyte, and a host that does not show a map should not carry
47
+ * it or even have to install it. A lazy `import()` is not enough - bundlers
48
+ * resolve the specifier at build time regardless of whether the branch runs,
49
+ * so esbuild fails outright on a consumer that has no MapLibre. Keeping the
50
+ * map behind its own entry point means it is absent from the module graph
51
+ * rather than merely unreached:
52
+ *
53
+ * import { mount } from "gtfs-garage-web";
54
+ * import { mapPart } from "gtfs-garage-web/map";
55
+ *
56
+ * mount(root, { map: mapPart }); // omit it and there is no map pane
57
+ */
58
+ export interface MapPart {
59
+ create(dom: Dom, state: AppState, source: ViewerSource, basemap?: string | null): Promise<MapHandle>;
60
+ }
61
+ export interface MapHandle {
62
+ restoreVisibility(): void;
63
+ refresh(): Promise<void>;
64
+ highlight(collection: FeatureCollection, options?: {
65
+ point?: boolean;
66
+ }): Promise<void>;
67
+ destroy(): void;
68
+ }
69
+ export interface Viewer {
70
+ /**
71
+ * Give up the map, the history subscription and the markup.
72
+ *
73
+ * Not optional for a host: React remounts every effect in development, so a
74
+ * viewer that cannot be taken down appears twice and holds two WebGL
75
+ * contexts, of which a browser allows only a handful.
76
+ */
77
+ destroy(): void;
78
+ }
79
+ export declare function mount(root: Element, options?: ViewerOptions): Promise<Viewer>;
package/dist/mount.js ADDED
@@ -0,0 +1,7 @@
1
+ import { R as s, b as a, a as e, m } from "./mount-Bpr3Nw4_.js";
2
+ export {
3
+ s as RestSource,
4
+ a as browserHistory,
5
+ e as memoryHistory,
6
+ m as mount
7
+ };
@@ -0,0 +1,263 @@
1
+ var h = Object.defineProperty;
2
+ var E = (d, i, e) => i in d ? h(d, i, { enumerable: !0, configurable: !0, writable: !0, value: e }) : d[i] = e;
3
+ var y = (d, i, e) => E(d, typeof i != "symbol" ? i + "" : i, e);
4
+ const q = { COLOR: { description: 'A color encoded as a six-digit hexadecimal number. The leading "#" is not included.', pattern: "^[0-9A-Fa-f]{6}$" }, CURRENCY_AMOUNT: { description: "A decimal value indicating a currency amount." }, CURRENCY_CODE: { description: "An ISO 4217 alphabetical currency code.", pattern: "^[A-Z]{3}$" }, DATE: { description: "Service day in the YYYYMMDD format. Since time within a service day may be above 24:00:00, a service day may contain information for the subsequent day(s).", pattern: "^\\d{8}$" }, EMAIL: { description: "An email address.", pattern: "^[^\\s@:]+@[^\\s@:]+$" }, FLOAT: { description: "A floating point number." }, ID: { description: "An internal ID, not intended to be shown to riders, and is a sequence of any UTF-8 characters. Using only printable ASCII characters is recommended." }, INTEGER: { description: "An integer.", pattern: "^[+-]?\\d+$" }, LANGUAGE_CODE: { description: "An IETF BCP 47 language code." }, LATITUDE: { description: "WGS84 latitude in decimal degrees. The value must be greater than or equal to -90.0 and less than or equal to 90.0.", minimum: -90, maximum: 90 }, LONGITUDE: { description: "WGS84 longitude in decimal degrees. The value must be greater than or equal to -180.0 and less than or equal to 180.0.", minimum: -180, maximum: 180 }, PHONE_NUMBER: { description: "A phone number." }, TEXT: { description: "A string of UTF-8 characters, which is aimed to be displayed and which must therefore be human readable." }, TIME: { description: "Time in the HH:MM:SS format (H:MM:SS is also accepted). For times occurring after midnight on the service day, enter the time as a value greater than 24:00:00.", pattern: "^\\d{1,2}:\\d{2}:\\d{2}$" }, TIMEZONE: { description: "TZ timezone from the IANA time-zones. Timezone names never contain the space character but may contain an underscore.", pattern: "^\\S+$" }, URL: { description: "A fully qualified URL that includes http:// or https://, and any special characters in the URL must be correctly escaped.", pattern: "^https?://" } }, T = /* @__PURE__ */ JSON.parse('{"agency":{"presence":"required","fields":{"agency_id":{"type":"ID","required":"conditional","condition":"Required when the feed contains more than one agency.","conditionScope":"feed","conditionCheck":{"kind":"row_count","file":"agency","minimum":2},"primaryKey":true},"agency_name":{"type":"TEXT","required":"always"},"agency_url":{"type":"URL","required":"always"},"agency_timezone":{"type":"TIMEZONE","required":"always"},"agency_lang":{"type":"LANGUAGE_CODE"},"agency_phone":{"type":"PHONE_NUMBER"},"agency_fare_url":{"type":"URL"},"agency_email":{"type":"EMAIL"},"cemv_support":{"type":"ENUM","values":{"0":"No Information","1":"Supported","2":"Not Supported"},"whenEmpty":{"code":"0","label":"No Information"}}}},"areas":{"presence":"optional","fields":{"area_id":{"type":"ID","required":"always","primaryKey":true},"area_name":{"type":"TEXT"}}},"attributions":{"presence":"optional","fields":{"attribution_id":{"type":"ID","primaryKey":true},"agency_id":{"type":"ID","references":{"table":"agency","field":"agency_id"}},"route_id":{"type":"ID","references":{"table":"routes","field":"route_id"}},"trip_id":{"type":"ID","references":{"table":"trips","field":"trip_id"}},"organization_name":{"type":"TEXT","required":"always"},"is_producer":{"type":"ENUM","values":{"0":"Not Assigned","1":"Assigned"},"whenEmpty":{"code":"0","label":"Not Assigned"}},"is_operator":{"type":"ENUM","values":{"0":"Not Assigned","1":"Assigned"},"whenEmpty":{"code":"0","label":"Not Assigned"}},"is_authority":{"type":"ENUM","values":{"0":"Not Assigned","1":"Assigned"},"whenEmpty":{"code":"0","label":"Not Assigned"}},"attribution_url":{"type":"URL"},"attribution_email":{"type":"EMAIL"},"attribution_phone":{"type":"PHONE_NUMBER"}}},"booking_rules":{"presence":"optional","fields":{"booking_rule_id":{"type":"ID","required":"always","primaryKey":true},"booking_type":{"type":"ENUM","required":"always","values":{"0":"Realtime","1":"Sameday","2":"Priorday"}},"prior_notice_duration_min":{"type":"INTEGER","required":"conditional","condition":"Required for booking_type=1 (same-day booking). Forbidden otherwise.","conditionScope":"row"},"prior_notice_duration_max":{"type":"INTEGER"},"prior_notice_start_day":{"type":"INTEGER"},"prior_notice_start_time":{"type":"TIME","required":"conditional","condition":"Required if prior_notice_start_day is defined. Forbidden otherwise.","conditionScope":"row"},"prior_notice_last_day":{"type":"INTEGER","required":"conditional","condition":"Required for booking_type=2 (prior-day booking). Forbidden otherwise.","conditionScope":"row"},"prior_notice_last_time":{"type":"TIME","required":"conditional","condition":"Required if prior_notice_last_day is defined. Forbidden otherwise.","conditionScope":"row"},"prior_notice_service_id":{"type":"TEXT"},"message":{"type":"TEXT"},"pickup_message":{"type":"TEXT"},"drop_off_message":{"type":"TEXT"},"phone_number":{"type":"PHONE_NUMBER"},"info_url":{"type":"URL"},"booking_url":{"type":"URL"}}},"calendar":{"presence":"conditional","condition":"Required unless all dates of service are defined in calendar_dates.txt. Optional otherwise.","conditionScope":"feed","conditionCheck":{"kind":"file_absent","file":"calendar_dates"},"fields":{"service_id":{"type":"ID","required":"always","primaryKey":true},"monday":{"type":"ENUM","required":"always","values":{"0":"Not Available","1":"Available"}},"tuesday":{"type":"ENUM","required":"always","values":{"0":"Not Available","1":"Available"}},"wednesday":{"type":"ENUM","required":"always","values":{"0":"Not Available","1":"Available"}},"thursday":{"type":"ENUM","required":"always","values":{"0":"Not Available","1":"Available"}},"friday":{"type":"ENUM","required":"always","values":{"0":"Not Available","1":"Available"}},"saturday":{"type":"ENUM","required":"always","values":{"0":"Not Available","1":"Available"}},"sunday":{"type":"ENUM","required":"always","values":{"0":"Not Available","1":"Available"}},"start_date":{"type":"DATE","required":"always"},"end_date":{"type":"DATE","required":"always"}}},"calendar_dates":{"presence":"conditional","condition":"Required if calendar.txt is omitted, in which case it must contain all dates of service. Optional otherwise.","conditionScope":"feed","conditionCheck":{"kind":"file_absent","file":"calendar"},"fields":{"service_id":{"type":"ID","required":"always","primaryKey":true,"references":{"table":"calendar","field":"service_id"}},"date":{"type":"DATE","required":"always","primaryKey":true},"exception_type":{"type":"ENUM","required":"always","values":{"1":"Service Added","2":"Service Removed"}}}},"fare_attributes":{"presence":"optional","fields":{"fare_id":{"type":"ID","required":"always","primaryKey":true},"price":{"type":"CURRENCY_AMOUNT","required":"always"},"currency_type":{"type":"CURRENCY_CODE","required":"always"},"payment_method":{"type":"ENUM","required":"always","values":{"0":"On Board","1":"Before Boarding"}},"transfers":{"type":"ENUM","required":"always","values":{"0":"No Transfer","1":"One Transfer","2":"Two Transfers"},"whenEmpty":{"label":"Unlimited transfers are permitted."}},"agency_id":{"type":"ID","required":"conditional","condition":"Required when the feed contains more than one agency.","conditionScope":"feed","conditionCheck":{"kind":"row_count","file":"agency","minimum":2},"references":{"table":"agency","field":"agency_id"}},"transfer_duration":{"type":"INTEGER"}}},"fare_leg_join_rules":{"presence":"optional","fields":{"from_network_id":{"type":"ID","required":"always"},"to_network_id":{"type":"ID","required":"always"},"from_stop_id":{"type":"ID","required":"conditional","condition":"Required if to_stop_id is defined.","conditionScope":"row","references":{"table":"stops","field":"stop_id"}},"to_stop_id":{"type":"ID","required":"conditional","condition":"Required if from_stop_id is defined.","conditionScope":"row","references":{"table":"stops","field":"stop_id"}}}},"fare_leg_rules":{"presence":"optional","fields":{"leg_group_id":{"type":"ID"},"network_id":{"type":"ID","primaryKey":true},"from_area_id":{"type":"ID","primaryKey":true,"references":{"table":"areas","field":"area_id"}},"to_area_id":{"type":"ID","primaryKey":true,"references":{"table":"areas","field":"area_id"}},"from_timeframe_group_id":{"type":"ID","primaryKey":true,"references":{"table":"timeframes","field":"timeframe_group_id"}},"to_timeframe_group_id":{"type":"ID","primaryKey":true,"references":{"table":"timeframes","field":"timeframe_group_id"}},"fare_product_id":{"type":"ID","required":"always","primaryKey":true,"references":{"table":"fare_products","field":"fare_product_id"}},"rule_priority":{"type":"INTEGER"}}},"fare_media":{"presence":"optional","fields":{"fare_media_id":{"type":"ID","required":"always","primaryKey":true},"fare_media_name":{"type":"TEXT"},"fare_media_type":{"type":"ENUM","required":"always","values":{"0":"None","1":"Paper Ticket","2":"Transit Card","3":"Contactless Emv","4":"Mobile App"}}}},"fare_products":{"presence":"optional","fields":{"fare_product_id":{"type":"ID","required":"always","primaryKey":true},"fare_product_name":{"type":"TEXT"},"amount":{"type":"CURRENCY_AMOUNT","required":"always"},"currency":{"type":"CURRENCY_CODE","required":"always"},"fare_media_id":{"type":"ID","primaryKey":true,"references":{"table":"fare_media","field":"fare_media_id"}},"rider_category_id":{"type":"ID","primaryKey":true,"references":{"table":"rider_categories","field":"rider_category_id"}}}},"fare_rules":{"presence":"optional","fields":{"fare_id":{"type":"ID","required":"always","primaryKey":true,"references":{"table":"fare_attributes","field":"fare_id"}},"route_id":{"type":"ID","primaryKey":true,"references":{"table":"routes","field":"route_id"}},"origin_id":{"type":"ID","primaryKey":true,"references":{"table":"stops","field":"zone_id"}},"destination_id":{"type":"ID","primaryKey":true,"references":{"table":"stops","field":"zone_id"}},"contains_id":{"type":"ID","primaryKey":true,"references":{"table":"stops","field":"zone_id"}}}},"fare_transfer_rules":{"presence":"optional","fields":{"from_leg_group_id":{"type":"ID","primaryKey":true,"references":{"table":"fare_leg_rules","field":"leg_group_id"}},"to_leg_group_id":{"type":"ID","primaryKey":true,"references":{"table":"fare_leg_rules","field":"leg_group_id"}},"duration_limit":{"type":"INTEGER","primaryKey":true},"duration_limit_type":{"type":"ENUM","required":"conditional","condition":"Required if duration_limit is defined. Forbidden if duration_limit is empty.","conditionScope":"row","values":{"0":"Departure To Arrival","1":"Departure To Departure","2":"Arrival To Departure","3":"Arrival To Arrival"}},"fare_transfer_type":{"type":"ENUM","required":"always","values":{"0":"A Plus Ab","1":"A Plus Ab Plus B","2":"Ab"}},"transfer_count":{"type":"INTEGER","primaryKey":true},"fare_product_id":{"type":"ID","primaryKey":true,"references":{"table":"fare_products","field":"fare_product_id"}}}},"feed_info":{"presence":"conditional","condition":"Required if translations.txt is provided. Recommended otherwise.","conditionScope":"feed","conditionCheck":{"kind":"file_present","file":"translations"},"fields":{"feed_publisher_name":{"type":"TEXT","required":"always"},"feed_publisher_url":{"type":"URL","required":"always"},"feed_lang":{"type":"LANGUAGE_CODE","required":"always"},"default_lang":{"type":"LANGUAGE_CODE"},"feed_start_date":{"type":"DATE"},"feed_end_date":{"type":"DATE"},"feed_version":{"type":"TEXT"},"feed_contact_email":{"type":"EMAIL"},"feed_contact_url":{"type":"URL"}}},"frequencies":{"presence":"optional","fields":{"trip_id":{"type":"ID","required":"always","primaryKey":true,"references":{"table":"trips","field":"trip_id"}},"start_time":{"type":"TIME","required":"always","primaryKey":true},"end_time":{"type":"TIME","required":"always"},"headway_secs":{"type":"INTEGER","required":"always"},"exact_times":{"type":"ENUM","values":{"0":"Frequency Based","1":"Schedule Based"},"whenEmpty":{"code":"0","label":"Frequency Based"}}}},"levels":{"presence":"conditional","condition":"Required when describing pathways with elevators (pathway_mode=5). Optional otherwise.","conditionScope":"feed","conditionCheck":{"kind":"rows_match","file":"pathways","column":"pathway_mode","equals":"5"},"fields":{"level_id":{"type":"ID","required":"always","primaryKey":true},"level_index":{"type":"FLOAT","required":"always"},"level_name":{"type":"TEXT"}}},"location_group_stops":{"presence":"optional","fields":{"location_group_id":{"type":"ID","required":"always"},"stop_id":{"type":"ID","required":"always"}}},"location_groups":{"presence":"optional","fields":{"location_group_id":{"type":"ID","required":"always","primaryKey":true},"location_group_name":{"type":"TEXT"}}},"locations":{"presence":"optional","fields":{"id":{"type":"ID","required":"always","primaryKey":true},"stop_name":{"type":"TEXT"},"stop_desc":{"type":"TEXT"},"geometry_type":{"type":"ENUM","required":"always","values":{"Polygon":"Polygon","MultiPolygon":"MultiPolygon"}},"geometry":{"type":"TEXT","required":"always"}}},"networks":{"presence":"conditionally_forbidden","condition":"Forbidden if network_id exists in routes.txt. Optional otherwise.","conditionScope":"feed","conditionCheck":{"kind":"column_present","file":"routes","column":"network_id"},"fields":{"network_id":{"type":"ID","required":"always","primaryKey":true},"network_name":{"type":"TEXT"}}},"pathways":{"presence":"optional","fields":{"pathway_id":{"type":"ID","required":"always","primaryKey":true},"from_stop_id":{"type":"ID","required":"always","references":{"table":"stops","field":"stop_id"}},"to_stop_id":{"type":"ID","required":"always","references":{"table":"stops","field":"stop_id"}},"pathway_mode":{"type":"ENUM","required":"always","values":{"1":"Walkway","2":"Stairs","3":"Moving Sidewalk","4":"Escalator","5":"Elevator","6":"Fare Gate","7":"Exit Gate"}},"is_bidirectional":{"type":"ENUM","required":"always","values":{"0":"Unidirectional","1":"Bidirectional"}},"length":{"type":"FLOAT"},"traversal_time":{"type":"INTEGER"},"stair_count":{"type":"INTEGER"},"max_slope":{"type":"FLOAT"},"min_width":{"type":"FLOAT"},"signposted_as":{"type":"TEXT"},"reversed_signposted_as":{"type":"TEXT"}}},"rider_categories":{"presence":"optional","fields":{"rider_category_id":{"type":"ID","required":"always","primaryKey":true},"rider_category_name":{"type":"TEXT","required":"always"},"is_default_fare_category":{"type":"ENUM","required":"always","values":{"0":"Not Default","1":"Is Default"}},"eligibility_url":{"type":"URL"}}},"route_networks":{"presence":"conditionally_forbidden","condition":"Forbidden if network_id exists in routes.txt. Optional otherwise.","conditionScope":"feed","conditionCheck":{"kind":"column_present","file":"routes","column":"network_id"},"fields":{"route_id":{"type":"ID","required":"always","primaryKey":true,"references":{"table":"routes","field":"route_id"}},"network_id":{"type":"ID","required":"always","references":{"table":"networks","field":"network_id"}}}},"routes":{"presence":"required","fields":{"route_id":{"type":"ID","required":"always","primaryKey":true},"agency_id":{"type":"ID","required":"conditional","condition":"Required when the feed contains more than one agency.","conditionScope":"feed","conditionCheck":{"kind":"row_count","file":"agency","minimum":2},"references":{"table":"agency","field":"agency_id"}},"route_short_name":{"type":"TEXT","required":"conditional","condition":"Required if route_long_name is empty.","conditionScope":"row"},"route_long_name":{"type":"TEXT","required":"conditional","condition":"Required if route_short_name is empty.","conditionScope":"row"},"route_desc":{"type":"TEXT"},"route_type":{"type":"ENUM","required":"always","values":{"0":"Light Rail","1":"Subway","2":"Rail","3":"Bus","4":"Ferry","5":"Cable Tram","6":"Aerial Lift","7":"Funicular","11":"Trolleybus","12":"Monorail"}},"route_url":{"type":"URL"},"route_color":{"type":"COLOR"},"route_text_color":{"type":"COLOR"},"route_sort_order":{"type":"INTEGER"},"continuous_pickup":{"type":"ENUM","values":{"0":"Allowed","1":"Not Available","2":"Must Phone","3":"On Request To Driver"},"whenEmpty":{"code":"1","label":"Not Available"}},"continuous_drop_off":{"type":"ENUM","values":{"0":"Allowed","1":"Not Available","2":"Must Phone","3":"On Request To Driver"},"whenEmpty":{"code":"1","label":"Not Available"}},"network_id":{"type":"ID"},"cemv_support":{"type":"ENUM","values":{"0":"No Information","1":"Supported","2":"Not Supported"},"whenEmpty":{"code":"0","label":"No Information"}}}},"shapes":{"presence":"optional","fields":{"shape_id":{"type":"ID","required":"always","primaryKey":true},"shape_pt_lat":{"type":"LATITUDE","required":"always"},"shape_pt_lon":{"type":"LONGITUDE","required":"always"},"shape_pt_sequence":{"type":"INTEGER","required":"always","primaryKey":true},"shape_dist_traveled":{"type":"FLOAT"}}},"stop_areas":{"presence":"optional","fields":{"area_id":{"type":"ID","required":"always","primaryKey":true,"references":{"table":"areas","field":"area_id"}},"stop_id":{"type":"ID","required":"always","primaryKey":true,"references":{"table":"stops","field":"stop_id"}}}},"stop_times":{"presence":"required","fields":{"trip_id":{"type":"ID","required":"always","primaryKey":true,"references":{"table":"trips","field":"trip_id"}},"arrival_time":{"type":"TIME","required":"conditional","condition":"Required for the first and last stop of a trip, and if timepoint=1.","conditionScope":"row_context"},"departure_time":{"type":"TIME","required":"conditional","condition":"Required for the first and last stop of a trip, and if timepoint=1.","conditionScope":"row_context"},"stop_id":{"type":"ID","required":"conditional","condition":"Required if location_group_id and location_id are empty. Forbidden if location_group_id or location_id are defined.","conditionScope":"row","references":{"table":"stops","field":"stop_id"}},"location_group_id":{"type":"ID","required":"conditional","condition":"Forbidden if stop_id or location_group_id are defined.","conditionScope":"row","references":{"table":"location_groups","field":"location_group_id"}},"location_id":{"type":"ID","required":"conditional","condition":"Forbidden if stop_id or location_group_id are defined.","conditionScope":"row","references":{"table":"locations","field":"id"}},"stop_sequence":{"type":"INTEGER","required":"always","primaryKey":true},"stop_headsign":{"type":"TEXT"},"start_pickup_drop_off_window":{"type":"TIME","required":"conditional","condition":"Required if location_group_id or location_id is defined. Forbidden if arrival_time or departure_time is defined.","conditionScope":"row"},"end_pickup_drop_off_window":{"type":"TIME","required":"conditional","condition":"Required if location_group_id or location_id is defined. Forbidden if arrival_time or departure_time is defined.","conditionScope":"row"},"pickup_type":{"type":"ENUM","values":{"0":"Regular","1":"Not Available","2":"Must Phone","3":"On Request To Driver"},"whenEmpty":{"code":"0","label":"Regular"}},"drop_off_type":{"type":"ENUM","values":{"0":"Regular","1":"Not Available","2":"Must Phone","3":"On Request To Driver"},"whenEmpty":{"code":"0","label":"Regular"}},"continuous_pickup":{"type":"ENUM","values":{"0":"Allowed","1":"Not Available","2":"Must Phone","3":"On Request To Driver"},"whenEmpty":{"code":"1","label":"Not Available"}},"continuous_drop_off":{"type":"ENUM","values":{"0":"Allowed","1":"Not Available","2":"Must Phone","3":"On Request To Driver"},"whenEmpty":{"code":"1","label":"Not Available"}},"shape_dist_traveled":{"type":"FLOAT"},"timepoint":{"type":"ENUM","values":{"0":"Approximate","1":"Exact"},"whenEmpty":{"code":"1","label":"Exact"}},"pickup_booking_rule_id":{"type":"ID","references":{"table":"booking_rules","field":"booking_rule_id"}},"drop_off_booking_rule_id":{"type":"ID","references":{"table":"booking_rules","field":"booking_rule_id"}}}},"stops":{"presence":"conditional","condition":"Optional if demand-responsive zones are defined in locations.geojson. Required otherwise.","conditionScope":"feed","conditionCheck":{"kind":"file_absent","file":"locations"},"fields":{"stop_id":{"type":"ID","required":"always","primaryKey":true},"stop_code":{"type":"TEXT"},"stop_name":{"type":"TEXT","required":"conditional","condition":"Required for location_type 0 (stop), 1 (station) and 2 (entrance/exit). Optional for 3 (generic node) and 4 (boarding area).","conditionScope":"row"},"tts_stop_name":{"type":"TEXT"},"stop_desc":{"type":"TEXT"},"stop_lat":{"type":"LATITUDE","required":"conditional","condition":"Required for location_type 0 (stop), 1 (station) and 2 (entrance/exit). Optional for 3 (generic node) and 4 (boarding area).","conditionScope":"row"},"stop_lon":{"type":"LONGITUDE","required":"conditional","condition":"Required for location_type 0 (stop), 1 (station) and 2 (entrance/exit). Optional for 3 (generic node) and 4 (boarding area).","conditionScope":"row"},"zone_id":{"type":"ID"},"stop_url":{"type":"URL"},"location_type":{"type":"ENUM","values":{"0":"Stop","1":"Station","2":"Entrance","3":"Generic Node","4":"Boarding Area"},"whenEmpty":{"code":"0","label":"Stop"}},"parent_station":{"type":"ID","required":"conditional","condition":"Required for location_type 2 (entrance/exit), 3 (generic node) and 4 (boarding area). Forbidden for location_type 1: a station is itself the parent.","conditionScope":"row","references":{"table":"stops","field":"stop_id"}},"stop_timezone":{"type":"TIMEZONE"},"wheelchair_boarding":{"type":"ENUM","values":{"0":"Unknown","1":"Accessible","2":"Inaccessible"},"whenEmpty":{"code":"0","label":"Unknown"}},"level_id":{"type":"ID","references":{"table":"levels","field":"level_id"}},"platform_code":{"type":"TEXT"},"stop_access":{"type":"ENUM","values":{"0":"Accessible Via Pathways","1":"Not Accessible Via Pathways"}}}},"timeframes":{"presence":"optional","fields":{"timeframe_group_id":{"type":"ID","required":"always","primaryKey":true},"start_time":{"type":"TIME","required":"conditional","condition":"Required if end_time is defined.","conditionScope":"row","primaryKey":true},"end_time":{"type":"TIME","required":"conditional","condition":"Required if start_time is defined.","conditionScope":"row","primaryKey":true},"service_id":{"type":"TEXT","required":"always","primaryKey":true}}},"transfers":{"presence":"optional","fields":{"from_stop_id":{"type":"ID","required":"conditional","condition":"Required unless transfer_type is 4 or 5, which identify the transfer by trip.","conditionScope":"row","primaryKey":true,"references":{"table":"stops","field":"stop_id"}},"to_stop_id":{"type":"ID","required":"conditional","condition":"Required unless transfer_type is 4 or 5, which identify the transfer by trip.","conditionScope":"row","primaryKey":true,"references":{"table":"stops","field":"stop_id"}},"transfer_type":{"type":"ENUM","required":"always","values":{"0":"Recommended","1":"Timed","2":"Minimum Time","3":"Impossible","4":"In Seat Transfer Allowed","5":"In Seat Transfer Not Allowed"}},"min_transfer_time":{"type":"INTEGER","required":"conditional","condition":"Required if transfer_type=2 (a minimum time is needed).","conditionScope":"row"},"from_trip_id":{"type":"ID","required":"conditional","condition":"Required if transfer_type is 4 or 5 (in-seat transfers).","conditionScope":"row","primaryKey":true,"references":{"table":"trips","field":"trip_id"}},"to_trip_id":{"type":"ID","required":"conditional","condition":"Required if transfer_type is 4 or 5 (in-seat transfers).","conditionScope":"row","primaryKey":true,"references":{"table":"trips","field":"trip_id"}},"from_route_id":{"type":"ID","primaryKey":true,"references":{"table":"routes","field":"route_id"}},"to_route_id":{"type":"ID","primaryKey":true,"references":{"table":"routes","field":"route_id"}}}},"translations":{"presence":"optional","fields":{"table_name":{"type":"TEXT","required":"always","primaryKey":true},"field_name":{"type":"TEXT","required":"always","primaryKey":true},"language":{"type":"LANGUAGE_CODE","required":"always","primaryKey":true},"translation":{"type":"TEXT","required":"always"},"record_id":{"type":"TEXT","required":"conditional","condition":"Required if field_value is empty.","conditionScope":"row","primaryKey":true},"record_sub_id":{"type":"TEXT","required":"conditional","condition":"Required if table_name is stop_times and record_id is defined.","conditionScope":"row","primaryKey":true},"field_value":{"type":"TEXT","required":"conditional","condition":"Required if record_id is empty.","conditionScope":"row","primaryKey":true}}},"trips":{"presence":"required","fields":{"trip_id":{"type":"ID","required":"always","primaryKey":true},"route_id":{"type":"ID","required":"always","references":{"table":"routes","field":"route_id"}},"service_id":{"type":"ID","required":"always","references":{"table":"calendar","field":"service_id"}},"trip_headsign":{"type":"TEXT"},"trip_short_name":{"type":"TEXT"},"direction_id":{"type":"ENUM","values":{"0":"Outbound","1":"Inbound"}},"block_id":{"type":"ID"},"shape_id":{"type":"ID","required":"conditional","condition":"Required if the trip has continuous pickup or drop-off behaviour defined.","conditionScope":"row_context","references":{"table":"shapes","field":"shape_id"}},"wheelchair_accessible":{"type":"ENUM","values":{"0":"Unknown","1":"Accessible","2":"Inaccessible"},"whenEmpty":{"code":"0","label":"Unknown"}},"bikes_allowed":{"type":"ENUM","values":{"0":"Unknown","1":"Allowed","2":"Not Allowed"},"whenEmpty":{"code":"0","label":"Unknown"}},"cars_allowed":{"type":"ENUM","values":{"0":"No Information","1":"Allowed","2":"Not Allowed"},"whenEmpty":{"code":"0","label":"No Information"}},"safe_duration_factor":{"type":"FLOAT"},"safe_duration_offset":{"type":"FLOAT"}}}}'), g = ["bikes_allowed", "booking_type", "cars_allowed", "cemv_support", "continuous_drop_off", "continuous_pickup", "direction_id", "drop_off_type", "duration_limit_type", "exact_times", "exception_type", "fare_media_type", "fare_transfer_type", "friday", "geometry_type", "is_authority", "is_bidirectional", "is_default_fare_category", "is_operator", "is_producer", "location_type", "monday", "pathway_mode", "payment_method", "pickup_type", "route_type", "saturday", "stop_access", "sunday", "thursday", "timepoint", "transfer_type", "transfers", "tuesday", "wednesday", "wheelchair_accessible", "wheelchair_boarding"], w = {
5
+ fieldTypes: q,
6
+ tables: T,
7
+ enumLikeColumns: g
8
+ }, f = w.tables, I = w.fieldTypes, v = new Set(w.enumLikeColumns), N = Object.fromEntries(
9
+ Object.entries(f).map(([d, i]) => {
10
+ var e;
11
+ return [d, (e = Object.entries(i.fields).find(([, t]) => t.primaryKey)) == null ? void 0 : e[0]];
12
+ }).filter(([, d]) => d)
13
+ );
14
+ function A(d) {
15
+ const i = N[d];
16
+ if (!i) return [];
17
+ const e = [];
18
+ for (const [t, r] of Object.entries(f))
19
+ for (const [o, l] of Object.entries(r.fields)) {
20
+ const a = l.references;
21
+ (a == null ? void 0 : a.table) === d && a.field === i && e.push({ table: t, column: o });
22
+ }
23
+ return e;
24
+ }
25
+ function R(d, i, e) {
26
+ var c, u, p, n;
27
+ const t = ((c = f[d]) == null ? void 0 : c.fields[i]) ?? {}, r = I[t.type ?? ""], o = t.references, l = o && ((u = e[o.table]) == null ? void 0 : u.includes(o.field)), a = ((n = Object.entries(((p = f[d]) == null ? void 0 : p.fields) ?? {}).find(([, s]) => s.primaryKey)) == null ? void 0 : n[0]) === i;
28
+ return {
29
+ name: i,
30
+ fk_table: l ? o.table : null,
31
+ fk_column: l ? o.field : null,
32
+ enum_like: v.has(i),
33
+ related: a ? A(d).filter((s) => {
34
+ var _;
35
+ return (_ = e[s.table]) == null ? void 0 : _.includes(s.column);
36
+ }) : [],
37
+ type: t.type ?? null,
38
+ values: t.values ?? null,
39
+ required: t.required ?? null,
40
+ condition: t.condition ?? null,
41
+ condition_scope: t.conditionScope ?? null,
42
+ // Feed-scope conditions are answered by counting rows and reading file
43
+ // lists. Left unanswered here rather than guessed: the viewer falls back to
44
+ // stating the condition, which is what it does for any unanswerable one.
45
+ condition_outcome: null,
46
+ when_empty: t.whenEmpty ? { code: t.whenEmpty.code ?? null, label: t.whenEmpty.label } : null,
47
+ pattern: (r == null ? void 0 : r.pattern) ?? null,
48
+ minimum: (r == null ? void 0 : r.minimum) ?? null,
49
+ maximum: (r == null ? void 0 : r.maximum) ?? null,
50
+ type_description: (r == null ? void 0 : r.description) ?? null
51
+ };
52
+ }
53
+ function D(d) {
54
+ return Object.entries(f).filter(([i, e]) => e.presence === "required" && !(i in d)).map(([i, e]) => ({
55
+ name: i,
56
+ presence: "required",
57
+ condition: e.condition ?? null,
58
+ condition_outcome: null
59
+ }));
60
+ }
61
+ function M(d) {
62
+ if (typeof (performance == null ? void 0 : performance.getEntriesByType) != "function") return;
63
+ const i = performance.getEntriesByType("resource").filter((e) => e.name.includes(d)).reduce((e, t) => e + (t.transferSize || 0), 0);
64
+ return i > 0 ? `${Math.round(i / 1024)} kB fetched` : void 0;
65
+ }
66
+ const b = "ParquetSource cannot draw a map yet - mount without the map part.", U = /* @__PURE__ */ new Set(["eq", "ne", "contains", "in", "is_empty", "is_not_empty"]);
67
+ function S(d, i) {
68
+ const e = (r) => `'${r.replace(/'/g, "''")}'`, t = [];
69
+ for (const r of i) {
70
+ if (!d.includes(r.column) || !U.has(r.op)) continue;
71
+ const o = `"${r.column}"`, l = r.value ?? "";
72
+ let a = r.op;
73
+ if (a === "eq" && l === "" ? a = "is_empty" : a === "ne" && l === "" && (a = "is_not_empty"), a === "eq") t.push(`${o} = ${e(l)}`);
74
+ else if (a === "ne") t.push(`${o} != ${e(l)}`);
75
+ else if (a === "contains") t.push(`${o} ILIKE ${e(`%${l}%`)}`);
76
+ else if (a === "is_empty") t.push(`(${o} IS NULL OR ${o} = '')`);
77
+ else if (a === "is_not_empty") t.push(`(${o} IS NOT NULL AND ${o} != '')`);
78
+ else if (a === "in") {
79
+ const c = l.split(",").map((u) => u.trim()).filter(Boolean);
80
+ c.length && t.push(`${o} IN (${c.map(e).join(", ")})`);
81
+ }
82
+ }
83
+ return t.length ? `WHERE ${t.join(" AND ")}` : "";
84
+ }
85
+ class O {
86
+ constructor(i) {
87
+ y(this, "connection", null);
88
+ y(this, "terminate", null);
89
+ y(this, "present", {});
90
+ y(this, "startupMs", 0);
91
+ y(this, "manifestMs", null);
92
+ y(this, "firstQueryMs", 0);
93
+ this.options = i;
94
+ }
95
+ /** Release the worker and the wasm instance. See the viewer's `destroy`. */
96
+ async close() {
97
+ var i, e;
98
+ await ((i = this.connection) == null ? void 0 : i.close()), await ((e = this.terminate) == null ? void 0 : e.call(this)), this.connection = null, this.terminate = null;
99
+ }
100
+ /**
101
+ * The file as an absolute URL.
102
+ *
103
+ * DuckDB runs in a worker and fetches these itself, so it has no page to
104
+ * resolve a relative path against - `/datasets/x` reaches nothing. Callers
105
+ * naturally write a path, so it is resolved here.
106
+ */
107
+ file(i) {
108
+ var t;
109
+ const e = new URL(this.options.baseUrl, ((t = globalThis.location) == null ? void 0 : t.origin) ?? "http://localhost");
110
+ return `'${new URL(`${e.pathname.replace(/\/$/, "")}/${i}.parquet`, e.origin).href}'`;
111
+ }
112
+ /**
113
+ * Start DuckDB on first use.
114
+ *
115
+ * Imported here rather than at the top of the file so that the megabytes of
116
+ * wasm are fetched when a dataset is actually opened, not when the module is.
117
+ */
118
+ async connect() {
119
+ if (this.connection) return this.connection;
120
+ const i = performance.now(), e = await import("@duckdb/duckdb-wasm"), t = await e.selectBundle(e.getJsDelivrBundles()), r = URL.createObjectURL(
121
+ new Blob([`importScripts("${t.mainWorker}");`], { type: "text/javascript" })
122
+ ), o = new Worker(r), l = new e.AsyncDuckDB(
123
+ new e.ConsoleLogger(e.LogLevel.WARNING),
124
+ o
125
+ );
126
+ return await l.instantiate(t.mainModule, t.pthreadWorker), URL.revokeObjectURL(r), this.terminate = () => l.terminate(), this.connection = await l.connect(), this.startupMs = performance.now() - i, this.connection;
127
+ }
128
+ /**
129
+ * The table list an exported dataset carries, if it carries one.
130
+ *
131
+ * `gtfs-garage --export` writes a manifest beside the Parquet so a reader
132
+ * need not guess. Without it the only fallback is trying all 32 tables GTFS
133
+ * defines, which for a seven-table feed measured at 123 requests before the
134
+ * first row appeared.
135
+ */
136
+ async fromManifest() {
137
+ const i = performance.now();
138
+ try {
139
+ const e = await fetch(`${this.options.baseUrl}/manifest.json`);
140
+ if (!e.ok) return null;
141
+ const t = await e.json();
142
+ return this.manifestMs = performance.now() - i, (t.version ?? 1) >= 2 ? t : { version: 1, tables: t.tables };
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+ /**
148
+ * What the dataset cost to open *here*.
149
+ *
150
+ * The server's load report describes a download, an unzip and a conversion.
151
+ * None of that happens in a browser reading Parquet: the cost is starting
152
+ * DuckDB, fetching the manifest, and the first queries. Reporting the
153
+ * conversion's timings instead would describe work this reader did not do.
154
+ */
155
+ metrics(i, e) {
156
+ var o;
157
+ const t = [
158
+ { label: "Start DuckDB", ms: this.startupMs, note: M(this.options.baseUrl) },
159
+ ...this.manifestMs === null ? [] : [{ label: "Read the manifest", ms: this.manifestMs }],
160
+ { label: "First query", ms: this.firstQueryMs }
161
+ ], r = i.reduce((l, a) => l + (a.parquet_bytes ?? 0), 0);
162
+ return {
163
+ phases: t,
164
+ // No `kind` and no zeroed timings: every server-load field is left out,
165
+ // because none of that work happened here.
166
+ total_ms: this.startupMs + (this.manifestMs ?? 0) + this.firstQueryMs,
167
+ total_bytes: ((o = e == null ? void 0 : e.totals) == null ? void 0 : o.uncompressed_bytes) ?? r,
168
+ stored_bytes: r,
169
+ files: i
170
+ };
171
+ }
172
+ async rows(i) {
173
+ return (await (await this.connect()).query(i)).toArray().map((t) => ({ ...t }));
174
+ }
175
+ async tables() {
176
+ var p;
177
+ const i = performance.now(), e = await this.connect(), t = this.options.tables ? null : await this.fromManifest(), r = this.options.tables ?? ((p = t == null ? void 0 : t.tables) == null ? void 0 : p.map((n) => n.name)) ?? Object.keys(f);
178
+ this.present = {};
179
+ const o = [], a = (await Promise.all(
180
+ r.map(async (n) => {
181
+ try {
182
+ const s = (await e.query(`DESCRIBE SELECT * FROM ${this.file(n)}`)).toArray(), _ = await e.query(`SELECT count(*) AS n FROM ${this.file(n)}`);
183
+ return {
184
+ table: n,
185
+ columns: s.map((m) => String(m.column_name)),
186
+ rowCount: Number(_.toArray()[0].n)
187
+ };
188
+ } catch (s) {
189
+ return o.push(s.message), null;
190
+ }
191
+ })
192
+ )).filter((n) => n !== null);
193
+ for (const n of a) this.present[n.table] = n.columns;
194
+ if (!a.length)
195
+ throw new Error(
196
+ `No tables found at ${this.options.baseUrl}. Pass the dataset's table list to skip probing, and check the dataset exists. First reason: ${o[0] ?? "nothing probed"}`
197
+ );
198
+ this.firstQueryMs = performance.now() - i - this.startupMs - (this.manifestMs ?? 0);
199
+ const c = new Map(((t == null ? void 0 : t.tables) ?? []).map((n) => [n.name, n])), u = a.map((n) => {
200
+ const s = c.get(n.table);
201
+ return {
202
+ name: (s == null ? void 0 : s.file) ?? `${n.table}.parquet`,
203
+ table: n.table,
204
+ bytes: (s == null ? void 0 : s.bytes) ?? (s == null ? void 0 : s.parquet_bytes) ?? 0,
205
+ compressed_bytes: (s == null ? void 0 : s.compressed_bytes) ?? null,
206
+ register_ms: 0,
207
+ convert_ms: null,
208
+ parquet_bytes: (s == null ? void 0 : s.parquet_bytes) ?? null,
209
+ count_ms: null,
210
+ row_count: n.rowCount,
211
+ columns: n.columns.length
212
+ };
213
+ });
214
+ return {
215
+ metrics: this.metrics(u, t),
216
+ source: this.options.name ?? this.options.baseUrl,
217
+ tables: a.map(({ table: n, columns: s, rowCount: _ }) => ({
218
+ name: n,
219
+ row_count: _,
220
+ columns: s.map((m) => R(n, m, this.present)),
221
+ forbidden: null
222
+ })),
223
+ missing: D(this.present)
224
+ };
225
+ }
226
+ async query(i, e, t, r) {
227
+ const o = this.present[i] ?? [], l = S(o, e), a = await this.rows(`SELECT count(*) AS n FROM ${this.file(i)} ${l}`), c = o.map((p) => `"${p}"`).join(", "), u = await this.rows(
228
+ `SELECT ${c} FROM ${this.file(i)} ${l} LIMIT ${r} OFFSET ${(t - 1) * r}`
229
+ );
230
+ return {
231
+ columns: o,
232
+ // Every value is text or null, as it is server-side; the viewer formats
233
+ // from the schema and must never receive a number it did not ask for.
234
+ rows: u.map((p) => o.map((n) => p[n] == null ? null : String(p[n]))),
235
+ total: Number(a[0].n),
236
+ page: t,
237
+ page_size: r
238
+ };
239
+ }
240
+ async distinct(i, e, t = 200) {
241
+ return (await this.rows(
242
+ `SELECT "${e}" AS value, count(*) AS n FROM ${this.file(i)}
243
+ GROUP BY 1 ORDER BY 2 DESC LIMIT ${t}`
244
+ )).map((o) => ({
245
+ value: o.value == null ? null : String(o.value),
246
+ count: Number(o.n)
247
+ }));
248
+ }
249
+ // Reached only if the viewer is mounted with a map, which this source does not
250
+ // support yet - building GeoJSON means porting the layer building, the vertex
251
+ // budget and the thinning that `core/geojson.py` does. Both throw rather than
252
+ // return nothing, which would look like a feed with no stops.
253
+ geojson(i, e) {
254
+ return Promise.reject(new Error(b));
255
+ }
256
+ geojsonStream(i, e, t) {
257
+ return Promise.reject(new Error(b));
258
+ }
259
+ }
260
+ export {
261
+ O as ParquetSource,
262
+ S as buildWhere
263
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The viewer as a React component.
3
+ *
4
+ * A thin wrapper, deliberately: the viewer itself stays framework-agnostic so
5
+ * that one implementation serves both the standalone application and a host
6
+ * that happens to use React. Nothing here knows what the viewer does.
7
+ */
8
+ import type { DatasetOptions } from "./mount-dataset";
9
+ import { type ViewerOptions } from "./mount";
10
+ export type { ViewerOptions, Viewer, ViewerParts, ViewerSource, GtfsSource } from "./mount";
11
+ export interface GtfsGarageProps extends ViewerOptions {
12
+ className?: string;
13
+ /**
14
+ * The viewer fills its container, and a container with no height renders an
15
+ * invisible map. The host decides the box, so it has to give it a size.
16
+ */
17
+ style?: React.CSSProperties;
18
+ }
19
+ export declare function GtfsGarage({ className, style, ...options }: GtfsGarageProps): import("react").JSX.Element;
20
+ export type { DatasetProvider, DatasetState } from "./dataset";
21
+ export interface GtfsGarageDatasetProps extends DatasetOptions {
22
+ className?: string;
23
+ style?: React.CSSProperties;
24
+ }
25
+ /**
26
+ * The viewer, including the wait for a dataset that is still being prepared.
27
+ *
28
+ * The host supplies a `dataset` that reports where things stand; the progress,
29
+ * failure and not-prepared states are drawn here rather than in every host.
30
+ */
31
+ export declare function GtfsGarageDataset({ className, style, ...options }: GtfsGarageDatasetProps): import("react").JSX.Element;
package/dist/react.js ADDED
@@ -0,0 +1,36 @@
1
+ import { jsx as f } from "react/jsx-runtime";
2
+ import { useRef as c, useEffect as i } from "react";
3
+ import { mountDataset as m } from "./dataset.js";
4
+ import { m as d } from "./mount-Bpr3Nw4_.js";
5
+ function p({ className: l, style: u, ...e }) {
6
+ const o = c(null), n = c(e);
7
+ return n.current = e, i(() => {
8
+ const s = o.current;
9
+ if (!s) return;
10
+ let t, a = !1;
11
+ return d(s, n.current).then(
12
+ (r) => a ? r.destroy() : t = r,
13
+ (r) => console.error("GTFS Garage failed to mount:", r)
14
+ ), () => {
15
+ a = !0, t == null || t.destroy();
16
+ };
17
+ }, []), /* @__PURE__ */ f("div", { ref: o, className: l, style: { height: "100%", ...u } });
18
+ }
19
+ function v({ className: l, style: u, ...e }) {
20
+ const o = c(null), n = c(e);
21
+ return n.current = e, i(() => {
22
+ const s = o.current;
23
+ if (!s) return;
24
+ let t, a = !1;
25
+ return m(s, n.current).then(
26
+ (r) => a ? r.destroy() : t = r,
27
+ (r) => console.error("GTFS Garage failed to mount:", r)
28
+ ), () => {
29
+ a = !0, t == null || t.destroy();
30
+ };
31
+ }, []), /* @__PURE__ */ f("div", { ref: o, className: l, style: { height: "100%", ...u } });
32
+ }
33
+ export {
34
+ p as GtfsGarage,
35
+ v as GtfsGarageDataset
36
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The packaged GTFS description, for code that has no server to ask.
3
+ *
4
+ * The REST source gets every column's type, keys and conditions from the API,
5
+ * which reads this same document in Python. A source querying Parquet in the
6
+ * browser has nobody to ask, so it reads the document directly and builds the
7
+ * same `ColumnInfo` from it.
8
+ */
9
+ import type { ColumnInfo, MissingFile } from "./sources/types";
10
+ interface Field {
11
+ type?: string;
12
+ values?: Record<string, string>;
13
+ required?: string;
14
+ condition?: string;
15
+ conditionScope?: string;
16
+ primaryKey?: boolean;
17
+ references?: {
18
+ table: string;
19
+ field: string;
20
+ };
21
+ whenEmpty?: {
22
+ code?: string | null;
23
+ label: string;
24
+ };
25
+ }
26
+ interface Table {
27
+ presence?: string;
28
+ condition?: string;
29
+ fields: Record<string, Field>;
30
+ }
31
+ declare const TABLES: Record<string, Table>;
32
+ /**
33
+ * What the schema says about one column, in the shape the UI consumes.
34
+ *
35
+ * `present` is the tables this feed actually has: a link is only offered when
36
+ * its target is there, which is the same rule the REST source applies.
37
+ */
38
+ export declare function columnInfo(table: string, column: string, present: Record<string, string[]>): ColumnInfo;
39
+ /** Files GTFS requires that this feed does not have. */
40
+ export declare function missingFiles(present: Record<string, string[]>): MissingFile[];
41
+ export { TABLES as schemaTables };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Test fixtures for the data contract.
3
+ *
4
+ * `ColumnInfo` gains a field whenever the schema learns to describe one more
5
+ * thing about a column, and every inline literal in a test breaks when it does.
6
+ * Building them here means a new field has one default to add, not one per test.
7
+ */
8
+ import type { ColumnInfo, TableInfo } from "./types";
9
+ export declare function columnInfo(overrides?: Partial<ColumnInfo>): ColumnInfo;
10
+ /**
11
+ * A column of a given GTFS field type, carrying what the schema publishes for
12
+ * it.
13
+ *
14
+ * Reads the generated document rather than repeating its patterns here, so a
15
+ * test asserting that an address is rejected exercises the rule the application
16
+ * ships instead of a copy that could drift from it. This is the same resolution
17
+ * `queries.py` performs server-side.
18
+ */
19
+ export declare function typedColumn(type: string, overrides?: Partial<ColumnInfo>): ColumnInfo;
20
+ export declare function tableInfo(overrides?: Partial<TableInfo>): TableInfo;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Reading newline-delimited JSON as it arrives.
3
+ *
4
+ * A whole-feed map layer is far too large to wait for in one piece, so the
5
+ * server sends a header line and then batches of features. Splitting is its own
6
+ * pure function because chunk boundaries fall wherever the network puts them,
7
+ * almost never on a newline, and that is the part worth testing directly.
8
+ */
9
+ /** A message per line: one header, then any number of feature batches. */
10
+ export type NdjsonMessage = {
11
+ type: "header";
12
+ total: number;
13
+ step: number;
14
+ } | {
15
+ type: "batch";
16
+ features: GeoFeatureLike[];
17
+ };
18
+ /** Loose on purpose: this layer does not care what is inside a feature. */
19
+ export interface GeoFeatureLike {
20
+ type: string;
21
+ geometry: unknown;
22
+ properties: Record<string, string>;
23
+ }
24
+ export interface SplitResult {
25
+ lines: string[];
26
+ /** The trailing partial line, to be prefixed onto the next chunk. */
27
+ rest: string;
28
+ }
29
+ export declare function splitLines(buffer: string): SplitResult;
30
+ /**
31
+ * Read a response body as NDJSON, invoking `onMessage` per line.
32
+ *
33
+ * A line that will not parse is skipped rather than thrown: losing one batch of
34
+ * a map layer should not abandon the rest of the feed.
35
+ */
36
+ export declare function readNdjson(body: ReadableStream<Uint8Array>, onMessage: (message: NdjsonMessage) => void): Promise<void>;