Skip to main content

Connectors

Alongside the standard SQL and warehouse data sources, the query service ships a set of connectors for transportation, traffic, weather and fleet APIs. Each is a query-service data source type: queries against it are JSON endpoint descriptors rather than SQL, and the fields below are entered once when the data source is created (see Data Sources).

ConnectorType stringReadsConfiguration
GTFS-Realtimegtfs_realtimeVehicle positions from a GTFS-Realtime feed, either a websocket JSON stream or an HTTP protobuf snapshot; over the HTTP protobuf form only, also trip updates ({"resource": "trip_updates"}, one row per stop_time_update) and service alerts ({"resource": "service_alerts"}, one row per alert)Feed URL, ws(s):// for the websocket form or http(s):// for the protobuf form (required); optional route id to display-name map (JSON); default sample window in seconds (websocket feeds only); Trip updates URL and Service alerts URL (both optional, HTTP protobuf: agencies commonly publish the three GTFS-Realtime feeds at distinct URLs)
Static GTFSgtfs_staticEvery .txt table in a GTFS zip archive, which covers the schedule (stops, routes, trips, stop_times, calendar), GTFS-Pathways (pathways, levels) and the GTFS-Flex tables (booking_rules, location_groups, location_group_stops, and the pickup and drop-off window columns of stop_times); {"resource": "list"} names the tables it holds, {"table": "stops"} returns one, with optional columns and filter. A Flex archive's locations.geojson is served as the locations table, one row per zone with its geometry, or as one GeoJSON cell with "format": "featurecollection"Archive URL (http(s)://, required); max rows per query (default 100000)
GBFS BikesharegbfsStation information and status from a GBFS discovery documentDiscovery URL (gbfs.json, required); feed language (default en)
GOFS On-DemandgofsAn on-demand (microtransit, paratransit, taxi) system's GOFS 1.0 feeds from its discovery document: system_information (default), service_brands, vehicle_types, zones (one row per zone, or a GeoJSON cell with "params": {"format": "featurecollection"}), operating_rules, calendars and booking_rules; array-valued fields arrive as JSON stringsDiscovery URL (gofs.json, required); feed language (default en)
TODS Operational DatatodsThe tables of a TODS v2 archive (run_events, vehicles, vehicle_assignments, employee_run_dates and the *_supplement files) with the same list, table, columns and filter query shape as Static GTFS. With a base GTFS URL configured, {"table": "trips", "merged": true} reads a base table with its supplement applied per the TODS evaluation rules (matching rows updated field by field, blank fields ignored, TODS_delete=1 removed, unmatched rows appended); trips, stops, stop_times, routes, calendar and calendar_dates merge this wayTODS archive URL (http(s)://, required); base GTFS archive URL (optional, needed for merged reads); max rows per query (default 100000)
WZDx Work ZoneswzdxA WZDx 4.x work zone feed as published by a DOT or a work zone data vendor: road_events (default, one row per road event with core_details flattened into columns, road_names, types_of_work, restrictions and related_road_events as JSON strings, and the geometry columns the map visualization reads), lanes (one row per lane per event), feed_info and data_sources. A device feed, when its URL is configured, serves devices (arrow boards, cameras, message signs and the other field devices). road_events and devices take "params": {"filter": {"event_type": "detour"}} against any returned column and "format": "featurecollection" for one GeoJSON cell. A 3.x feed is refused with an error naming the versionWork zone feed URL (required); device feed URL (optional); max rows per query (default 5000)
Waze Traffic AlertswazeAlerts and irregularities from a Waze partner feedFeed URL (required; the partner token and coverage polygon are embedded in it, so there is no shared default)
GO511 Trafficgo511Traffic incidents, roadwork, park-and-ride lots, cameras and mainline route speeds for the LA regionAPI base URL (default provided); API key (required, secret)
AirNow Air QualityairnowCurrent AQI observations by coordinateObservation API URL (default provided); API key (required, secret)
OpenWeatherMapopenweathermapCurrent weather by coordinateAPI URL (default provided); App ID / API key (required, secret)
TrafficLand CamerastrafficlandCamera video feedsAPI base URL (default provided); API key (required, secret); system name (required)
Geotab FleetgeotabFleet device status and inventoryServer (default provided); database, username and password (all required, password stored as secret); default results limit
MetroCloudAlliance TransitmetrocloudallianceTransit predictions, stops, carriers and vehicle locationsAPI base URL (default provided); API key (required, secret)
SoCalTransportsocaltransportScheduled arrival and departure times by transit node (regional commuter rail, Amtrak, and buses)Base URL (default provided); the transit node id is a query parameter, not configuration
NTCIP 1203 DMSntcip_dmsDynamic message sign identity, status and current message, polled over SNMPSNMP community string (required, secret); SNMP version (2c default, or 1); device list (JSON, required); per-device timeout (default 2s); max devices polled (default 50)
TMDD Center-to-CentertmddActive traffic events, and dynamic message sign inventory and status, from a traffic management center over TMDD v3.03d C2C SOAPC2C SOAP endpoint URL (required); organization id (required); user id and password (optional, but only as a pair, password stored as secret); TMDD version (3.03d, the only one implemented); SOAPAction header value (empty by default); message type id and version (both default 1); TLS verification (on by default) and an optional CA bundle path; max response size (default 10 MB); max records (default 10000)
Static GeoJSONstatic_geojsonLayers (rail lines, bus routes, service areas, or other named feature sets) from a local GeoJSON directoryLayer directory path (required)

Fields marked "secret" are stored encrypted, the same way any other data source credential is (see Data Sources: Notes for operators).

Latitude/longitude, route ids and similar per-request values are query parameters rather than connector configuration, so one configured connector serves every query against it instead of having a single location or route baked in.

One column needs a step before mapping: Waze alerts returns location as a JSON string of the form {"x": ..., "y": ...}, where x is the longitude and y the latitude. Extract the two values in the query before pointing a map visualization at them; the raw column will not plot as-is.

A query against GTFS-Realtime's trip_updates resource:

{"resource": "trip_updates", "params": {"routes": "1,2", "early_seconds": 60, "late_seconds": 300}}

params.routes is optional and substituted into {routes} in the URL. params.early_seconds (default 60) and params.late_seconds (default 300) define the on-time window: a stop_time_update whose delay falls inside that window is on time. Columns: trip_id, route_id, line, direction_id, vehicle_id, stop_id, stop_sequence, arrival_delay (seconds), departure_delay (seconds), arrival_time, departure_time, trip_schedule_relationship, stop_schedule_relationship, timestamp, on_time. on_time is None for a skipped stop, a canceled trip, or a stop the feed carries no delay for, and those rows are excluded from any average rather than counted late. The default window, no more than 1 minute early and no more than 5 minutes late, follows the common transit-industry convention; set early_seconds and late_seconds to match an agency's own standard where it differs.

service_alerts ({"resource": "service_alerts"}) returns one row per alert: alert_id, cause, effect, severity_level, header, description, url, active_from, active_to (None when any active period is open-ended), informed_route_ids, informed_stop_ids (comma-joined).

Spatial joins across query results​

A data source of type results runs SQL over the cached results of other queries, and where the host has the SpatiaLite library installed its SQL carries the SpatiaLite functions: ST_Within, ST_Intersects, ST_Distance, MakePoint, GeomFromGeoJSON and the rest of that set. That is how a boundary layer read from one connector gets joined to points read from another.

Geometry travels as text rather than as a geometry type: Static GeoJSON returns each feature's geometry as a GeoJSON string, so GeomFromGeoJSON is what turns that column into something a predicate can read. Where the point side already has latitude and longitude as separate numeric columns, MakePoint builds the point from them, longitude first. Where it does not, extract them in the query first: Waze alerts packs both into a JSON location string, as described above.

SELECT b.name AS district, COUNT(*) AS stops
FROM cached_query_12 b JOIN cached_query_34 s
ON ST_Within(MakePoint(s.stop_lon, s.stop_lat), GeomFromGeoJSON(b.geometry))
GROUP BY b.name

There is no spatial index behind this join. Every point is tested against every polygon, so the work is the product of the two row counts, and a query that answers instantly over a few hundred stops can crawl over a hundred thousand. Both fixes are in the query: put a LIMIT on the point side while you are still drafting, and filter on an ordinary property column first (a route id, an agency, a latitude and longitude range) so the predicate only ever runs over rows that could plausibly match.

If the library is absent, the data source still works and plain SQL over query results is unaffected. The query service logs one warning naming the library it tried, and a query calling ST_* fails with SQLite's own "no such function" error. Operators install it (libsqlite3-mod-spatialite on Debian and Ubuntu, libspatialite from Homebrew on macOS) and, where it sits outside the loader's search path, give the full path in REDASH_SPATIALITE_LIBRARY_PATH.

NTCIP 1203 DMS: a worked configuration example​

This connector polls dynamic message signs (highway signs that display scrolling text) over SNMP, per the NTCIP 1203 standard. Its device list is a JSON value rather than a single host and port, which the table above cannot show in one cell, so a full example is worth spelling out.

Configuring a data source of this type means filling in:

  • SNMP Community String: the read community configured on the signs themselves (a typical default is public, though it is usually changed). SNMP v1 and v2c send this string in clear text, so only point this connector at a network path you control, never across the open internet.

  • SNMP Version: 2c unless the signs only answer to 1.

  • Devices (JSON): the poll targets, one object per sign:

    [
    { "name": "I-5 NB MP12", "host": "10.0.1.20", "port": 161 },
    { "name": "I-5 SB MP12", "host": "10.0.1.21" },
    { "name": "US-101 NB MP4", "host": "10.0.2.5", "port": 1610 }
    ]

    port defaults to 161, the standard SNMP port, when omitted (the second entry above). name is what a query's params.devices selects by; it does not have to match anything on the device itself.

  • Per-Device Timeout: seconds allowed for one sign's SNMP transaction before giving up on it and moving to the next device. A sign that times out, or fails for any other reason, is marked error, not skipped: skipped is reserved for a sign never attempted at all, because the whole-query deadline (request_timeout, capped at 30 seconds) had already run out before its turn came. Reading a result table, error means "this sign was reached and something went wrong with it"; skipped means "this sign was never reached, raise request_timeout or poll fewer devices per query."

  • Max Devices Polled: a fleet-size cap applied after any params.devices selection, so a query against "all devices" cannot poll more than this many signs.

A query against this data source is a JSON resource selector, the same pattern the other transportation connectors use:

{ "resource": "dms_status", "params": { "devices": ["I-5 NB MP12"] } }

resource is one of dms_identity, dms_status or dms_message. Omitting params.devices polls every configured device (subject to the max-devices cap above). Every row carries a poll_status of healthy, error or skipped, so a single unreachable sign in a fleet of fifty does not fail the whole query; it appears as one row saying so.

TMDD Center-to-Center: a worked configuration example​

This connector polls a traffic management center over the TMDD v3.03d Center-to-Center SOAP interface and reads three things: active traffic events, dynamic message sign inventory, and dynamic message sign status. Everything it sends is a request message. The standard's control and command operations are not implemented, so a data source of this type cannot change anything at the center.

Configuring one means filling in:

  • C2C SOAP Endpoint URL (required): the center's Center-to-Center SOAP endpoint.
  • Organization ID (required): this system's own organization identifier, 1 to 32 characters, sent in every request so the center can attribute it.
  • User ID and Password: optional, but only as a pair. The authentication block a request carries is optional, and both of its children are mandatory once it is present, so half a credential can only build a request the center's own schema rejects. Give both or leave both empty. The user id is 1 to 32 characters, the password 1 to 256, and the password is stored encrypted.
  • TMDD Version: 3.03d is the only version implemented. Anything else is refused when a query runs, rather than sent and hoped for.
  • SOAPAction Header Value, Message Type ID and Message Type Version: three fields that exist because a value has to go out and the published XSD and WSDL do not settle which. See below.
  • Verify TLS Certificate and CA Bundle Path: agency centers commonly run a private CA, which is worth trusting explicitly rather than turning verification off. An explicit bundle path wins over the checkbox: set one and the certificate is verified against it whichever way the box was left.
  • Max Response Size (MB): default 10, enforced while the response body is still arriving rather than measured after it has all been read, so an oversized answer is abandoned mid-stream instead of after the memory has gone.
  • Max Records: default 10000. This is not a row limit; see below.

A query against this data source is a JSON resource selector, the same pattern the other transportation connectors use:

{ "resource": "events", "params": { "since": "2026-01-01T00:00:00Z", "limit": 100 } }

resource is one of events, dms_inventory or dms_status. events and dms_status accept since and limit; dms_inventory accepts limit only.

since and limit are applied after the center's response arrives​

Both are applied to the decoded records on this side, not sent to the center. No request type in the published v3.03d schema carries a result count or an updated-since filter, and the one element that looks like it might (request-times) has no stated semantics there, so sending one would risk silently returning the wrong rows.

The practical consequence is worth planning for: a large events feed is transferred in full every time, however small a limit you ask for, because these two params narrow what you read rather than what the center sends.

since is not available for dms_inventory, which decodes to no timestamp column at all. Passing it there fails the query and says so, rather than being quietly ignored. An ignored time filter hands back rows from outside the window you asked for, and nothing in the result says the window was never applied.

since takes an ISO-8601 timestamp, and a value carrying no offset is read as UTC. It compares against update_time for events and last_update for dms_status. A record whose own timestamp is missing is kept: both fields are optional in the schema, so a center omitting one is not saying the record is old, and dropping it would hide a live sign behind a filter meant only to narrow by time.

max_records fails the query, it does not truncate​

limit truncates on purpose: ask for 100 rows and you get at most 100. Max Records is the opposite control. It caps how many records the center's response is allowed to decode to, and exceeding it is an error naming the count rather than a shortened table, so a partial answer cannot be mistaken for the complete one. When you hit it, either raise the cap or narrow the query.

The cap is checked before since and limit are applied, so setting a limit cannot let an unexpectedly huge response through unremarked.

Coordinates are converted, and the raw integer is kept beside them​

latitude and longitude come back as decimal degrees, divided by 1,000,000 from the integers the center sent. The published XSD never writes "degree", "microdegree", or any unit at all on these types. The factor is implied by their value bounds and nothing else: latitude is bounded at plus or minus 90,000,000 over plus or minus 90 degrees, longitude at plus or minus 180,000,000 over plus or minus 180, and both divide out to 1,000,000 units per degree.

Because that factor is inferred rather than documented, every coordinate column has a _raw sibling, latitude_raw and longitude_raw, carrying the integer exactly as it arrived. If a center turns out to scale differently, the raw column still holds what it sent and you can do the arithmetic yourself. events and dms_inventory carry all four columns. dms_status carries no location.

Enumerated columns are passed through exactly as sent​

severity, status and direction on events, direction on dms_inventory, and oper_status on dms_status are enumerated fields, and the schema lets each of them arrive either as a bounded integer or as a string token. Whichever form the center sends is the form that lands in the column. The connector does not translate between the two.

The reason is that there is no mapping to apply. 88 of the 207 named simple types in the published TMDD.xsd are integer-or-string unions, and the schema asserts no mapping between the two arms anywhere. A matching count invites the assumption that the first integer means the first string, and one type in the same bundle disproves it: Time-reference-code is a union of 4 integers against 5 strings, so for that type no positional mapping exists to guess at. Any connector that translated would be inventing a mapping, and would be provably wrong somewhere.

The connector does enforce membership: a value in neither arm is refused, with an error naming both, rather than passed into a column the center's own schema would reject. If you want one representation in a report, map it in the query or the visualization, where the mapping your center actually uses is a decision you can see and change.

current_message is the center's own string, markup and all​

dms_status.current_message is returned exactly as the center sent it. The published XSD types it as an unrestricted string and its documentation says nothing about the content, so it may contain NTCIP MULTI markup, the tag language signs use for line breaks, page timing and justification ([nl], [np], [pt50o30], [jl3]). Nothing here strips or interprets it. If a dashboard needs rendered text, decode it downstream. Note that this differs from the NTCIP connector above, whose dms_message resource polls a sign directly and returns a raw message_multi and a decoded message_text beside each other.

message_type_id, message_type_version and soap_action​

Message Type ID and Message Type Version are mandatory in an event request header. Each is an unconstrained xs:unsignedByte, so any whole number from 0 to 255 is schema-valid, and the published XSD and WSDL do not define what any particular value means. Both default to 1, which is what gets sent unless your center documents values of its own, in which case enter them here. They appear only in the events request; the two DMS requests have no such header.

SOAPAction Header Value is empty by default, which sends SOAPAction: "". All 80 operations in the published WSDL declare the same raw value, and that value is two apostrophe characters rather than an empty string. Read as an authoring artifact it means "no action", which is the empty header; read literally it means the action URI is ''. The published XSD and WSDL cannot settle which was meant. So if your center rejects the empty header, put two apostrophes in this field and that is what will be sent, with no code change needed to match either reading.