< Back

Plot a Multiple Segment Flight Path with LeafletJS and OpenStreetMap

Recently, I got to play around with LeafletJS and OpenStreetMap when building an interactive flight path map Free World — an awesome travel community. Originally, we were only showing two data points, starting and ending coordinates. However, as our project morphed to support multi-segment journeys we needed our map to become more intelligent. Our goal was to produce a sweet looking segmented map like the this:

So, open up your editor and let’s get started

Reference the Leaflet CDN (or download it)

<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css">
<script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>

Add a div element where your map will be rendered

<div id="results-map"></div>

One note, the Leaflet stylesheet will add all the internal map styles, but depending on your layout, you may want to add your own height and width settings.

Define your segment datasource

In our case, I was pulling data from a MySQL database and JSON-encoding it via PHP. We then exposed the JSON array directly into a script block in the rendered HTML.

For this example, I’ll assume you’ll have a JSON array that looks like this:

var data = [
{
    "from":{"city":"Budapest","iata":"BUD","lat":"47.436933000000","lng":"19.25559200000000"},
    "to":{"city":"Warsaw","iata":"WAW","lat":"52.165750000000","lng":"20.96712200000000"}
},
{
    "from":{"city":"Warsaw","iata":"WAW","lat":"52.165750000000","lng":"20.96712200000000"},
    "to":{"city":"Chicago","iata":"ORD","lat":"41.978603000000","lng":"-87.90484200000000"}
},
{
    "from":{"city":"Chicago","iata":"ORD","lat":"41.978603000000","lng":"-87.90484200000000"},
    "to":{"city":"San Francisco","iata":"SFO","lat":"37.618972000000","lng":"-122.37488900000000"}
}
];

As you notice, each segment has a “from” and “to” object, which stores the GPS coordinates and airport details.

Write the Javascript rendering code

This is the guts of the feature where we interact with Leaflet’s API and create our segment rendering code. I’ll run through it function by function.

1. Wrapper
var MapBuilder = function () {

    var map;
    
    // internal functions will go here

    return {
    init: function () {

    }
  };
}();

The majority of our code will replace the internal functions comment. The “init” function is the entry point to get the whole process started.

2. Initialize the map
  var buildMap = function(from) {
        // determine starting coordinates
    var from_coord = new L.LatLng(from.lat, from.lng);

    // initialize map based on #results-map HTML div
    map = L.map('results-map', { zoomControl: false, worldCopyJump: true }).setView([from_coord.lat, from_coord.lng], 10);
  };

As you can see, we’ve created a new Leaflet map linked to the #results-map div in your HTML.

3. Build out the segments
  var buildSegment = function (from, to) {
    // build Leaflet coordinates from GPS
    var from_coord = new L.LatLng(from.lat, from.lng);
    var to_coord = new L.LatLng(to.lat, to.lng);

    // iron out longitude wrapping
    if (Math.abs(to_coord.lng - to_coord.lng) > 180) {
      to_coord = to_coord.wrap(179, -179);
    }

    // build arc coordinates
    var from_arc_coord = new arc.Coord(from_coord.lng, from_coord.lat);
    var to_arc_coord = new arc.Coord(to_coord.lng, to_coord.lat);

    // render our Great Circle
    var gc = new arc.GreatCircle(from_arc_coord, to_arc_coord, {'name': ''});
    var line = gc.Arc(150); // specify number of segments, higher = smoother

    // build Leaflet-friendly points array
    var points = [];

    for (var i = 0; i < line.geometries[0].coords.length; i++) {
      points.push(new L.LatLng(line.geometries[0].coords[i][1], line.geometries[0].coords[i][0]));
    }

    // draw polyline with blue color
    var polyline = new L.Polyline(points, {
      color: 'blue',
      weight: 2,
      opacity: 1,
      smoothFactor: 1
    }).addTo(map);

    // add markers showing the airport codes
    L.marker(from_coord).bindLabel(from.iata, { noHide: true }).addTo(map);
    L.marker(to_coord).bindLabel(to.iata, { noHide: true }).addTo(map);
  };

The bulk of the work is done here. Each segment in our JSON array will be passed into this function, which then renders the segment's flight path. The flight path is a series of plotted points along the Great Circle arc — that's rendered as a blue polyline. For extra flair, we finish by adding airport IATA labels to each of the segment's airports.

4. Update map bounds
  var fitMapBounds = function(from, to) {
    var from_coord = new L.LatLng(from.lat, from.lng);
    var to_coord = new L.LatLng(to.lat, to.lng);

    map.fitBounds([from_coord, to_coord], { 'padding': [50, 50]});
  };

Since we've added multiple segments, we need to re-center the bounds of the map to account for the final start and end points.

5. Init
  return {
    init: function () {
      // create Leaflet map
      buildMap(data.segments[0]);

      // add segments
      for(i in data.segments) {
        buildSegment(data.segments[i].from, data.segments.to);
      }

      // make sure bounds work based on start and final end coordinates
      fitMapBounds(data.segments[0], data.segments[2]);
    }
  };

The init function interacts with all of our internals described above. It loops through the JSON data's segments calling the buildSegment function on each pass and finally updates the map's bounds.

Our final step is to trigger this whole process. We do this by calling the MapBuilder when our javascript is ready to execute shown here:

(function() {
  MapBuilder.init();
})();
Here's the complete JS source:
/**
Builds LeafletJS map based on segment coordinates
 **/
var MapBuilder = function () {

  var map;

  var buildMap = function(from) {
    var from_coord = new L.LatLng(from.lat, from.lng);

    // init map
    map = L.map('results-map', { zoomControl: false, worldCopyJump: true }).setView([from_coord.lat, from_coord.lng], 10);

    // create the tile layer with correct attribution
    var osmUrl = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
    var osmAttrib = 'Map data © OpenStreetMap contributors';
    var osm = new L.TileLayer(osmUrl, { attribution: osmAttrib });

    map.addLayer(osm);
  };

  var buildSegment = function (from, to) {
    // build Leaflet coordinates from GPS
    var from_coord = new L.LatLng(from.lat, from.lng);
    var to_coord = new L.LatLng(to.lat, to.lng);

    // iron out longitude wrapping
    if (Math.abs(to_coord.lng - to_coord.lng) > 180) {
      to_coord = to_coord.wrap(179, -179);
    }

    // build arc coordinates
    var from_arc_coord = new arc.Coord(from_coord.lng, from_coord.lat);
    var to_arc_coord = new arc.Coord(to_coord.lng, to_coord.lat);

    // render our Great Circle
    var gc = new arc.GreatCircle(from_arc_coord, to_arc_coord, {'name': ''});
    var line = gc.Arc(150); // specify number of segments, higher = smoother

    // build Leaflet-friendly points array
    var points = [];

    for (var i = 0; i < line.geometries[0].coords.length; i++) {
      points.push(new L.LatLng(line.geometries[0].coords[i][1], line.geometries[0].coords[i][0]));
    }

    // draw polyline with blue color
    var polyline = new L.Polyline(points, {
      color: 'blue',
      weight: 2,
      opacity: 1,
      smoothFactor: 1
    }).addTo(map);

    // add markers showing the airport codes
    L.marker(from_coord).bindLabel(from.iata, { noHide: true }).addTo(map);
    L.marker(to_coord).bindLabel(to.iata, { noHide: true }).addTo(map);
  };

  var fitMapBounds = function(from, to) {
    var from_coord = new L.LatLng(from.lat, from.lng);
    var to_coord = new L.LatLng(to.lat, to.lng);

    map.fitBounds([from_coord, to_coord], { 'padding': [50, 50]});
  };

  return {
    init: function () {
      // create Leaflet map
      buildMap(data.segments[0]);

      // add segments
      for(i in data.segments) {
        buildSegment(data.segments[i].from, data.segments.to);
      }

      // make sure bounds work based on start and final end coordinates
      fitMapBounds(data.segments[0], data.segments[2]);
    }
  };

}();

// init MapBuilder when page is ready
(function() {
  MapBuilder.init();
})();

Connect with us

Thank You!

We really appreciate your interest in what we do.

We'll get back to you as soon as we can.