From Many to One: A Map Creation Story

It started one morning while trying to find ferry route schedules on the BC Ferries website. I was poking around to see what there was for any route details and came upon this auto-generated/auto-refreshing web page that displayed vessel locations for a route as a map image. My first thought, “wouldn’t it be cool to see all the ferries for all the routes on the map at once”.

This started off simple and I had every intention of only spending a couple of hours putting together a map of the vessels using their API. Well, no API existed for what I needed so that lead to…

Making A Vessel Location Library

A couple of things you should know before getting into the details of how this was done.

> As mentioned earlier, static maps exist for ferry routes. What wasn’t mentioned, each map image has vessel icons and if you hover your mouse cursor over the icon, an infobox window opens, with details on the vessel. Also, from what I can tell the map extent stays the same for each route map.

> A route web page refreshes every 30 seconds, showing map and route updates.

Calculating a Vessel Location

Before writing any code I wanted to determine the steps needed to calculate the location of the vessels based on the route web page. This is what I did…

  • Downloaded a ferry route web page from the BC Ferries website, example: Route 0 – Swartz Bay – Tsawwassen. For interest sake, the static maps are generated from MS Virtual Earth (Bing Maps).
  • In QGIS, I georeferenced the static map image from the downloaded web page and generated a georeferenced GeoTiff. This process ended up being a lot faster than initially thought thanks to the creators of the QuickMapServices plugin, making it easy to add a Bing Maps layer to QGIS and allowing for a “like for like” maps comparison when creating georeference control points. Oh, in case you’ve never georeferenced an image before, you can check out this video.
  • Calculated the geographic coordinate of the vessel. If you open up the source code for the downloaded web page, you’ll find a function called onMapHover, that triggers an infobox window when the user mouses over the vessel map icon (see code snippet below). Inside that function, there’s an if statement for each vessel on the map and each if statement condition defines the pixel bounding box of the vessel icon.
function onMapHover(e) {
	getMouseXY(e);
	
	var ferryInfo = document.getElementById('ferry_info');
	
        /** VVV PIXEL BOUNDING BOX HERE VVV **/
	if (x >= 352 && y >= 114 && x <= 366 && y <= 128) {
		if (infoBoxShowing != 3) {
			var html = '';
            html += '<table cellpadding="3" cellspacing="0" border="0" style="width: 230px; height: 145px; font: 11px Verdana, sans-serif;">';
            
    ... function trimmed ...

From the pixel bounding box, the X, Y location of the vessel can be calculated on the ground (sea).

// Calculate pixel coordinate of the vessel icon

         366  -  352                
pixelX = -----------  +  352  =  359
              2                     

         128 - 114                
pixelY = ---------  +  114  =  121
             2    

// Metadata from GeoTiff (via gdalinfo)
pixelWidth = 152.6966863522580127
pixelHeight = 152.6966863522580127
coordX_0 = -13763423.2 // Image upper left X coordinate.
coordY_0 = 6289823.9   // Image upper left Y coordinate.

// X Coordinate
X =  coordX_0 + (pixelX * pixelWidth)
-13708605.09 = -13763423.2 + 359 * 152.6966863522580127

// Y Coordinate
Y = coordY_0 - (pixelY * pixelHeight)
6271347.60 = 6289823.9 - 121 * 152.6966863522580127
  • Lastly, I converted the coordinates from Web Mercator to WGS84 using XYConverter.
Longitude, Latitude = -123.1464948, 48.9792872
  • Results were promising!

Vessel Location Library

I’m going to glaze over this a bit since the vessel location library is more or less an automated version of the detailed steps listed above. If you’re interested in what or how it was done, the source code is on GitHub.

Accessing the Vessel Locations

The final step before creating the web map was to make these vessel locations readily available to the map. Using an AWS Lambda function with the help of Zappa to help with the packaging and deploy, I exposed an endpoint that would fetch vessel location details by a ferry route number. It looks something like this…

from flask import Flask
from flask import request
from flask import jsonify
from bcferries_location.ferries import *
from flask_cors import CORS


app = Flask(__name__)
CORS(app)


@app.route("/", methods=["OPTIONS", "GET"])
def fetch_route_details():
    route = None
    try:
        route = int(request.args.get("route"))
    except:
        response = jsonify("{} not supported".format(route))
        response.status_code = 400
        return response

    vessels = None
    try:
        vessels = fetch_route(route)
        return jsonify([v.__json__() for v in vessels])
    except RouteNotAvailableError:
        err_msg = "Route not currently available"
    except RouteSourceError:
        err_msg = "Route source error"
    except TemporarilyOfflineError: 
        err_msg = "Route temporarily offline"
    except RoutePageFormatError:
        err_msg = "Unrecognized route page format"
    except Exception as e:
        print('error: {}'.format(e))
        err_msg = "Unknown error"
        
    response = jsonify(err_msg)
    response.status_code = 500
    return response

and outputs this…

[
    {
        "coords": [-123.87019380303718, 49.15511931155766],
        "destination": "Duke Point",
        "heading": "S",
        "name": "Queen of New Westminster",
        "route": "Tsawwassen - Duke Point",
        "speed": "16.1 knots"
    },
    {
        "coords": [-123.12997486261628, 48.99348536042082],
        "destination": "Tsawwassen",
        "heading": "E",
        "name": "Coastal Inspiration",
        "route": "Tsawwassen - Duke Point",
        "speed": "7.9 knots"
    }
]

Vessel Map

Hey, we're finally here! This map is pulling vessel locations from 13 different routes.

If you happen to be reading this between midnight and 6am PST and you're noticing the vessels on the map aren't moving, it's likely they are done running for the night and you'll have to check back later.