DEV Community

nickdenboer7-dev
nickdenboer7-dev

Posted on

I Built a Real-Time Public Transport Map for the Netherlands — Here’s What I Learned


Uploading image# I Built a Real-Time Public Transport Map for the Netherlands — Here’s What I Learned

What started as a simple idea —

“What if I could see public transport across the Netherlands moving on one live map?”

— quickly turned into a much bigger engineering project.

I’ve been building OVspot, a real-time public transport map that combines trains, buses, trams, ferries, stops, delays, journey progress and estimated positions in a single interface.

You can try it here:

https://ovspot.nl

And building it taught me that “show vehicles on a map” is definitely not as simple as it sounds.

The idea

Most public transport apps focus on planning a journey.

OVspot takes a different approach.

Instead of asking:

“How do I get from A to B?”

OVspot lets you explore what is actually happening on the network.

You can open the map and see:

  • trains
  • buses
  • trams
  • ferries
  • stations and stops
  • destinations
  • delays
  • vehicle journeys
  • upcoming stops
  • estimated and live positions

The goal is to make the Dutch public transport network feel visible.

Screenshot suggestion #1

Use a large desktop screenshot here showing the Netherlands with many vehicles visible.

Caption:

OVspot showing live and estimated public transport vehicles across the Netherlands.

The hard part: not every vehicle has GPS

One of the biggest challenges is that not all operators provide vehicle positions in the same way.

Some vehicles have real-time positioning data.

Others only have timetable information.

If I only displayed vehicles with GPS, large parts of the network would simply disappear.

So OVspot also calculates estimated positions.

For those trips, it uses GTFS data such as:

  • trips
  • stop_times
  • route shapes
  • shape_dist_traveled
  • departure and arrival times

The basic idea is:

scheduled trip progress
        ↓
distance travelled along route
        ↓
interpolate position on GTFS shape
        ↓
estimated lat/lon
Enter fullscreen mode Exit fullscreen mode

This allows OVspot to place a vehicle approximately where it should be according to the timetable.

Ferries were a good example

I first used this technique for the GVB ferries in Amsterdam.

Later, I realized the same system could be used for ferry operators around the Dutch Wadden Islands.

OVspot can now estimate positions for:

  • TESO — Den Helder ↔ Texel
  • Rederij Doeksen — Harlingen ↔ Terschelling / Vlieland
  • Wagenborg — Ameland and Schiermonnikoog services

These operators have GTFS route shapes and distance information, so their scheduled progress can be projected onto the actual ferry route.

The important part is that OVspot does not present these positions as GPS.

They are explicitly treated as estimated positions.

Screenshot suggestion #2

Zoom in on Texel, Terschelling or Ameland with a ferry visible.

Caption:

Ferry positions can be estimated from GTFS shapes when no live GPS position is available.

“Live” data has different levels of quality

Something I underestimated at first was how important data confidence is.

A position can come from:

  • live GPS
  • real-time journey updates
  • timetable interpolation
  • cached data after a feed outage

Those are very different things.

So OVspot distinguishes between states such as:

Live

The vehicle has recent real-time information.

Estimated

Its position has been calculated from available journey data.

Timetable

There is no real-time information available.

This sounds like a small UI detail, but I think it is essential.

A map should not imply more certainty than the underlying data actually provides.

Thousands of moving vehicles are expensive

The next challenge was performance.

Rendering 20 markers on a map is easy.

Rendering hundreds or thousands of moving vehicles with labels, interaction and continuous updates is not.

At one point I experimented with regular DOM-based markers.

That worked fine at low numbers.

At high vehicle counts, it absolutely did not.

I ended up working on things like:

  • canvas-based rendering
  • zoom-dependent labels
  • adaptive detail levels
  • viewport filtering
  • limiting DOM work
  • avoiding unnecessary rerenders
  • reducing expensive station labels
  • measuring frame rate during pan and zoom

The map now deliberately changes how much detail it draws depending on the zoom level and current load.

One of the biggest lessons:

Performance is not something you optimize after building a map like this. It becomes part of the product design.

Screenshot suggestion #3

Use a screenshot of a dense area such as Amsterdam, Rotterdam or Utrecht.

Caption:

Dense urban areas can contain hundreds of visible vehicles, making rendering strategy important.

The popup became surprisingly complicated

Clicking on a vehicle originally opened a tiny information box.

That slowly evolved into something much more detailed.

The current vehicle popup can include:

  • line
  • destination
  • carrier
  • journey number
  • delay
  • data quality
  • previous stop
  • next stop
  • complete journey progression
  • estimated arrival
  • route display

Even the wording required thought.

For example, I originally displayed:

“Nu hier” — “Here now”

next to the most recent stop.

But that was misleading.

The stop might simply be the last known point in the journey.

The vehicle could already be hundreds of meters away.

So the UI now only says “Nu hier” when the vehicle is actually:

  • within about 50 meters of the stop
  • and almost stationary

Otherwise that stop is shown as:

“Gepasseerd” — “Passed”

That is a tiny change, but it makes the interface much more truthful.

Screenshot suggestion #4

Show a vehicle popup with:

  • journey timeline
  • next stop
  • delay
  • Live / Estimated badge

This is probably the most useful screenshot for developers because it immediately shows that OVspot is more than just map markers.

Public transport data has a lot of edge cases

A huge part of the work happens before anything reaches the map.

Things I’ve encountered include:

  • missing route shapes
  • missing trip IDs
  • stale feeds
  • duplicate station names
  • different operator naming conventions
  • multiple platforms
  • parent stations
  • cross-border services
  • vehicles outside their expected route
  • impossible coordinates
  • deadhead trips that passengers should never see
  • different line numbering systems
  • services without GPS
  • timetable changes during the day

The frontend is probably the most visible part of OVspot.

But a lot of the project is really about turning inconsistent transport data into something predictable.

Detecting bad vehicle data

Another feature I’ve been working on is detecting suspicious positions.

For example:

  • coordinates at 0,0
  • vehicles outside a realistic geographic area
  • impossible speeds
  • a bus hundreds of meters away from its own route
  • stale vehicle updates

Instead of blindly trusting every feed message, OVspot tries to validate the data first.

That is especially important in a live map because one broken coordinate can suddenly put a bus somewhere in Africa.

Yes, that happened.

Search also became more complicated than expected

OVspot search now needs to understand several completely different things:

  • vehicle numbers
  • stations
  • stops
  • line numbers
  • journey numbers
  • vehicle blocks / duties

The useful behavior is not just returning results.

When you click one, the map should immediately:

  1. find the right object
  2. move to it
  3. zoom to a useful level
  4. open the relevant popup

Those tiny interaction details make a huge difference to how usable a live map feels.

What I learned

A few things this project has taught me:

1. Real-time data is rarely simply “live”

You need to understand how old it is, where it came from and how reliable it is.

2. GTFS is much more powerful than just departure times

Shapes and shape_dist_traveled make surprisingly good position estimation possible.

3. Map performance becomes a systems problem

Backend payload size, update frequency, rendering strategy and UI complexity all affect each other.

4. Small UX wording matters

“Here now” versus “passed” can completely change how users interpret the data.

5. Edge cases are the project

The normal case is easy.

The weird cases are where most of the engineering time goes.

What’s next?

There is still plenty I want to improve:

  • better position estimation
  • more international trains
  • improved ferry data
  • better station pages
  • more real-time quality indicators
  • stronger anomaly detection
  • faster mobile performance
  • richer vehicle journey information

OVspot is still evolving, and I’m finding new edge cases almost every day.

If you work with:

  • GTFS
  • GTFS-Realtime
  • public transport APIs
  • mapping
  • Leaflet
  • real-time systems
  • data visualization

I’d be very interested in your feedback.

You can try it here:

https://ovspot.nl

What would you improve first?

Top comments (0)