text: var app = (function (exports) {
    'use strict';
  
    function init(size, tileFactory) {
      // Initialize the tiles object
      const tiles = {};
  
      // Return methods for accessing and updating the tiles
      return {
        retrieve: (zxy) => getTileOrParent(zxy[0], zxy[1], zxy[2], 0, 0, size),
        prune,
        unrender,
        hideGroup,
        showGroup,
      };
  
      function getTileOrParent(
          z, x, y,     // Coordinates of the requested tile
          sx, sy, sw   // Cropping parameters--which part of the tile to use
          ) {
  
        // Retrieve the specified tile from the tiles object, add cropping info
        let id = z + "/" + x + "/" + y;
        let tile = tiles[id];
        let tilebox = { tile, sx, sy, sw };
  
        // If the tile exists and is ready, return it (along with the wrapped info)
        if (tile && tile.rendered) return tilebox;
  
        // Looks like the tile wasn't ready. Try using the parent tile
        if (z > 0 && sw > 1) { // Don't look too far back
          // Get coordinates and cropping parameters of the parent
          let pz = z - 1;
          let px = Math.floor(x / 2);
          let py = Math.floor(y / 2);
          let psx = sx / 2 + (x / 2 - px) * size;
          let psy = sy / 2 + (y / 2 - py) * size;
          let psw = sw / 2;
  
          tilebox = getTileOrParent(pz, px, py, psx, psy, psw); // recursive call!
        }
  
        // If the requested tile didn't exist, we need to order it from the factory
        // NOTE: orders are placed AFTER the recursive call for the parent tile,
        // so missing parents will be ordered first
        if (!tiles[id]) { 
          let newTile = tileFactory.create(z, x, y);
          if (newTile) tiles[id] = newTile;
        } else if (tile.loaded && !tile.rendered && !tile.rendering) { 
          // Tile exists but isn't rendered and is not in the middle of rendering
          // We now start redrawing it. Probably won't finish till later, BUT
          // what if it finishes right away? (i.e., just a recomposite or similar)
          tileFactory.redraw(tile);
        }
  
        return (tilebox && tilebox.tile && tilebox.tile.rendered)
          ? tilebox
          : undefined;
      }
  
      function prune(metric, threshold) {
        // Remove tiles far from current view (as measured by metric)
        var numTiles = 0;
  
        for ( let id in tiles ) {
          let tile = tiles[id];
          tile.priority = metric(tile.z, tile.x, tile.y);
          if (tile.priority < threshold) {
            numTiles ++;
            if (tile.rendering) tileFactory.priorities[tile.id] = tile.priority;
            continue;
          }
  
          // Tile is too far away. Cancel any ongoing work and delete it
          if (!tile.loaded) {
            console.log("rastermap cache: canceling load for tile " + id);
            tile.cancelLoad();
          }
          tile.canceled = true;
          delete tileFactory.priorities[tile.id];
          delete tiles[id];
        }
        return numTiles;
      }
  
      function unrender(group) {
        var groups = tileFactory.groups;
  
        var invalidate = 
          (groups.length <= 1)       ? invalidateTile
          : (group === undefined)    ? (tile, group) => invalidateAll(tile, groups)
          : (groups.includes(group)) ? invalidateGroup
          : () => true; // Bad group name. Do nothing
  
        Object.values(tiles).forEach( tile => invalidate(tile, group) );
      }
  
      function invalidateTile(tile, group) {
        tile.rendered = false;
      }
  
      function invalidateAll(tile, groups) {
        groups.forEach(group => tile.laminae[group].rendered = false);
        tile.rendered = false;
      }
  
      function invalidateGroup(tile, group) {
        tile.laminae[group].rendered = false;
        tile.rendered = false;
      }
  
      function hideGroup(group) {
        tileFactory.hideGroup(group);
        Object.values(tiles).forEach( tile => { tile.rendered = false; } );
      }
  
      function showGroup(group) {
        tileFactory.showGroup(group);
        Object.values(tiles).forEach( tile => { tile.rendered = false; } );
      }
    }
  
    // Wrap XMLHttpRequest to execute a callback with an (err, data) signature
    function xhrGet(href, type, callback) {
  
      var req = new XMLHttpRequest();
      req.responseType = type;
  
      // Add handlers for error, abort, load. Ignore all others
      req.onerror = errHandler;
      req.onabort = errHandler;
      req.onload = loadHandler;
  
      req.open('get', href);
      req.send();
  
      function errHandler(e) {
        let err = "XMLHttpRequest ended with an " + e.type;
        return callback(err);
      }
      function loadHandler(e) {
        if (req.responseType !== type) {
          let err = "XMLHttpRequest: Wrong responseType. Expected " +
            type + ", got " + req.responseType;
          return callback(err, req.response);
        }
        if (req.status !== 200) {
          let err = "XMLHttpRequest: HTTP " + req.status + " error from " + href;
          return callback(err, req.response);
        }
        return callback(null, req.response);
      }
  
      // Return the request, just in case we want to abort it with req.abort()
      return req;
    }
  
    function readJSON(dataHref, callback) {
      // Input dataHref is the path to a file containing JSON
  
      // Request the data - as text, since Edge doesn't support json responseType
      xhrGet(dataHref, "text", parseJSON);
  
      function parseJSON(err, data) {
        if (err) return callback(err, data);
        callback(null, JSON.parse(data), dataHref);
      }
    }
  
    function loadImage(href, callback) {
      const img = new Image();
      img.onerror = () => callback("ERROR in loadImage for href " + href);
      img.onload = checkImg;
      img.crossOrigin = "anonymous";
      img.src = href;
  
      function checkImg() {
        if (img.complete && img.naturalWidth !== 0) {
          return callback(null, img);
        } else {
          return callback("ERROR in loadImage for href " + href);
        }
      }
  
      return img;
    }
  
    // From mapbox-gl-js, style-spec/deref.js
    const refProperties = [
      'type', 
      'source', 
      'source-layer', 
      'minzoom', 
      'maxzoom', 
      'filter', 
      'layout'
    ];
  
    /**
     * Given an array of layers, some of which may contain `ref` properties
     * whose value is the `id` of another property, return a new array where
     * such layers have been augmented with the 'type', 'source', etc. properties
     * from the parent layer, and the `ref` property has been removed.
     *
     * The input is not modified. The output may contain references to portions
     * of the input.
     *
     * @private
     * @param {Array<Layer>} layers
     * @returns {Array<Layer>}
     */
    function derefLayers(layers) {
      layers = layers.slice(); // ??? What are we trying to achieve here?
  
      const map = Object.create(null); // stackoverflow.com/a/21079232/10082269
      layers.forEach( layer => { map[layer.id] = layer; } );
  
      for (let i = 0; i < layers.length; i++) {
        if ('ref' in layers[i]) {
          layers[i] = deref(layers[i], map[layers[i].ref]);
        }
      }
  
      return layers;
    }
  
    function deref(layer, parent) {
      const result = {};
  
      for (const k in layer) {
        if (k !== 'ref') {
          result[k] = layer[k];
        }
      }
  
      refProperties.forEach((k) => {
        if (k in parent) {
          result[k] = parent[k];
        }
      });
  
      return result;
    }
  
    function loadStyle(style, mapboxToken, callback) {
      if (typeof style === "object") {
        // style appears to be parsed JSON already. Prepare it for use
        return prepStyle(null, style, mapboxToken, callback);
      }
      // Style appears to be a URL string. Load the document, then prepare it
      var url = expandStyleURL(style, mapboxToken);
      var process = (err, doc) => prepStyle(err, doc, mapboxToken, callback);
      return readJSON(url, process);
    }
  
    function prepStyle(err, styleDoc, token, callback) {
      if (err) return callback(err);
      styleDoc.layers = derefLayers(styleDoc.layers);
  
      // Prepare the "sources" object
      var sKeys = Object.keys(styleDoc.sources);
      var numToDo = sKeys.length;
  
      // Add "sprite" object if needed
      if (styleDoc.sprite) {
        numToDo += 2;
        var spriteURLs = expandSpriteURLs(styleDoc.sprite, token);
        // We will replace the .sprite URL with an object containing
        // image and metadata
        styleDoc.sprite = {};
        // Retrieve both .json and .png files
        loadImage(spriteURLs.image, prepSpriteImage);
        readJSON(spriteURLs.meta, prepSpriteMeta);
      }
  
      sKeys.forEach( key => prepSource(styleDoc.sources, key, token, finishAll) );
        
      function prepSpriteImage(err, png) {
        if (err) finishAll(err);
        styleDoc.sprite.image = png;
        finishAll(null);
      }
  
      function prepSpriteMeta(err, json) {
        if (err) finishAll(err);
        styleDoc.sprite.meta = json;
        finishAll(null);
      }
  
      function finishAll(err) {
        if (err) return callback(err);
        if (--numToDo == 0) callback(null, styleDoc);
      }
    }
  
    function prepSource(sources, key, mbToken, callback) {
      var source = sources[key];
      var url = source.url;
      if (url === undefined) return callback(null); // No change
  
      // Load the referenced TileJSON document
      url = expandTileURL(url, mbToken);
      readJSON(url, merge);
  
      function merge(err, json) {
        if (err) callback(err);
        // Add any custom properties from the style document
        Object.keys(source).forEach( k2 => { json[k2] = source[k2]; } );
        // Replace current entry with the TileJSON data
        sources[key] = json;
        callback(null);
      }
    }
  
    function expandStyleURL(url, token) {
      var prefix = /^mapbox:\/\/styles\//;
      if ( !url.match(prefix) ) return url;
      var apiRoot = "https://api.mapbox.com/styles/v1/";
      return url.replace(prefix, apiRoot) + "?access_token=" + token;
    }
  
    function expandSpriteURLs(url, token) {
      // Returns an array containing urls to .png and .json files
      var prefix = /^mapbox:\/\/sprites\//;
      if ( !url.match(prefix) ) return {
        image: url + ".png", 
        meta: url + ".json",
      };
  
      // We have a Mapbox custom url. Expand to an absolute URL, as per the spec
      var apiRoot = "https://api.mapbox.com/styles/v1/";
      url = url.replace(prefix, apiRoot) + "/sprite";
      var tokenString = "?access_token=" + token;
      return {
        image: url + ".png" + tokenString, 
        meta: url + ".json" + tokenString,
      };
    }
  
    function expandTileURL(url, token) {
      var prefix = /^mapbox:\/\//;
      if ( !url.match(prefix) ) return url;
      var apiRoot = "https://api.mapbox.com/v4/";
      return url.replace(prefix, apiRoot) + ".json?secure&access_token=" + token;
    }
  
    // Wrapper for worker threads to enable a callback interface
    // Inspired by https://codeburst.io/promises-for-the-web-worker-9311b7831733
    function initWorker(codeHref) {
  
      const tasks = {};
      let globalMsgId = 0;
      let activeTasks = 0;
  
      const worker = new Worker(codeHref);
      worker.onmessage = handleMsg;
  
      return {
        startTask,
        cancelTask,
        numActive: () => activeTasks,
        terminate: worker.terminate,
      }
  
      function startTask(payload, callback) {
        activeTasks ++;
        const msgId = globalMsgId++;
        tasks[msgId] = { callback };
        worker.postMessage({ id: msgId, type: "start", payload });
        return msgId; // Returned ID can be used for later cancellation
      }
  
      function cancelTask(id) {
        if (tasks[id]) worker.postMessage({ id, type: "cancel" });
        return delete tasks[id];
      }
  
      function handleMsg(msgEvent) {
        const msg = msgEvent.data; // { id, type, key, payload }
        const task = tasks[msg.id];
        if (!task) return worker.postMessage({ id: msg.id, type: "cancel" });
  
        switch (msg.type) {
          case "error":
            task.callback(msg.payload);
            break; // Clean up below
  
          case "header":
            task.header = msg.payload;
            task.result = initJSON(msg.payload);
            return worker.postMessage({ id: msg.id, type: "continue" });
  
          case "data": 
            let features = task.result[msg.key].features;
            msg.payload.forEach( feature => features.push(feature) );
            return worker.postMessage({ id: msg.id, type: "continue" });
  
          case "done":
            let err = checkJSON(task.result, task.header)
              ? null
              : "ERROR: JSON from worker failed checks!";
            task.callback(err, task.result);
            break; // Clean up below
  
          default:
            task.callback("ERROR: worker sent bad message type!");
            break; // Clean up below
        }
  
        delete tasks[msg.id];
        activeTasks --;
      }
    }
  
    function initJSON(header) {
      const json = {};
      Object.keys(header).forEach(key => {
        json[key] = { type: "FeatureCollection", features: [] };
      });
      return json;
    }
  
    function checkJSON(json, header) {
      return Object.keys(header).every(checkFeatureCount);
  
      function checkFeatureCount(key) {
        return json[key].features.length === header[key];
      }
    }
  
    function initTileFactory(size, sources, styleGroups, loader) {
      // Input size is the pixel size of the canvas used for vector rendering
      // Input sources is an OBJECT of TileJSON descriptions of tilesets
      // Input styleGroups is an ARRAY of objects { name, visible } for groupings of
      // style layers that will be rendered to separate canvases before compositing
  
      // For now we ignore sources that don't have tile endpoints
      const tileSourceKeys = Object.keys(sources).filter( k => {
        return sources[k].tiles && sources[k].tiles.length > 0;
      });
  
      function orderTile(z, x, y, callback = () => true) {
        const loadTasks = {};
        var numToDo = tileSourceKeys.length;
        var baseLamina = initLamina(size);
  
        const tile = {
          z, x, y,
          id: z + "/" + x + "/" + y,
          priority: 0,
  
          sources: {},
          laminae: {},
          img: baseLamina.img,
          ctx: baseLamina.ctx,
  
          loaded: false,
          cancelLoad,
          canceled: false,
          rendering: baseLamina.rendering,
          rendered: baseLamina.rendered,
        };
  
        // Add canvases for separate rendering of layer groups, if supplied
        if (styleGroups && styleGroups.length > 1) {
          styleGroups.forEach( group => {
            tile.laminae[group.name] = initLamina(size);
          });
        }
  
        tileSourceKeys.forEach( loadTile );
  
        function loadTile(srcKey) {
          var src = sources[srcKey];
          var tileHref = tileURL(src.tiles[0], z, x, y);
          if (src.type === "vector") {
            //readMVT( tileHref, size, (err, data) => checkData(err, srcKey, data) );
            let readCallback = (err, data) => checkData(err, srcKey, data);
            let readPayload = { href: tileHref, size: size };
            loadTasks[srcKey] = loader.startTask(readPayload, readCallback);
          } else if (src.type === "raster") {
            loadImage( tileHref, (err, data) => checkData(err, srcKey, data) );
          }
        }
  
        function cancelLoad() {
          Object.values(loadTasks).forEach(task => loader.cancelTask(task));
        }
  
        function checkData(err, key, data) {
          // If data retrieval errors, don't stop. We could be out of the range of
          // one layer, but we may still be able to render the other layers
          if (err) console.log(err);
          // TODO: maybe stop if all layers have errors?
  
          tile.sources[key] = data;
          delete loadTasks[key];
          if (--numToDo > 0) return;
  
          tile.loaded = true;
          return callback(null, tile);
        }
        return tile;
      }
  
      return orderTile;
    }
  
    function initLamina(size) {
      let img = document.createElement("canvas");
      img.width = size;
      img.height = size;
      let ctx = img.getContext("2d");
      ctx.save(); // Save default styles
      return { img, ctx, rendering: false, rendered: false };
    }
  
    function tileURL(endpoint, z, x, y) {
      return endpoint.replace(/{z}/, z).replace(/{x}/, x).replace(/{y}/, y);
    }
  
    function getFeatures(layer, filterObj) {
      // Based on https://observablehq.com/@mbostock/d3-mapbox-vector-tiles
      if (!layer) return;
      var filter = prepFilter(filterObj);
  
      var features = layer.features.filter(filter);
  
      return (features.length < 1)
        ? false
        : { type: "FeatureCollection", features: features };
    }
  
    function prepFilter(filterObj) {
      // filterObj is a filter definition following the "deprecated" syntax:
      // https://docs.mapbox.com/mapbox-gl-js/style-spec/#other-filter
      if (!filterObj) return () => true;
  
      var type, key, vals;
  
      // If this is a combined filter, the vals are themselves filter definitions
      [type, ...vals] = filterObj;
      switch (type) {
        case "all": {
          let filters = vals.map(prepFilter);  // WARNING: Iteratively recursive!
          return (d) => filters.every( filt => filt(d) );
        }
        case "any": {
          let filters = vals.map(prepFilter);
          return (d) => filters.some( filt => filt(d) );
        }
        case "none": {
          let filters = vals.map(prepFilter);
          return (d) => filters.every( filt => !filt(d) );
        }
      }
  
      [type, key, ...vals] = filterObj;
      var getVal = initFeatureValGetter(key);
  
      switch (type) {
        // Existential Filters
        case "has": 
          return d => !!getVal(d); // !! forces a Boolean return
        case "!has": 
          return d => !getVal(d);
  
        // Comparison Filters
        case "==": 
          return d => getVal(d) === vals[0];
        case "!=":
          return d => getVal(d) !== vals[0];
        case ">":
          return d => getVal(d) > vals[0];
        case ">=":
          return d => getVal(d) >= vals[0];
        case "<":
          return d => getVal(d) < vals[0];
        case "<=":
          return d => getVal(d) <= vals[0];
  
        // Set Membership Filters
        case "in" :
          return d => vals.includes( getVal(d) );
        case "!in" :
          return d => !vals.includes( getVal(d) );
        default:
          console.log("prepFilter: unknown filter type = " + filterObj[0]);
      }
      // No recognizable filter criteria. Return a filter that is always true
      return () => true;
    }
  
    function initFeatureValGetter(key) {
      switch (key) {
        case "$type":
          // NOTE: data includes MultiLineString, MultiPolygon, etc-NOT IN SPEC
          return f => {
            let t = f.geometry.type;
            if (t === "MultiPoint") return "Point";
            if (t === "MultiLineString") return "LineString";
            if (t === "MultiPolygon") return "Polygon";
            return t;
          };
        case "$id":
          return f => f.id;
        default:
          return f => f.properties[key];
      }
    }
  
    function createCommonjsModule(fn, module) {
    	return module = { exports: {} }, fn(module, module.exports), module.exports;
    }
  
    var csscolorparser = createCommonjsModule(function (module, exports) {
    // (c) Dean McNamee <dean@gmail.com>, 2012.
    //
    // https://github.com/deanm/css-color-parser-js
    //
    // Permission is hereby granted, free of charge, to any person obtaining a copy
    // of this software and associated documentation files (the "Software"), to
    // deal in the Software without restriction, including without limitation the
    // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    // sell copies of the Software, and to permit persons to whom the Software is
    // furnished to do so, subject to the following conditions:
    //
    // The above copyright notice and this permission notice shall be included in
    // all copies or substantial portions of the Software.
    //
    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    // IN THE SOFTWARE.
  
    // http://www.w3.org/TR/css3-color/
    var kCSSColorTable = {
      "transparent": [0,0,0,0], "aliceblue": [240,248,255,1],
      "antiquewhite": [250,235,215,1], "aqua": [0,255,255,1],
      "aquamarine": [127,255,212,1], "azure": [240,255,255,1],
      "beige": [245,245,220,1], "bisque": [255,228,196,1],
      "black": [0,0,0,1], "blanchedalmond": [255,235,205,1],
      "blue": [0,0,255,1], "blueviolet": [138,43,226,1],
      "brown": [165,42,42,1], "burlywood": [222,184,135,1],
      "cadetblue": [95,158,160,1], "chartreuse": [127,255,0,1],
      "chocolate": [210,105,30,1], "coral": [255,127,80,1],
      "cornflowerblue": [100,149,237,1], "cornsilk": [255,248,220,1],
      "crimson": [220,20,60,1], "cyan": [0,255,255,1],
      "darkblue": [0,0,139,1], "darkcyan": [0,139,139,1],
      "darkgoldenrod": [184,134,11,1], "darkgray": [169,169,169,1],
      "darkgreen": [0,100,0,1], "darkgrey": [169,169,169,1],
      "darkkhaki": [189,183,107,1], "darkmagenta": [139,0,139,1],
      "darkolivegreen": [85,107,47,1], "darkorange": [255,140,0,1],
      "darkorchid": [153,50,204,1], "darkred": [139,0,0,1],
      "darksalmon": [233,150,122,1], "darkseagreen": [143,188,143,1],
      "darkslateblue": [72,61,139,1], "darkslategray": [47,79,79,1],
      "darkslategrey": [47,79,79,1], "darkturquoise": [0,206,209,1],
      "darkviolet": [148,0,211,1], "deeppink": [255,20,147,1],
      "deepskyblue": [0,191,255,1], "dimgray": [105,105,105,1],
      "dimgrey": [105,105,105,1], "dodgerblue": [30,144,255,1],
      "firebrick": [178,34,34,1], "floralwhite": [255,250,240,1],
      "forestgreen": [34,139,34,1], "fuchsia": [255,0,255,1],
      "gainsboro": [220,220,220,1], "ghostwhite": [248,248,255,1],
      "gold": [255,215,0,1], "goldenrod": [218,165,32,1],
      "gray": [128,128,128,1], "green": [0,128,0,1],
      "greenyellow": [173,255,47,1], "grey": [128,128,128,1],
      "honeydew": [240,255,240,1], "hotpink": [255,105,180,1],
      "indianred": [205,92,92,1], "indigo": [75,0,130,1],
      "ivory": [255,255,240,1], "khaki": [240,230,140,1],
      "lavender": [230,230,250,1], "lavenderblush": [255,240,245,1],
      "lawngreen": [124,252,0,1], "lemonchiffon": [255,250,205,1],
      "lightblue": [173,216,230,1], "lightcoral": [240,128,128,1],
      "lightcyan": [224,255,255,1], "lightgoldenrodyellow": [250,250,210,1],
      "lightgray": [211,211,211,1], "lightgreen": [144,238,144,1],
      "lightgrey": [211,211,211,1], "lightpink": [255,182,193,1],
      "lightsalmon": [255,160,122,1], "lightseagreen": [32,178,170,1],
      "lightskyblue": [135,206,250,1], "lightslategray": [119,136,153,1],
      "lightslategrey": [119,136,153,1], "lightsteelblue": [176,196,222,1],
      "lightyellow": [255,255,224,1], "lime": [0,255,0,1],
      "limegreen": [50,205,50,1], "linen": [250,240,230,1],
      "magenta": [255,0,255,1], "maroon": [128,0,0,1],
      "mediumaquamarine": [102,205,170,1], "mediumblue": [0,0,205,1],
      "mediumorchid": [186,85,211,1], "mediumpurple": [147,112,219,1],
      "mediumseagreen": [60,179,113,1], "mediumslateblue": [123,104,238,1],
      "mediumspringgreen": [0,250,154,1], "mediumturquoise": [72,209,204,1],
      "mediumvioletred": [199,21,133,1], "midnightblue": [25,25,112,1],
      "mintcream": [245,255,250,1], "mistyrose": [255,228,225,1],
      "moccasin": [255,228,181,1], "navajowhite": [255,222,173,1],
      "navy": [0,0,128,1], "oldlace": [253,245,230,1],
      "olive": [128,128,0,1], "olivedrab": [107,142,35,1],
      "orange": [255,165,0,1], "orangered": [255,69,0,1],
      "orchid": [218,112,214,1], "palegoldenrod": [238,232,170,1],
      "palegreen": [152,251,152,1], "paleturquoise": [175,238,238,1],
      "palevioletred": [219,112,147,1], "papayawhip": [255,239,213,1],
      "peachpuff": [255,218,185,1], "peru": [205,133,63,1],
      "pink": [255,192,203,1], "plum": [221,160,221,1],
      "powderblue": [176,224,230,1], "purple": [128,0,128,1],
      "rebeccapurple": [102,51,153,1],
      "red": [255,0,0,1], "rosybrown": [188,143,143,1],
      "royalblue": [65,105,225,1], "saddlebrown": [139,69,19,1],
      "salmon": [250,128,114,1], "sandybrown": [244,164,96,1],
      "seagreen": [46,139,87,1], "seashell": [255,245,238,1],
      "sienna": [160,82,45,1], "silver": [192,192,192,1],
      "skyblue": [135,206,235,1], "slateblue": [106,90,205,1],
      "slategray": [112,128,144,1], "slategrey": [112,128,144,1],
      "snow": [255,250,250,1], "springgreen": [0,255,127,1],
      "steelblue": [70,130,180,1], "tan": [210,180,140,1],
      "teal": [0,128,128,1], "thistle": [216,191,216,1],
      "tomato": [255,99,71,1], "turquoise": [64,224,208,1],
      "violet": [238,130,238,1], "wheat": [245,222,179,1],
      "white": [255,255,255,1], "whitesmoke": [245,245,245,1],
      "yellow": [255,255,0,1], "yellowgreen": [154,205,50,1]};
  
    function clamp_css_byte(i) {  // Clamp to integer 0 .. 255.
      i = Math.round(i);  // Seems to be what Chrome does (vs truncation).
      return i < 0 ? 0 : i > 255 ? 255 : i;
    }
  
    function clamp_css_float(f) {  // Clamp to float 0.0 .. 1.0.
      return f < 0 ? 0 : f > 1 ? 1 : f;
    }
  
    function parse_css_int(str) {  // int or percentage.
      if (str[str.length - 1] === '%')
        return clamp_css_byte(parseFloat(str) / 100 * 255);
      return clamp_css_byte(parseInt(str));
    }
  
    function parse_css_float(str) {  // float or percentage.
      if (str[str.length - 1] === '%')
        return clamp_css_float(parseFloat(str) / 100);
      return clamp_css_float(parseFloat(str));
    }
  
    function css_hue_to_rgb(m1, m2, h) {
      if (h < 0) h += 1;
      else if (h > 1) h -= 1;
  
      if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
      if (h * 2 < 1) return m2;
      if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6;
      return m1;
    }
  
    function parseCSSColor(css_str) {
      // Remove all whitespace, not compliant, but should just be more accepting.
      var str = css_str.replace(/ /g, '').toLowerCase();
  
      // Color keywords (and transparent) lookup.
      if (str in kCSSColorTable) return kCSSColorTable[str].slice();  // dup.
  
      // #abc and #abc123 syntax.
      if (str[0] === '#') {
        if (str.length === 4) {
          var iv = parseInt(str.substr(1), 16);  // TODO(deanm): Stricter parsing.
          if (!(iv >= 0 && iv <= 0xfff)) return null;  // Covers NaN.
          return [((iv & 0xf00) >> 4) | ((iv & 0xf00) >> 8),
                  (iv & 0xf0) | ((iv & 0xf0) >> 4),
                  (iv & 0xf) | ((iv & 0xf) << 4),
                  1];
        } else if (str.length === 7) {
          var iv = parseInt(str.substr(1), 16);  // TODO(deanm): Stricter parsing.
          if (!(iv >= 0 && iv <= 0xffffff)) return null;  // Covers NaN.
          return [(iv & 0xff0000) >> 16,
                  (iv & 0xff00) >> 8,
                  iv & 0xff,
                  1];
        }
  
        return null;
      }
  
      var op = str.indexOf('('), ep = str.indexOf(')');
      if (op !== -1 && ep + 1 === str.length) {
        var fname = str.substr(0, op);
        var params = str.substr(op+1, ep-(op+1)).split(',');
        var alpha = 1;  // To allow case fallthrough.
        switch (fname) {
          case 'rgba':
            if (params.length !== 4) return null;
            alpha = parse_css_float(params.pop());
            // Fall through.
          case 'rgb':
            if (params.length !== 3) return null;
            return [parse_css_int(params[0]),
                    parse_css_int(params[1]),
                    parse_css_int(params[2]),
                    alpha];
          case 'hsla':
            if (params.length !== 4) return null;
            alpha = parse_css_float(params.pop());
            // Fall through.
          case 'hsl':
            if (params.length !== 3) return null;
            var h = (((parseFloat(params[0]) % 360) + 360) % 360) / 360;  // 0 .. 1
            // NOTE(deanm): According to the CSS spec s/l should only be
            // percentages, but we don't bother and let float or percentage.
            var s = parse_css_float(params[1]);
            var l = parse_css_float(params[2]);
            var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
            var m1 = l * 2 - m2;
            return [clamp_css_byte(css_hue_to_rgb(m1, m2, h+1/3) * 255),
                    clamp_css_byte(css_hue_to_rgb(m1, m2, h) * 255),
                    clamp_css_byte(css_hue_to_rgb(m1, m2, h-1/3) * 255),
                    alpha];
          default:
            return null;
        }
      }
  
      return null;
    }
  
    try { exports.parseCSSColor = parseCSSColor; } catch(e) { }
    });
    var csscolorparser_1 = csscolorparser.parseCSSColor;
  
    function evalStyle(styleFunction, zoom) {
      var styleFunc = buildStyleFunc(styleFunction);
      return styleFunc(zoom);
    }
  
    function buildStyleFunc(style) {
      var styleFunc, getArg;
  
      if (typeof style !== "object" || Array.isArray(style)) {
        // Includes the case style === undefined
        styleFunc = () => style;
        styleFunc.type = "constant";
  
      } else if (!style.property || style.property === "zoom") {
        getArg = (zoom, feature) => zoom;
        styleFunc = getStyleFunc(style, getArg);
        styleFunc.type = "zoom";
  
      } else {
        getArg = (zoom, feature) => feature.properties[style.property];
        styleFunc = getStyleFunc(style, getArg);
        styleFunc.type = "property";
  
      } // NOT IMPLEMENTED: zoom-and-property functions
  
      return styleFunc;
    }
  
    function getStyleFunc(style, getArg) {
      if (style.type === "identity") return getArg;
  
      // We should be building a stop function now. Make sure we have enough info
      var stops = style.stops;
      if (!stops || stops.length < 2 || stops[0].length !== 2) {
        console.log("buildStyleFunc: style = " + JSON.stringify(style));
        console.log("ERROR in buildStyleFunc: failed to understand style!");
        return;
      }
  
      var stopFunc = buildStopFunc(stops, style.base);
      return (zoom, feature) => stopFunc( getArg(zoom, feature) );
    }
  
    function buildStopFunc(stops, base = 1) {
      const izm = stops.length - 1;
      const interpolate = getInterpolator(stops[0][1]);
  
      return function(x) {
        let iz = stops.findIndex(stop => stop[0] > x);
  
        if (iz === 0) return stops[0][1]; // x is below first stop
        if (iz < 0) return stops[izm][1]; // x is above last stop
  
        let t = interpFactor(base, stops[iz-1][0], x, stops[iz][0]);
  
        return interpolate(stops[iz-1][1], stops[iz][1], t);
      }
    }
  
    function getInterpolator(sampleVal) {
      var type = typeof sampleVal;
  
      // Linear interpolator for numbers
      if (type === "number") return (v1, v2, t) => v1 + t * (v2 - v1);
  
      var isColor = (type === "string" && csscolorparser_1(sampleVal));
      return (isColor)
        ? (v1, v2, t) => interpColor(csscolorparser_1(v1), csscolorparser_1(v2), t)
        : (v1, v2, t) => v1; // Assume step function for other types
    }
  
    function interpFactor(base, x0, x, x1) {
      // Follows mapbox-gl-js, style-spec/function/index.js.
      // NOTE: https://github.com/mapbox/mapbox-gl-js/issues/2698 not addressed!
      const range = x1 - x0;
      if (range === 0) return 0;
  
      const dx = x - x0;
      if (base === 1) return dx / range;
  
      return (Math.pow(base, dx) - 1) / (Math.pow(base, range) - 1);
    }
  
    function interpColor(c0, c1, t) {
      // Inputs c0, c1 are 4-element RGBA arrays as returned by parseCSSColor
      let c = [];
      for (let i = 0; i < 4; i++) {
        c[i] = c0[i] + t * (c1[i] - c0[i]);
      }
      return "rgba(" +
        Math.round(c[0]) + ", " +
        Math.round(c[1]) + ", " + 
        Math.round(c[2]) + ", " +
        c[3] + ")";
    }
  
    // Renders layers that cover the whole tile (like painting with a roller)
    function initRoller(canvSize) {
  
      return {
        fillBackground,
        drawRaster,
      };
  
      function fillBackground(ctx, style, zoom) {
        // Cover the tile with a bucket of paint
        ctx.fillStyle = evalStyle(style.paint["background-color"], zoom);
        ctx.globalAlpha = evalStyle(style.paint["background-opacity"], zoom);
        ctx.fillRect(0, 0, canvSize, canvSize);
      }
  
      function drawRaster(ctx, style, zoom, image) {
        // Cover the tile with a prettily patterned wallpaper
        var paint = style.paint;
        if (paint !== undefined) {
          ctx.globalAlpha = evalStyle(paint["raster-opacity"], zoom);
          // Missing raster-hue-rotate, raster-brightness-min/max,
          // raster-saturation, raster-contrast
        }
        // TODO: we are forcing one tile to cover the canvas!
        // In some cases (e.g. Mapbox Satellite Streets) the raster tiles may
        // be half the size of the vector canvas, so we need 4 of them...
        ctx.drawImage(image, 0, 0, canvSize, canvSize);
      }
    }
  
    // Adds floating point numbers with twice the normal precision.
    // Reference: J. R. Shewchuk, Adaptive Precision Floating-Point Arithmetic and
    // Fast Robust Geometric Predicates, Discrete & Computational Geometry 18(3)
    // 305–363 (1997).
    // Code adapted from GeographicLib by Charles F. F. Karney,
    // http://geographiclib.sourceforge.net/
  
    function adder() {
      return new Adder;
    }
  
    function Adder() {
      this.reset();
    }
  
    Adder.prototype = {
      constructor: Adder,
      reset: function() {
        this.s = // rounded value
        this.t = 0; // exact error
      },
      add: function(y) {
        add(temp, y, this.t);
        add(this, temp.s, this.s);
        if (this.s) this.t += temp.t;
        else this.s = temp.t;
      },
      valueOf: function() {
        return this.s;
      }
    };
  
    var temp = new Adder;
  
    function add(adder, a, b) {
      var x = adder.s = a + b,
          bv = x - a,
          av = x - bv;
      adder.t = (a - av) + (b - bv);
    }
  
    var pi = Math.PI;
    var tau = pi * 2;
  
    var abs = Math.abs;
    var sqrt = Math.sqrt;
  
    function noop() {}
  
    function streamGeometry(geometry, stream) {
      if (geometry && streamGeometryType.hasOwnProperty(geometry.type)) {
        streamGeometryType[geometry.type](geometry, stream);
      }
    }
  
    var streamObjectType = {
      Feature: function(object, stream) {
        streamGeometry(object.geometry, stream);
      },
      FeatureCollection: function(object, stream) {
        var features = object.features, i = -1, n = features.length;
        while (++i < n) streamGeometry(features[i].geometry, stream);
      }
    };
  
    var streamGeometryType = {
      Sphere: function(object, stream) {
        stream.sphere();
      },
      Point: function(object, stream) {
        object = object.coordinates;
        stream.point(object[0], object[1], object[2]);
      },
      MultiPoint: function(object, stream) {
        var coordinates = object.coordinates, i = -1, n = coordinates.length;
        while (++i < n) object = coordinates[i], stream.point(object[0], object[1], object[2]);
      },
      LineString: function(object, stream) {
        streamLine(object.coordinates, stream, 0);
      },
      MultiLineString: function(object, stream) {
        var coordinates = object.coordinates, i = -1, n = coordinates.length;
        while (++i < n) streamLine(coordinates[i], stream, 0);
      },
      Polygon: function(object, stream) {
        streamPolygon(object.coordinates, stream);
      },
      MultiPolygon: function(object, stream) {
        var coordinates = object.coordinates, i = -1, n = coordinates.length;
        while (++i < n) streamPolygon(coordinates[i], stream);
      },
      GeometryCollection: function(object, stream) {
        var geometries = object.geometries, i = -1, n = geometries.length;
        while (++i < n) streamGeometry(geometries[i], stream);
      }
    };
  
    function streamLine(coordinates, stream, closed) {
      var i = -1, n = coordinates.length - closed, coordinate;
      stream.lineStart();
      while (++i < n) coordinate = coordinates[i], stream.point(coordinate[0], coordinate[1], coordinate[2]);
      stream.lineEnd();
    }
  
    function streamPolygon(coordinates, stream) {
      var i = -1, n = coordinates.length;
      stream.polygonStart();
      while (++i < n) streamLine(coordinates[i], stream, 1);
      stream.polygonEnd();
    }
  
    function geoStream(object, stream) {
      if (object && streamObjectType.hasOwnProperty(object.type)) {
        streamObjectType[object.type](object, stream);
      } else {
        streamGeometry(object, stream);
      }
    }
  
    var areaRingSum = adder();
  
    var areaSum = adder();
  
    var deltaSum = adder();
  
    var sum = adder();
  
    function ascending(a, b) {
      return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
    }
  
    function bisector(compare) {
      if (compare.length === 1) compare = ascendingComparator(compare);
      return {
        left: function(a, x, lo, hi) {
          if (lo == null) lo = 0;
          if (hi == null) hi = a.length;
          while (lo < hi) {
            var mid = lo + hi >>> 1;
            if (compare(a[mid], x) < 0) lo = mid + 1;
            else hi = mid;
          }
          return lo;
        },
        right: function(a, x, lo, hi) {
          if (lo == null) lo = 0;
          if (hi == null) hi = a.length;
          while (lo < hi) {
            var mid = lo + hi >>> 1;
            if (compare(a[mid], x) > 0) hi = mid;
            else lo = mid + 1;
          }
          return lo;
        }
      };
    }
  
    function ascendingComparator(f) {
      return function(d, x) {
        return ascending(f(d), x);
      };
    }
  
    var ascendingBisect = bisector(ascending);
  
    var lengthSum = adder();
  
    function identity(x) {
      return x;
    }
  
    var areaSum$1 = adder(),
        areaRingSum$1 = adder(),
        x00,
        y00,
        x0,
        y0;
  
    var areaStream = {
      point: noop,
      lineStart: noop,
      lineEnd: noop,
      polygonStart: function() {
        areaStream.lineStart = areaRingStart;
        areaStream.lineEnd = areaRingEnd;
      },
      polygonEnd: function() {
        areaStream.lineStart = areaStream.lineEnd = areaStream.point = noop;
        areaSum$1.add(abs(areaRingSum$1));
        areaRingSum$1.reset();
      },
      result: function() {
        var area = areaSum$1 / 2;
        areaSum$1.reset();
        return area;
      }
    };
  
    function areaRingStart() {
      areaStream.point = areaPointFirst;
    }
  
    function areaPointFirst(x, y) {
      areaStream.point = areaPoint;
      x00 = x0 = x, y00 = y0 = y;
    }
  
    function areaPoint(x, y) {
      areaRingSum$1.add(y0 * x - x0 * y);
      x0 = x, y0 = y;
    }
  
    function areaRingEnd() {
      areaPoint(x00, y00);
    }
  
    var x0$1 = Infinity,
        y0$1 = x0$1,
        x1 = -x0$1,
        y1 = x1;
  
    var boundsStream = {
      point: boundsPoint,
      lineStart: noop,
      lineEnd: noop,
      polygonStart: noop,
      polygonEnd: noop,
      result: function() {
        var bounds = [[x0$1, y0$1], [x1, y1]];
        x1 = y1 = -(y0$1 = x0$1 = Infinity);
        return bounds;
      }
    };
  
    function boundsPoint(x, y) {
      if (x < x0$1) x0$1 = x;
      if (x > x1) x1 = x;
      if (y < y0$1) y0$1 = y;
      if (y > y1) y1 = y;
    }
  
    // TODO Enforce positive area for exterior, negative area for interior?
  
    var X0 = 0,
        Y0 = 0,
        Z0 = 0,
        X1 = 0,
        Y1 = 0,
        Z1 = 0,
        X2 = 0,
        Y2 = 0,
        Z2 = 0,
        x00$1,
        y00$1,
        x0$2,
        y0$2;
  
    var centroidStream = {
      point: centroidPoint,
      lineStart: centroidLineStart,
      lineEnd: centroidLineEnd,
      polygonStart: function() {
        centroidStream.lineStart = centroidRingStart;
        centroidStream.lineEnd = centroidRingEnd;
      },
      polygonEnd: function() {
        centroidStream.point = centroidPoint;
        centroidStream.lineStart = centroidLineStart;
        centroidStream.lineEnd = centroidLineEnd;
      },
      result: function() {
        var centroid = Z2 ? [X2 / Z2, Y2 / Z2]
            : Z1 ? [X1 / Z1, Y1 / Z1]
            : Z0 ? [X0 / Z0, Y0 / Z0]
            : [NaN, NaN];
        X0 = Y0 = Z0 =
        X1 = Y1 = Z1 =
        X2 = Y2 = Z2 = 0;
        return centroid;
      }
    };
  
    function centroidPoint(x, y) {
      X0 += x;
      Y0 += y;
      ++Z0;
    }
  
    function centroidLineStart() {
      centroidStream.point = centroidPointFirstLine;
    }
  
    function centroidPointFirstLine(x, y) {
      centroidStream.point = centroidPointLine;
      centroidPoint(x0$2 = x, y0$2 = y);
    }
  
    function centroidPointLine(x, y) {
      var dx = x - x0$2, dy = y - y0$2, z = sqrt(dx * dx + dy * dy);
      X1 += z * (x0$2 + x) / 2;
      Y1 += z * (y0$2 + y) / 2;
      Z1 += z;
      centroidPoint(x0$2 = x, y0$2 = y);
    }
  
    function centroidLineEnd() {
      centroidStream.point = centroidPoint;
    }
  
    function centroidRingStart() {
      centroidStream.point = centroidPointFirstRing;
    }
  
    function centroidRingEnd() {
      centroidPointRing(x00$1, y00$1);
    }
  
    function centroidPointFirstRing(x, y) {
      centroidStream.point = centroidPointRing;
      centroidPoint(x00$1 = x0$2 = x, y00$1 = y0$2 = y);
    }
  
    function centroidPointRing(x, y) {
      var dx = x - x0$2,
          dy = y - y0$2,
          z = sqrt(dx * dx + dy * dy);
  
      X1 += z * (x0$2 + x) / 2;
      Y1 += z * (y0$2 + y) / 2;
      Z1 += z;
  
      z = y0$2 * x - x0$2 * y;
      X2 += z * (x0$2 + x);
      Y2 += z * (y0$2 + y);
      Z2 += z * 3;
      centroidPoint(x0$2 = x, y0$2 = y);
    }
  
    function PathContext(context) {
      this._context = context;
    }
  
    PathContext.prototype = {
      _radius: 4.5,
      pointRadius: function(_) {
        return this._radius = _, this;
      },
      polygonStart: function() {
        this._line = 0;
      },
      polygonEnd: function() {
        this._line = NaN;
      },
      lineStart: function() {
        this._point = 0;
      },
      lineEnd: function() {
        if (this._line === 0) this._context.closePath();
        this._point = NaN;
      },
      point: function(x, y) {
        switch (this._point) {
          case 0: {
            this._context.moveTo(x, y);
            this._point = 1;
            break;
          }
          case 1: {
            this._context.lineTo(x, y);
            break;
          }
          default: {
            this._context.moveTo(x + this._radius, y);
            this._context.arc(x, y, this._radius, 0, tau);
            break;
          }
        }
      },
      result: noop
    };
  
    var lengthSum$1 = adder(),
        lengthRing,
        x00$2,
        y00$2,
        x0$3,
        y0$3;
  
    var lengthStream = {
      point: noop,
      lineStart: function() {
        lengthStream.point = lengthPointFirst;
      },
      lineEnd: function() {
        if (lengthRing) lengthPoint(x00$2, y00$2);
        lengthStream.point = noop;
      },
      polygonStart: function() {
        lengthRing = true;
      },
      polygonEnd: function() {
        lengthRing = null;
      },
      result: function() {
        var length = +lengthSum$1;
        lengthSum$1.reset();
        return length;
      }
    };
  
    function lengthPointFirst(x, y) {
      lengthStream.point = lengthPoint;
      x00$2 = x0$3 = x, y00$2 = y0$3 = y;
    }
  
    function lengthPoint(x, y) {
      x0$3 -= x, y0$3 -= y;
      lengthSum$1.add(sqrt(x0$3 * x0$3 + y0$3 * y0$3));
      x0$3 = x, y0$3 = y;
    }
  
    function PathString() {
      this._string = [];
    }
  
    PathString.prototype = {
      _radius: 4.5,
      _circle: circle(4.5),
      pointRadius: function(_) {
        if ((_ = +_) !== this._radius) this._radius = _, this._circle = null;
        return this;
      },
      polygonStart: function() {
        this._line = 0;
      },
      polygonEnd: function() {
        this._line = NaN;
      },
      lineStart: function() {
        this._point = 0;
      },
      lineEnd: function() {
        if (this._line === 0) this._string.push("Z");
        this._point = NaN;
      },
      point: function(x, y) {
        switch (this._point) {
          case 0: {
            this._string.push("M", x, ",", y);
            this._point = 1;
            break;
          }
          case 1: {
            this._string.push("L", x, ",", y);
            break;
          }
          default: {
            if (this._circle == null) this._circle = circle(this._radius);
            this._string.push("M", x, ",", y, this._circle);
            break;
          }
        }
      },
      result: function() {
        if (this._string.length) {
          var result = this._string.join("");
          this._string = [];
          return result;
        } else {
          return null;
        }
      }
    };
  
    function circle(radius) {
      return "m0," + radius
          + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius
          + "a" + radius + "," + radius + " 0 1,1 0," + 2 * radius
          + "z";
    }
  
    function index(projection, context) {
      var pointRadius = 4.5,
          projectionStream,
          contextStream;
  
      function path(object) {
        if (object) {
          if (typeof pointRadius === "function") contextStream.pointRadius(+pointRadius.apply(this, arguments));
          geoStream(object, projectionStream(contextStream));
        }
        return contextStream.result();
      }
  
      path.area = function(object) {
        geoStream(object, projectionStream(areaStream));
        return areaStream.result();
      };
  
      path.measure = function(object) {
        geoStream(object, projectionStream(lengthStream));
        return lengthStream.result();
      };
  
      path.bounds = function(object) {
        geoStream(object, projectionStream(boundsStream));
        return boundsStream.result();
      };
  
      path.centroid = function(object) {
        geoStream(object, projectionStream(centroidStream));
        return centroidStream.result();
      };
  
      path.projection = function(_) {
        return arguments.length ? (projectionStream = _ == null ? (projection = null, identity) : (projection = _).stream, path) : projection;
      };
  
      path.context = function(_) {
        if (!arguments.length) return context;
        contextStream = _ == null ? (context = null, new PathString) : new PathContext(context = _);
        if (typeof pointRadius !== "function") contextStream.pointRadius(pointRadius);
        return path;
      };
  
      path.pointRadius = function(_) {
        if (!arguments.length) return pointRadius;
        pointRadius = typeof _ === "function" ? _ : (contextStream.pointRadius(+_), +_);
        return path;
      };
  
      return path.projection(projection).context(context);
    }
  
    // Renders layers made of points, lines, polygons (like painting with a brush)
    function brush(ctx, style, zoom, data) {
  
      // Initialize the D3 path generator. 
      // First param is the projection. Keep the data's native coordinates for now
      const path = index(null, ctx);
  
      var layout = style.layout;
      var paint = style.paint;
      var method;
  
      const dataDependencies = [];
  
      // Set rendering context state based on values specified in the style.
      // For data-dependent styles, store the state FUNCTIONS in dataDependencies
      switch (style.type) {
        case "circle":
          let setRadius = (radius) => { if (radius) path.pointRadius(radius); };
          setState("", paint["circle-radius"], setRadius);
          setState("fillStyle", paint["circle-color"]);
          setState("globalAlpha", paint["circle-opacity"]);
          method = "fill";
          break;
  
        case "line":
          if (layout) {
            setState("lineCap", layout["line-cap"]);
            setState("lineJoin", layout["line-join"]);
            setState("miterLimit", layout["line-miter-limit"]);
            // Missing line-round-limit
          }
          setState("lineWidth", paint["line-width"]);
          setState("globalAlpha", paint["line-opacity"]);
          setState("strokeStyle", paint["line-color"]);
          // Missing line-gap-width, line-translate, line-translate-anchor,
          //  line-offset, line-blur, line-gradient, line-pattern, line-dasharray
          method = "stroke";
          break;
  
        case "fill":
          setState("fillStyle", paint["fill-color"]);
          setState("globalAlpha", paint["fill-opacity"]);
          // Missing fill-outline-color, fill-translate, fill-translate-anchor,
          //  fill-pattern
          method = "fill";
          break;
  
        default:
          // Missing fill-extrusion, heatmap, hillshade
          return console.log("ERROR in brush.draw: layer.type = " +
            style.type + " not supported!");
      }
  
      // Draw the features in the data
      //draw(ctx, path, data, dataDependencies, zoom, method);
      if (dataDependencies.length == 0) return drawPath();
  
      return sortAndDraw();
      //data.features.forEach(feature => {
      //  dataDependencies.forEach( dep => {
      //    dep.stateFunc( dep.styleFunc(zoom, feature) )
      //  });
      // drawPath(feature, method);
      //});
  
      function setState(option, val, stateFunc) { // Nested for access to zoom
        if (!stateFunc) stateFunc = (val) => { ctx[option] = val; };
  
        let styleFunc = buildStyleFunc(val);
        if (styleFunc.type !== "property") return stateFunc(styleFunc(zoom));
  
        dataDependencies.push({ styleFunc, stateFunc });
      }
  
      function drawPath() {
        ctx.beginPath();
        path(data);
        ctx[method]();
      }
  
      function sortAndDraw() {
        // Build an array of features, style values, and a sortable id
        let features = data.features.map( feature => {
          let vals = dataDependencies.map( dep => dep.styleFunc(zoom, feature) );
          let id = vals.join("");
          return { id, vals, feature };
        });
  
        // Sort the array
        features.sort( (a, b) => (a.id < b.id) ? -1 : 1 );
  
        // Loop through the array, accumulating paths and rendering
        let numFeatures = features.length;
        let i = 0;
        while (i < numFeatures) {
          // Set state for this group of features (only when style id changes)
          dataDependencies.forEach( (dep, index) => {
            dep.stateFunc(features[i].vals[index]);
          });
          // Add these features to the path
          ctx.beginPath();
          let id = features[i].id;
          while (i < numFeatures && features[i].id === id) {
            path(features[i].feature);
            i++;
          }
          // Render these features
          ctx[method]();
        }
      }
    }
  
    function getTokenParser(tokenText) {
      if (!tokenText) return () => undefined;
      const tokenPattern = /{([^{}]+)}/g;
  
      // We break tokenText into pieces that are either plain text or tokens,
      // then construct an array of functions to parse each piece
      var tokenFuncs = [];
      var charIndex  = 0;
      while (charIndex < tokenText.length) {
        // Find the next token
        let result = tokenPattern.exec(tokenText);
  
        if (!result) {
          // No tokens left. Parse the plain text after the last token
          let str = tokenText.substring(charIndex);
          tokenFuncs.push(props => str);
          break;
        } else if (result.index > charIndex) {
          // There is some plain text before the token
          let str = tokenText.substring(charIndex, result.index);
          tokenFuncs.push(props => str);
        }
  
        // Add a function to process the current token
        let token = result[1];
        tokenFuncs.push(props => props[token]);
        charIndex = tokenPattern.lastIndex;
      }
      
      // We now have an array of functions returning either a text string or
      // a feature property
      // Return a function that assembles everything
      return function(properties) {
        return tokenFuncs.reduce(concat, "");
        function concat(str, tokenFunc) {
          let text = tokenFunc(properties) || "";
          return str += text;
        }
      };
    }
  
    function getFontString(fontSize, fontFace) {
      // Round fontSize to the nearest 0.1 pixel
      fontSize = Math.round(10.0 * fontSize) * 0.1;
  
      // Get the last word of the first font string
      var lastWord;
      if (fontFace) lastWord = fontFace[0].split(" ").splice(-1)[0].toLowerCase();
      
      var fontStyle;
      switch (lastWord) {
        case "bold":
          fontStyle = "bold";
          break;
        case "italic":
          fontStyle = "italic";
          break;
      }
  
      return (fontStyle)
        ? fontStyle + " " + fontSize + 'px "PT Sans", sans-serif'
        : fontSize + 'px "PT Sans", sans-serif';
    }
  
    function initTextLabeler(ctx, style, zoom) {
      var labelText, labelLength, labelHeight, x, y;
      var posShift = [0, 0];
      var layout = style.layout;
  
      var textField = evalStyle(layout["text-field"], zoom);
      var textParser = getTokenParser(textField);
  
      // Construct the ctx.font string from text-size and text-font
      var fontSize = evalStyle(layout["text-size"], zoom) || 16;
      var fontFace = evalStyle(layout["text-font"], zoom);
      ctx.font = getFontString(fontSize, fontFace);
  
      // Get some basic style parameters
      let lineHeight = evalStyle(layout["text-line-height"], zoom) || 1.2;
      let textPadding = evalStyle(layout["text-padding"], zoom) || 2.0;
      let textOffset = evalStyle(layout["text-offset"], zoom) || [0, 0];
  
      // Set text-anchor
      var anchor = evalStyle(layout["text-anchor"], zoom);
      setAnchor(anchor);
  
      // Setup the text transform function
      var transformCode = evalStyle(layout["text-transform"], zoom);
      var transform = constructTextTransform(transformCode);
  
      // Set text color and halo properties
      var paint = style.paint;
      ctx.fillStyle   = evalStyle(paint["text-color"], zoom);
      ctx.strokeStyle = evalStyle(paint["text-halo-color"], zoom);
      var haloWidth   = evalStyle(paint["text-halo-width"], zoom) || 0;
      if (haloWidth > 0) {
        ctx.lineWidth = haloWidth * 2.0;
        ctx.lineJoin = "round";
      }
  
      return {
        measure,
        draw,
      };
  
      function measure(feature) {
        labelText = textParser(feature.properties);
        if (!labelText) return;
  
        labelText = transform(labelText);
        labelLength = ctx.measureText(labelText).width;
        labelHeight = fontSize * lineHeight;
  
        var coords = feature.geometry.coordinates;
        // Compute coordinates of bottom left corner of text
        x = coords[0] + textOffset[0] * fontSize + posShift[0] * labelLength;
        y = coords[1] + textOffset[1] * labelHeight + posShift[1] * labelHeight;
  
        // Return a bounding box object
        return [
          [x - textPadding, y - labelHeight - textPadding],
          [x + labelLength + textPadding, y + textPadding]
        ];
      }
  
      function draw() {
        if (!labelText) return;
  
        if (haloWidth > 0) ctx.strokeText(labelText, x, y);
        ctx.fillText(labelText, x, y);
      }
  
      function setAnchor(anchor) {
        // Set baseline. We let Canvas2D use textBaseline = "bottom", and use
        // posShift to shift the text box for other requested baselines
        ctx.textBaseline = "bottom";
        switch (anchor) {
          case "top-left":
          case "top-right":
          case "top":
            //ctx.textBaseline = "top";
            posShift[1] = 1.0;
            break;
          case "bottom-left":
          case "bottom-right":
          case "bottom":
            posShift[1] = 0.0;
            //ctx.textBaseline = "bottom";
            break;
          case "left":
          case "right":
          case "center":
          default:
            //ctx.textBaseline = "middle";
            posShift[1] = 0.5;
        }
        // Set textAlign. We let Canvas2D use textAlign = "left", and use
        // posShift to shift the text box for other requested alignments
        ctx.textAlign = "left";
        switch (anchor) {
          case "top-left":
          case "bottom-left":
          case "left":
            //ctx.textAlign = "left";
            posShift[0] = 0.0;
            break;
          case "top-right":
          case "bottom-right":
          case "right":
            //ctx.textAlign = "right";
            posShift[0] = -1.0;
            break;
          case "top":
          case "bottom":
          case "center":
          default:
            //ctx.textAlign = "center";
            posShift[0] = -0.5;
        }
        return;
      }
    }
  
    function constructTextTransform(code) {
      switch (code) {
        case "uppercase":
          return f => f.toUpperCase();
        case "lowercase":
          return f => f.toLowerCase();
        case "none":
        default:
          return f => f;
      }
    }
  
    function initIconLabeler(ctx, style, zoom, sprite) {
      var layout = style.layout;
      var spriteID, spriteMeta, x, y;
  
      // Get sprite metadata
      var spriteName = evalStyle(layout["icon-image"], zoom);
      var iconParser = getTokenParser(spriteName);
  
      var iconPadding = evalStyle(layout["icon-padding"], zoom) || 2;
  
      return {
        measure,
        draw,
      };
  
      function measure(feature) {
        spriteID = iconParser(feature.properties);
        if (!spriteID) return;
  
        spriteMeta = sprite.meta[spriteID];
  
        var coords = feature.geometry.coordinates;
        x = Math.round(coords[0] - spriteMeta.width / 2);
        y = Math.round(coords[1] - spriteMeta.height / 2);
  
        return [
          [x - iconPadding, y - iconPadding],
          [x + spriteMeta.width + iconPadding, y + spriteMeta.height + iconPadding]
        ];
      } 
  
      function draw() {
        if (!spriteID) return;
  
        ctx.drawImage(
            sprite.image,
            spriteMeta.x,
            spriteMeta.y,
            spriteMeta.width,
            spriteMeta.height,
            x,
            y,
            spriteMeta.width,
            spriteMeta.height
            );
      }
    }
  
    function initLabeler(sprite) {
      const boxes = [];
  
      return function(ctx, style, zoom, data) {
        var layout = style.layout;
        if (layout["symbol-placement"] === "line") return;
  
        const textLabeler = initTextLabeler(ctx, style, zoom);
        const iconLabeler = initIconLabeler(ctx, style, zoom, sprite);
  
        data.features.forEach(drawLabel);
  
        function drawLabel(feature) {
          var textBox = textLabeler.measure(feature);
          if ( collides(textBox) ) return;
  
          var iconBox = iconLabeler.measure(feature);
          if ( collides(iconBox) ) return;
  
          if (textBox) boxes.push(textBox);
          if (iconBox) boxes.push(iconBox);
  
          // Draw the labels
          iconLabeler.draw();
          textLabeler.draw();
          return;
        }
      }
  
      function collides(newBox) {
        if (!newBox) return false;
        return boxes.some( box => intersects(box, newBox) );
      }
    }
  
    function intersects(box1, box2) {
      // box[0] = [xmin, ymin]; box[1] = [xmax, ymax]
      if (box1[0][0] > box2[1][0]) return false;
      if (box2[0][0] > box1[1][0]) return false;
      if (box1[0][1] > box2[1][1]) return false;
      if (box2[0][1] > box1[1][1]) return false;
  
      return true;
    }
  
    function initRenderer(canvSize, styleLayers, styleGroups, sprite, chains) {
      // Input canvSize is an integer, for the pixel size of the (square) tiles
      // Input styleLayers points to the .layers property of a Mapbox style document
      //   Specification: https://docs.mapbox.com/mapbox-gl-js/style-spec/
      // Input styleGroups is a list of style layer groups identified by a
      //   "tilekiln-group" property of each layer
      // Input sprite (if defined) is an object with image and meta properties
  
      // Initialize roller, to paint single layers onto the canvas
      const roller = initRoller(canvSize);
  
      // Sort styles into groups
      const styles = {};
      styleGroups.forEach( group => {
        styles[group.name] = sortStyleGroup(styleLayers, group.name);
      });
  
      var getLamina, composite;
      if (styleGroups.length > 1) { 
        // Define function to return the appropriate lamina (partial rendering)
        getLamina = (tile, groupName) => tile.laminae[groupName];
        // Define function to composite all laminae canvases into the main canvas
        composite = (tile) => {
          tile.ctx.clearRect(0, 0, canvSize, canvSize);
          styleGroups.forEach( group => {
            if (!group.visible) return;
            tile.ctx.drawImage(tile.laminae[group.name].img, 0, 0);
          });
        };
      } else {
        // Only one group of style layers. Render directly to the main canvas
        getLamina = (tile, groupName) => tile;
        // Compositing is not needed: return a dummy no-op function
        composite = (tile) => true;
      }
  
      return {
        drawGroup,
        composite,
      };
  
      function drawGroup(tile, groupName = "none", callback = () => undefined) {
        if (!styles[groupName]) return callback(null, tile);
        let lamina = getLamina(tile, groupName);
        if (lamina.rendered) return callback(null, tile);
  
        lamina.ctx.clearRect(0, 0, canvSize, canvSize);
        const labeler = initLabeler(sprite);
  
        //styles[groupName].forEach( style => drawLayer(style, tile.z, tile.sources) );
  
        // Draw the layers: asynchronously, but in order
        // Create a chain of functions, one for each layer.
        const drawCalls = styles[groupName].map(style => {
          return () => drawLayer(lamina.ctx, labeler, style, tile.z, tile.sources);
        });
        // Execute the chain, with copyResult as the final callback
        chains.chainSyncList(drawCalls, returnResult, tile.id);
  
        function returnResult() {
          lamina.rendered = true;
          return callback(null, tile);
        }
      }
  
      function drawLayer(ctx, labeler, style, zoom, sources) {
        // Quick exits if this layer is not meant to be displayed
        if (style.layout && style.layout["visibility"] === "none") return;
        if (style.minzoom !== undefined && zoom < style.minzoom) return;
        if (style.maxzoom !== undefined && zoom > style.maxzoom) return;
  
        // Start from default canvas state: restore what we saved
        ctx.restore();
        // restore POPS the saved state off a stack. So if we want to restore again
        // later, we need to re-save what we just restored
        ctx.save();
  
        let type = style.type;
        if (type === "background") return roller.fillBackground(ctx, style, zoom);
  
        var source = sources[ style["source"] ];
        if (!source) return;
  
        if (type === "raster") return roller.drawRaster(ctx, style, zoom, source);
  
        var mapLayer = source[ style["source-layer"] ];
        var mapData = getFeatures(mapLayer, style.filter);
        if (!mapData) return;
  
        return (type === "symbol") 
          ? labeler(ctx, style, zoom, mapData)
          : brush(ctx, style, zoom, mapData);
      }
    }
  
    function sortStyleGroup(layers, groupName) {
      // Get the layers belonging to this group
      var group = (groupName === "none")
        ? layers.filter(layer => !layer["tilekiln-group"]) // Layers with no group specified
        : layers.filter(layer => layer["tilekiln-group"] === groupName);
  
      // Reverse the order of the symbol layers
      var labels = group.filter(layer => layer.type === "symbol").reverse();
  
      // Append reordered symbol layers to non-symbol layers
      return group.filter(layer => layer.type !== "symbol").concat(labels);
    }
  
    function initChainer() {
      const priorities = {};
      const timeouts = [];
      const messageName = "zero-timeout-message";
  
      window.addEventListener("message", handleMessage, true);
  
      return {
        chainSyncList,
        chainAsyncList,
        priorities,  // Update externally as a hash of { id: priority } values
      };
  
      function chainSyncList(funcs, finalCallback, taskId) {
        // Input funcs is an array of synchronous zero-argument functions
        // Turn them into asynchronous functions taking a callback
        const cbFuncs = funcs.map( func => (cb) => {
          func();
          setZeroTimeout(cb, taskId);
        });
  
        // Execute them as a chain. Start the chain asynchronously
        setZeroTimeout( () => callInOrder(cbFuncs, finalCallback), taskId );
      }
  
      function chainAsyncList(funcs, finalCallback, taskId) {
        // Input funcs is an array of functions taking a callback as an argument.
        // Wrap them to make the callbacks asynchronous and ID'd
        const wrapFuncs = funcs.map( func => (cb) => {
          func( () => setZeroTimeout(cb, taskId) );
        });
  
        // Execute them as a chain. Start the chain asynchronously
        setZeroTimeout( () => callInOrder(wrapFuncs, finalCallback), taskId );
      }
  
      // http://derpturkey.com/chained-callback-pattern-in-javascript/
      function callInOrder(funcs, finalCallback) {
        funcs.push(finalCallback);
        chain(funcs.shift());
  
        function chain(func) {
          if (func) func( () => chain(funcs.shift()) );
        }
      }
  
      // https://dbaron.org/log/20100309-faster-timeouts
      // func is a function taking zero arguments
      function setZeroTimeout(func, id) {
        timeouts.push({ id, func });
  
        // Don't let this message be picked up by another window:
        // set the targetOrigin to the current window origin
        let loc = window.location;
        let targetOrigin = loc.protocol + "//" + loc.hostname;
        if (loc.port !== "") targetOrigin += ":" + loc.port;
  
        window.postMessage(messageName, targetOrigin);
      }
  
      function handleMessage(evnt) {
        if (evnt.source != window || evnt.data !== messageName) return;
        evnt.stopPropagation();
        if (timeouts.length < 1) return;
  
        // Get the task with the smallest priority
        // TODO: what does this sort function do with undefined values?
        timeouts.sort( (a, b) => (priority(a.id) <= priority(b.id)) ? -1 : 1 );
        var task = timeouts.shift();
  
        // If priority is undefined, this task has been canceled.
        if (priority(task.id) !== undefined) task.func();
      }
  
      function priority(id) {
        if (id === undefined) return 0; // No task ID.
        return priorities[id];
      }
    }
  
    function init$1(params) {
      // Process parameters, substituting defaults as needed
      var canvSize = params.size || 512;
      var styleURL = params.style;   // REQUIRED
      var mbToken  = params.token;   // May be undefined
      var callback = params.callback || ( () => undefined );
  
      // Declare some variables & methods that will be defined inside a callback
      var groupNames, tileFactory, renderer, t0, t1, t2;
      var styleGroups = [];
      var activeDrawCalls = 0;
  
      function setGroupVisibility(name, visibility) {
        var group = styleGroups.find(group => group.name === name);
        if (group) group.visible = visibility;
      }
  
      // Initialize a worker thread to read and parse MVT tiles
      const readThread = initWorker("./worker.bundle.js");
  
      // Initialize handler for chaining functions asynchronously
      const chains = initChainer();
  
      const api = { // Initialize properties, update when styles load
        style: {},    // WARNING: directly modifiable from calling program
        groups: [],
  
        create: () => undefined,
        hideGroup: (name) => setGroupVisibility(name, false),
        showGroup: (name) => setGroupVisibility(name, true),
        redraw: () => undefined,
        activeDrawCalls: () => activeDrawCalls,
        priorities: chains.priorities,
  
        ready: false,
      };
  
      // Get the style info
      loadStyle(styleURL, mbToken, setup);
  
      return api;
  
      function setup(err, styleDoc) {
        if (err) callback(err);
  
        // Get layer group names from styleDoc
        groupNames = styleDoc.layers
          .map( layer => layer["tilekiln-group"] || "none" )
          .filter(uniq);
  
        // Make sure the groups are in order, not interleaved
        var groupCheck = groupNames.slice().sort().filter(uniq);
        if (groupNames.length !== groupCheck.length) {
          err = "tilekiln setup: Input layer groups are not in order!";
          return callback(err);
        }
        
        function uniq(x, i, a) {
          return ( !i || x !== a[i-1] ); // x is not a repeat of the previous value
        }
  
        // Construct an object to track visibility of each group
        styleGroups = groupNames.map( name => {
          return { name, visible: true };
        });
  
        tileFactory = initTileFactory(canvSize, styleDoc.sources, 
          styleGroups, readThread);
        renderer = initRenderer(canvSize, styleDoc.layers, 
          styleGroups, styleDoc.sprite, chains);
  
        // Update api
        // TODO: we could initialize renderer without styles, then send it the
        // styles when ready. This could avoid the need to rewrite the API.
        api.style = styleDoc;
        api.create = create;
  
        api.redraw = drawAll;
        api.groups = groupNames;
        api.ready = true;
  
        return callback(null, api);
      }
  
      function create(z, x, y, cb = () => undefined, reportTime) {
        if (reportTime) t0 = performance.now();
        var tile = tileFactory(z, x, y, render);
        function render(err) {
          if (err) cb(err);
          if (reportTime) {
            t1 = performance.now();
            cb("Calling drawAll");
          }
          drawAll(tile, cb, reportTime);
        }
        return tile;
      }
  
      function drawAll(tile, callback = () => true, reportTime) {
        // If tile has been canceled, exit without even executing the callback
        if (tile.canceled) return;
  
        if (tile.rendering) {
          console.log("ERROR in tilekiln.drawAll: tile already rendering!");
          console.log("  Not sure what to do... Continuing!");
        }
        if (tile.rendered) {
          console.log("ERROR in tilekiln.drawAll: tile is already rendered??");
          console.log("  Not sure what to do... Continuing!");
        }
  
        // Flag this tile as in the process of rendering
        tile.rendering = true;
        activeDrawCalls ++;
        // Set initial priority for rendering tasks
        chains.priorities[tile.id] = tile.priority;
  
        //var numToDo = styleGroups.length;
        //styleGroups.forEach(group => {
        //  if (!group.visible) return;
        //  let cb = (err, tile) => checkAll(err, tile, group.name);
        //  renderer.drawGroup(tile, group.name, cb);
        //});
  
        // Make a chain of functions to draw each group
        const drawCalls = styleGroups.filter(grp => grp.visible).map(makeDrawCall);
  
        function makeDrawCall(group) {
          // Wrap a drawGroup call to make a function taking only a callback
          // as an argument
          return (cb) => {
            // Modify the callback to check the tile first
            let checkCb = (err, tile) => {
              check(err, tile, group.name);
              cb();
            };
            renderer.drawGroup(tile, group.name, checkCb);
          };
        }
  
        // Execute the chain, with putTogether as the final callback
        chains.chainAsyncList(drawCalls, putTogether, tile.id);
  
        function putTogether() {
          renderer.composite(tile);
  
          tile.rendered = true;
          tile.rendering = false;
          activeDrawCalls --;
  
          if (!reportTime) return callback(null, tile);
          t2 = performance.now();
          return callback(null, tile, t2 - t1, t1 - t0);
        }
  
        function check(err, tile, groupName) {
          if (err) return callback(err);
          if (reportTime) {
            let dt = (performance.now() - t0).toFixed(1);
            callback("check: " + groupName + ", dt = " + dt + "ms");
          }
        }
      }
    }
  
    function setParams(userParams, context) {
      const params = {
        tileSize: userParams.tileSize || 512,
        width: userParams.width || context.canvas.width,
        height: userParams.height || context.canvas.height,
        center: userParams.center || [0.5, 0.5], // X, Y in map coordinates
      };
  
      // Compute number of tiles in each direction.
      params.nx = Math.floor(params.width / params.tileSize);
      params.ny = Math.floor(params.height / params.tileSize);
      if (params.nx * params.tileSize !== params.width ||
          params.ny * params.tileSize !== params.height ) {
        console.log("width, height, tileSize = " +
            params.width + ", " + params.height + ", " + params.tileSize);
        return console.log("ERROR: width, height are not multiples of tileSize!!");
      }
      console.log("map size: " + params.width + "x" + params.height);
  
      // Define a min zoom (if not supplied), such that there are always enough
      // tiles to cover the grid without repeating
      params.minZoom = (userParams.minZoom === undefined)
        ? Math.floor( Math.min(Math.log2(params.nx), Math.log2(params.ny)) )
        : Math.max(0, Math.floor(userParams.minZoom));
      // Make sure any supplied max zoom is an integer larger than minZoom
      params.maxZoom = (userParams.maxZoom === undefined)
        ? 22
        : Math.max(params.minZoom, Math.floor(userParams.maxZoom));
      // Make sure initial zoom is an integer within range
      params.zoom = (userParams.zoom === undefined)
        ? params.minZoom
        : Math.floor(userParams.zoom);
      params.zoom = Math.min(Math.max(params.minZoom, params.zoom), params.maxZoom);
  
      params.center[0] = Math.min(Math.max(0.0, params.center[0]), 1.0);
      params.center[1] = Math.min(Math.max(0.0, params.center[1]), 1.0);
  
      return params;
    }
  
    // TODO: verify code for non-Mercator projections. Assumes x is periodic
    function initTileCoords( params ) {
      var zoom, nTiles, xTile0, yTile0;
      const origin = new Float64Array(2);
      const scale = new Float64Array(2);
  
      // Set initial values
      setCenterZoom(params.center, params.zoom);
  
      return {
        // Methods to report info about current map state
        getScale: (i) => scale[i],
        getZXY,
  
        // Methods to compute positions within current map
        toLocal,
        xyToMapPixels,
  
        // Methods to update map state
        setCenterZoom,
        fitBoundingBox,
        move,
      };
  
      function getZXY(zxy, ix, iy) {
        // Report the ZXY of a given tile within the grid
        zxy[0] = zoom;
        zxy[1] = wrap(xTile0 + ix, nTiles);
        zxy[2] = wrap(yTile0 + iy, nTiles);
      }
  
      function toLocal(local, global) {
        // TODO: wrapping is problematic
        local[0] = wrap(global[0] - origin[0], 1.0) * scale[0];
        local[1] = (global[1] - origin[1]) * scale[1];
      }
  
      function xyToMapPixels(local, global) {
        toLocal(local, global);
        local[0] *= params.width;
        local[1] *= params.height;
      }
  
      function setCenterZoom(center, newZoom) {
        // 0. Remember old values
        var oldZ = zoom;
        var oldX = xTile0;
        var oldY = yTile0;
  
        // 1. Make sure the supplied zoom is within range and an integer
        zoom = Math.min(Math.max(params.minZoom, newZoom), params.maxZoom);
        zoom = Math.floor(zoom); // TODO: should this be Math.round() ?
        nTiles = 2 ** zoom; // Number of tiles at this zoom level
  
        // 2. Find the integer tile numbers of the top left corner of the rectangle
        //    whose center will be within 1/2 tile of (centerX, centerY)
        xTile0 = Math.round(center[0] * nTiles - params.nx / 2.0);
        xTile0 = wrap(xTile0, nTiles); // in case we crossed the antimeridian
  
        yTile0 = Math.round(center[1] * nTiles - params.ny / 2.0);
        yTile0 = Math.min(Math.max(0, yTile0), nTiles - params.ny); // Don't cross pole
  
        // 3. Return a flag indicating whether map parameters were updated
        if (zoom === oldZ && xTile0 === oldX && yTile0 === oldY) return false;
        updateTransform();
        return true;
      }
  
      function fitBoundingBox(p1, p2) {
        // Inputs p1, p2 are 2D arrays containing pairs of X/Y coordinates
        // in the range [0,1] X [0,1] with (0,0) at the top left corner.
        // ASSUMES p2 is SouthEast of p1 although we may have p2[0] < p1[0]
        // if the box crosses the antimeridian (longitude = +/- PI)
  
        // Compute box width and height, with special handling for antimeridian
        var boxWidth = p2[0] - p1[0];
        if (boxWidth < 0) boxWidth += 1.0; // Crossing antimeridian
        var boxHeight = p2[1] - p1[1];
        if (boxHeight < 0) return false;
  
        // Calculate the maximum zoom level at which the bounding box will fit
        // within the map. Note: we want to be able to pan without having to change
        // zoom. Hence the bounding box must always fit within gridSize - 1.
        // (allows panning to where p1[0] is near the right edge of a tile.)
  
        // Width/height of a tile: 1 / 2 ** zoom. Hence we need
        //  (gridSize? - 1) / 2 ** zoom > boxSize in both X and Y.
        var zoomX = Math.log2( (params.nx - 1) / boxWidth );
        var zoomY = Math.log2( (params.ny - 1) / boxHeight );
  
        // Compute the coordinates of the center of the box
        var centerX = (p1[0] + boxWidth / 2.0);
        if (centerX > 1) centerX -= 1;
        var centerY = 0.5 * (p1[1] + p2[1]);
  
        return setCenterZoom( [centerX, centerY], Math.min(zoomX, zoomY) );
      }
  
      function move(dz, dx, dy) {
        var dzi = Math.round(dz);
        var dxi = Math.round(dx);
        var dyi = Math.round(dy);
  
        // Don't zoom beyond the limits of the API
        dzi = Math.min(Math.max(0 - zoom, dzi), params.maxZoom - zoom);
  
        var changed = (dzi || dxi || dyi);
  
        // Panning first
        xTile0 = wrap(xTile0 + dxi, nTiles);
        yTile0 = wrap(yTile0 + dyi, nTiles);
  
        while (dzi > 0) {  // Zoom in
          zoom++;
          xTile0 = Math.floor(2 * xTile0 + params.nx / 2.0);
          yTile0 = Math.floor(2 * yTile0 + params.ny / 2.0);
          dzi--;
        }
        while (dzi < 0) {  // Zoom out
          zoom--;
          xTile0 = wrap( Math.ceil( (xTile0 - params.nx / 2.0) / 2 ), nTiles );
          yTile0 = wrap( Math.ceil( (yTile0 - params.ny / 2.0) / 2 ), nTiles );
          dzi++;
        }
  
        updateTransform();
        return changed;
      }
  
      function updateTransform() {
        nTiles = 2 ** zoom;
        origin[0] = xTile0 / nTiles;
        origin[1] = yTile0 / nTiles;
        scale[0] = nTiles / params.nx; // Problematic if < 1 ?
        scale[1] = nTiles / params.ny;
      }
    }
  
    function wrap(x, xmax) {
      while (x < 0) x += xmax;
      while (x >= xmax) x -= xmax;
      return x;
    }
  
    function initRenderer$1(context, params) {
      const size = params.tileSize;
  
      // Resize drawingbuffer to fit the specified number of tiles
      context.canvas.width = params.width;
      context.canvas.height = params.height;
  
      return {
        draw,
        clear,
      };
  
      function clear() {
        return context.clearRect(0, 0, params.width, params.height);
      }
  
      function draw(tilebox, ix, iy) {
        context.drawImage(
            tilebox.tile.img,  // Image to read, and paint to the canvas
            tilebox.sx,        // First x-pixel in tile to read
            tilebox.sy,        // First y-pixel in tile to read
            tilebox.sw,        // Number of pixels to read in x
            tilebox.sw,        // Number of pixels to read in y
            ix * size,         // First x-pixel in canvas to paint
            iy * size,         // First y-pixel in canvas to paint
            size,              // Number of pixels to paint in x
            size               // Number of pixels to paint in y
            );
        return;
      }
    }
  
    // Creates a distance metric function that indicates, for a given tile
    // specified by z, x, y indices, how far away it is from the current grid
    function initTileMetric(params, getZXY) {
      const zxy = [];
  
      return function(z, x, y) {
        getZXY(zxy, 0, 0);  // Get indices of the tile at the top left corner
        let nTiles = 2 ** zxy[0];
  
        // Find edges of tile and map, in units of tiles at current map zoom
        var zoomFac = 2 ** (zxy[0] - z);
        var tile = {
          x1: x * zoomFac,
          x2: (x + 1) * zoomFac,
          y1: y * zoomFac,
          y2: (y + 1) * zoomFac,
        };
        var map = {
          x1: zxy[1],
          x2: zxy[1] + params.nx + 1, // Note: may extend across antimeridian!
          y1: zxy[2],
          y2: zxy[2] + params.ny + 1, // Note: may extend across a pole!
        };
  
        // Find horizontal distance between current tile and edges of current map
        //  hdist < 0: part of input tile is within map
        //  hdist = 0: tile edge touches edge of map
        //  hdist = n: tile edge is n tiles away from edge of map,
        //             where a "tile" is measured at map zoom level
  
        // Note: need to be careful with distances crossing an antimeridian or pole
        var xdist = Math.min(
            // Test for non-intersection with tile in raw position
            Math.max(map.x1 - tile.x2, tile.x1 - map.x2),
            // Re-test with tile shifted across antimeridian 
            Math.max(map.x1 - (tile.x2 + nTiles), (tile.x1 + nTiles) - map.x2)
            );
        var ydist = Math.min(
            // Test for non-intersection with tile in raw position
            Math.max(map.y1 - tile.y2, tile.y1 - map.y2),
            // Re-test with tile shifted across pole 
            Math.max(map.y1 - (tile.y2 + nTiles), (tile.y1 + nTiles) - map.y2)
            );
        // Use the largest distance
        var hdist = Math.max(xdist, ydist);
  
        // Adjust for zoom difference
        return hdist - 1.0 + 1.0 / zoomFac;
      }
    }
  
    function initGrid(userParams, context, tiles) {
      const params = setParams(userParams, context);
      if (!params) return;
  
      // Initialize coordinates, tile distance metric, and renderer
      const coords = initTileCoords(params);
      const renderer = initRenderer$1(context, params);
  
      // Initialize status tracking for tile loading
      const oneTileComplete = 1. / params.nx / params.ny;
      var complete = 0.0;
  
      // Initialize array of tileboxes and function to reset it
      const boxes = []; //Array(params.ny).fill([]);
      reset();
  
      return {
        // Report status or data
        loaded: () => complete,
        boxes,
        // Clear or update the canvas
        reset,
        clear,
        drawTiles,
  
        // Coordinate methods to set the position and zoom of the map
        move:       (dz, dx, dy) => { if (coords.move(dz, dx, dy))       clear(); },
        fitBoundingBox: (p1, p2) => { if (coords.fitBoundingBox(p1, p2)) clear(); },
        setCenterZoom:    (c, z) => { if (coords.setCenterZoom(c, z))    clear(); },
  
        // Methods to convert coordinates, or report conversion parameters
        toLocal:       coords.toLocal,
        xyToMapPixels: coords.xyToMapPixels,
        getScale:      coords.getScale,
  
        // Metric to evaluate distance of a tile from the current grid
        tileDistance: initTileMetric(params, coords.getZXY),
      };
  
      function reset() {
        //boxes.fill([]); // Doesn't work... not sure why not
        for (let iy = 0; iy < params.ny; iy++) {
          boxes[iy] = [];
        }
        complete = 0.0;
      }
      function clear() { // TODO: Do we ever need reset without clear?
        reset();
        renderer.clear();
      }
  
      function drawTiles() {
        // Quick exit if map is already complete.
        if (complete === 1.0) return false; // No change!
  
        var updated = false;
        const zxy = [];
  
        // Loop over tiles in the map
        for (let iy = 0; iy < params.ny; iy++) {
          var row = boxes[iy];
          for (let ix = 0; ix < params.nx; ix++) {
            coords.getZXY(zxy, ix, iy);
            var currentZ = (row[ix]) 
              ? row[ix].tile.z
              : undefined;
            if (currentZ === zxy[0]) continue; // This tile already done
  
            var newbox = tiles.retrieve( zxy );
            if (!newbox) continue; // No image available for this tile
            if (newbox.tile.z === currentZ) continue; // Tile already written
  
            row[ix] = newbox;
            renderer.draw(newbox, ix, iy);
            updated = true;
  
            if (newbox.tile.z === zxy[0]) complete += oneTileComplete;
          }
        }
        return updated;
      }
    }
  
    function unwrapExports (x) {
    	return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
    }
  
    function createCommonjsModule$1(fn, module) {
    	return module = { exports: {} }, fn(module, module.exports), module.exports;
    }
  
    var helpers = createCommonjsModule$1(function (module, exports) {
    Object.defineProperty(exports, "__esModule", { value: true });
    /**
     * @module helpers
     */
    /**
     * Earth Radius used with the Harvesine formula and approximates using a spherical (non-ellipsoid) Earth.
     *
     * @memberof helpers
     * @type {number}
     */
    exports.earthRadius = 6371008.8;
    /**
     * Unit of measurement factors using a spherical (non-ellipsoid) earth radius.
     *
     * @memberof helpers
     * @type {Object}
     */
    exports.factors = {
        centimeters: exports.earthRadius * 100,
        centimetres: exports.earthRadius * 100,
        degrees: exports.earthRadius / 111325,
        feet: exports.earthRadius * 3.28084,
        inches: exports.earthRadius * 39.370,
        kilometers: exports.earthRadius / 1000,
        kilometres: exports.earthRadius / 1000,
        meters: exports.earthRadius,
        metres: exports.earthRadius,
        miles: exports.earthRadius / 1609.344,
        millimeters: exports.earthRadius * 1000,
        millimetres: exports.earthRadius * 1000,
        nauticalmiles: exports.earthRadius / 1852,
        radians: 1,
        yards: exports.earthRadius / 1.0936,
    };
    /**
     * Units of measurement factors based on 1 meter.
     *
     * @memberof helpers
     * @type {Object}
     */
    exports.unitsFactors = {
        centimeters: 100,
        centimetres: 100,
        degrees: 1 / 111325,
        feet: 3.28084,
        inches: 39.370,
        kilometers: 1 / 1000,
        kilometres: 1 / 1000,
        meters: 1,
        metres: 1,
        miles: 1 / 1609.344,
        millimeters: 1000,
        millimetres: 1000,
        nauticalmiles: 1 / 1852,
        radians: 1 / exports.earthRadius,
        yards: 1 / 1.0936,
    };
    /**
     * Area of measurement factors based on 1 square meter.
     *
     * @memberof helpers
     * @type {Object}
     */
    exports.areaFactors = {
        acres: 0.000247105,
        centimeters: 10000,
        centimetres: 10000,
        feet: 10.763910417,
        inches: 1550.003100006,
        kilometers: 0.000001,
        kilometres: 0.000001,
        meters: 1,
        metres: 1,
        miles: 3.86e-7,
        millimeters: 1000000,
        millimetres: 1000000,
        yards: 1.195990046,
    };
    /**
     * Wraps a GeoJSON {@link Geometry} in a GeoJSON {@link Feature}.
     *
     * @name feature
     * @param {Geometry} geometry input geometry
     * @param {Object} [properties={}] an Object of key-value pairs to add as properties
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature
     * @param {string|number} [options.id] Identifier associated with the Feature
     * @returns {Feature} a GeoJSON Feature
     * @example
     * var geometry = {
     *   "type": "Point",
     *   "coordinates": [110, 50]
     * };
     *
     * var feature = turf.feature(geometry);
     *
     * //=feature
     */
    function feature(geom, properties, options) {
        if (options === void 0) { options = {}; }
        var feat = { type: "Feature" };
        if (options.id === 0 || options.id) {
            feat.id = options.id;
        }
        if (options.bbox) {
            feat.bbox = options.bbox;
        }
        feat.properties = properties || {};
        feat.geometry = geom;
        return feat;
    }
    exports.feature = feature;
    /**
     * Creates a GeoJSON {@link Geometry} from a Geometry string type & coordinates.
     * For GeometryCollection type use `helpers.geometryCollection`
     *
     * @name geometry
     * @param {string} type Geometry Type
     * @param {Array<any>} coordinates Coordinates
     * @param {Object} [options={}] Optional Parameters
     * @returns {Geometry} a GeoJSON Geometry
     * @example
     * var type = "Point";
     * var coordinates = [110, 50];
     * var geometry = turf.geometry(type, coordinates);
     * // => geometry
     */
    function geometry(type, coordinates, options) {
        switch (type) {
            case "Point": return point(coordinates).geometry;
            case "LineString": return lineString(coordinates).geometry;
            case "Polygon": return polygon(coordinates).geometry;
            case "MultiPoint": return multiPoint(coordinates).geometry;
            case "MultiLineString": return multiLineString(coordinates).geometry;
            case "MultiPolygon": return multiPolygon(coordinates).geometry;
            default: throw new Error(type + " is invalid");
        }
    }
    exports.geometry = geometry;
    /**
     * Creates a {@link Point} {@link Feature} from a Position.
     *
     * @name point
     * @param {Array<number>} coordinates longitude, latitude position (each in decimal degrees)
     * @param {Object} [properties={}] an Object of key-value pairs to add as properties
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature
     * @param {string|number} [options.id] Identifier associated with the Feature
     * @returns {Feature<Point>} a Point feature
     * @example
     * var point = turf.point([-75.343, 39.984]);
     *
     * //=point
     */
    function point(coordinates, properties, options) {
        if (options === void 0) { options = {}; }
        var geom = {
            type: "Point",
            coordinates: coordinates,
        };
        return feature(geom, properties, options);
    }
    exports.point = point;
    /**
     * Creates a {@link Point} {@link FeatureCollection} from an Array of Point coordinates.
     *
     * @name points
     * @param {Array<Array<number>>} coordinates an array of Points
     * @param {Object} [properties={}] Translate these properties to each Feature
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north]
     * associated with the FeatureCollection
     * @param {string|number} [options.id] Identifier associated with the FeatureCollection
     * @returns {FeatureCollection<Point>} Point Feature
     * @example
     * var points = turf.points([
     *   [-75, 39],
     *   [-80, 45],
     *   [-78, 50]
     * ]);
     *
     * //=points
     */
    function points(coordinates, properties, options) {
        if (options === void 0) { options = {}; }
        return featureCollection(coordinates.map(function (coords) {
            return point(coords, properties);
        }), options);
    }
    exports.points = points;
    /**
     * Creates a {@link Polygon} {@link Feature} from an Array of LinearRings.
     *
     * @name polygon
     * @param {Array<Array<Array<number>>>} coordinates an array of LinearRings
     * @param {Object} [properties={}] an Object of key-value pairs to add as properties
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature
     * @param {string|number} [options.id] Identifier associated with the Feature
     * @returns {Feature<Polygon>} Polygon Feature
     * @example
     * var polygon = turf.polygon([[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]], { name: 'poly1' });
     *
     * //=polygon
     */
    function polygon(coordinates, properties, options) {
        if (options === void 0) { options = {}; }
        for (var _i = 0, coordinates_1 = coordinates; _i < coordinates_1.length; _i++) {
            var ring = coordinates_1[_i];
            if (ring.length < 4) {
                throw new Error("Each LinearRing of a Polygon must have 4 or more Positions.");
            }
            for (var j = 0; j < ring[ring.length - 1].length; j++) {
                // Check if first point of Polygon contains two numbers
                if (ring[ring.length - 1][j] !== ring[0][j]) {
                    throw new Error("First and last Position are not equivalent.");
                }
            }
        }
        var geom = {
            type: "Polygon",
            coordinates: coordinates,
        };
        return feature(geom, properties, options);
    }
    exports.polygon = polygon;
    /**
     * Creates a {@link Polygon} {@link FeatureCollection} from an Array of Polygon coordinates.
     *
     * @name polygons
     * @param {Array<Array<Array<Array<number>>>>} coordinates an array of Polygon coordinates
     * @param {Object} [properties={}] an Object of key-value pairs to add as properties
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature
     * @param {string|number} [options.id] Identifier associated with the FeatureCollection
     * @returns {FeatureCollection<Polygon>} Polygon FeatureCollection
     * @example
     * var polygons = turf.polygons([
     *   [[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]],
     *   [[[-15, 42], [-14, 46], [-12, 41], [-17, 44], [-15, 42]]],
     * ]);
     *
     * //=polygons
     */
    function polygons(coordinates, properties, options) {
        if (options === void 0) { options = {}; }
        return featureCollection(coordinates.map(function (coords) {
            return polygon(coords, properties);
        }), options);
    }
    exports.polygons = polygons;
    /**
     * Creates a {@link LineString} {@link Feature} from an Array of Positions.
     *
     * @name lineString
     * @param {Array<Array<number>>} coordinates an array of Positions
     * @param {Object} [properties={}] an Object of key-value pairs to add as properties
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature
     * @param {string|number} [options.id] Identifier associated with the Feature
     * @returns {Feature<LineString>} LineString Feature
     * @example
     * var linestring1 = turf.lineString([[-24, 63], [-23, 60], [-25, 65], [-20, 69]], {name: 'line 1'});
     * var linestring2 = turf.lineString([[-14, 43], [-13, 40], [-15, 45], [-10, 49]], {name: 'line 2'});
     *
     * //=linestring1
     * //=linestring2
     */
    function lineString(coordinates, properties, options) {
        if (options === void 0) { options = {}; }
        if (coordinates.length < 2) {
            throw new Error("coordinates must be an array of two or more positions");
        }
        var geom = {
            type: "LineString",
            coordinates: coordinates,
        };
        return feature(geom, properties, options);
    }
    exports.lineString = lineString;
    /**
     * Creates a {@link LineString} {@link FeatureCollection} from an Array of LineString coordinates.
     *
     * @name lineStrings
     * @param {Array<Array<Array<number>>>} coordinates an array of LinearRings
     * @param {Object} [properties={}] an Object of key-value pairs to add as properties
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north]
     * associated with the FeatureCollection
     * @param {string|number} [options.id] Identifier associated with the FeatureCollection
     * @returns {FeatureCollection<LineString>} LineString FeatureCollection
     * @example
     * var linestrings = turf.lineStrings([
     *   [[-24, 63], [-23, 60], [-25, 65], [-20, 69]],
     *   [[-14, 43], [-13, 40], [-15, 45], [-10, 49]]
     * ]);
     *
     * //=linestrings
     */
    function lineStrings(coordinates, properties, options) {
        if (options === void 0) { options = {}; }
        return featureCollection(coordinates.map(function (coords) {
            return lineString(coords, properties);
        }), options);
    }
    exports.lineStrings = lineStrings;
    /**
     * Takes one or more {@link Feature|Features} and creates a {@link FeatureCollection}.
     *
     * @name featureCollection
     * @param {Feature[]} features input features
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature
     * @param {string|number} [options.id] Identifier associated with the Feature
     * @returns {FeatureCollection} FeatureCollection of Features
     * @example
     * var locationA = turf.point([-75.343, 39.984], {name: 'Location A'});
     * var locationB = turf.point([-75.833, 39.284], {name: 'Location B'});
     * var locationC = turf.point([-75.534, 39.123], {name: 'Location C'});
     *
     * var collection = turf.featureCollection([
     *   locationA,
     *   locationB,
     *   locationC
     * ]);
     *
     * //=collection
     */
    function featureCollection(features, options) {
        if (options === void 0) { options = {}; }
        var fc = { type: "FeatureCollection" };
        if (options.id) {
            fc.id = options.id;
        }
        if (options.bbox) {
            fc.bbox = options.bbox;
        }
        fc.features = features;
        return fc;
    }
    exports.featureCollection = featureCollection;
    /**
     * Creates a {@link Feature<MultiLineString>} based on a
     * coordinate array. Properties can be added optionally.
     *
     * @name multiLineString
     * @param {Array<Array<Array<number>>>} coordinates an array of LineStrings
     * @param {Object} [properties={}] an Object of key-value pairs to add as properties
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature
     * @param {string|number} [options.id] Identifier associated with the Feature
     * @returns {Feature<MultiLineString>} a MultiLineString feature
     * @throws {Error} if no coordinates are passed
     * @example
     * var multiLine = turf.multiLineString([[[0,0],[10,10]]]);
     *
     * //=multiLine
     */
    function multiLineString(coordinates, properties, options) {
        if (options === void 0) { options = {}; }
        var geom = {
            type: "MultiLineString",
            coordinates: coordinates,
        };
        return feature(geom, properties, options);
    }
    exports.multiLineString = multiLineString;
    /**
     * Creates a {@link Feature<MultiPoint>} based on a
     * coordinate array. Properties can be added optionally.
     *
     * @name multiPoint
     * @param {Array<Array<number>>} coordinates an array of Positions
     * @param {Object} [properties={}] an Object of key-value pairs to add as properties
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature
     * @param {string|number} [options.id] Identifier associated with the Feature
     * @returns {Feature<MultiPoint>} a MultiPoint feature
     * @throws {Error} if no coordinates are passed
     * @example
     * var multiPt = turf.multiPoint([[0,0],[10,10]]);
     *
     * //=multiPt
     */
    function multiPoint(coordinates, properties, options) {
        if (options === void 0) { options = {}; }
        var geom = {
            type: "MultiPoint",
            coordinates: coordinates,
        };
        return feature(geom, properties, options);
    }
    exports.multiPoint = multiPoint;
    /**
     * Creates a {@link Feature<MultiPolygon>} based on a
     * coordinate array. Properties can be added optionally.
     *
     * @name multiPolygon
     * @param {Array<Array<Array<Array<number>>>>} coordinates an array of Polygons
     * @param {Object} [properties={}] an Object of key-value pairs to add as properties
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature
     * @param {string|number} [options.id] Identifier associated with the Feature
     * @returns {Feature<MultiPolygon>} a multipolygon feature
     * @throws {Error} if no coordinates are passed
     * @example
     * var multiPoly = turf.multiPolygon([[[[0,0],[0,10],[10,10],[10,0],[0,0]]]]);
     *
     * //=multiPoly
     *
     */
    function multiPolygon(coordinates, properties, options) {
        if (options === void 0) { options = {}; }
        var geom = {
            type: "MultiPolygon",
            coordinates: coordinates,
        };
        return feature(geom, properties, options);
    }
    exports.multiPolygon = multiPolygon;
    /**
     * Creates a {@link Feature<GeometryCollection>} based on a
     * coordinate array. Properties can be added optionally.
     *
     * @name geometryCollection
     * @param {Array<Geometry>} geometries an array of GeoJSON Geometries
     * @param {Object} [properties={}] an Object of key-value pairs to add as properties
     * @param {Object} [options={}] Optional Parameters
     * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature
     * @param {string|number} [options.id] Identifier associated with the Feature
     * @returns {Feature<GeometryCollection>} a GeoJSON GeometryCollection Feature
     * @example
     * var pt = turf.geometry("Point", [100, 0]);
     * var line = turf.geometry("LineString", [[101, 0], [102, 1]]);
     * var collection = turf.geometryCollection([pt, line]);
     *
     * // => collection
     */
    function geometryCollection(geometries, properties, options) {
        if (options === void 0) { options = {}; }
        var geom = {
            type: "GeometryCollection",
            geometries: geometries,
        };
        return feature(geom, properties, options);
    }
    exports.geometryCollection = geometryCollection;
    /**
     * Round number to precision
     *
     * @param {number} num Number
     * @param {number} [precision=0] Precision
     * @returns {number} rounded number
     * @example
     * turf.round(120.4321)
     * //=120
     *
     * turf.round(120.4321, 2)
     * //=120.43
     */
    function round(num, precision) {
        if (precision === void 0) { precision = 0; }
        if (precision && !(precision >= 0)) {
            throw new Error("precision must be a positive number");
        }
        var multiplier = Math.pow(10, precision || 0);
        return Math.round(num * multiplier) / multiplier;
    }
    exports.round = round;
    /**
     * Convert a distance measurement (assuming a spherical Earth) from radians to a more friendly unit.
     * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet
     *
     * @name radiansToLength
     * @param {number} radians in radians across the sphere
     * @param {string} [units="kilometers"] can be degrees, radians, miles, or kilometers inches, yards, metres,
     * meters, kilometres, kilometers.
     * @returns {number} distance
     */
    function radiansToLength(radians, units) {
        if (units === void 0) { units = "kilometers"; }
        var factor = exports.factors[units];
        if (!factor) {
            throw new Error(units + " units is invalid");
        }
        return radians * factor;
    }
    exports.radiansToLength = radiansToLength;
    /**
     * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into radians
     * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet
     *
     * @name lengthToRadians
     * @param {number} distance in real units
     * @param {string} [units="kilometers"] can be degrees, radians, miles, or kilometers inches, yards, metres,
     * meters, kilometres, kilometers.
     * @returns {number} radians
     */
    function lengthToRadians(distance, units) {
        if (units === void 0) { units = "kilometers"; }
        var factor = exports.factors[units];
        if (!factor) {
            throw new Error(units + " units is invalid");
        }
        return distance / factor;
    }
    exports.lengthToRadians = lengthToRadians;
    /**
     * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into degrees
     * Valid units: miles, nauticalmiles, inches, yards, meters, metres, centimeters, kilometres, feet
     *
     * @name lengthToDegrees
     * @param {number} distance in real units
     * @param {string} [units="kilometers"] can be degrees, radians, miles, or kilometers inches, yards, metres,
     * meters, kilometres, kilometers.
     * @returns {number} degrees
     */
    function lengthToDegrees(distance, units) {
        return radiansToDegrees(lengthToRadians(distance, units));
    }
    exports.lengthToDegrees = lengthToDegrees;
    /**
     * Converts any bearing angle from the north line direction (positive clockwise)
     * and returns an angle between 0-360 degrees (positive clockwise), 0 being the north line
     *
     * @name bearingToAzimuth
     * @param {number} bearing angle, between -180 and +180 degrees
     * @returns {number} angle between 0 and 360 degrees
     */
    function bearingToAzimuth(bearing) {
        var angle = bearing % 360;
        if (angle < 0) {
            angle += 360;
        }
        return angle;
    }
    exports.bearingToAzimuth = bearingToAzimuth;
    /**
     * Converts an angle in radians to degrees
     *
     * @name radiansToDegrees
     * @param {number} radians angle in radians
     * @returns {number} degrees between 0 and 360 degrees
     */
    function radiansToDegrees(radians) {
        var degrees = radians % (2 * Math.PI);
        return degrees * 180 / Math.PI;
    }
    exports.radiansToDegrees = radiansToDegrees;
    /**
     * Converts an angle in degrees to radians
     *
     * @name degreesToRadians
     * @param {number} degrees angle between 0 and 360 degrees
     * @returns {number} angle in radians
     */
    function degreesToRadians(degrees) {
        var radians = degrees % 360;
        return radians * Math.PI / 180;
    }
    exports.degreesToRadians = degreesToRadians;
    /**
     * Converts a length to the requested unit.
     * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet
     *
     * @param {number} length to be converted
     * @param {Units} [originalUnit="kilometers"] of the length
     * @param {Units} [finalUnit="kilometers"] returned unit
     * @returns {number} the converted length
     */
    function convertLength(length, originalUnit, finalUnit) {
        if (originalUnit === void 0) { originalUnit = "kilometers"; }
        if (finalUnit === void 0) { finalUnit = "kilometers"; }
        if (!(length >= 0)) {
            throw new Error("length must be a positive number");
        }
        return radiansToLength(lengthToRadians(length, originalUnit), finalUnit);
    }
    exports.convertLength = convertLength;
    /**
     * Converts a area to the requested unit.
     * Valid units: kilometers, kilometres, meters, metres, centimetres, millimeters, acres, miles, yards, feet, inches
     * @param {number} area to be converted
     * @param {Units} [originalUnit="meters"] of the distance
     * @param {Units} [finalUnit="kilometers"] returned unit
     * @returns {number} the converted distance
     */
    function convertArea(area, originalUnit, finalUnit) {
        if (originalUnit === void 0) { originalUnit = "meters"; }
        if (finalUnit === void 0) { finalUnit = "kilometers"; }
        if (!(area >= 0)) {
            throw new Error("area must be a positive number");
        }
        var startFactor = exports.areaFactors[originalUnit];
        if (!startFactor) {
            throw new Error("invalid original units");
        }
        var finalFactor = exports.areaFactors[finalUnit];
        if (!finalFactor) {
            throw new Error("invalid final units");
        }
        return (area / startFactor) * finalFactor;
    }
    exports.convertArea = convertArea;
    /**
     * isNumber
     *
     * @param {*} num Number to validate
     * @returns {boolean} true/false
     * @example
     * turf.isNumber(123)
     * //=true
     * turf.isNumber('foo')
     * //=false
     */
    function isNumber(num) {
        return !isNaN(num) && num !== null && !Array.isArray(num) && !/^\s*$/.test(num);
    }
    exports.isNumber = isNumber;
    /**
     * isObject
     *
     * @param {*} input variable to validate
     * @returns {boolean} true/false
     * @example
     * turf.isObject({elevation: 10})
     * //=true
     * turf.isObject('foo')
     * //=false
     */
    function isObject(input) {
        return (!!input) && (input.constructor === Object);
    }
    exports.isObject = isObject;
    /**
     * Validate BBox
     *
     * @private
     * @param {Array<number>} bbox BBox to validate
     * @returns {void}
     * @throws Error if BBox is not valid
     * @example
     * validateBBox([-180, -40, 110, 50])
     * //=OK
     * validateBBox([-180, -40])
     * //=Error
     * validateBBox('Foo')
     * //=Error
     * validateBBox(5)
     * //=Error
     * validateBBox(null)
     * //=Error
     * validateBBox(undefined)
     * //=Error
     */
    function validateBBox(bbox) {
        if (!bbox) {
            throw new Error("bbox is required");
        }
        if (!Array.isArray(bbox)) {
            throw new Error("bbox must be an Array");
        }
        if (bbox.length !== 4 && bbox.length !== 6) {
            throw new Error("bbox must be an Array of 4 or 6 numbers");
        }
        bbox.forEach(function (num) {
            if (!isNumber(num)) {
                throw new Error("bbox must only contain numbers");
            }
        });
    }
    exports.validateBBox = validateBBox;
    /**
     * Validate Id
     *
     * @private
     * @param {string|number} id Id to validate
     * @returns {void}
     * @throws Error if Id is not valid
     * @example
     * validateId([-180, -40, 110, 50])
     * //=Error
     * validateId([-180, -40])
     * //=Error
     * validateId('Foo')
     * //=OK
     * validateId(5)
     * //=OK
     * validateId(null)
     * //=Error
     * validateId(undefined)
     * //=Error
     */
    function validateId(id) {
        if (!id) {
            throw new Error("id is required");
        }
        if (["string", "number"].indexOf(typeof id) === -1) {
            throw new Error("id must be a number or a string");
        }
    }
    exports.validateId = validateId;
    // Deprecated methods
    function radians2degrees() {
        throw new Error("method has been renamed to `radiansToDegrees`");
    }
    exports.radians2degrees = radians2degrees;
    function degrees2radians() {
        throw new Error("method has been renamed to `degreesToRadians`");
    }
    exports.degrees2radians = degrees2radians;
    function distanceToDegrees() {
        throw new Error("method has been renamed to `lengthToDegrees`");
    }
    exports.distanceToDegrees = distanceToDegrees;
    function distanceToRadians() {
        throw new Error("method has been renamed to `lengthToRadians`");
    }
    exports.distanceToRadians = distanceToRadians;
    function radiansToDistance() {
        throw new Error("method has been renamed to `radiansToLength`");
    }
    exports.radiansToDistance = radiansToDistance;
    function bearingToAngle() {
        throw new Error("method has been renamed to `bearingToAzimuth`");
    }
    exports.bearingToAngle = bearingToAngle;
    function convertDistance() {
        throw new Error("method has been renamed to `convertLength`");
    }
    exports.convertDistance = convertDistance;
    });
  
    unwrapExports(helpers);
    var helpers_1 = helpers.earthRadius;
    var helpers_2 = helpers.factors;
    var helpers_3 = helpers.unitsFactors;
    var helpers_4 = helpers.areaFactors;
    var helpers_5 = helpers.feature;
    var helpers_6 = helpers.geometry;
    var helpers_7 = helpers.point;
    var helpers_8 = helpers.points;
    var helpers_9 = helpers.polygon;
    var helpers_10 = helpers.polygons;
    var helpers_11 = helpers.lineString;
    var helpers_12 = helpers.lineStrings;
    var helpers_13 = helpers.featureCollection;
    var helpers_14 = helpers.multiLineString;
    var helpers_15 = helpers.multiPoint;
    var helpers_16 = helpers.multiPolygon;
    var helpers_17 = helpers.geometryCollection;
    var helpers_18 = helpers.round;
    var helpers_19 = helpers.radiansToLength;
    var helpers_20 = helpers.lengthToRadians;
    var helpers_21 = helpers.lengthToDegrees;
    var helpers_22 = helpers.bearingToAzimuth;
    var helpers_23 = helpers.radiansToDegrees;
    var helpers_24 = helpers.degreesToRadians;
    var helpers_25 = helpers.convertLength;
    var helpers_26 = helpers.convertArea;
    var helpers_27 = helpers.isNumber;
    var helpers_28 = helpers.isObject;
    var helpers_29 = helpers.validateBBox;
    var helpers_30 = helpers.validateId;
    var helpers_31 = helpers.radians2degrees;
    var helpers_32 = helpers.degrees2radians;
    var helpers_33 = helpers.distanceToDegrees;
    var helpers_34 = helpers.distanceToRadians;
    var helpers_35 = helpers.radiansToDistance;
    var helpers_36 = helpers.bearingToAngle;
    var helpers_37 = helpers.convertDistance;
  
    var invariant = createCommonjsModule$1(function (module, exports) {
    Object.defineProperty(exports, "__esModule", { value: true });
  
    /**
     * Unwrap a coordinate from a Point Feature, Geometry or a single coordinate.
     *
     * @name getCoord
     * @param {Array<number>|Geometry<Point>|Feature<Point>} coord GeoJSON Point or an Array of numbers
     * @returns {Array<number>} coordinates
     * @example
     * var pt = turf.point([10, 10]);
     *
     * var coord = turf.getCoord(pt);
     * //= [10, 10]
     */
    function getCoord(coord) {
        if (!coord) {
            throw new Error("coord is required");
        }
        if (!Array.isArray(coord)) {
            if (coord.type === "Feature" && coord.geometry !== null && coord.geometry.type === "Point") {
                return coord.geometry.coordinates;
            }
            if (coord.type === "Point") {
                return coord.coordinates;
            }
        }
        if (Array.isArray(coord) && coord.length >= 2 && !Array.isArray(coord[0]) && !Array.isArray(coord[1])) {
            return coord;
        }
        throw new Error("coord must be GeoJSON Point or an Array of numbers");
    }
    exports.getCoord = getCoord;
    /**
     * Unwrap coordinates from a Feature, Geometry Object or an Array
     *
     * @name getCoords
     * @param {Array<any>|Geometry|Feature} coords Feature, Geometry Object or an Array
     * @returns {Array<any>} coordinates
     * @example
     * var poly = turf.polygon([[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]);
     *
     * var coords = turf.getCoords(poly);
     * //= [[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]
     */
    function getCoords(coords) {
        if (Array.isArray(coords)) {
            return coords;
        }
        // Feature
        if (coords.type === "Feature") {
            if (coords.geometry !== null) {
                return coords.geometry.coordinates;
            }
        }
        else {
            // Geometry
            if (coords.coordinates) {
                return coords.coordinates;
            }
        }
        throw new Error("coords must be GeoJSON Feature, Geometry Object or an Array");
    }
    exports.getCoords = getCoords;
    /**
     * Checks if coordinates contains a number
     *
     * @name containsNumber
     * @param {Array<any>} coordinates GeoJSON Coordinates
     * @returns {boolean} true if Array contains a number
     */
    function containsNumber(coordinates) {
        if (coordinates.length > 1 && helpers.isNumber(coordinates[0]) && helpers.isNumber(coordinates[1])) {
            return true;
        }
        if (Array.isArray(coordinates[0]) && coordinates[0].length) {
            return containsNumber(coordinates[0]);
        }
        throw new Error("coordinates must only contain numbers");
    }
    exports.containsNumber = containsNumber;
    /**
     * Enforce expectations about types of GeoJSON objects for Turf.
     *
     * @name geojsonType
     * @param {GeoJSON} value any GeoJSON object
     * @param {string} type expected GeoJSON type
     * @param {string} name name of calling function
     * @throws {Error} if value is not the expected type.
     */
    function geojsonType(value, type, name) {
        if (!type || !name) {
            throw new Error("type and name required");
        }
        if (!value || value.type !== type) {
            throw new Error("Invalid input to " + name + ": must be a " + type + ", given " + value.type);
        }
    }
    exports.geojsonType = geojsonType;
    /**
     * Enforce expectations about types of {@link Feature} inputs for Turf.
     * Internally this uses {@link geojsonType} to judge geometry types.
     *
     * @name featureOf
     * @param {Feature} feature a feature with an expected geometry type
     * @param {string} type expected GeoJSON type
     * @param {string} name name of calling function
     * @throws {Error} error if value is not the expected type.
     */
    function featureOf(feature, type, name) {
        if (!feature) {
            throw new Error("No feature passed");
        }
        if (!name) {
            throw new Error(".featureOf() requires a name");
        }
        if (!feature || feature.type !== "Feature" || !feature.geometry) {
            throw new Error("Invalid input to " + name + ", Feature with geometry required");
        }
        if (!feature.geometry || feature.geometry.type !== type) {
            throw new Error("Invalid input to " + name + ": must be a " + type + ", given " + feature.geometry.type);
        }
    }
    exports.featureOf = featureOf;
    /**
     * Enforce expectations about types of {@link FeatureCollection} inputs for Turf.
     * Internally this uses {@link geojsonType} to judge geometry types.
     *
     * @name collectionOf
     * @param {FeatureCollection} featureCollection a FeatureCollection for which features will be judged
     * @param {string} type expected GeoJSON type
     * @param {string} name name of calling function
     * @throws {Error} if value is not the expected type.
     */
    function collectionOf(featureCollection, type, name) {
        if (!featureCollection) {
            throw new Error("No featureCollection passed");
        }
        if (!name) {
            throw new Error(".collectionOf() requires a name");
        }
        if (!featureCollection || featureCollection.type !== "FeatureCollection") {
            throw new Error("Invalid input to " + name + ", FeatureCollection required");
        }
        for (var _i = 0, _a = featureCollection.features; _i < _a.length; _i++) {
            var feature = _a[_i];
            if (!feature || feature.type !== "Feature" || !feature.geometry) {
                throw new Error("Invalid input to " + name + ", Feature with geometry required");
            }
            if (!feature.geometry || feature.geometry.type !== type) {
                throw new Error("Invalid input to " + name + ": must be a " + type + ", given " + feature.geometry.type);
            }
        }
    }
    exports.collectionOf = collectionOf;
    /**
     * Get Geometry from Feature or Geometry Object
     *
     * @param {Feature|Geometry} geojson GeoJSON Feature or Geometry Object
     * @returns {Geometry|null} GeoJSON Geometry Object
     * @throws {Error} if geojson is not a Feature or Geometry Object
     * @example
     * var point = {
     *   "type": "Feature",
     *   "properties": {},
     *   "geometry": {
     *     "type": "Point",
     *     "coordinates": [110, 40]
     *   }
     * }
     * var geom = turf.getGeom(point)
     * //={"type": "Point", "coordinates": [110, 40]}
     */
    function getGeom(geojson) {
        if (geojson.type === "Feature") {
            return geojson.geometry;
        }
        return geojson;
    }
    exports.getGeom = getGeom;
    /**
     * Get GeoJSON object's type, Geometry type is prioritize.
     *
     * @param {GeoJSON} geojson GeoJSON object
     * @param {string} [name="geojson"] name of the variable to display in error message
     * @returns {string} GeoJSON type
     * @example
     * var point = {
     *   "type": "Feature",
     *   "properties": {},
     *   "geometry": {
     *     "type": "Point",
     *     "coordinates": [110, 40]
     *   }
     * }
     * var geom = turf.getType(point)
     * //="Point"
     */
    function getType(geojson, name) {
        if (geojson.type === "FeatureCollection") {
            return "FeatureCollection";
        }
        if (geojson.type === "GeometryCollection") {
            return "GeometryCollection";
        }
        if (geojson.type === "Feature" && geojson.geometry !== null) {
            return geojson.geometry.type;
        }
        return geojson.type;
    }
    exports.getType = getType;
    });
  
    unwrapExports(invariant);
    var invariant_1 = invariant.getCoord;
    var invariant_2 = invariant.getCoords;
    var invariant_3 = invariant.containsNumber;
    var invariant_4 = invariant.geojsonType;
    var invariant_5 = invariant.featureOf;
    var invariant_6 = invariant.collectionOf;
    var invariant_7 = invariant.getGeom;
    var invariant_8 = invariant.getType;
  
    var booleanPointInPolygon_1 = createCommonjsModule$1(function (module, exports) {
    Object.defineProperty(exports, "__esModule", { value: true });
  
    // http://en.wikipedia.org/wiki/Even%E2%80%93odd_rule
    // modified from: https://github.com/substack/point-in-polygon/blob/master/index.js
    // which was modified from http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
    /**
     * Takes a {@link Point} and a {@link Polygon} or {@link MultiPolygon} and determines if the point
     * resides inside the polygon. The polygon can be convex or concave. The function accounts for holes.
     *
     * @name booleanPointInPolygon
     * @param {Coord} point input point
     * @param {Feature<Polygon|MultiPolygon>} polygon input polygon or multipolygon
     * @param {Object} [options={}] Optional parameters
     * @param {boolean} [options.ignoreBoundary=false] True if polygon boundary should be ignored when determining if
     * the point is inside the polygon otherwise false.
     * @returns {boolean} `true` if the Point is inside the Polygon; `false` if the Point is not inside the Polygon
     * @example
     * var pt = turf.point([-77, 44]);
     * var poly = turf.polygon([[
     *   [-81, 41],
     *   [-81, 47],
     *   [-72, 47],
     *   [-72, 41],
     *   [-81, 41]
     * ]]);
     *
     * turf.booleanPointInPolygon(pt, poly);
     * //= true
     */
    function booleanPointInPolygon(point, polygon, options) {
        if (options === void 0) { options = {}; }
        // validation
        if (!point) {
            throw new Error("point is required");
        }
        if (!polygon) {
            throw new Error("polygon is required");
        }
        var pt = invariant.getCoord(point);
        var geom = invariant.getGeom(polygon);
        var type = geom.type;
        var bbox = polygon.bbox;
        var polys = geom.coordinates;
        // Quick elimination if point is not inside bbox
        if (bbox && inBBox(pt, bbox) === false) {
            return false;
        }
        // normalize to multipolygon
        if (type === "Polygon") {
            polys = [polys];
        }
        var insidePoly = false;
        for (var i = 0; i < polys.length && !insidePoly; i++) {
            // check if it is in the outer ring first
            if (inRing(pt, polys[i][0], options.ignoreBoundary)) {
                var inHole = false;
                var k = 1;
                // check for the point in any of the holes
                while (k < polys[i].length && !inHole) {
                    if (inRing(pt, polys[i][k], !options.ignoreBoundary)) {
                        inHole = true;
                    }
                    k++;
                }
                if (!inHole) {
                    insidePoly = true;
                }
            }
        }
        return insidePoly;
    }
    exports.default = booleanPointInPolygon;
    /**
     * inRing
     *
     * @private
     * @param {Array<number>} pt [x,y]
     * @param {Array<Array<number>>} ring [[x,y], [x,y],..]
     * @param {boolean} ignoreBoundary ignoreBoundary
     * @returns {boolean} inRing
     */
    function inRing(pt, ring, ignoreBoundary) {
        var isInside = false;
        if (ring[0][0] === ring[ring.length - 1][0] && ring[0][1] === ring[ring.length - 1][1]) {
            ring = ring.slice(0, ring.length - 1);
        }
        for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) {
            var xi = ring[i][0];
            var yi = ring[i][1];
            var xj = ring[j][0];
            var yj = ring[j][1];
            var onBoundary = (pt[1] * (xi - xj) + yi * (xj - pt[0]) + yj * (pt[0] - xi) === 0) &&
                ((xi - pt[0]) * (xj - pt[0]) <= 0) && ((yi - pt[1]) * (yj - pt[1]) <= 0);
            if (onBoundary) {
                return !ignoreBoundary;
            }
            var intersect = ((yi > pt[1]) !== (yj > pt[1])) &&
                (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi);
            if (intersect) {
                isInside = !isInside;
            }
        }
        return isInside;
    }
    /**
     * inBBox
     *
     * @private
     * @param {Position} pt point [x,y]
     * @param {BBox} bbox BBox [west, south, east, north]
     * @returns {boolean} true/false if point is inside BBox
     */
    function inBBox(pt, bbox) {
        return bbox[0] <= pt[0] &&
            bbox[1] <= pt[1] &&
            bbox[2] >= pt[0] &&
            bbox[3] >= pt[1];
    }
    });
  
    var booleanPointInPolygon = unwrapExports(booleanPointInPolygon_1);
  
    function initSelector(size, boxes) {
      // This closure just stores the tile size and a link to the tile boxes
  
      return {
        getTilePos,
        select,
      };
  
      function getTilePos(mapX, mapY) {
        // Compute tile index
        var ix = Math.floor(mapX / size);
        var iy = Math.floor(mapY / size);
  
        // Get a link to the tile box
        if (!boxes[iy]) return;
        var box = boxes[iy][ix];
        if (!box) return;
  
        // Compute pixel within tile
        var frac = box.sw / size;  // Fraction of the tile we will use
        var tileX = (mapX - ix * size) * frac + box.sx;
        var tileY = (mapY - iy * size) * frac + box.sy;
  
        return { x: tileX, y: tileY, z: box.tile.z, frac, tile: box.tile };
      }
  
      function select(mapX, mapY, threshold, source, layer) {
        var tPos = getTilePos(mapX, mapY);
        if (!tPos) return;
  
        // Get a link to the features from the requested layer
        var layers = tPos.tile.sources[source];
        if (!layers) return;
        // TODO: Make sure it's a vector source?
        var data = layers[layer];
        if (!data || data.features.length < 1) return;
        var features = data.features;
  
        // Get type of features in data. ASSUMES all same type
        var type = features[0].geometry.type;
  
        var feature;
        switch (type) {
          case "Point":
            // Scale the threshold by frac to make it a displayed distance
            // rather than a distance in local tile coordinates
            feature = findNearest(tPos.x, tPos.y, threshold * tPos.frac, features);
            break;
          case "Polygon":
          case "MultiPolygon":
            var pt = [tPos.x, tPos.y];
            feature = features.find(poly => booleanPointInPolygon(pt, poly));
            break;
          default:
            return; // Unknown feature type!
        }
  
        if (!feature) return; // No actual feature selected
  
        // Make a deep copy of the feature (not just a link to the original)
        feature = JSON.parse(JSON.stringify(feature));
  
        if (type === "Point") {
          // Transform the feature coordinates from tile coordinates back to 
          //  global map coordinates.  TODO: Make this work for Polygons?
          var coords = feature.geometry.coordinates;
          var numTiles = 2 ** tPos.tile.z;
          coords[0] = (tPos.tile.x + coords[0] / size) / numTiles;
          coords[1] = (tPos.tile.y + coords[1] / size) / numTiles;
        }
  
        return feature;
      }
  
      function findNearest(x, y, threshold, features) {
        var minDistance = Infinity;
        var minIndex = 0;
  
        features.forEach(checkDistance);
  
        function checkDistance(feature, index) {
          var p = feature.geometry.coordinates;
          var distance = Math.sqrt( (p[0] - x)**2 + (p[1] - y)**2 );
          if (distance < minDistance) {
            minDistance = distance;
            minIndex = index;
          }
        }
  
        if (minDistance > threshold) return;
        return features[minIndex];
      }
    }
  
    function init$2(params, context) {
      // Check if we have a valid canvas rendering context
      var haveRaster = context instanceof CanvasRenderingContext2D;
      if (!haveRaster) {
        console.log("WARNING in rastermap.init: not a 2D rendering context!");
        //return false;
      }
  
      // Initialize tile factory
      const factory = init$1({
        size: params.tileSize,
        style: params.style,
        token: params.token,
      });
  
      // Initialize a cache of loaded tiles
      const tiles = init(params.tileSize, factory);
      var numCachedTiles = 0;
  
      // Initialize grid of rendered tiles
      const grid = initGrid(params, context, tiles);
  
      // Initialize feature selection methods
      const selector = initSelector(params.tileSize, grid.boxes);
  
      return {
        // Method to update the rendering of the map
        drawTiles,
  
        // Methods to set the position and zoom of the map
        move: grid.move,
        fitBoundingBox: grid.fitBoundingBox,
        setCenterZoom: grid.setCenterZoom,
  
        // Methods to convert coordinates, or report conversion parameters
        toLocal: grid.toLocal,
        xyToMapPixels: grid.xyToMapPixels,
        getScale: grid.getScale,
  
        // Methods to interrogate or change the styling of the map
        style: () => factory.style,
        redraw:    (group) => (tiles.unrender(group),  grid.reset()),
        hideGroup: (group) => (tiles.hideGroup(group), grid.reset()),
        showGroup: (group) => (tiles.showGroup(group), grid.reset()),
  
        // Methods to interrogate the data in the map
        boxes: grid.boxes,
        getTilePos: selector.getTilePos,
        select: selector.select,
  
        // Methods to report ongoing tasks and memory usage
        loaded: grid.loaded,
        activeDrawCalls: factory.activeDrawCalls,
        numCachedTiles: () => numCachedTiles,
      };
  
      function drawTiles() {
        var updated = grid.drawTiles();
        // Clean up -- don't let images object get too big
        numCachedTiles = tiles.prune(grid.tileDistance, 1.5);
        return updated;
      }
    }
  
    const params = Object.freeze({
      // URL of a Mapbox Style document
      style: "mapbox://styles/mapbox/streets-v8",
  
      // Token, for expanding Mapbox shorthand URLs
      token: "pk.eyJ1IjoiamhlbWJkIiwiYSI6ImNqcHpueHpyZjBlMjAzeG9kNG9oNzI2NTYifQ.K7fqhk2Z2YZ8NIV94M-5nA",
      
      // Number of zoom levels
      maxZoom: 20,
      
      // Width of a tile in pixels (ASSUMES square tiles)
      tileSize: 512,
      
      // Size of map to display, in pixels
      width: 1024,
      height: 1024,
    });
  
    // Maximum latitude for Web Mercator: 85.0113 degrees. Beware rounding!
    const maxMercLat = 2.0 * Math.atan( Math.exp(Math.PI) ) - Math.PI / 2.0;
  
    function lonLatToXY(projected, geodetic) {
      // Input geodetic is a pointer to a 2- (or 3?)-element array, containing
      // longitude and latitude of a point on the ellipsoid surface
      // Output projected is a pointer to a 2-element array containing
      // the projected X/Y coordinates
  
      projected[0] = lonToX( geodetic[0] );
      projected[1] = latToY( geodetic[1] );
      return;
    }
  
    function lonToX(lon) {
      // Convert input longitude in radians to a Web Mercator x-coordinate
      // where x = 0 at lon = -PI, x = 1 at lon = +PI
      return 0.5 + 0.5 * lon / Math.PI;
    }
  
    function latToY(lat) {
      // Convert input latitude in radians to a Web Mercator y-coordinate
      // where y = 0 at lat = 85.05113 deg, y = 1 at lat = -85.05113 deg
      var clipLat = Math.min(Math.max(-maxMercLat, lat), maxMercLat);
      var y = 0.5 - 0.5 / Math.PI * // Note sign flip;
      Math.log( Math.tan(Math.PI / 4.0 + clipLat / 2.0) );
      // Clip range to [0,1], since y does not wrap around
      return Math.min(Math.max(0.0, y), 1.0);
    }
  
    // Very similar to greggman's module:
  
    // Make sure the canvas drawingbuffer is the same size as the display
    // webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
    function resizeCanvasToDisplaySize(canvas) {
      let width = canvas.clientWidth;
      let height = canvas.clientHeight;
      if (canvas.width !== width || canvas.height !== height) {
        // Resize drawingbuffer to match resized display
        canvas.width = width;
        canvas.height = height;
        return true;
      }
      return false;
    }
  
    function initGeometry(canvas, map, width, height) {
      // Input canvas is an HTML Canvas element
      // Input map is an instance of rastermap
      // Input width, height are the pixel sizes of the rastermap grid
  
      return {
        updatePoly,
        toPixels,
      };
  
      function updatePoly(pOld, pNew) {
        if (polyMatch(pOld, pNew)) return false;
        pOld.length = 0;
        pNew.forEach( pt => pOld.push([pt[0], pt[1]]) ); 
        return true;
      }
  
      function polyMatch(poly1, poly2) {
        if (poly1.length !== poly2.length) return false;
        return poly1.every( (pt, i) => pointMatch(pt, poly2[i]) );
      }
  
      function pointMatch(p1, p2) {
        return (p1[0] === p2[0] && p1[1] === p2[1]);
      }
  
      function toPixels(pix, raw) {
        // Update scaling of the overlay canvas vs. the map drawingbuffer
        let xscale = canvas.width / width;
        let yscale = canvas.height / height;
  
        // Clear the pix array
        pix.length = 0;
  
        // Convert raw polygon from world X/Y to map pixels
        const pixPt = [];
        raw.forEach(pt => {
          map.xyToMapPixels( pixPt, pt );
          pixPt[0] *= xscale;
          pixPt[1] *= yscale;
          pix.push([pixPt[0], pixPt[1]]);
        });
      }
    }
  
    function initRenderer$2(canvas) {
      // Input canvas is an HTML Canvas element
  
      const ctx = canvas.getContext("2d");
  
      return {
        drawPoly,
        drawBox,
        clear: () => ctx.clearRect(0, 0, canvas.width, canvas.height),
      };
  
      function drawPoly(poly, params) {
        setup(params);
  
        ctx.beginPath();
        ctx.moveTo(poly[0][0], poly[0][1]);
        for (let i = 1; i < poly.length; i++) {
          ctx.lineTo(poly[i][0], poly[i][1]);
        }
        ctx.stroke();
      }
  
      function drawBox(box, params) {
        setup(params);
  
        ctx.strokeRect(
          box[0][0],
          box[0][1],
          box[1][0] - box[0][0],
          box[1][1] - box[0][1]
        );
      }
  
      function setup(params) {
        if (!params) params = {};
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.lineWidth = params.lineWidth || 5;
        ctx.strokeStyle = params.strokeStyle || "#FF0000";
      }
    }
  
    function init$3(canvas, map, width, height) {
      // Input canvas is an HTML Canvas element
      // Input map is an instance of rastermap
      // Input width, height are the pixel sizes of the rastermap grid
  
      if (!canvas || canvas.nodeName !== "CANVAS") {
        return console.log("ERROR in mapOverlay: No valid canvas element!");
      } else if (!map || !map.xyToMapPixels) {
        return console.log("ERROR in mapOverlay: No valid rastermap instance!");
      }
  
      // Initialize geometry methods and renderer
      const geometry = initGeometry(canvas, map, width, height);
      const renderer = initRenderer$2(canvas);
  
      // Declare arrays for the polygon, in global and local coordinates
      const rawQC = [], pixQC = [];
  
      return {
        reset,
        draw,
      };
  
      function reset() {
        renderer.clear();
        rawQC.length = 0;
      }
  
      function draw(poly, mapChanged) {
        // Check for a valid polygon. TODO: Check each element is a valid point?
        if (poly.length < 2) return;
  
        // Check if anything changed since last call
        var polyChanged = geometry.updatePoly(rawQC, poly);
        var canvasResized = resizeCanvasToDisplaySize(canvas);
        if (!polyChanged && !canvasResized && !mapChanged) return;
        if (canvas.width < 5 || canvas.height < 5) return;
  
        // Convert box to map pixels and draw
        geometry.toPixels(pixQC, rawQC);
        return (pixQC.length === 2) 
          ? renderer.drawBox(pixQC)
          : renderer.drawPoly(pixQC);
      }
    }
  
    const degrees = 180.0 / Math.PI;
  
    function main() {
      // Setup 2D map
      const display = document.getElementById("rasterCanvas").getContext("2d");
      const overlay = document.getElementById("vectorCanvas");
      const map = init$2(params, display); //, overlay);
      const boxQC = init$3(overlay, map, params.width, params.height);
      boxQC.p1 = [];
      boxQC.p2 = [];
      boxQC.visible = false;
  
      // Handle a supplied bounding box
      var westDeg = document.getElementById("west");
      var eastDeg = document.getElementById("east");
      var northDeg = document.getElementById("north");
      var southDeg = document.getElementById("south");
      var bboxSet = document.getElementById("bboxSet");
      bboxSet.addEventListener("click", function(click) {
        var topLeft = [westDeg.value / degrees, northDeg.value / degrees];
        var btRight = [eastDeg.value / degrees, southDeg.value / degrees];
        lonLatToXY(boxQC.p1, topLeft); 
        lonLatToXY(boxQC.p2, btRight);
        map.fitBoundingBox(boxQC.p1, boxQC.p2);
        boxQC.draw([boxQC.p1, boxQC.p2], true);
        boxQC.visible = true;
      }, false);
      var bboxClear = document.getElementById("bboxClear");
      bboxClear.addEventListener("click", function(click) {
        boxQC.reset();
        boxQC.visible = false;
      }, false);
  
      // Setup panning controls
      var up = document.getElementById("up");
      up.addEventListener("click", function(click) { move(0, 0, -1); }, false);
      var down = document.getElementById("down");
      down.addEventListener("click", function(click) { move(0, 0, 1); }, false);
      var left = document.getElementById("left");
      left.addEventListener("click", function(click) { move(0, -1, 0); }, false);
      var right = document.getElementById("right");
      right.addEventListener("click", function(click) { move(0, 1, 0); }, false);
  
      // Setup zoom controls
      var zoomIn = document.getElementById("zoomIn");
      zoomIn.addEventListener("click", function(click) { move(1, 0, 0); }, false);
      var zoomOut = document.getElementById("zoomOut");
      zoomOut.addEventListener("click", function(click) { move(-1, 0, 0); }, false);
  
      function move(dz, dx, dy) {
        map.move(dz, dx, dy);
        if (boxQC.visible) boxQC.draw([boxQC.p1, boxQC.p2], true);
      }
  
      // Track loading status
      var loaded = document.getElementById("completion");
      // Start animation loop
      requestAnimationFrame(checkRender);
      function checkRender(time) {
        map.drawTiles();
        var percent = map.loaded() * 100;
        if (percent < 100) {
          loaded.innerHTML = "Loading: " + percent.toFixed(0) + "%";
        } else {
          loaded.innerHTML = "Complete! " + percent.toFixed(0) + "%";
        }
        requestAnimationFrame(checkRender);
      }
    }
  
    exports.main = main;
  
    return exports;
  
  }({}));
  