Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Details

Privacy

Go PRO Window blinds lowered to protect code. Code Editor with window blinds (raised) and a light blub turned on.

Keep it secret; keep it safe.

Private Pens are hidden everywhere on CodePen, except to you. You can still share them and other people can see them, they just can't find them through searching or browsing.

Upgrade to PRO

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

Template

Make Template?

Templates are Pens that can be used to start other Pens. You can view all of your templates, or learn more in the documentation.

Screenshot

Screenshots of Pens are shown in mobile browsers, RSS feeds, to users who chose images instead of iframes, and in social media sharing.

Uploading

This Pen is using the default Screenshot, generated by CodePen. Upgrade to PRO to upload your own thumbnail that will be displayed on previews of this pen throughout the site and when sharing to social media.

Upgrade to PRO

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

HTML

            
              <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAXiHCkp2p8cPGz6mLJC_huOf14DIuPSV8&v=3"></script>
<section class="controls">
  <section class="parameters">
    <h3>Parameters</h3>
    <fieldset>
      <legend>Travel details</legend>
      <p>How far can I go in <input id="input-time" type="text" value="0:30" size="5" pattern="^\d+:[0-5][0-9]$" placeholder="HH:MM" required /> by <select id="select-travel-mode">
            <option value="driving">car</option>
            <option value="bicycling">bicycle</option>
            <option value="transit" selected>public transit</option>
            <option value="walking">walking</option>
          </select>?
      </p>
      <p>
        <label>Starting location:
          <br/>
          <input id="input-location-string" type="text" placeholder="drag the red marker" disabled />
        </label>
      </p>
    </fieldset>
    <fieldset>
      <legend>Area of interest</legend>
      <p>
        <label>Northeast corner:
          <br/>
          <input id="input-bounds-NE-string" type="text" placeholder="scroll to an area of interest" disabled/>
        </label>
      </p>
      <p>
        <label>Sothwest corner:
          <br/>
          <input id="input-bounds-SW-string" type="text" placeholder="scroll to an area of interest" disabled/>
        </label>
      </p>
    </fieldset>
    <fieldset>
      <legend>Quadtree details</legend>
      <p>
        <label> Minimum subdivisions:
          <br/>
          <input id="input-minLevel" type="number" value="1" min="0" steps="1" max="3" required />
        </label>
      </p>
      <p>
        <label> Depth:
          <br/>
          <input id="input-maxLevel" type="number" value="3" min="0" steps="1" max="5" required />
        </label>
      </p>
    </fieldset>
    <fieldset>
      <legend>Import parameters</legend>
      <textarea id="textarea-import"></textarea>
      <br/>
      <button id="button-import-parameters">Import from JSON</button>
    </fieldset>
  </section>
  <section>
    <p>
      <button id="button-export-parameters">Export parameters as JSON</button>
    </p>
    <p>
      <button id="button-run">Generate isochrone</button>
    </p>
  </section>
  <section>
    <h3>Output</h3>
    <p>
      <progress id="progress-output" value="0" max="2"></progress>
    </p>
    <p>
      <label>Data in JSON format:
        <br/>
        <textarea id="textarea-output"></textarea>
      </label>
    </p>
  </section>
</section>
<section class="map-container">
  <div id="map" class="max-size"></div>
  <section class="notices">
    <h4>Copyright notices</h4>
    <p id="p-output-copyrights">The copyright notice for the routing service will go here.</p>
    <h4>Warnings for some routes</h4>
    <p id="p-output-warnings">So far, there are no warnings for any routes.</p>
    <h4>Contact details for transit agencies</h4>
    <p id="p-output-agencies">If the transport mode “public transit” is selected, contact details will appear here.</p>
  </section>
</section>
            
          
!

CSS

            
              body {
  margin: 0;
  display: flex;
  flex-flow: row;
}

.controls {
  flex: 0 0 auto;
  padding: 8px;
  box-sizing: border-box;
  height: 100vh;
  overflow-y: scroll;
}

.controls textarea {
  resize: none;
}

.map-container {
  flex: 1 0 auto;
  height: 100vh;
  display: flex;
  flex-flow: column;
  justify-content: center;
  align-items: center;
}

.max-size {
  height: 70vmin;
  width: 70vmin;
}

.notices {
  font-size: xx-small;
  overflow-y: auto;
  height: 20vmin;
  width: 70vmin;
}
            
          
!

JS

            
              var ICON_SPRITE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAIAQMAAADZb60gAAAABlBMVEUAAAD///+l2Z/dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wsUDA4ixe5gjQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAgSURBVAjXY2D4z8Ag8dyJQeW7ChBLQLEKg8R3JwagHACWhAl1O9gwuQAAAABJRU5ErkJggg==";

//  directionsRequest manager to help queuing of requests, filled in init fn
var dirReqManager = {};

//  will become a google.maps.Map object in an init function
var map;

//  DOM elements, listeners added in init function
var ctrlElements = {
  runBtn: document.getElementById("button-run"),
  exportParametersBtn: document.getElementById("button-export-parameters"),
  importParametersBtn: document.getElementById("button-import-parameters")
};

var inputElements = {
  time: document.getElementById("input-time"),
  minLevel: document.getElementById("input-minLevel"),
  maxLevel: document.getElementById("input-maxLevel"),
  selectTravelMode: document.getElementById("select-travel-mode"),
  locationString: document.getElementById("input-location-string"),
  boundsNEString: document.getElementById("input-bounds-NE-string"),
  boundsSWString: document.getElementById("input-bounds-SW-string"),
  locationMarker: null,
  importTextarea: document.getElementById("textarea-import")
};

var outputElements = {
  textarea: document.getElementById("textarea-output"),
  progressBar: document.getElementById("progress-output"),
  copyrights: document.getElementById("p-output-copyrights"),
  warnings: document.getElementById("p-output-warnings"),
  agencies: document.getElementById("p-output-agencies"),
};

//  init functions

function initEventListeners() {
  //  run
  ctrlElements.runBtn.addEventListener("click", function runBtnClicked() {
    dirReqManager.start();
    generateIsochrone(readParameters(), function IsochroneGenerated(result) {
      // set display style of GeoJson on a google map
      map.data.setStyle(function styleFn(feature) {
        var isochronesFeature = map.data.getFeatureById("isochrones");
        var timeConstraint = isochronesFeature.getProperty("travelDuration");
        if (feature.getId() === "startingPoint") {
          var travelMode = feature.getProperty("travelMode").toLowerCase();
          var timeText = ". Time constraint: " + secondsToHHMM(timeConstraint);
          return {
            icon: {
              url: ICON_SPRITE,
              size: new google.maps.Size(8, 8),
              origin: new google.maps.Point(16, 0),
              anchor: new google.maps.Point(4, 4)
            },
            title: "Starting point. Travel mode: " + travelMode + timeText
          };
        } else if (feature.getId() === "bounds") {
          return {
            visible: false
          };
        } else if (feature.getId() === "isochrones") {
          return {
            strokeColor: "#00f"
          };
        } else if (feature.getGeometry().getType() === "Point") {
          var duration = feature.getProperty("travelDuration");
          var timeShown = (isNaN(duration)) ? duration : secondsToHHMM(duration);
          var styleOptions = {
            icon: {
              url: ICON_SPRITE,
              size: new google.maps.Size(8, 8),
              origin: new google.maps.Point(8, 0),
              anchor: new google.maps.Point(4, 4)
            },
            title: "Travel duration: " + timeShown
          };
          if ((duration > timeConstraint) || isNaN(duration)) {
            //  sprite for "1"
            styleOptions.icon.origin = new google.maps.Point(8, 0);
            return styleOptions;
          } else {
            //  sprite for "0"
            styleOptions.icon.origin = new google.maps.Point(0, 0);
            return styleOptions;
          }
        }
      });

      // GeoJson on map and as text output
      map.data.addGeoJson(result);
      outputElements.textarea.value = JSON.stringify(result);
      dirReqManager.stop();
      dirReqManager.composeMessages();
    });
  });

  //  import and export
  ctrlElements.exportParametersBtn.addEventListener("click", exportParameters);
  ctrlElements.importParametersBtn.addEventListener("click", importParameters);

  //  show bounds and location parameters to the user
  map.addListener("bounds_changed", function boundsChanged() {
    inputElements.boundsNEString.value = map.getBounds().getNorthEast();
    inputElements.boundsSWString.value = map.getBounds().getSouthWest();
  });
  var marker = inputElements.locationMarker;
  marker.addListener("position_changed", function markerChanged() {
    inputElements.locationString.value = marker.getPosition();
  });
}

//  initMap() is executed on API load, so it's the first thing to execute
function initMap() {
  map = new google.maps.Map(document.getElementById("map"), {
    center: {
      lat: 51.51338981,
      lng: -0.08898497
    },
    zoom: 11
  });
  inputElements.locationMarker = new google.maps.Marker({
    position: {
      lat: 51.51338981,
      lng: -0.08898497
    },
    map: map,
    draggable: true,
    title: "Location"
  });

  initEventListeners();
  initDirRequestManager();
}

function initDirRequestManager() {
  dirReqManager.service = new google.maps.DirectionsService();
  dirReqManager.queue = [];
  dirReqManager.results = [];

  dirReqManager.start = function() {
    dirReqManager.stop(); //  to ensure only one is running at a time
    dirReqManager.timer = setInterval(function intervalComplete() {
      if (dirReqManager.queue.length > 0) {
        dirReqManager.processNextRequest();
      }
    }, 250);
  };
  dirReqManager.stop = function() {
    clearInterval(dirReqManager.timer);
  };
  dirReqManager.counters = {
    reusedRequests: 0,
    sentRequests: 0,
    successfulRequests: 0,
    overQueryLimit: 0
  };
  dirReqManager.processNextRequest = function() {
    var requestObject = dirReqManager.queue.shift();
    var req = requestObject.request;
    var cb = requestObject.callback;
    var resultID;

    //  checks if a past result can be used
    function isRepeatedRequest(request) {
      for (var i = 0; i < dirReqManager.results.length; i++) {
        var oldReq = dirReqManager.results[i].request;
        //  possibly in the future: departureTime, trafficModel, transitOptions
        if (
          oldReq.travelMode === request.travelMode &&
          (oldReq.origin.lat === request.origin.lat) &&
          (oldReq.origin.lng === request.origin.lng) &&
          (oldReq.destination.lat === request.destination.lat) &&
          (oldReq.destination.lng === request.destination.lng)
        ) {
          resultID = i;
          return true;
        }
      }
      return false;
    }
    if (isRepeatedRequest(req)) {
      dirReqManager.counters.reusedRequests++;
      cb(resultID);
    } else {
      dirReqManager.counters.sentRequests++;
      dirReqManager.service.route(req, function reqAnswered(result, status) {
        switch (status) {
          case "OK":
          case "ZERO_RESULTS":
            resultID = dirReqManager.results.length;
            dirReqManager.results[resultID] = result;
            dirReqManager.counters.successfulRequests++;
            cb(resultID);
            break;
          case "OVER_QUERY_LIMIT":
            dirReqManager.stop(); //  stop, wait a bit, then resume
            dirReqManager.counters.overQueryLimit++;
            dirReqManager.queue.unshift(requestObject);
            setTimeout(dirReqManager.start, 1000);
            break;
          default: // all other errors
            dirReqManager.stop();
            console.error(status);
            break;
        }
      });
    }
  };
  dirReqManager.composeMessages = function() {
    var messages = {
      copyrights: [],
      warnings: [],
      transitAgencies: []
    };
    for (var i = 0; i < dirReqManager.results.length; i++) {
      if (dirReqManager.results[i].status === "OK") {
        var route = dirReqManager.results[i].routes[0];
        if (messages.copyrights.indexOf(route.copyrights) === -1) {
          messages.copyrights.push(route.copyrights);
        }
        for (var j = 0; j < route.warnings.length; j++) {
          if (messages.warnings.indexOf(route.warnings[j]) === -1) {
            messages.warnings.push(route.warnings[j]);
          }
        }
        for (var k = 0; k < route.legs[0].steps.length; k++) {
          if (route.legs[0].steps[k].transit) {
            var agencies = route.legs[0].steps[j].transit.line.agencies;
            for (var l = 0; l < agencies.length; l++) {
              var aTag = "<a href=\"" + agencies[l].url + "\" >";
              var link = aTag + agencies[l].name + "</a>";
              if (messages.transitAgencies.indexOf(link) === -1) {
                messages.transitAgencies.push(link);
              }
            }
          }
        }
      }
    }
    if (messages.copyrights.length > 0) {
      var copyrightsMessage = messages.copyrights.join("<br/>\n");
      outputElements.copyrights.innerHTML = copyrightsMessage;
    }
    if (messages.warnings.length > 0) {
      var warningsMessage = messages.warnings.join("<br/>\n");
      outputElements.warnings.innerHTML = warningsMessage;
    }
    if (messages.transitAgencies > 0) {
      var agenciesMessage = messages.transitAgencies.join("<br/>\n");
      outputElements.agencies.innerHTML = agenciesMessage;
    }
  };
}

//  some utilities

function HHMMToSeconds(timeString) {
  var hours = parseInt(timeString.split(":")[0], 10);
  var minutes = parseInt(timeString.split(":")[1], 10);
  return 3600 * hours + 60 * minutes;
}

function secondsToHHMM(numberOfSeconds) {
  var hours = Math.floor(numberOfSeconds / 3600);
  var minutes = Math.floor((numberOfSeconds % 3600) / 60);
  if (minutes < 10) {
    minutes = "0".concat(minutes);
  }
  return hours + ":" + minutes;
}

function latLngToCart(p) {
  //  degrees -> radians (for the trig fucntions) -> x,y,z
  var DEGREE_TO_RAD = Math.PI / 180;
  var pRad = {
    lat: p.lat * DEGREE_TO_RAD,
    lng: p.lng * DEGREE_TO_RAD
  };
  var pCart = [
    Math.cos(pRad.lat) * Math.cos(pRad.lng),
    Math.cos(pRad.lat) * Math.sin(pRad.lng),
    Math.sin(pRad.lat)
  ];
  return pCart;
}

function cartToLatLng(pCart) { // pCart[1,2,3] = x,y,z
  var RAD_TO_DEGREE = 180 / Math.PI;
  //  hyp: dist from earth centre to a point directly above/below p
  //  hyp = (x^2 + y^2)^0.5
  var hyp = Math.sqrt(pCart[0] * pCart[0] + pCart[1] * pCart[1]);
  var pRad = {
    lat: Math.atan2(pCart[2], hyp), //  atan2(z, hyp)
    lng: Math.atan2(pCart[1], pCart[0]) //  atan2(y, x)
  };
  var p = {
    lat: pRad.lat * RAD_TO_DEGREE,
    lng: pRad.lng * RAD_TO_DEGREE
  };
  return p;
}

function avgOfLatLngs(p1, p2, weight1) {
  weight1 = (!weight1) ? 0.5 : weight1; //  weighting optional, default 0.5
  //  simple average would lead to weird things around the date line
  //  so we convert to Cartesian coords -> take the average -> convert back
  var p1Cart = latLngToCart(p1);
  var p2Cart = latLngToCart(p2);
  var pAvgCart = []; // average in Cartesian coordinates
  for (var i = 0; i < 3; i++) {
    pAvgCart[i] = weight1 * p1Cart[i] + (1 - weight1) * p2Cart[i];
  }
  var pAvg = cartToLatLng(pAvgCart);
  return pAvg;
}

//  reading, importing, exporting input

function readParameters() {
  var input = {};
  var mapCornerNE = map.getBounds().getNorthEast(); //  helpers
  var mapCornerSW = map.getBounds().getSouthWest();
  input.mapBounds = {
    north: mapCornerNE.lat(),
    east: mapCornerNE.lng(),
    south: mapCornerSW.lat(),
    west: mapCornerSW.lng()
  };
  input.location = {
    lat: inputElements.locationMarker.position.lat(),
    lng: inputElements.locationMarker.position.lng()
  };
  input.timeConstraint = HHMMToSeconds(inputElements.time.value);
  switch (inputElements.selectTravelMode.value) {
    case "driving":
      input.travelMode = google.maps.TravelMode.DRIVING;
      break;
    case "bicycling":
      input.travelMode = google.maps.TravelMode.BICYCLING;
      break;
    case "transit":
      input.travelMode = google.maps.TravelMode.TRANSIT;
      break;
    case "walking":
      input.travelMode = google.maps.TravelMode.WALKING;
      break;
    default:
      input.travelMode = google.maps.TravelMode.DRIVING;
      break;
  }
  input.minLevel = parseInt(inputElements.minLevel.value, 10);
  input.maxLevel = parseInt(inputElements.maxLevel.value, 10);
  return input;
}

//  does not set inputs directly, instead sets UI elements to imported values
//  this is so that imported parameters can still be modified
function importParameters() {
  var mapElement = document.getElementById("map");
  var importedInput = JSON.parse(inputElements.importTextarea.value);
  map.panTo(importedInput.mapCentre);
  map.setZoom(importedInput.mapZoom);
  inputElements.locationMarker.setPosition(importedInput.location);
  inputElements.time.value = secondsToHHMM(importedInput.timeConstraint);
  switch (importedInput.travelMode) {
    case "DRIVING":
      inputElements.selectTravelMode.options[0].selected = true;
      break;
    case "BICYCLING":
      inputElements.selectTravelMode.options[1].selected = true;
      break;
    case "TRANSIT":
      inputElements.selectTravelMode.options[2].selected = true;
      break;
    case "WALKING":
      inputElements.selectTravelMode.options[3].selected = true;
      break;
    default:
      inputElements.selectTravelMode.options[0].selected = true;
      break;
  }
  inputElements.minLevel.value = importedInput.minLevel;
  inputElements.maxLevel.value = importedInput.maxLevel;
}

function exportParameters() {
  var currentInput = readParameters();
  //  mapBounds cannot be imported, so we settle for dims+centre+zoom instead
  exportInput = {
    mapCentre: {
      lat: map.getCenter().lat(),
      lng: map.getCenter().lng()
    },
    mapZoom: map.getZoom(),
    location: currentInput.location,
    timeConstraint: currentInput.timeConstraint,
    travelMode: currentInput.travelMode,
    minLevel: currentInput.minLevel,
    maxLevel: currentInput.maxLevel
  };
  outputElements.textarea.value = JSON.stringify(exportInput);
}

//  main function

function generateIsochrone(input, callback) {
  var durationPoints = [];

  function saveDuration(dest, callback) {
    var duration; //  in seconds, to be found out below
    for (var i = 0; i < durationPoints.length; i++) {
      if (
        dest.lat === durationPoints[i][0].lat &&
        dest.lng === durationPoints[i][0].lng
      ) {
        duration = durationPoints[i][1];
        break;
      }
    }
    if (duration) { //  if duration was assigned already
      callback(duration);
    } else {
      var request = {
        origin: input.location,
        destination: dest,
        travelMode: input.travelMode
      };
      dirReqManager.queue.push({
        request: request,
        callback: function reqAnswered(rID) {
          var res = dirReqManager.results[rID];
          if (res.status === "OK") {
            duration = res.routes[0].legs[0].duration.value;
          } else if (res.status === "ZERO_RESULTS") {
            duration = "no route";
          }
          durationPoints.push([dest, duration]);
          callback(duration);
        }
      });
    }
  }

  //  square constructor
  function Square(bnds) {
    this.bounds = bnds;
    this.level = 0; //  0 for root, +1 for each subdivision
    this.edges = []; // edges to other squares through which the contour passes
    this.corners = []; // corners in NE-SE-SW-NW order
    this.corners[0] = {
      pos: { // NE
        lat: this.bounds.north,
        lng: this.bounds.east
      },
      value: null //  to be filled later
    };
    this.corners[1] = {
      pos: { // SE
        lat: this.bounds.south,
        lng: this.bounds.east
      },
      value: null
    };
    this.corners[2] = {
      pos: { // ...
        lat: this.bounds.south,
        lng: this.bounds.west
      },
      value: null
    };
    this.corners[3] = {
      pos: {
        lat: this.bounds.north,
        lng: this.bounds.west
      },
      value: null
    };

    //  creates 4 children squares
    this.subdivide = function() {
      this.children = []; //  array child squares in NE-SE-SW-NW order
      var centre = {};
      centre.lat = avgOfLatLngs(this.corners[0].pos, this.corners[1].pos).lat;
      centre.lng = avgOfLatLngs(this.corners[1].pos, this.corners[2].pos).lng;
      var childBnds = []; //  helper array to make all the LatLngBounds handier
      //  not sure how to write shorter, WIP
      childBnds[0] = {
        north: this.corners[0].pos.lat,
        east: this.corners[0].pos.lng,
        south: centre.lat,
        west: centre.lng
      };
      childBnds[1] = {
        north: centre.lat,
        east: this.corners[1].pos.lng,
        south: this.corners[1].pos.lat,
        west: centre.lng
      };
      childBnds[2] = {
        north: centre.lat,
        east: centre.lng,
        south: this.corners[2].pos.lat,
        west: this.corners[2].pos.lng
      };
      childBnds[3] = {
        north: this.corners[3].pos.lat,
        east: centre.lng,
        south: centre.lat,
        west: this.corners[3].pos.lng
      };

      for (var i = 0; i < 4; i++) {
        this.children[i] = new Square(childBnds[i]);
        this.children[i].level = this.level + 1;
      }
    };
    this.getCornerValues = function(callback) {
      var that = this;
      var k = 0; // number of responses so far
      function getCornerValue(corner, cb) {
        if (!corner.value) {
          saveDuration(corner.pos, function(result) {
            corner.value = result;
            cb();
          });
        } else cb();
      }

      function cornerValueSaved() {
        k++;
        if (k === 4) callback();
      }
      for (var i = 0; i < 4; i++) {
        getCornerValue(this.corners[i], cornerValueSaved);
      }
    };
    this.contourCase = function(c) {
      var n = 0;
      for (var i = 0; i < 4; i++) {
        var cornerValue = this.corners[i].value;
        if ((cornerValue > c) || isNaN(cornerValue)) { // isNaN when "no route"
          n += 1 << i; // #1,#2,#3,#4 bit in base2 for NW,SW,SE,NE
        }
      }
      return n;
    };
  }

  //  recursive traversal function
  var activeSquares = 0; // counter to keep track of number of active squares
  function buildTreeFrom(sq, callback) {
    activeSquares++;
    outputElements.progressBar.max++;
    if (sq.level < input.minLevel) {
      sq.subdivide();
      for (var i = 0; i < 4; i++) {
        buildTreeFrom(sq.children[i], callback);
      }
      activeSquares--;
      outputElements.progressBar.value++;
      if (activeSquares === 0) {
        callback();
      }
    } else {
      sq.rect = new google.maps.Rectangle();
      sq.rect.setOptions({
        map: map,
        bounds: sq.bounds,
        fillColor: "#00f"
      });
      sq.getCornerValues(function gotCornerValues() {
        sq.cc = sq.contourCase(input.timeConstraint);
        var isBoundary = (sq.cc !== 0) && (sq.cc !== 15);
        if (isBoundary) {
          isAmbiguity = (sq.cc == 5) || (sq.cc == 10);
          if ((sq.level < input.maxLevel) || isAmbiguity) {
            sq.subdivide();
            for (var i = 0; i < 4; i++) {
              buildTreeFrom(sq.children[i], callback);
            }
          }
        }
        sq.rect.setOptions({
          fillOpacity: 0,
          strokeWeight: 1,
          strokeOpacity: 0.25
        });
        activeSquares--;
        outputElements.progressBar.value++;
        if (activeSquares === 0) {
          callback();
        }
      });
    }
  }

  function findEdges() {
    var edges = [];

    function connectChildrenH(sq) {
      connectNeighboursH(sq.children[0], sq.children[3]);
      connectNeighboursH(sq.children[1], sq.children[2]);
    }

    function connectChildrenV(sq) {
      connectNeighboursV(sq.children[0], sq.children[1]);
      connectNeighboursV(sq.children[3], sq.children[2]);
    }

    function connectNeighboursH(sqE, sqW) { //  horizontally
      if (!sqE.children && !sqW.children) {
        //  3d(SW) or 4th(NW) corner > timeConstraint, but not both
        var sqEEdge = Boolean(sqE.cc & 4) ^ Boolean(sqE.cc & 8);
        //  NE, SE
        var sqWEdge = Boolean(sqW.cc & 1) ^ Boolean(sqW.cc & 2);
        var isContourEdge = sqEEdge || sqWEdge;
        if (isContourEdge) {
          edges.push([sqE, sqW, sqEEdge, sqWEdge, "EW"]);
        }
      } else if (!sqE.children) {
        connectNeighboursH(sqE, sqW.children[0]);
        connectNeighboursH(sqE, sqW.children[1]);
        connectChildrenH(sqW);
      } else if (!sqW.children) {
        connectNeighboursH(sqE.children[2], sqW);
        connectNeighboursH(sqE.children[3], sqW);
        connectChildrenH(sqE);
      } else {
        connectNeighboursH(sqE.children[2], sqW.children[1]);
        connectNeighboursH(sqE.children[3], sqW.children[0]);
        connectChildrenH(sqE);
        connectChildrenH(sqW);
      }
    }

    function connectNeighboursV(sqN, sqS) { //  vertically
      if (!sqN.children && !sqS.children) { //  two leaf nodes
        //  2nd(SE) or 3rd SW) corner < timeConstraint, but not both
        var sqNEdge = Boolean(sqN.cc & 2) ^ Boolean(sqN.cc & 4);
        //  contour goes through 1st(NE) or 4th(NW) corner
        var sqSEdge = Boolean(sqS.cc & 1) ^ Boolean(sqS.cc & 8);
        var isContourEdge = sqNEdge || sqSEdge;
        if (isContourEdge) {
          edges.push([sqN, sqS, sqNEdge, sqSEdge, "NS"]);
        }
      } else if (!sqN.children) {
        connectNeighboursV(sqN, sqS.children[0]);
        connectNeighboursV(sqN, sqS.children[3]);
        connectChildrenV(sqS);
      } else if (!sqS.children) {
        connectNeighboursV(sqN.children[1], sqS);
        connectNeighboursV(sqN.children[2], sqS);
        connectChildrenV(sqN);
      } else {
        connectNeighboursV(sqN.children[1], sqS.children[0]);
        connectNeighboursV(sqN.children[2], sqS.children[3]);
        connectChildrenV(sqN);
        connectChildrenV(sqS);
      }
    }
    connectChildrenH(rootSquare);
    connectChildrenV(rootSquare);
    return edges;
  }

  function fixEdges(edges, callback) {
    function notAllFixed() { //  used as callback until all edges are fixed.
      fixEdges(findEdges(), callback);
    }
    var squaresToRevisit = [];
    for (var i = 0; i < edges.length; i++) {
      var sq1 = edges[i][0]; //  N or E square of edge
      var sq2 = edges[i][1]; //  S or W
      var sq1Edge = edges[i][2];
      var sq2Edge = edges[i][3];
      if (sq1.level !== sq2.level) {
        var biggerSq;
        var biggerEdge;
        var smallerSq;
        var smallerEdge;
        if (sq1.level < sq2.level) { // lower level = greater size
          biggerSq = sq1;
          biggerEdge = sq1Edge;
          smallerSq = sq2;
          smallerEdge = sq2Edge;
        } else if (sq1.level > sq2.level) {
          biggerSq = sq2;
          biggerEdge = sq2Edge;
          smallerSq = sq1;
          smallerEdge = sq1Edge;
        }
        //  !biggerEdge: contour is only observed in smaller edge
        //  This is to avoid infinite propagation after 1 subdivision
        if ((squaresToRevisit.indexOf(biggerSq) === -1) && !biggerEdge) {
          squaresToRevisit.push(biggerSq);
        }
      }
    }
    if (squaresToRevisit.length === 0) { // if there are no bad edges left
      callback(edges);
    } else {
      for (var k = 0; k < squaresToRevisit.length; k++) {
        squaresToRevisit[k].subdivide();
        for (var n = 0; n < 4; n++) {
          buildTreeFrom(squaresToRevisit[k].children[n], notAllFixed);
        }
      }
    }
  }

  function findStrictEdges(edges) {
    function isKnownEdgeOf(sq1, sq2) {
      for (var i = 0; i < sq2.edges.length; i++) {
        if (sq1 === sq2.edges[i][0]) {
          return true;
        }
      }
      return false;
    }
    var strictEdges = [];
    for (var i = 0; i < edges.length; i++) {
      if (edges[i]) {
        var sq1 = edges[i][0];
        var sq2 = edges[i][1];
        var edge1 = edges[i][2];
        var edge2 = edges[i][3];
        var orientation = edges[i][4];
        if (edge1 && edge2) {
          strictEdges.push([sq1, sq2, orientation]);
          if (!isKnownEdgeOf(sq1, sq2)) {
            if (orientation === "NS") {
              sq2.edges.push([sq1, 0]);
            } else { // orientation === "EW"
              sq2.edges.push([sq1, 1]);
            }
          }
          if (!isKnownEdgeOf(sq2, sq1)) {
            if (orientation === "NS") {
              sq1.edges.push([sq2, 2]);
            } else { // orientation === "EW"
              sq1.edges.push([sq2, 3]);
            }
          }
        }
      }
    }
    return strictEdges;
  }

  function findContourSquares(edges) {
    var squares = [];
    for (var i = 0; i < edges.length; i++) {
      sq1 = edges[i][0];
      sq2 = edges[i][1];
      if (squares.indexOf(sq1) === -1) { //  only push() if not already in array
        squares.push(sq1);
      }
      if (squares.indexOf(sq2) === -1) {
        squares.push(sq2);
      }
    }
    return squares;
  }

  function findContourRowsOfSquares(contourSquares) {
    var rowsOfSquares = [];

    function isKnown(sq) {
      for (var m = 0; m < rowsOfSquares.length; m++) {
        if (rowsOfSquares[m].indexOf(sq) !== -1) {
          return true;
        }
      }
      return false;
    }

    function findStart() {
      for (var i = 0; i < contourSquares.length; i++) {
        if (!isKnown(contourSquares[i])) {
          return contourSquares[i];
        }
      }
    }

    function newLine() {
      var nextLine = [];
      nextLine[0] = findStart();
      if (nextLine[0]) {
        nextLine[1] = nextLine[0].edges[0][0];
        rowsOfSquares.push(nextLine);
        extendLine(nextLine);
      }
    }

    function extendLine(line) {
      currentSq = line[line.length - 1];
      lastSq = line[line.length - 2];
      if (currentSq === line[0]) { // closing circle (reaching start point)
        newLine();
      } else if (currentSq.edges.length === 1) { // end of line
        if (line[0].edges.length === 1) { //  both ends clear
          newLine();
        } else { // continue from other end
          line.reverse();
          extendLine(line);
        }
      } else if (currentSq.edges[0][0] === lastSq) { // middle piece
        line.push(currentSq.edges[1][0]);
        extendLine(line);
      } else if (currentSq.edges[1][0] === lastSq) {
        line.push(currentSq.edges[0][0]);
        extendLine(line);
      }
    }
    newLine();
    return rowsOfSquares;
  }

  function findContourEdgeBounds(contourRowsOfSquares) {
    var contourEdgeBounds = [];

    function findStartEdgeBounds(sq) {
      var nextSquareDirection = sq.edges[0][1]; //  0,1,2,3: N,E,S,W
      var e = []; //  if contour goes through N,E,S,W edge
      e[0] = Boolean(sq.cc & 1) ^ Boolean(sq.cc & 8);
      e[1] = Boolean(sq.cc & 1) ^ Boolean(sq.cc & 2);
      e[2] = Boolean(sq.cc & 2) ^ Boolean(sq.cc & 4);
      e[3] = Boolean(sq.cc & 4) ^ Boolean(sq.cc & 8);
      if (e[0] && (nextSquareDirection !== 0)) { // N
        return [sq.corners[0], sq.corners[3]];
      }
      for (var k = 1; k < 4; k++) {
        if (e[k] && (nextSquareDirection !== k)) { // k = edge orientation E,S,W
          return [sq.corners[k - 1], sq.corners[k]];
        }
      }
    }

    function findEdgeBounds(sq1, sq2) {
      var edgeDirection;
      if (sq1.edges[0][0] === sq2) {
        edgeDirection = sq1.edges[0][1];
      } else { // sq1.edges[1][0] === sq2
        edgeDirection = sq1.edges[1][1];
      }
      //  sq1.level higher => sq1 has more accurate values
      if (sq1.level >= sq2.level) {
        if (edgeDirection === 0) { // north case
          return [sq1.corners[0], sq1.corners[3]];
        }
        for (var m = 1; m < 4; m++) { //  E,S,W cases
          if (m === edgeDirection) {
            return [sq1.corners[m - 1], sq1.corners[m]];
          }
        }
      } else { // sq2.level higher => sq2 has more accurate values
        var reverseEdgeDirection = (edgeDirection + 2) % 4; //  N <-> S, E <-> W
        if (reverseEdgeDirection === 0) { //  north as seen from sq2
          return [sq2.corners[0], sq2.corners[3]];
        }
        for (var n = 1; n < 4; n++) {
          if (n === reverseEdgeDirection) {
            return [sq2.corners[n - 1], sq2.corners[n]];
          }
        }
      }
    }

    for (var i = 0; i < contourRowsOfSquares.length; i++) {
      var edgeBoundsList = [];
      var firstSq = contourRowsOfSquares[i][0];
      var lastSq = contourRowsOfSquares[i][contourRowsOfSquares[i].length - 1];
      //  connect to edge of map, but there is no connection square
      if (firstSq.edges.length === 1) {
        edgeBoundsList[0] = findStartEdgeBounds(firstSq);
      } else { // if closed loop, connect start and end
        edgeBoundsList[0] = findEdgeBounds(firstSq, lastSq);
      }
      for (var j = 0; j < contourRowsOfSquares[i].length - 1; j++) {
        var sq1 = contourRowsOfSquares[i][j];
        var sq2 = contourRowsOfSquares[i][j + 1];
        edgeBoundsList.push(findEdgeBounds(sq1, sq2));
      }
      if (lastSq.edges.length === 1) {
        edgeBoundsList.push(findStartEdgeBounds(lastSq));
      } else { // if the next connection is to starting square
        edgeBoundsList.push(findEdgeBounds(lastSq, firstSq));
      }
      contourEdgeBounds.push(edgeBoundsList);
    }
    return contourEdgeBounds;
  }

  function findContourLines(connectedEdgeBounds) {
    var contourValue = input.timeConstraint; // contour value
    var multiLineCoords = [];

    function interpolateLin(p1, v1, p2, v2, c) {
      var w1; //  weight for the average
      if (v1 === "no route") {
        w1 = 0.333; //  if one is unreachable, put point close to the other
      } else if (v2 === "no route") {
        w1 = 0.666; //  1/8 is somewhat arbitrary for "close"
      } else {
        w1 = 1 - (c - v1) / (v2 - v1); // as c -> v1, weighting -> 100% to p1
      }
      return avgOfLatLngs(p1, p2, w1);
    }

    for (var i = 0; i < connectedEdgeBounds.length; i++) {
      var lineCoords = [];
      for (var j = 0; j < connectedEdgeBounds[i].length; j++) {
        var pos1 = connectedEdgeBounds[i][j][0].pos;
        var value1 = connectedEdgeBounds[i][j][0].value;
        var pos2 = connectedEdgeBounds[i][j][1].pos;
        var value2 = connectedEdgeBounds[i][j][1].value;
        var coord = interpolateLin(pos1, value1, pos2, value2, contourValue);
        lineCoords.push(coord);
        //  lineCoords.push(avgOfLatLngs(pos1, pos2)); // for no interpolation
      }
      multiLineCoords.push(lineCoords);
    }
    return multiLineCoords;
  }

  function dataToGeoObj(points, lines) {
    var geoObj = {
      type: "FeatureCollection",
      features: [{
        id: "startingPoint",
        type: "Feature",
        properties: {
          travelMode: input.travelMode
        },
        geometry: {
          type: "Point", // careful: here the format is [lng, lat]
          coordinates: [input.location.lng, input.location.lat]
        }
      }, {
        id: "bounds",
        type: "Feature",
        properties: null,
        geometry: {
          type: "Polygon",
          coordinates: [
            [
              [rootSquare.corners[0].pos.lng, rootSquare.corners[0].pos.lat],
              [rootSquare.corners[1].pos.lng, rootSquare.corners[1].pos.lat],
              [rootSquare.corners[2].pos.lng, rootSquare.corners[2].pos.lat],
              [rootSquare.corners[3].pos.lng, rootSquare.corners[3].pos.lat],
              [rootSquare.corners[0].pos.lng, rootSquare.corners[0].pos.lat]
            ]
          ]
        }
      }, {
        id: "isochrones",
        type: "Feature",
        properties: {
          travelDuration: input.timeConstraint
        },
        geometry: {
          type: "MultiLineString",
          coordinates: []
        }
      }]
    };
    //  add all the points
    for (var k = 0; k < points.length; k++) {
      var geoJsonPoint = {
        type: "Feature",
        properties: {
          travelDuration: points[k][1]
        },
        geometry: {
          type: "Point",
          coordinates: [points[k][0].lng, points[k][0].lat]
        }
      };
      geoObj.features.push(geoJsonPoint);
    }
    //  add the contour lines
    for (var i = 0; i < lines.length; i++) {
      var linePoints = [];
      for (var j = 0; j < lines[i].length; j++) {
        var edgePoint = lines[i][j];
        linePoints.push([edgePoint.lng, edgePoint.lat]);
      }
      geoObj.features[2].geometry.coordinates.push(linePoints);
    }
    return geoObj;
  }

  //  initialise root square
  rootSquare = new Square(input.mapBounds);

  buildTreeFrom(rootSquare, function treeBuilt() {
    fixEdges(findEdges(), function edgesFixed(fixedEdges) {
      var strictContourEdges = findStrictEdges(fixedEdges);
      var contourSquares = findContourSquares(strictContourEdges);
      var contourSquareRows = findContourRowsOfSquares(contourSquares);
      var contourEdgeBounds = findContourEdgeBounds(contourSquareRows);
      var contourLines = findContourLines(contourEdgeBounds);
      var dataGeoObj = dataToGeoObj(durationPoints, contourLines);
      callback(dataGeoObj);
      //  finish the progress indicator
      outputElements.progressBar.value = outputElements.progressBar.max;
    });
  });
}

google.maps.event.addDomListener(window, 'load', initMap);
            
          
!
999px

Console