No description
  • Go 99.8%
  • Makefile 0.2%
Find a file
2026-10-08 21:37:53 +02:00
dgt30 initial release 2026-10-08 21:37:53 +02:00
e2e initial release 2026-10-08 21:37:53 +02:00
etraffic initial release 2026-10-08 21:37:53 +02:00
geo initial release 2026-10-08 21:37:53 +02:00
.gitignore initial release 2026-10-08 21:37:53 +02:00
adapters_test.go initial release 2026-10-08 21:37:53 +02:00
catalog.go initial release 2026-10-08 21:37:53 +02:00
charging.go initial release 2026-10-08 21:37:53 +02:00
client.go initial release 2026-10-08 21:37:53 +02:00
core_test.go initial release 2026-10-08 21:37:53 +02:00
decode.go initial release 2026-10-08 21:37:53 +02:00
doc.go initial release 2026-10-08 21:37:53 +02:00
example_test.go initial release 2026-10-08 21:37:53 +02:00
go.mod initial release 2026-10-08 21:37:53 +02:00
go.sum initial release 2026-10-08 21:37:53 +02:00
live_regression_test.go initial release 2026-10-08 21:37:53 +02:00
locations.go initial release 2026-10-08 21:37:53 +02:00
Makefile initial release 2026-10-08 21:37:53 +02:00
README.md initial release 2026-10-08 21:37:53 +02:00
regional.go initial release 2026-10-08 21:37:53 +02:00
regional_test.go initial release 2026-10-08 21:37:53 +02:00
roadsigns.go initial release 2026-10-08 21:37:53 +02:00
robustness_test.go initial release 2026-10-08 21:37:53 +02:00
shapefile.go initial release 2026-10-08 21:37:53 +02:00
shapefile_test.go initial release 2026-10-08 21:37:53 +02:00
TESTING.md initial release 2026-10-08 21:37:53 +02:00
types.go initial release 2026-10-08 21:37:53 +02:00
xml.go initial release 2026-10-08 21:37:53 +02:00
zones.go initial release 2026-10-08 21:37:53 +02:00

dgt

Go clients and decoders for Spanish traffic and mobility feeds. Requires Go 1.26 or newer.

  • Typed snapshots from the official NAP feeds.
  • Local radius/bounding-box filters and GeoJSON conversion.
  • The traffic website's encoded JSON endpoints in etraffic.
  • Credentialed DGT 3.0 MQTT subscriptions in dgt30.

The module path is code.rbel.co/rubiojr/dgt. This checkout is ready to use locally; it hasn't been published.

Fetch a feed

import (
    "context"
    "net/http"
    "time"

    "code.rbel.co/rubiojr/dgt"
    "code.rbel.co/rubiojr/dgt/geo"
)

func nearbyCameras(ctx context.Context) ([]dgt.Camera, error) {
    client := dgt.NewClient(&http.Client{Timeout: 30 * time.Second})
    snapshot, err := client.Cameras(ctx, dgt.FeedDGTCameras)
    if err != nil {
        return nil, err
    }

    return geo.WithinRadius(snapshot.Items, dgt.Point{
        Latitude: 42.26, Longitude: -3.94,
    }, 25_000)
}

Reuse clients. Calls fetch one source at a time. Applications own polling, caching, persistence, retries and merging sources. A typical refresh schedule is one minute for DGT incidents/messages and one hour for camera/sign catalogues. Other feeds have different intervals; inspect dgt.Feeds().

Snapshot[T] includes Feed, FetchedAt, optional PublishedAt, Mode and Warnings. Offline decoders leave FetchedAt zero. A successful download does not establish that the publisher's data is recent.

snapshot, err := dgt.DecodeIncidents(reader, dgt.FeedSCTIncidents)

Decoders accept an io.Reader and leave it open. IDs are source-local: pair them with the feed ID when storing data.

Public feeds

All 26 public NAP catalogue entries below have connectors. ZBE discovery reads the municipal resource index; fetching a zone is a separate call.

Method Feed constants
Incidents FeedDGTIncidents, FeedSCTIncidents, FeedBasqueIncidents, FeedDGTSRTI, FeedMadridIncidents, FeedMadridIncidentsMap, FeedMadridCalle30Incidents, FeedMadridPlannedWorks
Cameras FeedDGTCameras, FeedMadridCameras, FeedMadridCalle30Cameras
SignLocations FeedDGTSignLocations, FeedMadridSignLocations
SignMessages FeedDGTSignMessages, FeedMadridSignMessages
SpeedCameras FeedDGTSpeedCameras
EnforcementSections FeedDGTEnforcementSections
RiskSections FeedDGTMotorcycleRisk, FeedWildlifeRisk, FeedCaminoSections
RiskPoints FeedCaminoPoints
ChargingStations FeedChargingStations
RoadSigns FeedSpeedLimits, FeedValenciaRoadSigns, FeedGipuzkoaRoadSigns
DiscoverLowEmissionZoneFeeds, LowEmissionZones FeedLowEmissionZones

These methods have corresponding Decode… functions. DecodeLowEmissionZones(reader) identifies the publication generically; a fetched zone snapshot identifies the individual municipal resource.

resources, err := client.DiscoverLowEmissionZoneFeeds(ctx)
if err != nil {
    return err
}
zones, err := client.LowEmissionZones(ctx, resources[0])

JoinSigns(locations.Items, messages.Items) combines separately fetched sign locations and displays from the same provider. Keep their publication timestamps alongside the joined data.

Source details that matter

  • Coverage: DGT incident/camera/sign feeds exclude Catalonia and the Basque Country. SCT and Basque incident feeds are separate. Municipal feeds cover their published networks.
  • Geometry: DATEX endpoints produce a straight segment marked GeometryEndpoints. It isn't the full road alignment. The website often supplies richer geometry.
  • Camino coordinates: the inspected publications appear to have swapped latitude/longitude. Values are preserved and reported in Warnings.
  • Bad sign coordinates: two published DGT signs had out-of-range coordinates during verification. They retain SourceGeometry and a warning; normalized geometry is absent.
  • Road-sign updates: RoadSigns returns Snapshot[Change[RoadSign]]. Preserve Mode and apply add/modify/delete operations. A delta is not a full inventory. ROSATTE Remove is normalized to ChangeDelete.
  • Unspecified CRS: the national speed-limit publication contains projected coordinates without enough CRS metadata. They stay in SourceGeometry. Madrid's sign shapefile explicitly declares EPSG:25830 and is converted to WGS84.
  • ZBE rules: nested regulation trees are preserved in Regulations, including negation, conditions, exceptions and schedules. The library doesn't evaluate whether a particular vehicle may enter.
  • Charging: socket powers are watts. A ChargingStation corresponds to an infrastructure site and preserves station/refill-point IDs. Live socket availability is not supplied by this catalogue.
  • Madrid times: offset-free timestamps are interpreted as Europe/Madrid and reported with a warning. The Calle 30 incident/works feeds were empty in the live checks; unrecognized future record layouts return an error when encountered.
  • Madrid incident KML: when the publication has no source ID, derived-… IDs are deterministic hashes of description and coordinates. They change if those fields change.

Geographic helpers

nearby, err := geo.WithinRadius(snapshot.Items, center, 10_000)
visible, err := geo.WithinBounds(snapshot.Items, geo.Bounds{
    West: -3.9, South: 40.2, East: -3.4, North: 40.7,
})
feature, err := geo.NewFeature(incident.ID, incident.Geometry, map[string]any{
    "road": incident.Road,
    "geometry_quality": incident.Geometry.Quality,
})

Radius queries measure against points, segments and polygons, including holes. Unknown geometry is excluded. Distances use a spherical Earth model. Bounding-box queries use GeoJSON-style straight segments and support boxes crossing the antimeridian. GeoJSON output uses longitude-first coordinates and JSON null for unknown geometry.

Website JSON API

front := etraffic.NewClient(httpClient)
legend, err := front.Legend(ctx)
cameras, err := front.Cameras(ctx)
incidents, err := front.Incidents(ctx, etraffic.Filters{
    Causes: []string{"Obras"},
})

SignLocations, SignMessages and Campaigns are also available. The adapter decodes the Base64/XOR envelope. Incident.Geometry is a JSON geometry object, decoded from the nested string. Classifications retain the website's Spanish labels. Discover them with Legend; empty incident filters are rejected. Subfilters take precedence, matching the frontend.

These endpoints are frontend implementation details. Their stability and coverage differ from the documented NAP feeds.

DGT 3.0

connected, err := dgt30.NewClient(dgt30.Config{
    ClientID: clientID,
    Username: username,
    Password: password,
})
if err != nil {
    return err
}
return connected.SubscribeSlowVehicles(ctx, func(event dgt30.PositionEvent) error {
    return storePosition(event)
})

The default broker is ssl://production.cmobility30.es:8883. DGT must assign credentials/client IDs and allowlist the connecting IP. Each simultaneous subscription needs a distinct assigned client ID.

Typed subscriptions cover special vehicles, arrow boards, sports cones, sports beacons and parking free-capacity observations. Subscribe also accepts an explicit topic and a raw JSON callback. Callbacks run serially with a bounded queue. Cancellation, callback errors and connection loss end the subscription; the caller controls retries. Callbacks should honor their application's cancellation. Delivery is not an exactly-once processing guarantee.

Sports-beacon messages are positions, not reconstructed road sections. Static parking REST data and a construction-cone-specific consumer are not implemented. The catalogue's Connector, Supported and Notes fields make these distinctions explicit. Construction-cone and commercial HERE/TomTom entries remain discoverable metadata, as do the two map applications. Their catalogue listing does not imply anonymous access or implemented connectors.

Errors and limits

Errors wrap their causes. Use errors.Is for ErrUnknownFeed, ErrFeedKind, ErrUnsupported, ErrTooLarge and ErrUnsupportedCRS; use errors.As for *HTTPError, which includes StatusCode and RetryAfter.

The HTTP client has a two-minute per-call deadline, respects shorter caller/client deadlines, and limits expanded publications to 256 MiB. WithMaxBytes can lower the network-response limit. XML nesting, per-record element count and publication record count (100,000) are bounded. Malformed publications return an error and an empty snapshot, avoiding partial cache replacement. Documented coordinate anomalies are retained with warnings.

WithFeedURL supports mirrors and tests. Invalid client options are reported before network I/O. Unit tests use local servers and fixtures. They never contact publishers.

Tests

go test -race -cover ./...
staticcheck ./...
govulncheck ./...

# Live HTTP checks: one response at a time, at least five seconds between requests.
DGT_E2E=1 go test -tags=e2e -count=1 -p=1 -parallel=1 -timeout=15m -v ./e2e

Set DGT_E2E_DELAY_SECONDS to a larger integer to slow the HTTP suite further. The minimum is five seconds. The limiter covers redirects as well as the main requests. Run one live suite at a time; pacing is shared within the process, not across separate invocations. The ZBE test fetches the index and one municipal resource. Camera tests fetch metadata, not thousands of images.

The MQTT e2e test is separately gated:

DGT30_E2E=1 DGT30_CLIENT_ID=… DGT30_USERNAME=… DGT30_PASSWORD=… \
  go test -tags=e2e -count=1 -run '^TestDGT30$' -timeout=1m -v ./e2e

It makes one connection and waits up to 45 seconds for one real slow-vehicle event. DGT30_BROKER overrides the broker. Without credentials, MQTT decoding and lifecycle are tested against documented events and a local TLS/MQTT broker.

See TESTING.md for the red/green history and live findings.

Sources and reuse

Feed URLs and metadata come from the DGT National Access Point, the traffic map, and DGT 3.0 documentation. The catalogue snapshot was checked on 2026-10-08.

Data licences and attribution requirements belong to each publisher. Inspect the feed's documentation/licence links; the catalogue contains entries with different access and reuse terms.