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 +86 -0
- package/dist/dataset.d.ts +73 -0
- package/dist/dataset.js +64 -0
- package/dist/dom.d.ts +24 -0
- package/dist/gtfs-garage-web.css +1 -0
- package/dist/map/basemap.d.ts +23 -0
- package/dist/map/geo.d.ts +4 -0
- package/dist/map/layers.d.ts +28 -0
- package/dist/map/map.d.ts +60 -0
- package/dist/map/redraw.d.ts +29 -0
- package/dist/map-part.d.ts +10 -0
- package/dist/map.js +334 -0
- package/dist/markup.d.ts +24 -0
- package/dist/mount-Bpr3Nw4_.js +1064 -0
- package/dist/mount-dataset.d.ts +29 -0
- package/dist/mount.d.ts +79 -0
- package/dist/mount.js +7 -0
- package/dist/parquet.js +263 -0
- package/dist/react.d.ts +31 -0
- package/dist/react.js +36 -0
- package/dist/schema.d.ts +41 -0
- package/dist/sources/fixtures.d.ts +20 -0
- package/dist/sources/ndjson.d.ts +36 -0
- package/dist/sources/parquet.d.ts +85 -0
- package/dist/sources/rest.d.ts +35 -0
- package/dist/sources/types.d.ts +252 -0
- package/dist/state.d.ts +35 -0
- package/dist/ui/feed-loader.d.ts +35 -0
- package/dist/ui/feed-metrics.d.ts +47 -0
- package/dist/ui/filters.d.ts +18 -0
- package/dist/ui/history.d.ts +48 -0
- package/dist/ui/load-progress.d.ts +7 -0
- package/dist/ui/navigation.d.ts +60 -0
- package/dist/ui/sidebar.d.ts +21 -0
- package/dist/ui/table.d.ts +97 -0
- package/dist/ui/values.d.ts +61 -0
- package/package.json +94 -0
|
@@ -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>;
|
package/dist/mount.d.ts
ADDED
|
@@ -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
package/dist/parquet.js
ADDED
|
@@ -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
|
+
};
|
package/dist/react.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/schema.d.ts
ADDED
|
@@ -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>;
|