Search Open menu

Blog

A Basic Method to Detect Ship to Ship Transfers (STS) from AIS Data

Photo of M. Furkan Oruc
M. Furkan OrucSales Engineer
Spire Maritime

In this tutorial, a basic approach to detect ship to ship transfers from AIS Data is presented. Access to Spire Sample AIS Data and Coding proficiency in Python is sufficient to recreate results. The source code can be found within the attached Github repository.

Transshipment at sea involves the transfer of cargo or materials from one vessel to another while both are at sea. While this practice is common among fishing fleets, it also extends to the transfer of oil or gas between tankers. Monitoring these activities is essential, especially for detecting Illegal, Unreported, and Unregulated (IUU) fishing, which relies heavily on transshipments between fishing vessels.

For oil and gas transfers, as well as bunkering at sea, the stakes are even higher. The risk of spills poses severe environmental threats. Due to this, operations require sophisticated coordination and expertise from both the crews and shipowners. In the meantime, some ship-to-ship (STS) transfers are driven by attempts to evade sanctions or bypass regulatory restrictions, which underscores the need for oversight by authorities.

However, it is important to recognize that not all STS operations are illicit. In fact, they can offer significant economic and logistical benefits to ship operators. For large tankers, STS can help avoid expensive port fees and is often the only viable option when a port’s draught requirements exceed the ship’s maximum draught capacity. In these cases, STS becomes a crucial component of maritime logistics, enabling more efficient and cost-effective operations.

In this tutorial, we will be ingesting a sample Spire Historical AIS Dataset, and apply our STS detection algorithm. There can be many ways to detect STS. The approach in this tutorial can be summarized as follows:

  1. Within each 10-minute time block, the first position of all vessels is captured.
  2. Proximity of all vessel positions with respect to one another are calculated for each time bin.
  3. Vessel pairs which are detected to be under 50 meters distance to one another are highlighted.
  4. For all vessel pairs, proximity is checked in consecutive time bins.
    1. If a vessel pair is found to be in close proximity in 12 consecutive time blocks, which corresponds to 2 hours, that pair is flagged to be a candidate STS.
    2. If a pair of vessels has multiple two-hour periods of proximity, each period is recorded and reported.
  5. Final output includes MMSIs of vessel pairs which are captured to be in close proximity, alongside the number of periods they possess.

Decision Criteria and Assumptions

There are 3 key arguments that can be refined based on needs:

  1. Length of the time bin
  2. Minimum distance between ships to assign a close proximity flag
  3. Minimum number of consecutive periods to track proximity to report STS

The first position for each vessel in each 10 minute time period represents that vessel’s position for the entirety of that time block. This approach is adopted instead of a position to position comparison since it allows to directly compare vessel’s positions regardless of the time difference between vessels AIS messages. Since AIS positions of vessels are captured at different minutes and seconds, even within close time windows, accurately calculating the distance between two vessels requires interpolating one of the positions to align both at the exact same time.

The minimum distance between ships is determined by using the average width of a medium-sized oil tanker. This distance can decrease to track transshipment between small fishing vessels, or tracking STS consisting of very large tankers.

Records show that an STS operation can take place anywhere from a couple hours to a day or two. Determining the lower limit as 2-hours allow to not miss any operations, while accounting for larger operations via tracking multiple time blocks, ships are detected to be in close proximity.

Allowing False Positives to Minimize False Negatives

The approach adopted with the algorithm focuses on detecting any potential STS candidates, while allowing the detection of some False Positives along the line.

This is to ensure that no STS event goes unrecorded, even though this comes with the cost of detected non-STS events. The reason is that, False Positives can be eliminated in a later step, but  missing an STS operation can be very costly.

Experiments show that, when the algorithm is run near ports, it is inevitable to capture berths in close proximity. To address this challenge, the Spire Port Events API offers an effective solution. This API provides real-time and historical data on vessel arrivals and departures at ports. By querying the portEventsByVessel endpoint with MMSIs, you can determine whether a vessel is currently within the boundaries of any port’s or its anchorage’s polygon at the time of the search. Port Events API’s distinction of Ports, Terminals and Anchorages are crucial since public records show that some STS transfers may happen within anchorage areas. Since draught changes are recorded for both anchorage entrances and exits in the API, this data can be valuable for tracking loading and unloading activities within sea.

This leads to another fact that analytical approaches in these types of domain specific applications do still require subject matter expertise in the loop. Though, adoption of analytics can dramatically improve the efficiency in the analysis process.

Case Study: Pilbara Region, Australia

Pilbara is one of the largest export ports. Some of the bulk material being exported include iron ore, salt, lithium, natural gas and ammonia.

In this study, a daily snapshot of Spire AIS Traffic is exported on September 14th, 2024 from the region. The Area of Interest (AOI) is home to an anchorage area, alongside the route to and from the port. The extracted data sample has the Satellite, Terrestrial and Enhanced Satellite AIS collection types. Below is a visual of the traffic in the area.

AIS Traffic in the Pilbara Region, September 14th 2024

Python Code

Initially, we define the function that’ll help to calculate the great circle distance between to geographical locations, using the Haversine formula.

import numpy as np

def haversine_vectorized(lonlat1, lonlat2):
    """
    Calculate the great circle distance between two points
    on the earth specified in decimal degrees.
    """
    # Convert decimal degrees to radians
    lat1, lon1 = np.radians(lonlat1[:, 0]), np.radians(lonlat1[:, 1])
    lat2, lon2 = np.radians(lonlat2[:, 0]), np.radians(lonlat2[:, 1])

    # Haversine formula
    dlon = lon2 - lon1
    dlat = lat2 - lat1
    a = np.sin(dlat / 2.0)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2.0)**2
    c = 2 * np.arcsin(np.sqrt(a))
    km = 6371.0088 * c  # Earth radius in kilometers
    return km

Now, we start shaping the main function. Below component of the function ensures the conversion of the timestamp column to the datetime format, and divides the time window to time bins as provided in the arguments. To record proximity events and their respective positions, lists and dictionaries are initialized.

  • pair_counters: Keeps track of how many consecutive times a pair of vessels has been in proximity. 
  • proximity_events: Records the number of proximity events for each pair.
  • relevant_positions: Collects all the relevant positions involved in proximity events. 
  • proximity_id: A unique identifier for each proximity event. 
  • proximity_event_map: Maps each pair of vessels to their unique proximity_id.
def check_proximity_with_time_vectorized(df, min_distance=0.05, min_periods=12, time_bin_input='10min'):
    print("Starting proximity check...")

    df['position_timestamp'] = pd.to_datetime(df['position_timestamp'])
    df.sort_values(by='position_timestamp', inplace=True)
    df.reset_index(drop=True, inplace=True)

    time_bin_size = time_bin_input
    df['time_bin'] = df['position_timestamp'].dt.floor(time_bin_size)
    time_groups = df.groupby('time_bin')

    min_time = df['time_bin'].min()
    max_time = df['time_bin'].max()
    all_time_bins = pd.date_range(start=min_time, end=max_time, freq=time_bin_size)

    pair_counters = {}
    proximity_events = defaultdict(int)
    relevant_positions = []
    proximity_id = 0
    proximity_event_map = {}

Determining close pairs

In this section, the loop goes through each time bin and skips the time bin if there are no records for that time bin. Also skips the time bin if there are fewer than 2 unique vessels (mmsi). As a next step, filters the group to keep only the first position record for each vessel. 

Then,  latitude and longitude values are extracted and stacked as position coordinates alongside  retrieving the MMSI values. As a result, this creates all possible pairs of vessel combinations.

In the next step, distance between all vessel pairs are calculated and if the distance is below the argument limit, the pair is recorded in close_indices. Close pairs are retrieved with the help of close_indices.

for i, time_bin in enumerate(all_time_bins):
    if time_bin not in time_groups.groups:
        continue

    group = time_groups.get_group(time_bin)
    if len(group['mmsi'].unique()) < 2:
        continue

    vessel_df = group.groupby('mmsi').first().reset_index()
    if len(vessel_df['mmsi'].unique()) < 2:
        continue

    latitudes = vessel_df['latitude'].values
    longitudes = vessel_df['longitude'].values
    positions = np.column_stack((latitudes, longitudes))
    mmsis = vessel_df['mmsi'].values

    vessel_indices = np.arange(len(positions))
    vessel_pairs = list(combinations(vessel_indices, 2))

    if not vessel_pairs:
        continue

    indices1 = np.array([i for i, j in vessel_pairs])
    indices2 = np.array([j for i, j in vessel_pairs])

    pos1 = positions[indices1]
    pos2 = positions[indices2]
    mmsi1 = mmsis[indices1]
    mmsi2 = mmsis[indices2]

    distances = haversine_vectorized(pos1, pos2)
    close_indices = np.where(distances < min_distance)[0]

    close_pairs = [tuple(sorted((mmsi1[idx], mmsi2[idx]))) for idx in close_indices]

Then, for each close_indices pair, a proximity_id is assigned and relevant_positions are recorded to be returned in the final output.

for idx in close_indices:
    m1, m2 = mmsi1[idx], mmsi2[idx]
    pair = tuple(sorted((m1, m2)))

    # Assign a new proximity event ID if the pair is not already in the map
    if pair not in proximity_event_map:
        proximity_event_map[pair] = proximity_id
        proximity_id += 1

    # Use the assigned proximity event ID
    current_proximity_id = proximity_event_map[pair]

    # Collect relevant positions in the list
    lat1, lon1 = pos1[idx]
    lat2, lon2 = pos2[idx]
    timestamp = time_bin

    relevant_positions.append({
        'proximity_id': current_proximity_id,
        'mmsi1': m1,
        'mmsi2': m2,
        'timestamp': timestamp,
        'lat1': lat1,
        'lon1': lon1,
        'lat2': lat2,
        'lon2': lon2
    })

State management for pairs

In the final section, for each pair, if we are in the initial time block, they are added to the pair_counters list for tracking. If we are not in the initial time block, the loop starts for existing pair_counters. For each pair within pair_counter, it is checked if that time block consists of that pair, and if so, consecutive window counter (pair_counter) increases for that pair, and that pair is removed from the close_pairs list. If a certain pair is already equal or above the minimum number of consecutive windows, that pair is added to the proximity_events. 
If a pair is detected within close_pairs, it’s immediately deleted from close_pairs after pair_counter is updated for that. This is the key to the algorithm, and ensures no pair remains within the close_pairs list of that time block, and ensures single count of all observations.

Finally, proximity events are reported in a sorted fashion.

if i == 0:
    for pair in close_pairs:
        pair_counters[pair] = 1
else:
    current_bin_pairs = defaultdict(int)
    for pair in list(pair_counters.keys()):
        if pair in close_pairs:
            pair_counters[pair] += 1
            close_pairs.remove(pair)
            if pair_counters[pair] >= min_periods:
                proximity_events[pair] += 1
                del pair_counters[pair]
        else:
            del pair_counters[pair]

    for pair in close_pairs:
        pair_counters[pair] = 1

# Convert the list of relevant positions to a DataFrame
relevant_positions_df = pd.DataFrame(relevant_positions)

# Sort the proximity_events dictionary by the event count in descending order
sorted_proximity_events = dict(sorted(proximity_events.items(), key=lambda x: x[1], reverse=True))

return pair_counters, sorted_proximity_events, relevant_positions_df

In our AOI, final return consists of an MMSI couple, as below:

Starting proximity check…

{(357530000, 636023069): 11}

When we zoom in, we can observe a double anchorage pattern for two vessels that are conducting their STS. This pattern belongs to the world’s first ammonia transfer at anchorage, by A Global Centre for Maritime Decarbonisation-led consortium.

Double anchorage pattern during the STS.

The approach presented can be improved by:

  • Taking shipTypes into account for segmentation of STS activities,
  • Developing more comprehensive decision criteria,
  • Incorporating historical patterns of vessel pairs and
  • Training a machine learning classification model to predict future STS events.
 

Related resources

Port Events API Upgrades Summer 2024

Photo of Mark Deverill
Mark DeverillDirector Maritime Data Operations
Spire Maritime

Spire Maritime are pleased to announce an upgrade to the Port Events products, which effects Port Events API options in Maritime 2.0 GraphQL. 

API documentation: https://documentation.spire.com/maritime-2-0/port-events/  

  • PortEventsByVessel  
  • PortEventsByLocation 
  • PortEventsByShipType  

These changes are expected to be released late July or early August 2024.

Service Improvements. 

The new port events will contain the following improvements: 

 Improved data quality from an improved algorithm.  

Jumping events caused by inaccurate AIS positions resolved. 

More accurate recognition of ATA and ATD. 

Easier application of changes to polygons. 

Quicker API response. 

Greater load capacity of the events API through improved scalability. 

New data fields: 

  • entryTime Earliest known time when vessel crossed into the polygon area associated with the event. 
  • vessel.staticData.timestamp Timestamp when vessel static information was received. 

Constant event ID values. Previously event IDs changed when updating from open to closed status.  

Improved events history. Port events history will be regenerated with all the improvements listed above.  

Changing event ID values.  

Clients already using Spire Maritime port events data should be aware that to achieve these improvements it is necessary to change the ID values of port event records. This will be for both historic events when querying old activity and new events going forward. This change will be immediate upon roll out of the service updates. 

 Port Event Entry Time 

There is a new field of information in the improved port events data, this is entryTime, which records the timestamp of the AIS position that first recognises the vessel being within the polygon area used to recognise the port Event. That can be the first AIS position inside the port area, anchorage area or terminal area polygon.

The plot below is an example of a vessel AIS track for a port call, showing the point where the port entryTime is recorded and where the ATA stop position is recorded.

Release of Validated Static Data in Maritime 2.0 GraphQL Vessels

Photo of Mark Deverill
Mark DeverillDirector Maritime Data Operations
Spire Maritime

Validated Static Data (VSD) was released 2023-11-15 and is now available to all users of Maritime 2.0 GraphQL Vessels API. If you need help with this feature please contact [email protected]

Recognising that AIS messages can sometimes report incorrect or missing information for vessels, Spire Maritime uses its database of researched Vessel Characteristics (VC) information to report what we consider to be validated values for key information.

Validated values are available for a vessel’s name, imo, callsign, width, length and shipType. They are returned from the VC database when we are confident that a vessel can be identified correctly, and included in a new validated section of the staticData query response. The validated values will often be the same as those reported by the vessel in it’s AIS messages but sometimes will be different, and will be those from the VC database. Note that matching to VC data, and returning of validated values, is only applicable to vessels with IMO numbers and those reporting AIS using class A AIS.

Also, when running API calls that are filtered on a vessel’s imo, name or callsign , if the staticData.validated data is matched by the parameters of the API call, the entry will be included in the API call results (even if the non-validated data does not match).

One of the most valuable use cases of this feature is the recognition of a vessel’s correct imo number when either the wrong IMO or no IMO number is being reported by the vessel in its AIS messages. Below is an example of a Maritime 2.0 GraphQL vessels query, correcting the IMO number returned by AIS:

API request

query VslIMO {
  vessels( imo:9250995 lastUpdate : {startTime:"2023-07-01T00:00:00Z" } ) {
    nodes { id
      staticData { 
        name imo mmsi aisClass  callsign
        flag shipType updateTimestamp 
        dimensions { length width a b c d }
        validated {
          name imo callsign shipType
          dimensions { length width } }
      }
      lastPositionUpdate {
        latitude longitude timestamp collectionType }
      currentVoyage {
        eta destination
        matchedPort { port { name unlocode } } }
    } } }

Note how, in the API response below, the staticData.validated.imo value returned is the IMO number that was searched for, but the staticData.imo number, reported by the vessel in it’s AIS messages, is actually different (in this case containing an extra zero). Before the release of the VSD feature, searching for imo:9250995 would have returned no results; now, with VSD, this is recognised as the validated IMO number for the vessel and included in the API search results:

API Response

{
  "data": {
    "vessels": {
      "nodes": [
        {
          "id": "4db14c7d-0728-3b4f-845c-36c02f671b73",
          "staticData": {
            "name": "DIMITRA C",
            "imo": 92500995,
            "mmsi": 256058000,
            "aisClass": "A",
            "callsign": "9HA3802",
            "flag": "MT",
            "shipType": "CONTAINER",
            "updateTimestamp": "2023-07-24T04:37:24.675Z",
            "dimensions": {
              "length": 294,
              "width": 40,
              "a": 218,
              "b": 76,
              "c": 27,
              "d": 13
            },
            "validated": {
                 "name": "DIMITRA C",
                 "imo": 9250995,
                 "callsign": "9HA3802",
                 "shipType": "CONTAINER",
                 "dimensions": {
                   "length": 294,
                   "width": 40
                }
            }
          },
          "lastPositionUpdate": {
            "latitude": 25.98669,
            "longitude": -76.66380833,
            "timestamp": "2023-07-24T13:32:39.000Z",
            "collectionType": "DYNAMIC"
          },
          "currentVoyage": {
            "eta": "2023-07-27T23:30:00.000Z",
            "destination": "MXVER",
            "matchedPort": {
              "port": {
                "name": "Veracruz",
                "unlocode": "MXVER"
              }
            }
          }
        }
      ]
    }
  }
}

The main data elements that VSD corrects are IMO number, shipType length and width dimensions. 
For all except shipType a different value will be returned in the staticData.validated section of the API response. 
For shipType the validated ShipType value is automatically replicated to the top level shipType, 
because it is a derived value not necessarily what is transmitted in the AIS message. 
So for instance if a vessel incorrectly transmits in it's AIS messages that it is a catrgo vessel but the Spire characteristics data validates the vessel as a CONTAINERSHIP, then the vessel shipType will be returned as CONTAINER in all shipType fields.

Here are counts of vessels with VSD corrections 24 hours.
VSD CategoryVessel Count
Vessels with IMO 0 corrected905
Vessels with length corrected14802
Vessels with name corrected6298
Vessels with IMO non 0 corrected25

For more information about VSD in Maritime 2.0 GraphQL Vessels API see the documentation entry here , or please contact [email protected]

Building a simple AIS position vessel tracker using AIS and Mapbox GL

Photo of Simão Oliveira
Simão OliveiraWeb and Applications Manager
Spire Maritime

This tutorial is a simple introduction to using Spire Maritime’s AIS solutions, in this case the Latest Vessel Information (LVI) Web Service to obtain and display a vessel’s most recent position on a map. This is an entry-level tutorial. All you need to be able to develop it is basic knowledge of HTML, CSS and Javascript. You can also download or clone the source code for this demo on Github, and simply deploy it and run it locally.

This tutorial uses Mapbox GL as the web mapping framework. If you prefer to use Leaflet.js, please refer to this equivalent article which will achieve similar results.

You will need a valid Spire LVI authKey and the MMSI of the vessel whose position you are trying to display. Reach out to our team to get started with a Spire Maritime AIS subscription.

Why Spire Maritime’s LVI?

Spire Maritime’s Latest Vessel Information (LVI) constitutes one of the easiest ways to get started with vessel tracking data. This web service works much in the same way that an API does; using specific parameters on the URL you will be calling, you can start making calls easily and simply, and get results in a format that fits your needs.

What are the building blocks of the web map visualization?

In this tutorial we will be requesting data in the JSON format. JSON means JavaScript Object Notation, and it is a simple data structuration format that is based on the Javascript language structure and integrates seamlessly into it. Considering that we are developing a web-based, frontend-only demo here, this is the perfect format.

From a mapping perspective, you can pick and choose any JS library you prefer. In this demo we will be using the Mapbox GL JS library. Mapbox GL is commercial if you use proprietary styles or layers, but we will be using an open source tile layer which allows us to use it for free for this use. Mapbox GL has high performance, and multiple rendering modes that will fit your needs. Other alternatives include OpenLayers, one of the most well known open source alternatives and a robust alternative; or Leaflet.js, a very well maintained JS mapping platform that has very good community support, as well as a healthy ecosystem of plugins.

Other than that, we will simply use a sprinkle of HTML and CSS.

Deploying the demo’s basic structure

Let’s get started! You will need a simple index.html file, with very basic source code. Here is what it looks like:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>LVI tracker | Spire Maritime</title>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/v1.10.1/mapbox-gl.css">
<link href="css/main.css" rel="stylesheet" defer>
</head>
<body>
<div id="map" class="map loading"></div>

<script src="https://api.mapbox.com/mapbox-gl-js/v1.10.1/mapbox-gl.js"></script>
<script src="js/main.js"></script>
</body>
</html>

As you can see, from a pure HTML mark-up perspective, there’s very little going on here. We have a simple <div> with a "map" id, which we will use to render our map using Mapbox GL; the dependencies for the mapping library are all served using an external CDN, in this case jsdelivr. We reference both the library’s CSS and JS source files, in the <head> and the bottom of the <body> elements, respectively. Contrary to the Leaflet.js implementation we covered before, we don’t need an additional plugin to handle marker rotations, so that’s all we need.

We also integrate a local CSS and JS file. Let’s look at the CSS first:

#map{
	position: fixed !important;
	left:0;
	top:0;
	width:100%;
	height: 100%;
}

#map.loading::before{
	position: absolute;
	left:0;
	top:0;
	z-index: 1000;
	background:url(../img/loader.svg) rgba(255,255,255,.8) no-repeat center center / 40px 40px;
	width: 100%;
	height: 100%;
	display: block;
	content:'';
}

.boatIcon{
	background:url(../img/vessel.svg) no-repeat center center / cover;
	width:16px;
	height: 30px;
}

This very simple CSS structure simply ‘fixates’ the map on the page. We want it to be a full screen, no scroll experience. Additionally, we add a .loading class to provide user feedback; this class will be removed, via Javascript, once the call to Spire Maritime’s LVI is successful. Finally, we integrate a simple .boatIcon instruction to style the marker that will represent our targeted vessel, including height and width instructions to set the marker dimensions.

Getting started writing JS code

Now let’s start writing our main.js file. The main tasks we will accomplish are:

  • Feed a user-defined mmsi URL parameter to our code (so we can change it dynamically)
  • Authenticate with Spire Maritime’s Latest Vessel Information web service
  • Initialize a backdrop world map
  • Fetch the most recent position for a vessel based on the vessel’s MMSI
  • Display a marker on the vessel’s position, including the most recent heading of the vessel
  • Provide very basic error handling messages if the vessel information is not available

Let’s start off by plugging into the load event of the window object; this allows us to only run the code once the entire page’s resources are ready. You might want to change this if your code running order changes (for example, to load the map at a later stage once a user clicks a button, for example).

Then, we check for the existence of both the mmsi and the key parameters on the page’s URL and finally, we extract them to constants and verify if the mmsi is 9 characters long, the typical format of a vessel’s MMSI. If any of these conditions fails, we use the basic alert() JS functionality to provide an error message to the user.

This data validation stage can, and should, be made more robust in both validation methods and UX once you build up your platform, but should at least help understanding any errors in your code or URL at this stage:

window.addEventListener("load", function(){
  if(window.location.href.includes('mmsi=') && window.location.href.includes('key=') ){
    const queryString = window.location.search;
    const urlParams = new URLSearchParams(queryString);
    const mmsi=urlParams.getAll('mmsi')[0];
    const key=urlParams.getAll('key')[0];

  if(mmsi.length==9) initMap(mmsi, key);
    else alert('Invalid mmsi provided.');
  }else{
    alert('Please provide a valid mmsi and key');
  }
});

Initializing the Mapbox GL map

Next, we declare the initMap() function which is the heart of this demo. Note the mmsi and key parameters this function requires as parameters. These will identify the vessel we’re trying to target, as well as the authKey (or token) which allows you to query the Web Service. If you don’t have a Spire Maritime AIS subscription, reach out to our sales team and request one.

First, let’s initialize a Mapbox GL map:

const map = new mapboxgl.Map({
      container: 'map',
      style:{
          'version': 8,
          'sources': {
              'raster-tiles':
              {
                  'type': 'raster',
                  'tiles': [
                      'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'
                  ],
                  'tileSize': 256
              }
          },
          'layers': [
              {
                  'id': 'simple-tiles',
                  'type': 'raster',
                  'source': 'raster-tiles',
                  'minzoom': 0,
                  'maxzoom': 22
              }
          ]
      },
      center: [20,0],
      zoom: 3,
      pitch: 0,
      bearing: 0,
      scrollZoom: true
});

map.addControl(new mapboxgl.NavigationControl());

Note the different options passed to the mapboxgl.Map object; the container property, "map", constitutes the ID of the HTML element where we want to display the map. Refer back to the HTML mark-up above; if you change this ID, you’ll need to change it here as well.

Other options set include the possibility of changing zoom using the mouse’s scrollwheel (using the scrollZoom property), and defining the map base layer in the style property. In this demo we used the OpenStreetMap base layer, but there is a plethora of choices online. Just make sure to provide attribution according to the instructions of the basemap owner. Also note: if you use a Mapbox GL proprietary style you will need to set your Mapbox GL commercial token; refer to Mapbox GL’s documentation for more information on this.

After initializing the map, we use addControl() to add a navigation control. This includes zoom and pitch controls which provide an additional way to interact with our map.

Once the map is initialized, we need to actually make a call to the vessel tracking data web service; since we are using LVI in this demo, make sure to refresh your knowledge on our LVI documentation page.

Retrieving the latest vessel position information from Spire Maritime’s LVI

Please note – this is a simple conceptual demo, meant to be used locally. Never expose your authKey by integrating it into client-side code. If you want to integrate a live API or Web Service into a front-facing platform, make sure to develop a backend proxy call that hides the authentication token from curious eyes.

We’re going to be using javascript’s ES6’s fetch() API to make the call to the server. You could, however, use any other technique that makes inter-server calls in the browser, like $.ajax() (if you’re using jQuery), XMLHttpRequest, or any similar techniques in the framework of your preference.

Let’s look at how this call is made:

fetch('https://services.exactearth.com/gws/wfs?authKey='+key+'&service=WFS&request=GetFeature&version=1.1.0&typeName=exactAIS:LVI&outputFormat=json&cql_filter=mmsi='+mmsi)

First, we call the endpoint’s URL, integrating the key and mmsi parameters, which are part of the initMap function that wraps this code:

https://services.exactearth.com/gws/wfs?authKey='+key+'&service=WFS&request=GetFeature&version=1.1.0&typeName=exactAIS:LVI&outputFormat=json&cql_filter=mmsi='+mmsi

Note the cql_filter parameter. This is a very user-friendly way to query for specific data using LVI; you can also use the filter parameter, written in XML, if you prefer. Ideally, make sure to URL encode the cql_filter parameter using the Javascript native function encodeURI().

Also, note how we specifically ask for data in the JSON format using the outputFormat parameter. LVI is very flexible and allows you to query in CSV, XML or JSON, the latter being the ideal format for our use case.

Handling the AIS data response

The next step is handling LVIs response. We’ll need to handle very basically a couple of error situations, like error codes from the server or a lack of the required parameters; if the server responds with a valid response, we will treat the returned data accordingly to display it on the map. Here is what an example response should look like:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": "9082612747464471583",
      "geometry": {
        "type": "Point",
        "coordinates": [122.24226667,38.17826667]
      },
      "geometry_name": "position",
      "properties": {
        "mmsi": 219018271,
        "imo": 9619907,
        "vessel_name": "MAERSKMCKINNEYMOLLER",
        "callsign": "OWIZ2",
        "vessel_type": "Cargo",
        "vessel_type_code": 79,
        "vessel_type_cargo": "No Additional Information",
        "vessel_class": "A",
        "length": 399,
        "width": 60,
        "flag_country": "Denmark",
        "flag_code": 219,
        "destination": "CNHSK>CNTST",
        "eta": "07071600",
        "draught": 11,
        "longitude": 122.24226666666667,
        "latitude": 38.178266666666666,
        "sog": 9.7,
        "cog": 101.1,
        "rot": 0,
        "heading": 100,
        "nav_status": "Under Way Using Engine",
        "nav_status_code": 0,
        "source": "T-AIS",
        "ts_pos_utc": "20230706074410",
        "ts_static_utc": "20230706061452",
        "ts_insert_utc": "20230706074427",
        "dt_pos_utc": "2023-07-06 07:44:10",
        "dt_static_utc": "2023-07-06 06:14:52",
        "dt_insert_utc": "2023-07-06 07:44:27",
        "vessel_type_main": "Container Ship",
        "vessel_type_sub": "",
        "message_type": 1,
        "eeid": 9082612747464471583
      }
    }
  ],
  "totalFeatures": 1,
  "numberMatched": 1,
  "numberReturned": 1,
  "timeStamp": "2023-07-06T07:49:58.188Z",
  "crs": {
    "type": "name",
    "properties": {
      "name": "urn:ogc:def:crs:EPSG::4326"
    }
  }
}

So our reference object here is features[0].properties, and the properties we’re interested in are the GeoJSON-ready features[0]geometry.coordinates object (you could also use the latitude and longitude properties in an array). We will also need the heading property to represent the rotation of the vessel on the map.

So let’s write our vessel displaying functionality:

First we check if there is a count of objects inside myJson.features. LVI only keeps positions that are at maximum 6 hours old, so if a vessel has stopped transmitting AIS, this object might be empty (we will handle that later); we want to make sure there is one entry inside (LVI only return one entry per MMSI queried):

if(typeof myJson.features!=='undefined' && myJson.features.length){}

Then we create two constants, one with the position, using the geometry property, and another with the heading:

const position=myJson.features[0].geometry.coordinates;
const heading=myJson.features[0].properties.heading ? myJson.features[0].properties.heading : 0;

Now, let’s create a new <div> element programmatically, which we will use to custom style a Mapbox GL marker . This is where we apply the styling class and dimensions we defined previously, on top of the main.js file and on the main.css stylesheet. Mapbox GL renders pure html to the page’s Document Object Model (DOM), so you can take advantage of CSS to style your marker easily and simply. We do that by applying the 'boatIcon' className to the aforementioned element:

const el = document.createElement('div');
el.className = 'boatIcon';

Contrary to the Leaflet.js demo version of this tutorial, we don’t need to set the marker’s size via JS, we can use CSS to do this (refer to the main.css instructions to change the size of the marker as required, especially if you intend to change the marker’s image).

Finally, we create the Mapbox GL Marker object, including the element el we defined as the base object which allows  us to custom style it. We then use two different methods of the Marker class: setLngLat(position) and setRotation(heading) , which, pretty self-explanatorily, set the position of the marker in the map, as well as its rotation. setLngLat() accepts a position in Longitude, Latitude format (in our case we feed it as an array, but you could use an object instead) and setRotation() accepts a rotation value in radial degrees, so we can use the heading property of LVI out of the box since it is also set in radial degrees.

And once the marker is created, let’s add it to the map and change the map’s position and zoom to focus on the added marker:

if(typeof myJson.features!=='undefined' && myJson.features.length){

      const position=myJson.features[0].geometry.coordinates;
      const heading=myJson.features[0].properties.heading ? myJson.features[0].properties.heading : 0;

      const el = document.createElement('div');
      el.className = 'boatIcon';

      const marker = new mapboxgl.Marker(el)
      .setLngLat(position)
      .setRotation(heading)
      .addTo(map);

      map.setCenter(position);
      map.setZoom(10);

      document.getElementById('map').classList.remove('loading');
    }else{
      alert('No vessel with mmsi '+parseInt(mmsi));
      document.getElementById('map').classList.remove('loading');
}

The last elements of this code are simply to remove the loading class from the map to inform the user we finished loading and rendering our vessel position, and handling an error in case there is no information for the provided MMSI.

To display a vessel position, open the local index.html file and append ?key=yourtokenhere&mmsi=123456789 to the URL in your browser, where yourtokenhere is replaced by the LVI token, and 123456789 is replaced by the MMSI of the vessel you want to display, for example:

file:///Users/user/Desktop/index.html?token=yourtokenhere&mmsi=219018271

That’s it! You should now have a working vessel position tracker, which you can dynamically query with any MMSI of your choice! You can clone or download the entire source code on Github.

Image of the developed visualization tool

What ‘s next?

You can now start building up upon your vessel tracker. Maybe show a Popup when hovering your mouse over the vessel, showing its latest static AIS information? Call the Historical Vessel Tracks web service to display the vessel’s historical route and add it to the map? Or add a WMS map layer that displays all vessels in the proximity? These are all at hand’s reach – so get coding 😊

And if you don’t have a Spire Maritime subscription, reach out to our team of experts and we’ll make sure you can start your subscription, or a 7 day trial, as soon as possible.

 

Building a simple AIS position vessel tracker using AIS and Leaflet.js

Photo of Simão Oliveira
Simão OliveiraWeb and Applications Manager
Spire Maritime

This tutorial is a simple introduction to using Spire Maritime’s AIS solutions, in this case the Latest Vessel Information (LVI) Web Service to obtain and display a vessel’s most recent position on a map. This is an entry-level tutorial. All you need to be able to develop it is basic knowledge of HTML, CSS and Javascript. You can also download or clone the source code for this demo on Github, and simply deploy it and run it locally.

This tutorial uses Leaflet.js as the web mapping framework. If you prefer to use Mapbox GL, please refer to this equivalent article which will achieve similar results.

You will need a valid Spire LVI authKey and the MMSI of the vessel whose position you are trying to display. Reach out to our team to get started with a Spire Maritime AIS subscription.

Why Spire Maritime’s LVI?

Spire Maritime’s Latest Vessel Information (LVI) constitutes one of the easiest ways to get started with vessel tracking data. This web service works much in the same way that an API does; using specific parameters on the URL you will be calling, you can start making calls easily and simply, and get results in a format that fits your needs.

What are the building blocks of the web map visualization?

In this tutorial we will be requesting data in the JSON format. JSON means JavaScript Object Notation, and it is a simple data structuration format that is based on the Javascript language structure and integrates seamlessly into it. Considering that we are developing a web-based, frontend-only demo here, this is the perfect format.

From a mapping perspective, you can pick and choose any JS library you prefer. The Mapbox JS library is commercial but also high performance, and has multiple rendering modes that will fit your needs; OpenLayers is one of the most well known open source alternatives, and is a robust alternative. But in this demo we will be using Leaflet.js, a very well maintained JS mapping platform that has very good community support, as well as a healthy ecosystem of plugins.

Other than that, we will simply use a sprinkle of HTML and CSS.

Deploying the demo’s basic structure

Let’s get started! You will need a simple index.html file, with very basic source code. Here is what it looks like:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>LVI tracker | Spire Maritime</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/leaflet.css">
  <link href="css/main.css" rel="stylesheet" defer>
</head>
<body>
  <div id="map" class="map loading"></div>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/leaflet-src.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/leaflet.rotatedMarker.min.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

As you can see, from a pure HTML mark-up perspective, there’s very little going on here. We have a simple <div> with a "map" id, which we will use to render our map using Leaflet.js; the dependencies for the mapping library are all served using an external CDN, in this case jsdelivr. We reference both the library’s CSS and JS source files, in the <head> and the bottom of the <body> elements, respectively. We also add a specific plug-in, Leaflet.RotatedMarker; it will allow us to apply rotation to a marker to display the vessel’s heading. More on that later.

We also integrate a local CSS and JS file. Let’s look at the CSS first:

#map{
	position: fixed !important;
	left:0;
	top:0;
	width:100%;
	height: 100%;
}

#map.loading::before{
	position: absolute;
	left:0;
	top:0;
	z-index: 1000;
	background:url(../img/loader.svg) rgba(255,255,255,.8) no-repeat center center / 40px 40px;
	width: 100%;
	height: 100%;
	display: block;
	content:'';
}

.boatIcon{
	background:url(../img/vessel.svg) no-repeat center center / cover;
}

This very simple CSS structure simply ‘fixates’ the map on the page. We want it to be a full screen, no scroll experience. Additionally, we add a .loading class to provide user feedback; this class will be removed, via Javascript, once the call to Spire Maritime’s LVI is successful. Finally, we integrate a simple .boatIcon instruction to style the marker that will represent our targeted vessel.

Getting started writing JS code

Now let’s start writing our main.js file. The main tasks we will accomplish are:

  • Feed a user-defined mmsi URL parameter to our code (so we can change it dynamically)
  • Authenticate with Spire Maritime’s Latest Vessel Information web service
  • Initialize a backdrop world map
  • Fetch the most recent position for a vessel based on the vessel’s MMSI
  • Display a marker on the vessel’s position, including the most recent heading of the vessel
  • Provide very basic error handling messages if the vessel information is not available

Let’s start off by plugging into the load event of the window object; this allows us to only run the code once the entire page’s resources are ready. You might want to change this if your code running order changes (for example, to load the map at a later stage once a user clicks a button, for example).

Then, we check for the existence of both the mmsi and the key parameters on the page’s URL and finally, we extract them to constants and verify if the mmsi is 9 characters long, the typical format of a vessel’s MMSI. If any of these conditions fails, we use the basic alert() JS functionality to provide an error message to the user.

This data validation stage can, and should, be made more robust in both validation methods and UX once you build up your platform, but should at least help understanding any errors in your code or URL at this stage:

window.addEventListener("load", function(){
  if(window.location.href.includes('mmsi=') && window.location.href.includes('key=') ){
    const queryString = window.location.search;
    const urlParams = new URLSearchParams(queryString);
    const mmsi=urlParams.getAll('mmsi')[0];
    const key=urlParams.getAll('key')[0];

  if(mmsi.length==9) initMap(mmsi, key);
    else alert('Invalid mmsi provided.');
  }else{
    alert('Please provide a valid mmsi and key');
  }
});

Initializing the Leaflet map

Next, we declare the initMap() function which is the heart of this demo. Note the mmsi and key parameters this function requires as parameters. These will identify the vessel we’re trying to target, as well as the authKey (or token) which allows you to query the Web Service. If you don’t have a Spire Maritime AIS subscription, reach out to our sales team and request one.

First, let’s initialize a Leaflet.js map:

const map = L.map('map', {
  center:[20, 0],
  zoom:3,
  minZoom: 2,
  scrollWheelZoom:true,
  layers:[L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')],
  zoomControl:true,
  doubleClickZoom:true,
  dragging:true
});

Note the different options passed to the L.map object; the first argument, "map", constitutes the ID of the HTML element where we want to display the map. Refer back to the HTML mark-up above; if you change this ID, you’ll need to change it here as well. Next, there is an object of default options which you can change. We add a zoomControl button set, we activate the possibility of changing zoom using the mouse’s scrollwheel, we allow click and drag, and we define the map base layer. In this demo we used the OpenStreetMap base layer, but there is a plethora of choices online. Just make sure to provide attribution according to the instructions of the basemap owner.

Once the map is initialized, we need to actually make a call to the vessel tracking data web service; since we are using LVI in this demo, make sure to refresh your knowledge on our LVI documentation page.

Retrieving the latest vessel position information from Spire Maritime’s LVI

Please note – this is a simple conceptual demo, meant to be used locally. Never expose your authKey by integrating it into client-side code. If you want to integrate a live API or Web Service into a front-facing platform, make sure to develop a backend proxy call that hides the authentication token from curious eyes.

We’re going to be using javascript’s ES6’s fetch() API to make the call to the server. You could, however, use any other technique that makes inter-server calls in the browser, like $.ajax() (if you’re using jQuery), XMLHttpRequest, or any similar techniques in the framework of your preference.

Let’s look at how this call is made:

fetch('https://services.exactearth.com/gws/wfs?authKey='+key+'&service=WFS&request=GetFeature&version=1.1.0&typeName=exactAIS:LVI&outputFormat=json&cql_filter=mmsi='+mmsi)

First, we call the endpoint’s URL, integrating the key and mmsi parameters, which are part of the initMap function that wraps this code:

https://services.exactearth.com/gws/wfs?authKey='+key+'&service=WFS&request=GetFeature&version=1.1.0&typeName=exactAIS:LVI&outputFormat=json&cql_filter=mmsi='+mmsi

Note the cql_filter parameter. This is a very user-friendly way to query for specific data using LVI; you can also use the filter parameter, written in XML, if you prefer. Ideally, make sure to URL encode the cql_filter parameter using the Javascript native function encodeURI().

Also, note how we specifically ask for data in the JSON format using the outputFormat parameter. LVI is very flexible and allows you to query in CSV, XML or JSON, the latter being the ideal format for our use case.

Handling the AIS data response

The next step is handling LVIs response. We’ll need to handle very basically a couple of error situations, like error codes from the server or a lack of the required parameters; if the server responds with a valid response, we will treat the returned data accordingly to display it on the map. Here is what an example response should look like:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": "9082612747464471583",
      "geometry": {
        "type": "Point",
        "coordinates": [122.24226667,38.17826667]
      },
      "geometry_name": "position",
      "properties": {
        "mmsi": 219018271,
        "imo": 9619907,
        "vessel_name": "MAERSKMCKINNEYMOLLER",
        "callsign": "OWIZ2",
        "vessel_type": "Cargo",
        "vessel_type_code": 79,
        "vessel_type_cargo": "No Additional Information",
        "vessel_class": "A",
        "length": 399,
        "width": 60,
        "flag_country": "Denmark",
        "flag_code": 219,
        "destination": "CNHSK>CNTST",
        "eta": "07071600",
        "draught": 11,
        "longitude": 122.24226666666667,
        "latitude": 38.178266666666666,
        "sog": 9.7,
        "cog": 101.1,
        "rot": 0,
        "heading": 100,
        "nav_status": "Under Way Using Engine",
        "nav_status_code": 0,
        "source": "T-AIS",
        "ts_pos_utc": "20230706074410",
        "ts_static_utc": "20230706061452",
        "ts_insert_utc": "20230706074427",
        "dt_pos_utc": "2023-07-06 07:44:10",
        "dt_static_utc": "2023-07-06 06:14:52",
        "dt_insert_utc": "2023-07-06 07:44:27",
        "vessel_type_main": "Container Ship",
        "vessel_type_sub": "",
        "message_type": 1,
        "eeid": 9082612747464471583
      }
    }
  ],
  "totalFeatures": 1,
  "numberMatched": 1,
  "numberReturned": 1,
  "timeStamp": "2023-07-06T07:49:58.188Z",
  "crs": {
    "type": "name",
    "properties": {
      "name": "urn:ogc:def:crs:EPSG::4326"
    }
  }
}

So our reference object here is features[0].properties, and the properties we’re interested in are the GeoJSON-ready geometry object (you could also use the latitude and longitude properties, but Leaflet.js supports GeoJSON so let’s make our lives easier). We will also need the heading property to represent the rotation of the vessel on the map.

So let’s write our vessel displaying functionality:

First we check if there is a count of objects inside myJson.features. LVI only keeps positions that are at maximum 6 hours old, so if a vessel has stopped transmitting AIS, this object might be empty (we will handle that later); we want to make sure there is one entry inside (LVI only return one entry per MMSI queried):

if(typeof myJson.features!=='undefined' && myJson.features.length){}

Then we create two constants, one with the position, using the geometry property, and another with the heading:

const position=myJson.features[0].geometry;
const heading=myJson.features[0].properties.heading ? myJson.features[0].properties.heading : 0;

Now, let’s create a new Leaflet GeoJSON object, which will be displayed using a L.marker. Note how we can use the pointToLayer callback method of L.GeoJSON to custom style it. This is where we apply the styling class and dimensions we defined previously, on top of the main.js file and on the main.css stylesheet. Leaflet.js renders pure html to the page’s Document Object Model (DOM), so you can take advantage of CSS to style your marker easily and simply. We do that by applying the 'boatIcon' class.

We also set iconSize and iconAnchor properties which define the size of the object on the map in pixels; note how iconAnchor is half the size of iconSize’s width and height, which will set the icon’s anchor in the middle of the marker element. If you were to use a different size image for the marker, you would want to adjust both these properties to match.

Finally, we return the Leaflet marker object, including the icon we defined and the rotationAngle property. This is a functionality enabled by the Leaflet.RotatedMarker plugin, and accepts a rotation value in radial degrees, so we can use the heading property of LVI out of the box since it is also set in radial degrees.

And once the marker is created, let’s add it to the map and change its position to focus on the added marker:

if(typeof myJson.features!=='undefined' && myJson.features.length){
  const position=myJson.features[0].geometry;
  const heading=myJson.features[0].properties.heading ? myJson.features[0].properties.heading : 0;
  const marker=L.geoJSON(position, {
    pointToLayer: function(f, latlng) {
      vesselIcon=new L.divIcon({className: 'boatIcon', iconSize:[16,30], iconAnchor:[8,15]});
      return L.marker(latlng, {icon: vesselIcon, rotationAngle:heading});
    }
  }).addTo(map);

  map.setView([myJson.features[0].properties.latitude, myJson.features[0].properties.longitude], 6);
  document.getElementById('map').classList.remove('loading');
}else{
  alert('No vessel with mmsi '+parseInt(mmsi));
  document.getElementById('map').classList.remove('loading');
}

The last elements of this code are simply to remove the loading class from the map to inform the user we finished loading and rendering our vessel position, and handling an error in case there is no information for the provided MMSI.

To display a vessel position, open the local index.html file and append ?key=yourtokenhere&mmsi=123456789 to the URL in your browser, where yourtokenhere is replaced by the LVI token, and 123456789 is replaced by the MMSI of the vessel you want to display, for example:

file:///Users/user/Desktop/index.html?token=yourtokenhere&mmsi=219018271

That’s it! You should now have a working vessel position tracker, which you can dynamically query with any MMSI of your choice! You can clone or download the entire source code on Github.

Image of the developed visualization tool

What ‘s next?

You can now start building up upon your vessel tracker. Maybe show a tooltip when hovering your mouse over the vessel, showing its latest static AIS information? Call the Historical Vessel Tracks web service to display the vessel’s historical route and add it to the map? Or add a WMS map layer that displays all vessels in the proximity? These are all at hand’s reach – so get coding 😊

And if you don’t have a Spire Maritime subscription, reach out to our team of experts and we’ll make sure you can start your subscription, or a 7 day trial, as soon as possible.

 

AIS data in Power BI via Spire Maritime 2.0 API

Photo of Emmanuel Rosetti
Emmanuel RosettiSales Engineer
Spire Maritime

Accessing Spire Maritime 2.0 GraphQL API Data in Power BI to power dashboards with AIS data

As the world’s oceans are constantly in motion, it’s important for maritime industry professionals to have access to real-time data to keep track of vessels and other relevant information. Spire Maritime is a leading provider of this type of data and offers a GraphQL API that allows users to query and retrieve up-to-date data about vessels, ports events, and more. In this post, we’ll explore how to access Spire Maritime 2.0 GraphQL API data on Power BI by installing a custom connector called SpireGraphQLAPI_PowerBIBETA.

The SpireGraphQLAPI_PowerBIBETA Connector is a custom connector that enables users to connect to the Spire Maritime 2.0 GraphQL API directly from Power BI.

It offers an easy-to-use interface for querying our GraphQL API and retrieving data from it.

Here are the steps to get started with the SpireGraphQLAPI_PowerBIBETA Connector:

Prerequisites:
  • Power BI Desktop installed on your machine. NOTE: only Power BI Desktop installed from the following link works: Microsoft Power BI Download. Power BI coming from the Microsoft App Store is not supported at this time.
  • A Spire GraphQL API account with a valid token

Step 1: Download and Install the SpireGraphQLAPI_PowerBI Connector

You can download the SpireGraphQLAPI_PowerBIBETA Connector from the Spire GitHub repository.

Extract the files from the downloaded ZIP file and copy the file ‘SpireGraphQLAPI_PowerBI.mez’ to the following location: C:\Users\{YourUsername}\Documents\Power BI Desktop\Custom Connectors.

Step 2: Allow Custom Connectors in Power BI Desktop

You need to enable the “Allow any extension to load without validation or warning” option in the Security settings to allow Power BI Desktop to use the custom connector.

To do this, go to File > Options and settings > Security and select the option under Data Extensions section. Then, restart Power BI Desktop.

Step 3: Connect to Spire Maritime 2.0 GraphQL API Data

First, open Power BI Desktop.

Then, go to Home > Get Data > More > Other > SpireGraphQLAPI_PowerBI (Beta).

Power BI Get Data Other

Once the following window opens, enter your Spire Maritime API token in the “Account key” field.

Account key screen

Then select the service you want to use from the drop-down list (Vessels 2.0, Vessels 2.0 with Vessel to Port ETA, or Port Events API).

Navigator table with Spire GraphQL Queries

Enter the necessary query parameters for the selected service.

Then click “Apply” to load a preview of the data

Finally, click “Load” or “Transform Data” to retrieve the data in your project (Make sure to check the query previously in the navigation table).

Step 4: Transform and Visualize the AIS Data in Power BI

At this point, you can transform the data as needed using the Query Editor or directly create your report or dashboard using the AIS data.

Conclusion:

In summary, the SpireGraphQLAPI_PowerBIBETA Connector provides an easy way to connect to the Spire GraphQL API and retrieve AIS data from the Vessels 2.0 and Port Events API services.

By following this tutorial, you can easily access this valuable AIS data and create insightful reports and dashboards in Power BI.

Power BI dashboard example

Power BI dashboard with Spire Maritime 2.0 data

 

NB: This connector is currently in Beta version and is not fully supported by Spire Maritime.

Using the Historical Positions API without calling the Vessels API

Photo of Mark Deverill
Mark DeverillDirector Maritime Data Operations
Spire Maritime

Following the release of the updated Historical Positions API in October 2022, it is no longer necessary to call the Vessels API first.

The historical positions API requires a Vessel ID when requesting data for a single vessel. This ID value was previously only available from the Vessels API.

Following the API update in October 2022, the vessel ID value required to call the Historical Positions API is now available from the Maritime 2.0 GraphQL API. Users of the Historical Positions API are advised to now use Maritime 2.0 GraphQL API to obtain the vessel ID value that is then passed to the Historical Positions API.

Example of historical AIS positions for vessel COSCO BOSTON

  1. Find the Vessel ID value for the vessel
    Call Maritime 2.0 GraphQL API using the name or imo number as the reference:

    query {
      vessels(
        imo: 9335173
        lastUpdate:{
          startTime:"2022-02-27T00:00:00.000Z"
        }
      ){
        totalCount {
          relation
          value
        }
        nodes {
          id
          updateTimestamp 
          staticData {
            name
            callsign
            timestamp
            updateTimestamp
            shipType
            mmsi
            imo
            callsign
            dimensions {
              width
              length
            }
          }
        }
      }
    }
    

    API result

    {
      "data": {
          "vessels": {
             "totalCount": {
               "relation": "EQUAL",
               "value": 1
             },
             "nodes": [ {
               "id": "41873e9b-180d-34c1-b258-8eee3b07a098",
               "updateTimestamp": "2023-02-27T10:42:36.926Z",
               "staticData": {
                 "name": "COSCO BOSTON",
                 "callsign": "3ELF2",
                 "timestamp": "2023-02-27T04:41:00.027Z",
                 "updateTimestamp": "2023-02-27T04:41:03.022Z",
                 "shipType": "CONTAINER",
                 "mmsi": 372934000,
                 "imo": 9335173,
                 "dimensions": {
                   "width": 32,
                   "length": 293
                 }
               }
             } ]
          }
        }
    }
    
    

    Note it is the top level ID in each Vessel Query result that is the id value to pass to the Historical positions API. From the query above the id value to use is:

     "id": "41873e9b-180d-34c1-b258-8eee3b07a098"

    The id returned by the above Maritime 2.0 GraphQL vessels query is the same as that returned by the Vessels API for the same vessel.

  2. Use the vessel ID value in calling Historical Positions API
    Using the vessel ID value now returned from Maritime 2.0 GraphQL, call the historical positions API. The vessel ID value is used to construct the URL call as below:

    https://ais.spire.com/vessels/41873e9b-180d-34c1-b258-8eee3b07a098/positions?timestamp_after=2023-02-27T00:00:00Z&timestamp_before=2023-02-27T09:59:59Z

    While using the internal Vessel ID value, this is actually 1:1 with an instance of a vessel based on MMSI number.

    If required then the Historical Positions API can be queried using a known MMSI number, without first calling the Maritime 2.0 GraphQL API. For example, to query Historical Positions API for MMSI 372934000, use mmsi_372934000 instead of the vessel ID value when constructing the call to Vessels API:

    https://ais.spire.com/vessels/mmsi_372934000/positions?timestamp_after=2023-02-27T00:00:00Z&timestamp_before=2023-02-27T09:59:59Z

    Example Results:

    {
      "paging":{
         "limit":100,
        "self":"/vessels/mmsi_372934000/positions",
        "next":"FjqFcBi_JFoJAA8H0gBgAABBT5_hQsl2gxi_JVU=",
        "previous":"removed"
      },
      "data":[{
          "_links":{
            "self":"/vessels/positions/FjqFcBi_xjUFAFsL6gE1AABBRtV4QsmuZRi-xcY=",
            "vessel":"/vessels/mmsi_372934000"
          },
          "id": "FjqFcBi_xjUFAFsL6gE1AABBRtV4QsmuZRi-xcY=",
          "mmsi":372934000,
          "vessel_id":"mmsi_372934000",
          "timestamp":"2023-02-27T00:02:45+00:00",
          "created_at":"2023-02-27T00:00:54+00:00",
          "geometry":{
          "type":"Point",
          "coordinates":[
            100.840614,
            12.427116
          ]
        }
      }]
    }
    

Vessels API End Of Life Notice for 2023-09-30

Photo of Mark Deverill
Mark DeverillDirector Maritime Data Operations
Spire Maritime

Spire Maritime announce end of life for the Vessels API from 2023-10-01.

Since early 2022, users of Vessels API have been advised to migrate to the Maritime 2.0 Vessels 2.0 GraphQL API. All product enhancements & bug fixes now only occur in the Maritime 2.0 GraphQL API. There are no reasons to continue to use the Vessels API and customers are now expected to migrate to the newer product.

All clients using the Vessels API available on https://ais.spire.com/vessels or https://api.sense.spire.com/vessels are requested to switch to using the Maritime 2.0 Vessels 2.0 GraphQL API by 2023-09-30.

In 23-10-01 the Vessels API will be turned off.

Maritime 2.0 GraphQL API is the future of Vessels Data API Services

Spire Maritime are continually working to improve the performance of our API services and to scale the systems to perform well with the continually growing volumes of data and clients. Maritime 2.0 GraphQL is the API platform that provides stable, scalable, performant vessels data services going forward.

The benefits of the Maritime 2.0 GraphQL API:

  • Improved scalability & performance of API platform
  • New more flexible and easy to use APIs using GraphQL.
  • Improved and more accurate vessel identification mechanism:
    • Recognizing and resolving where possible duplicate vessels reporting using the same MMSI number.
    • Recognizing and resolving where possible duplicate vessels reporting using the same IMO number.
  • Improved Terrestrial AIS with additional data not available in Vessels API
  • Option to subscribe to real time satellite AIS.
  • Extended coverage of Vessel Characteristics data, twice as many fields and vessels as reported by EVD data in Vessels API.
  • Additional feature options:
    • port, terminal, anchorage and canal events through graphQL query endpoints portEventsByShipType, portEventsByShipVessel or portEventsByShipLocation
    • Future new features all released through graphQL.

Supporting customer migration

Spire Maritime are happy to offer online technical training in use of the Maritime 2.0 GraphQL API to each client migrating from the Vessels API. If you think this would be useful then please request such training by emailing [email protected] .

In March 2022 we held a webinar demonstrating the differences between the Vessels API and Maritime 2.0 Vessels GraphQL API, and information necessary for customers to migrate from the REST API to Maritime 2.0 Vessels 2.0 GraphQL API.

Vessels Historical Positions API New Implementation 2022-11-17

Photo of Mark Deverill
Mark DeverillDirector Maritime Data Operations
Spire Maritime

The new version of the Historical Positions API will be released 2022-11-17 10:00 UTC.

This will look like the current implementation, but has been coded to perform better in a new computing environment.

All users of the Historical Positions API will be automatically switched to the new implementation.

What are the benefits of the new API implementation?

  • Improved scalability of platform
  • Improved performance
  • Improved Dynamic AIS providing
    • 3x volume of Dynamic AIS messages providing updates to the API
    • 30% increase in vessels reported by Dynamic AIS
    • Additional field coverage from Dynamic AIS
  • Ability to request Historical Position History by MMSI number
  • Ability to call Historical Positions API directly without calling Vessels API first.

What are the changes in the new API implementation?

vessel_id values used to make per vessel requests to the historical positions API are changing format. In the old implementation of the API they are unique ID values returned by Vessels API like this:

vessel_id: "45afbf87-a111-4cdc-8d0b-20452e282e55"

In the new implementation of the API they are based on the MMSI number and look like this

vessel_id: "mmsi_636016306"

It is this change of ID number that now allows the Historical Positions API to be called

  1. without calling Vessels API first if the MMSI is known
  2. by users of Maritime 2.0 graphQL QPI using a vessels MMSI number

Filter Deprecation

As part of improvements to the API system performance, the new implementation will be removing filter options from the Historical Positions API that are no longer used.

The filters to be removed are:

  • previous
  • before

Note: previous and before filters are being removed because they are not use. All users page through API results going forward using the next filter.

Comparing the output of the updated Historical Positions API and the legacy Historical Positions API

Here is to an example of the results for the same vessel from the old and new versions of the Vessels API. It should be clear that core data is the same and the format in which data is returned is the same.

Customer Support

If you have any questions about the change, please reach out to the Spire Technical support team using our Support portal or by sending an email to [email protected]

Vessels API New Implementation 2022-11-17

Photo of Mark Deverill
Mark DeverillDirector Maritime Data Operations
Spire Maritime

Vessels API Upgrade & Feature Deprecation

The new implementation of Vessels API will be released 2022-11-17 10:00 UTC

This will look like the current implementation, but has been coded to perform better in a new computing environment.

qAll users of the Vessels API will be automatically switched to the new implementation.

What are the benefits of the new API implementation?

  • Improved scalability of platform
  • Improved performance
  • Improved Dynamic AIS providing
    • 3x volume of Dynamic AIS messages providing updates to the API
    • 30% increase in vessels reported by Dynamic AIS
    • Complete coverage of AIS positions and static data messages fields.
  • Improved matching of destination to port locodes
  • Improved generation of predicted_route for vessels with matched destination port locodes

As part of improvements to the API system performance, the new implementation will be removing filter options from the Vessels API that are no longer used.

The filters to be removed are:

Group 1

  • previous
  • before

Note: previous and before filters are being removed because they are unused and no longer needed. All users page through API results going forward using the next filter.

Group 2

  • predicted_position_within
  • last_known_or_predicted_position_within

The group 2 filters are being deprecated because they were based on the predicted AI feature which is no longer supported.

Group 3

  • general_classification(replaced by enhanced_data.vessel_and_trading_type. vessel_type)
  • individual_classification (replaced by enhanced_data.vessel_and_trading_type.subtype)
  • gross_tonnage (replaced by enhanced_data.capacity.gross_tonnage)
  • lifeboats
  • person_capacity

Group 3 fields have not been supported since 2019 and are largely replaced by the data available in the Enhanced Vessel Data (EVD) option of Vessels API. They will still be returned but with null values.

Group 4

  • predicted_position
  • predictions

The predicted_position and predictions objects will be deprecated and no longer supported. They will still be returned but with null values. For many clients this has been the case already.

Comparing the output of the updated Vessels API and the legacy Vessels API

Here is an example that shows the results for the same vessel from the old and new versions of the Vessels API.

It should be clear that core data is the same and the format in which data is returned is the same. The differences are as announced above, including a predicted route returned from the new API which had not been calculated by the old API.

Customer Support

If you have any questions about the change, please reach out to the Spire Technical support team using our Support portal or by sending an email to [email protected]

Messages API performance update

Photo of Mark Deverill
Mark DeverillDirector Maritime Data Operations
Spire Maritime

Following the recent DNS update routing Messages API traffic via the new server cluster, we will release a new implementation of the Messages API.

This will look like the current implementation, but has been coded to perform better in a new computing environment.

All users of the Messages API will be automatically switched to the new implementation at this time.

What does not change?

  • Tokens, URL and API parameters & format of API results will not change.

What will change?

  • As part of the new API platform, on top of improved performance there will also be an improved version of Dynamic AIS™, and an additional source of Terrestrial AIS that adds coverage mainly in the Gulf of Mexico and US waterways.
  • For subscribers to Dynamic AIS the volume of messages will increase 2-3 times.
  • For subscribers to Spire global Terrestrial AIS the volume will increase about 10%

The response from the new implementation will match the response from the old Messages API in format, value ranges and how filters work. Internal system values may change which would be values in fields id, msg_id, since and after values. When the swap to the new implementation is made any requests made to the new implementation using pagination values from the old API will still work.

Anyone wishing to test the new system should contact Spire support to request Beta testing access. Otherwise we expect the new Messages API implementation to go live by mid-November.

Below are examples of responses from the old and new implementations of Messages API to illustrate the backward compatibility of the system.

API call used

https://ais.spire.com/messages?fields=decoded&msg_type=1&limit=1

Current API result returned:

{
  "paging": {
    "since": "GjQKJDRmYmQ0OTI2LWVhMjYtNTI5Yy04YmQ2LTY3YmE2M2VhZjMwNRIMCKDL6ZkGEPjukOEC",
    "actual": "1+",
    "limit": 1
  },
  "data": [
    {
      "id": "4fbd4926-ea26-529c-8bd6-67ba63eaf305",
      "nmea": "!AIVDM,1,1,,A,17WK5ggP0242RUH>QFK`
    }
  ]
}

If you have any questions concerning this update to the Messages API system then please log a support ticket requesting further guidance.

Vessels shipTypes expansion (August 2022)

Photo of Mark Deverill
Mark DeverillDirector Maritime Data Operations
Spire Maritime

Explaining the new shipType values being reported in Maritime 2.0 GraphQL Vessels

From 2022-08-30 Maritime 2.0 GraphQL for vessels will report a wider set of shipType values identified in AIS messages. When establishing Maritime 2.0 focus was given to mapping the AIS reported ship types to the real commercial ship types of cargo trading vessels, such as bulkers, tankers, gas carriers and containerships. This was done by matching AIS reported vessels to our Vessel Characteristics database. Unfortunately some of the smaller vessel types reported by AIS were all grouped together in one type category called OTHER.

As of 2022-08-30 we are updating Maritime 2.0 to report all specific vessel types identified in AIS messages.

Please note that while some vessels will still be reported with type OTHER, those with newly reported shipType values will gain a new ID in the data results. Overall the number of vessels reported as shipType OTHER will reduce significantly as vessels start to be reported with the new shipType values. Also vessels reported as PASSENGER vessels by AIS were reported as VEHICLE_PASSENGER. From this release only vessels identified in Spire Vessel Characteristics data as mixed vehicle or passenger will retain the shipType value VEHICLE_PASSENGER.

The new shipTypes values not previously reported in Maritime 2.0 are shown in the table below:

ANTI_POLLUTION
Anti Pollution Vessel
previously reported as OTHER up to 2022-08-30
DIVE_VESSEL
Dive Vessel
previously reported as OTHER up to 2022-08-30
DREDGER
Dredger
previously reported as OTHER up to 2022-08-30
HIGH_SPEED_CRAFT
High Speed Craft
previously reported as OTHER up to 2022-08-30
LAW_ENFORCEMENT
Law Enforcement
previously reported as OTHER up to 2022-08-30
MEDICAL_TRANS
Medical Transport
previously reported as OTHER up to 2022-08-30
MILITARY_OPS
Military Operations
previously reported as OTHER up to 2022-08-30
PASSENGER
Passenger
previously reported as VEHICLE_PASSENGER up to 2022-08-30
PILOT_VESSEL
Pilot Vessel
previously reported as OTHER up to 2022-08-30
PLEASURE_CRAFT
Pleasure Craft
previously reported as OTHER up to 2022-08-30
PORT_TENDER
Port Tender
previously reported as OTHER up to 2022-08-30
SAILING
Sailing
previously reported as OTHER up to 2022-08-30
SEARCH_AND_RESCUE
Search and Rescue
previously reported as OTHER up to 2022-08-30
SPECIAL_CRAFT
Special Craft
previously reported as OTHER up to 2022-08-30
 

Timestamp filtering in Vessels 2.0

Photo of Mark Deverill
Mark DeverillDirector Maritime Data Operations
Spire Maritime

Explaining the lastTimestamp filter in Maritime 2.0 GraphQL Vessels

From 2022-06 Maritime 2.0 GraphQL for vessels will gain a new filter, lastTimestamp.

Unlike the filter lastPositionUpdate which filters for vessels by the time when a position update was received, the new filter lastTimestamp filters vessels on the message timestamp of any update from AIS static or position messages.

If lastTimestamp filter is not specified in a query then it will be set by default to filter out vessels with no AIS message in the last 30 days. This is intended to not return, unless specifically required, vessels that have not been updated in the previous 30 days. This is intended to reduce the volume of data being returned, unless vessels with older updates are specifically requested.

Example, comparing the use of lastTimestamp and lastPositionUpdate filter.

query {
  vessels(lastPositionUpdate: {startTime: "2022-05-23T00:00:00.00Z"}) {
    totalCount {
      value
      relation
    }
  }
}

Returns an estimates 210,392 vessels:

{
  "data": {
    "vessels": {
      "totalCount": {
        "value": 210392,
        "relation": "LOWER_OR_EQUAL"
      }     
    }
  }
}

However, using the lastTimestamp filter as below, can return a slightly higher number of vessels, as it now includes vessels that have only had updates from AIS static messages too.

query {
  vessels(lastTimestamp: {startTime: "2022-05-23T00:00:00.00Z"}) {
    totalCount {
      value
      relation
    }
  }
}

Yields to:

{
  "data": {
    "vessels": {
      "totalCount": {
        "value": 211921,
        "relation": "LOWER_OR_EQUAL"
      }     
    }
  }
}

The higher number of vessels returned using the lastTimestamp filter, 211921 compared to 210392, indicates that about 1500 vessels had updates from Static AIS messages but not yet from AIS position messages in the period being queried. This can be due to many reasons, including latency in AIS messages or updates to the system.

The new filter lastTimestamp allows vessels with to be queried by when AIS messages were last received. Note this is different than when the updates occurred as is used in the lastPositionTimestamp.

Example of filter differences

An AIS position message transmitted at 2022-05-23T09:59 and updated into the system at 2022-05-23T10:02 (latency of 2 minutes) would be returned by a query using filter:

lastPositionUpdate:{startTime:"2022-05-23T10:00:00.00Z"}

…but not by a query using filter:

lastPositionUpdate:{startTime:"2022-05-23T09:00:00.00Z" endTime:"2022-05-23T09:59:59.59Z"}

This is because the updateTimestamp would be 10:01 and outside the filtered time range. However, after being updated into the API database, the vessel with the same position message would be returned by a filter:

lastTimestamp:{startTime:"2022-05-23T09:00:00.00Z" endTime:"2022-05-23T09:59:59.59Z"}

If the query is run after 10:01 when the message is recorded in the database.

Querying without timestamp filters

Now, compare querying GraphQL vessels without a timestamp. Using this query to return all vessels by shipType values that we consider the Merchant Fleet

{
  vessels(
    shipType: [
      CAR_CARRIER, COMBINATION_CARRIER, TANKER_PRODUCT,
      CONTAINER, DRY_BULK, GENERAL_CARGO, 
      LIVESTOCK, REEFER, ROLL_ON_ROLL_OFF, 
      VEHICLE_PASSENGER, GAS_CARRIER, GENERAL_TANKER, 
      LNG_CARRIER, TANKER_CHEMICALS, TANKER_CRUDE
    ]
  ) {
    totalCount {
      relation
      value
    }
  }
}

on the GraphQL Vessels system pre release of the default lastTimestamp filter we get the following result:

{
  "data": {
    "vessels": {
      "totalCount": {
        "value": 172075,
        "relation": "LOWER_OR_EQUAL"
      }     
    }
  }
}

On the updated GraphQL Vessels system with the default lastTimestamp filter set to 30 days we get the following result:

{
  "data": {
    "vessels": {
      "totalCount": {
        "value": 165933,
        "relation": "LOWER_OR_EQUAL"
      }     
    }
  }
}

Showing that about 6000 vessels have been excluded from query results because they do not have recent messages of any kind.

You can of course choose to still receive reports of vessels with updates more than 30 days old by using the lastTimestamp filter to specifically set a time from which all updated vessels are received filtered by any other parameters specified:

{
  vessels(
    lastTimestamp: {startTime: "2022-01-01T00:00:00.00Z"},
    shipType: [
      CAR_CARRIER, COMBINATION_CARRIER, TANKER_PRODUCT,
      CONTAINER, DRY_BULK, GENERAL_CARGO, 
      LIVESTOCK, REEFER, ROLL_ON_ROLL_OFF, 
      VEHICLE_PASSENGER, GAS_CARRIER, GENERAL_TANKER, 
      LNG_CARRIER, TANKER_CHEMICALS, TANKER_CRUDE
    ]
  ) {
    totalCount {
      relation
      value
    }
  }
}

which returns:

{
  "data": {
    "vessels": {
      "totalCount": {
        "value": 165938,
        "relation": "LOWER_OR_EQUAL"
      }     
    }
  }
}
 

Rate limiter updates

Photo of Anton Kabysh
Anton KabyshSoftware Engineer
Spire Maritime

This blog post describes upcoming changes in rate limitation policies for the Maritime 2.0 API platform.

We recently introduced rate-limiting to prevent DDoS attacks and gracefully handle load on Maritime 2.0. The full documentation on rate limit behavior is now available.

Rate limiter for multi-root queries

As we see gradual adoption of multi-root queries among our customers, we have revised our rate-limiter policy to handle such cases appropriately. Because each root query executed in parallel and represents a complete and self-sufficient request to our underlying API, we should treat each root query as an independent request, and count it as one from the rate limiter perspective. So with new rate limiter policy, multi-root queries will be treated as several independent requests for rate limiter.

For example, currently the following query is treated as 1 request. After the update it will be counted as 2 standalone queries:

{
  # first query for tankers
  tankers: vessels(shipType: [TANKER_CRUDE, TANKER_PRODUCT, TANKER_CHEMICALS]) {
    nodes {
      staticData {
        mmsi
        name
      }
    }
  }
  # second query for cargo
  cargo: vessels(shipType: [CONTAINER, GENERAL_CARGO]) {
    nodes {
      staticData {
        mmsi
        name
      }
    }
  }
}

The response with a new policy will look like:

{
  "data": {
    "tankers": { ... },
    "cargo": { ... }
   },
   "extensions": {
     "requestQuota": {
       "limit": "60 req/m (burst 60)",
       "remaining": 58
     }
   }
}

As you can see, rate limiter subtracted 2 from the remaining quota, one for the cargo query and one for tankers query.

Breaking changes

There is also a subtle breaking change in the API which might be important for data integrators. To cope with wider usage of multi-root queries we should adjust the data format for some edge cases.

For example, take the multi-root query below:

{
  tankers: vessels(shipType: [TANKER_CRUDE, TANKER_PRODUCT, TANKER_CHEMICALS]) {
    ...
  }
  cargo: vessels(shipType: [CONTAINER, GENERAL_CARGO]) {
    ...
  }
}

Now, let’s imagine that the tankers query completed successfully, but that the cargo query timed out for any reason; ideally a partial data response for the tankers query should still be returned. However, before this update this was not possible, and the response’s data would be null:

{
  "data": null,
  "errors": [{
    "message": "This request timed out",
    "path": ["cargo"],
    "extensions": {
      "code": "TIMEOUT_ERROR"
    }
  }],
  "extensions": {
    "requestQuota": {
      "limit": "60 req/m (burst 60)",
      "remaining": 58
    }
  }
}

We fixed that edge case, and now a partial data response for the tankers query can be returned, along with an errors entry for the cargo query. The response would now look like this:

{
"data": {
"tankers": { ... },
"cargo": null,
},
"errors": [{
"message": "This request timed out",
"path": ["cargo"],
"extensions": {
"code": "TIMEOUT_ERROR"
}
}],
"extensions": {
"requestQuota": {
"limit": "60 req/m (burst 60)",
"remaining": 58
}
}
}

These changes also affect simpler queries; for example, if an error was to be triggered during the following query:

{
vessels(shipType: [TANKER_CRUDE, TANKER_PRODUCT, TANKER_CHEMICALS]) {
nodes {
staticData {
mmsi
name
}
}
}
}

The response might contain data: null or data: { vessels: null } depending on the nature of the error:

{
"data": null,
"errors": [...],
}
// or
{
"data": {
"vessels": null
},
"errors": [...],
}

The second case would not have been possible earlier, and we encourage our customers to handle this case in your Spire API client.

 

How does Vessels 2.0 compare to Vessels API?

Photo of Mark Deverill
Mark DeverillDirector Maritime Data Operations
Spire Maritime

Maritime 2.0 deprecates the REST Vessels API that was launched by Spire in 2018; this service has become obsolete and unable to scale with the increasing volumes of data now available in Spire Maritime data services, whereas Maritime 2.0 is built to scale and serve an expanding set of API features.

What is the difference in Request Type?

  • Maritime 2.0 makes an HTTPS POST Request.
  • Vessels API makes a HTTPS GET Request

The example API calls below use the curl command, a common command line utility for calling and testing APIs.

Maritime 2.0 example POST request

curl --location --request POST \

'https://api.spire.com/graphql' \

--header 'Authorization: Bearer aAAAaaA1AaAaa1A1A1AaaaA1aaaAaAAa' \

--header 'Content-Type: application/json' \

--data-raw '{"query":"query { vessels( imo:9430222) {pageInfo {hasNextPage endCursor} nodes {id updateTimestamp staticData {aisClass flag name callsign timestamp updateTimestamp shipType shipSubType mmsi imo callsign dimensions {a b c d width length } } \

lastPositionUpdate {accuracy collectionType course heading latitude longitude maneuver navigationalStatus rot speed timestamp updateTimestamp } \

currentVoyage { destination draught eta timestamp updateTimestamp matchedPort {matchScore port { name unlocode centerPoint { latitude longitude} } } } } } }","variables":{}}'

 

Compare this to the simpler but less flexible Vessels API request:

curl --location --request GET \

'https://ais.spire.com/vessels/?imo=9430222' \

--header 'Authorization: Bearer {Your token here}'

How do the API responses’ format compare?

The response from Maritime 2.0 is similar to that from Vessels API – the data that is returned is in a similar JSON format; but the data model for the graphQL response is slightly different. There are equivalent data items for all AIS data items that were available from the Vessels API.

Sample Maritime 2.0 API response

{
  "id": "4263b0ab-9ccd-4f84-82b9-65e7d75243b8",
  "updateTimestamp": "2022-02-16T00:44:48.306Z",
  "staticData": {
      "aisClass": "A",
      "flag": "PA",
      "name": "BBC RIO",
      "callsign": "3E3336",
      "timestamp": "2021-08-31T07:24:41.208Z",
      "updateTimestamp": "2022-02-15T08:39:37.033Z",
      "shipType": "GENERAL_CARGO",
      "imo": 9430222,"mmsi": 352978219,
      "dimensions": {
        "a": 144,
        "b": 17,
        "c": 15,
        "d": 10,
        "width": 25,
        "length": 161
      }
  },
  "lastPositionUpdate": {
      "accuracy": "LOW",
      "collectionType": "DYNAMIC",
      "course": 86.1,
      "heading": 86,
      "latitude": 1.267245,
      "longitude": 104.22576833333332,
      "maneuver": "NOT_AVAILABLE",
      "navigationalStatus": "UNDER_WAY_USING_ENGINE",
      "rot": 1.1160072,
      "speed": 12.9,
      "timestamp": "2022-02-14T20:18:27.000Z",
      "updateTimestamp": "2022-02-16T00:44:48.306Z"
  },
  "currentVoyage": {
      "destination": "SINGAPORE",
      "draught": 8.2,
      "eta": "2021-10-17T12:00:00.000Z",
      "timestamp": "2021-10-17T09:37:17.099Z",
      "updateTimestamp": "2022-02-15T08:39:37.033Z"
    }
}

Sample Vessels API Response

{
    "id": "fd096ff2-0d1b-48d8-88f5-0fadada65139",
    "name": "DIYYINAH-I",
    "mmsi": 636014943,
    "imo": 9487251,
    "call_sign": "A8XN8",
    "ship_type": "Tanker",
    "class": "A",
    "flag": "LR",
    "length": 228,
    "width": 32,
    "ais_version": 0,
    "created_at": "2017-08-11T19:24:21.193385+00:00",
    "updated_at": "2019-09-04T16:40:09.386823+00:00",
    "static_updated_at": "2019-09-02T17:26:19+00:00",
    "position_updated_at": "2019-09-04T16:40:09.386823+00:00",
    "last_known_position": {
        "timestamp": "2019-09-04T14:59:17+00:00",
        "geometry": {
            "type": "Point",
            "coordinates":[96.11947,5.95611]
        },
        "heading": 285.0,
        "speed": 13.2,
        "rot": 0.0,
        "accuracy": null,
        "collection_type": "satellite",
        "draught": 8.5,
        "maneuver": 0.0,
        "course": 284.5
    },
    "most_recent_voyage": {
        "eta": "2019-09-16T12:00:00+00:00",
        "destination": "FUJAIRAH"
    }
}

How do API calls compare?

graphQL requires a more detailed “query” to be submitted when requesting data. Vessels API is a fairly fixed GET request, where the only values passed are specific filters being applied to the query. The response format is fixed and does not need specifying.

The graphQL request requires all filters to be specified, but also all required data fields for the response to be listed.

Vessels API v1. (http GET) API call for a list of vessels by IMO number

https://ais.spire.com/vessels/?limit=1&imo=9758428,9729269,9596272

Maritime 2.0 graphQL (http POST) API call for a list of vessels by IMO number

query { vessels(imo:[9758428,9729269,9596272] first:1000 ) {
    pageInfo { hasNextPage endCursor }
    nodes { id updateTimestamp
      staticData { aisClass flag name callsign timestamp updateTimestamp shipType shipSubType mmsi imo callsign dimensions { a b c d width length }  }
      lastPositionUpdate { accuracy collectionType course heading latitude longitude maneuver navigationalStatus rot speed timestamp updateTimestamp }
      currentVoyage { destination draught eta matchedPort { matchScore port { name unlocode centerPoint { latitude longitude } } } timestamp updateTimestamp
} } } }

Query what you want

graphQL allows specification of only the required data to be returned. Below is a sample graphQL query listing all available fields.

nodes {id updateTimestamp
  staticData {aisClass flag name callsign timestamp updateTimestamp shipType shipSubType mmsi imo      dimensions { a b c d width length } }
  lastPositionUpdate { accuracy collectionType course heading
     latitude longitude maneuver navigationalStatus rot speed 
     timestamp updateTimestamp }
  currentVoyage { destination draught eta
        matchedPort {matchScore port {name unlocode 
             centerPoint { latitude longitude } } }    timestamp updateTimestamp }
    }   } }

Here is the same graphQL query but with unrequired fields removed, showing that graphQL allows specification of only the required data items to be returned:

nodes {id updateTimestamp
  staticData { name callsign updateTimestamp shipType mmsi imo 
     dimensions {width length } }
  lastPositionUpdate { collectionType course heading
     latitude longitude navigationalStatus rot speed 
     timestamp }
  currentVoyage { destination draught eta
        matchedPort {port {name unlocode } }
        updateTimestamp }
    }
} }

How can I prepare for Maritime 2.0 (GraphQL)?

Photo of Mark Deverill
Mark DeverillDirector Maritime Data Operations
Spire Maritime

Advice for Spire clients on preparing for the next release of Spire Maritime 2.0 API using GraphQL.

Vessels API is the service that provides cleansed AIS data and, optionally, other premium features Spire Maritime offers, like routing, calculated ETAs, and Enhanced Vessel Data.

Maritime 2.0 is the second iteration of this premium solution and has been designed to take Spire Maritime forward, allowing for the easy integration of new features and for scaling performance as Spire data volumes continue to grow.

In this blog post, we hope to answer your questions about Spire Maritime 2.0 and any plans to migrate to it. However, if your questions are not answered here, then please submit a question through the client support form.

What are the benefits of Maritime 2.0 over Vessels API?

  • New, more flexible, and easy-to.use APIs using graphQL
  • Improved and more accurate vessel identification mechanism:
    • Recognizing and resolving where possible duplicate vessels reporting using the same MMSI number.
    • Recognizing and resolving where possible duplicate vessels reporting using the same IMO number.
    • Maintaining a single vessel history even when changing MMSI number.
  • More data available with:
    • Additional AIS sources – The latest terrestrial partner added March 2021 is only available in the new services. Satellite AIS data from the next generation of Spire satellites will only be included in the new services.
    • Improved and extended vessel characteristics – An additional vessel characteristics partner has been added along with additional data fields added to the new service.
  • A single endpoint for all API services: Going forward a single GraphQL endpoint will be used to deliver all core Maritime API services.
  • Better performance: Quicker and more stable than the V1 API.
  • Faster technology development: The new platform creates a scalable architecture that allows for quicker integration of new features.
  • New features exclusive to 2.0 API: Such as weather routing and port events, will be developed on this API.

When will Maritime 2.0 be available?

Spire released Maritime 2.0 (formerly known as the Vessels 2.0 API) for Beta testing on August 6, 2021. The GA release is anticipated to be mid October 2021. As with all IT development projects, this is subject to the passing of rigorous testing and no unseen technical difficulties.

How long will the Vessels API remain available?

Spire will not disable the legacy Vessels API until all clients have been migrated. We hope to achieve migration of all users by the end of 2021 and will be providing documentation, sample programs, training, and support to help make the transition as easy as possible.

Do licenses/contracts for Vessels API cover cover the GraphQL-based Maritime 2.0 API?

Spire customers have contracts granting access to different data sets and different solutions. Anyone whose contract lists Vessels API will automatically have access to Maritime 2.0.

Do we need to change the ETL code to switch to the new API?

While Spire strives to make change as minimally disruptive as possible, the data format of results from Maritime 2.0 will be different from that of the Vessels API. We expect significant similarity with only subtle differences that are still being worked out. Once the schema of Maritime 2.0 results is confirmed, details will be published and advice prepared on how to map between the 1.0 and 2.0 API results.

We anticipate ETL or DTS code will need updating to work with the new API.

Is there a change in the API structure?

The Vessels API is REST-based, whereas Maritime 2.0 is GraphQL-based. Both are HTTPS based and return results in JSON format, but the structure of the results will be different, requiring different data mappings (see the answer above about ETL). Also, the method of making data requests, known as a query in GraphQL, changes somewhat. The differences will be made clear when migration guides are released.

What will Spire do to help clients migrate from the Vessels API to Maritime 2.0?

Spire Maritime will be releasing detailed documentation about the new API and sample programs to query the API that demonstrate how to request, receive, and transform data from the new Maritime 2.0 GraphQL API. We will also be arranging webinars demonstrating how to use the new API.

Finally, as we do currently, we will offer bespoke support to all clients who use the new API. This support will be available from the Spire Maritime sales engineering team and help may be requested through the customer support form.