Blog

True nerd stuff: Plotting vessel positions with AIS, MQTT & WebSockets

By  
Matti Tahvonen
Matti Tahvonen
·
On Apr 24, 2025 6:05:38 PM
·

I recently blogged about my enhanced web UI for a ferry reservation system we use on my "home island." It has lately been gaining more users and feature requests. As an old orienteer, I desperately wanted to see where the ferry was in real time, so I could optimize those last couple of minutes in case it was a bit early or late.

As a fan of geospatial web app features, it wasn’t too hard to build customized vessel tracking that works almost in real-time. My latest coding session with this app might serve as an example or inspiration for a couple of things:

  • Where can I acquire vessel positions from public sources?
  • How to connect a Java app to an MQTT data source?
  • How to distribute nearly real-time vessel positions efficiently to end users using Vaadin and its superb WebSocket-based communication channel?

The code in this hobby app of mine isn’t very well polished, but with some explanations, I hope it can help you build your own solutions using Java and Vaadin.

Screenshot from a mobile phone tracking a moving vessel via AIS->MQTT->WebSocket->Vaadin.

Where is the AIS data coming from?

The AIS (Automatic Identification System) is a global maritime system designed to prevent accidents at sea. Vessels share their locations as data bursts over VHF radios. In the era of the internet, there are network-connected AIS stations that share data online, and some stations even operate in space, such as satellites that read VHF signals. 😎

To track my "home ferry," I had previously used generic AIS tracking apps like VesselFinder. However, due to their scale, they often have a bit of latency in their data, and I wasn't happy with the background map. The major AIS tracker websites and apps also provide APIs (for $$$), but there are some open (and free) sources available, typically with some usage and area limits. For my solution, I decided to use the open MQTT API provided by Finland, although it has area and usage restrictions.

Java and MQTT

MQTT is a protocol often used in IoT implementations, offering minimal overhead for devices and transmitted data. The Finnish service runs on top of WebSockets, with a JSON string payload. However, in its most optimal form, it could run over raw TCP and binary data. The likely reasoning for using WebSockets is that it allows the API to be accessed directly from browsers. Since my app runs on Java with full server powers, it doesn’t really matter—WebSocket connection is only configured in a slightly different manner.

I integrated the HiveMQ MQTT library and drafted a small Spring service class called VikenService (Viken is the name of the ferry that goes to my "home island"). This class connects to the MQTT service, subscribes to my favorite ferry based on its MMSI ID ("AIS ID"), and maps the JSON string into a Java data structure. Although currently bolted to a single ferry, it would be quite easy to extend this class to track other local ferries as well.

public record VesselData(

   @JsonDeserialize(using = UnixTimestampDeserializer.class)
   Instant time,
   double sog,
   double cog,
   int navStat,
   int rot,
   boolean posAcc,
   boolean raim,
   int heading,
   double lon,
   double lat
)

ObjectMapper om = new ObjectMapper();

mqtt5AsyncClient.subscribe(Mqtt5Subscribe.builder()
       .topicFilter("vessels-v2/230987260/location")
       .qos(MqttQos.EXACTLY_ONCE)
       .build(),  mqtt5Publish -> {

   ByteBuffer payload = mqtt5Publish.getPayload().get();
   byte[] arr = new byte[payload.remaining()];
   payload.get(arr);
   try {
       var lastStatus = om.readValue(arr, VesselData.class);

       // Notify current listeners of the last status

       listeners.forEach((Consumer<VesselData> l) -> l.accept(lastStatus));

       // A bit of history is maintained for new subscribers

       lastStatuses.add(lastStatus);
       if(lastStatuses.size() > 10) {
           lastStatuses.removeFirst();
       }

   } catch (IOException e) {
       throw new RuntimeException(e);
   }
});

We now have an almost real-time position of the vessel on the server, updated right after the latest signal comes in via MQTT. And the good part is that we are only utilizing a single connection to the AIS service, even if the web app has tens of thousands of users.

Plotting the positions with Vaadin and WebSocket connection

The last step is to transfer the vessel positions to the actual web application users. For me, this was the most trivial part, but it might need the most explanation if you're not familiar with Vaadin or using "server push".

For plotting, you naturally need a slippy map component and a background map. My current favorite choice for a slippy map is MapLibre: it has a permissive license, modern WebGL rendering, and is built from the ground up for vector tiles that render sharply at every zoom level. As a map nerd, the background map part was a bit trickier, as I wanted lots of details, both on land and at sea. I compiled a custom background layer utilizing three different open data sources from Finland, but I’ll write a post or two about that part separately.

A cool little component in the Vaadin wrapper for MapLibre is a component called TrackerMarker, which is designed to plot moving "things" on the map. It is essentially a composition of a (customizable) SVG marker and a configurable length "tail." In the UI code, I essentially just need to register as a consumer of the VesselData objects and feed the data to the TrackerMarker component’s addPoint method. The most essential code part of my Consumer<VesselData>looks like this:

if (viken == null) {
   viken = new TrackerMarker(map);
}

viken.addPoint(vikenStatus.coordinate(), vikenStatus.time(), (int) vikenStatus.cog());

Almost everything else in the callback is only fine-tuning and decorating the UI. I wanted custom CSS (the app allows tracking your own position as well, so it’s good to have a custom color), and it can show additional details with the cool new Popover component available since Vaadin 24.5.

An important little detail is the Command lambda and how it is used at the end of the method. Vaadin automatically syncs the new positions to the browser if you have enabled the @Push configuration (which enables WebSocket-based communication). However, the tricky part is concurrency.

Our web UI is now being updated by the thread listening to MQTT data, not by any triggered UI events. Thus, I pass that lambda expression to UI.access to safely apply the logic when there are no other ongoing UI interactions for that particular instance (e.g., execution of some UI event-triggered logic).

if (ui != null) {
   ui.access(command);
} else {
   // first sync call
   command.execute();
}

Due to the implementation details in this example, the first update might come right away when registering the listener, even before the UI reference is saved. As this happens in the UI thread (the constructor of the class), it is safe to execute the logic directly in this case.

The full source code of my app is available on GitHub for inspiration and reference. Note that the background map of the app only covers Finland, so in case you test plotting your location with the geolocation API, you’ll probably only see a white background.

New to Vaadin? Build real-time web apps using just Java. Get started today!

Matti Tahvonen
Matti Tahvonen
Matti Tahvonen has a long history in Vaadin R&D: developing the core framework from the dark ages of pure JS client side to the GWT era and creating number of official and unofficial Vaadin add-ons. His current responsibility is to keep you up to date with latest and greatest Vaadin related technologies. You can follow him on Twitter – @MattiTahvonen
Other posts by Matti Tahvonen