util/common.js

var $ = require('jquery');
var proj4 = require('proj4');
proj4 = proj4.__esModule ? proj4.default : proj4;

var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

var svgForeignObject = '<svg xmlns="http://www.w3.org/2000/svg">' +
  '<foreignObject width="100%" height="100%">' +
  '</foreignObject>' +
  '</svg>';

var m_timingData = {},
    m_timingKeepRecent = 200,
    m_threshold = 15,
    m_originalRequestAnimationFrame,
    m_htmlToImageSupport;

/**
 * Takes a variable number of arguments and returns the first numeric value
 * it finds.
 *
 * @param {...*} var_args Any number of arguments.
 * @returns {number} The first numeric argument, or `undefined` if there are no
 *      numeric arguments.
 * @private
 */
function setNumeric() {
  var i;
  for (i = 0; i < arguments.length; i += 1) {
    if (isFinite(arguments[i])) {
      return arguments[i];
    }
  }
}

/**
 * Contains utility classes and methods used by geojs.
 * @namespace geo.util
 */
var util = {
  /**
   * Check if a point is inside of a polygon.  The point and polygon must be in
   * the same coordinate system.  A point exactly on the edge is not considered
   * inside.
   *
   * @param {geo.point2D} point The test point.
   * @param {geo.point2D[]|geo.polygonObject} outer The outer boundary of the
   *    polygon or a polygon object that has both the inner and outer
   *    boundaries.
   * @param {Array.<geo.point2D[]>} [inner] A list of inner boundaries
   *    (holes).
   * @param {object} [range] If specified, this is the extent of the outer
   *    polygon and is used for early detection.
   * @param {geo.point2D} range.min The minimum value of coordinates in
   *    the outer polygon.
   * @param {geo.point2D} range.max The maximum value of coordinates in
   *    the outer polygon.
   * @returns {boolean} `true` if the point is inside or on the border of the
   *    polygon.
   * @memberof geo.util
   */
  pointInPolygon: function (point, outer, inner, range) {
    if (outer.outer) {
      inner = outer.inner;
      outer = outer.outer;
    }
    if (outer.length < 3) {
      // we need 3 coordinates for this to make sense
      return false;
    }
    if (range && range.min && range.max) {
      if (point.x < range.min.x || point.y < range.min.y ||
          point.x > range.max.x || point.y > range.max.y) {
        return false;
      }
    }
    return util.distanceToPolygon2d(point, inner ? {outer: outer, inner: inner} : outer, true) > 0;
  },

  /**
   * Return a point in the basis of the triangle.  If the point is located on
   * a vertex of the triangle, it will be at `vert0`: (0, 0), `vert1`:
   * (1, 0), `vert2`: (0, 1).  If it is within the triangle, its coordinates
   * will be 0 <= x <= 1, 0 <= y <= 1, x + y <= 1.  The point and vertices
   * must be in the same coordinate system.
   *
   * @param {geo.point2D} point The point to convert.
   * @param {geo.point2D} vert0 Vertex 0 of the triangle.
   * @param {geo.point2D} vert1 Vertex 1 (x direction) of the triangle.
   * @param {geo.point2D} vert2 Vertex 2 (y direction) of the triangle.
   * @returns {geo.point2D} The point in the triangle basis, or `undefined`
   *    if the triangle is degenerate.
   * @memberof geo.util
   */
  pointToTriangleBasis2d: function (point, vert0, vert1, vert2) {
    var a = vert1.x - vert0.x,
        b = vert2.x - vert0.x,
        c = vert1.y - vert0.y,
        d = vert2.y - vert0.y,
        x = point.x - vert0.x,
        y = point.y - vert0.y,
        det = a * d - b * c;
    if (det) {
      return {x: (x * d - y * b) / det, y: (x * -c + y * a) / det};
    }
  },

  /**
   * Check if an object an HTML Image element that is fully loaded.
   *
   * @param {object} img An object that might be an HTML Image element.
   * @param {boolean} [allowFailedImage] If `true`, an image element that has
   *     a source and has failed to load is also considered 'ready' in the
   *     sense that it isn't expected to change to a better state.
   * @returns {boolean} `true` if this is an image that is ready.
   * @memberof geo.util
   */
  isReadyImage: function (img, allowFailedImage) {
    if (img instanceof Image && img.complete && img.src) {
      if ((img.naturalWidth && img.naturalHeight) || allowFailedImage) {
        return true;
      }
    }
    return false;
  },

  /**
   * Check if an object an HTMLVideoElement element that is loaded.
   *
   * @param {object} vid An object that might be an HTMLVideoElement.
   * @param {boolean} [allowFailedVideo] If `true`, an video element that has
   *     a source and has failed to load is also considered 'ready' in the
   *     sense that it isn't expected to change to a better state.
   * @returns {boolean} `true` if this is a video that is ready.
   * @memberof geo.util
   */
  isReadyVideo: function (vid, allowFailedVideo) {
    if (vid instanceof HTMLVideoElement && vid.src &&
        vid.HAVE_CURRENT_DATA !== undefined) {
      if ((vid.videoWidth && vid.videoHeight && vid.readyState >= vid.HAVE_CURRENT_DATA) ||
          (allowFailedVideo && vid.error)) {
        return true;
      }
    }
    return false;
  },

  /**
   * Test if an object is a function.
   *
   * @param {object} f An object that might be a function.
   * @returns {boolean} `true` if the object is a function.
   * @memberof geo.util
   */
  isFunction: function (f) {
    return typeof f === 'function';
  },

  /**
   * Return a function.  If the supplied object is a function, return it.
   * Otherwise, return a function that returns the argument.
   *
   * @param {object} f An object that might be a function.
   * @returns {function} A function.  Either `f` or a function that returns
   *    `f`.
   * @memberof geo.util
   */
  ensureFunction: function (f) {
    if (util.isFunction(f)) {
      return f;
    } else {
      return function () { return f; };
    }
  },

  /**
   * Check if a value coerces to a number that is finite, not a NaN, and not
   * `null`, `false`, or the empty string.
   *
   * @param {object} val The value to check.
   * @returns {boolean} True if `val` is a non-null, non-false, finite number.
   */
  isNonNullFinite: function (val) {
    return isFinite(val) && val !== null && val !== false && val !== '';
  },

  /**
   * Return a random string of length n || 8.  The string consists of
   * mixed-case ASCII alphanumerics.
   *
   * @param {number} [n=8] The length of the string to return.
   * @returns {string} A string of random characters.
   * @memberof geo.util
   */
  randomString: function (n) {
    var s, i, r;
    n = n || 8;
    s = '';
    for (i = 0; i < n; i += 1) {
      r = Math.floor(Math.random() * chars.length);
      s += chars.substring(r, r + 1);
    }
    return s;
  },

  /**
   * Normalize a coordinate object into {@link geo.geoPosition} form.  The
   * input can be a 2 or 3 element array or an object with a variety of
   * properties.
   *
   * @param {object|array} p The point to convert.
   * @returns {geo.geoPosition} The point as an object with `x`, `y`, and `z`
   *    properties.
   * @memberof geo.util
   */
  normalizeCoordinates: function (p) {
    p = p || {};
    if (Array.isArray(p)) {
      return {
        x: p[0],
        y: p[1],
        z: p[2] || 0
      };
    }
    return {
      x: setNumeric(
        p.x,
        p.longitude,
        p.lng,
        p.lon,
        0
      ),
      y: setNumeric(
        p.y,
        p.latitude,
        p.lat,
        0
      ),
      z: setNumeric(
        p.z,
        p.elevation,
        p.elev,
        p.height,
        0
      )
    };
  },

  /**
   * Create an integer array contains elements from one integer to another
   * integer.
   *
   * @param {number} start The start integer.
   * @param {number} end The end integer.
   * @param {number} [step=1] The step.
   * @returns {number[]} An array of integers.
   * @memberof geo.util
   */
  range: function (start, end, step) {
    step = step || 1;
    var results = [];
    for (var i = start; i <= end; i += step) {
      results.push(i);
    }
    return results;
  },

  /**
   * Compare two arrays and return if their contents are equal.
   * @param {array} a1 First array to compare.
   * @param {array} a2 Second array to compare.
   * @returns {boolean} `true` if the contents of the arrays are equal.
   * @memberof geo.util
   */
  compareArrays: function (a1, a2) {
    return (a1.length === a2.length && a1.every(function (el, idx) {
      return el === a2[idx];
    }));
  },

  /**
   * Create a `vec3` that is always an array.  This should only be used if it
   * will not be used in a WebGL context.  Plain arrays usually use 64-bit
   * float values, whereas `vec3` defaults to 32-bit floats.
   *
   * @returns {array} Zeroed-out vec3 compatible array.
   * @memberof geo.util
   */
  vec3AsArray: function () {
    return [0, 0, 0];
  },

  /**
   * Create a `mat3` that is always an array.  This should only be used if it
   * will not be used in a WebGL context.  Plain arrays usually use 64-bit
   * float values, whereas `mat3` defaults to 32-bit floats.
   *
   * @returns {array} Identity `mat3` compatible array.
   * @memberof geo.util
   */
  mat3AsArray: function () {
    return [
      1, 0, 0,
      0, 1, 0,
      0, 0, 1
    ];
  },

  /**
   * Create a `mat4` that is always an array.  This should only be used if it
   * will not be used in a WebGL context.  Plain arrays usually use 64-bit
   * float values, whereas `mat4` defaults to 32-bit floats.
   *
   * @returns {array} Identity `mat4` compatible array.
   * @memberof geo.util
   */
  mat4AsArray: function () {
    return [
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1
    ];
  },

  /**
   * Get a buffer for a vgl geometry source.  If a buffer already exists and
   * is the correct size, return it.  Otherwise, allocate a new buffer; any
   * data in an old buffer is discarded.
   *
   * @param {geo.vgl.geometryData} geom The geometry to reference and modify.
   * @param {string} srcName The name of the source.
   * @param {number} len The number of elements for the array.
   * @param {number} [allowLarger=0.2] If the existing buffer is larger than
   *    requested, don't reallocate it unless it exceeds the size of
   *    `len * (1 + allowLarger)`.
   * @param {number} [allocateLarger=0.1] If reallocating an existing buffer,
   *    allocate `len * (1 + allocateLarger)` to reduce the need to reallocate
   *    on subsequent calls.  If this is the first allocation (the previous
   *    size was 0), `len` is allocated.
   * @returns {Float32Array} A buffer for the named source.
   * @memberof geo.util
   */
  getGeomBuffer: function (geom, srcName, len, allowLarger, allocateLarger) {
    allowLarger = allowLarger === undefined ? 0.2 : allowLarger;
    allocateLarger = allocateLarger === undefined ? 0.1 : allocateLarger;
    var src = geom.sourceByName(srcName),
        data = src.data(),
        allow = Math.floor((allowLarger + 1) * len);

    data = src.data();
    /* If the current buffer is either the length we want or no larger than a
     * factor of allowBigger more in size, just return it. */
    if (data instanceof Float32Array && (data.length === len || (data.length >= len && data.length <= allow))) {
      return data;
    }
    /* If we need to allocate a new buffer (smaller or larger), and we have an
     * existing, non-zero-length buffer, allocate a larger than needed buffer.
     * Add an extra factor of allocateLarger. */
    var allocate = len;
    if (data instanceof Float32Array && data.length && len && allocateLarger > 0) {
      allocate = Math.floor((allocateLarger + 1) * len);
    }
    data = new Float32Array(allocate);
    src.setData(data);
    return data;
  },

  /**
   * Ensure that the input and modifiers properties of all actions are
   * objects, not plain strings.
   *
   * @param {geo.actionRecord[]} actions An array of actions to adjust as
   *    needed.
   * @memberof geo.util
   */
  adjustActions: function (actions) {
    var action, i;
    for (i = 0; i < actions.length; i += 1) {
      action = actions[i];
      if ($.type(action.input) === 'string') {
        var actionEvents = {};
        actionEvents[action.input] = true;
        action.input = actionEvents;
      }
      if (!action.modifiers) {
        action.modifiers = {};
      }
      if ($.type(action.modifiers) === 'string') {
        var actionModifiers = {};
        actionModifiers[action.modifiers] = true;
        action.modifiers = actionModifiers;
      }
    }
  },

  /**
   * Add an action to the list of handled actions.
   *
   * @param {geo.actionRecord[]} actions An array of actions to adjust as
   *    needed.
   * @param {geo.actionRecord} action An object defining the action.  Use
   *    `action`, `name`, and `owner` to make this entry distinct if it will
   *    need to be removed later.
   * @param {boolean} toEnd The action is added at the beginning of the
   *    actions list unless `toEnd` is `true`.  Earlier actions prevent later
   *    actions with the similar input and modifiers.
   * @memberof geo.util
   */
  addAction: function (actions, action, toEnd) {
    if (toEnd) {
      actions.push(action);
    } else {
      actions.unshift(action);
    }
    util.adjustActions(actions);
  },

  /**
   * Check if an action is in the actions list.  An action matches if the
   * `action`, `name`, and `owner` match.  A `null` or `undefined` value will
   * match all actions.  If using a {@link geo.actionRecord} object, this is
   * the same as passing (`action.action`, `action.name`, `action.owner`).
   *
   * @param {geo.actionRecord[]} actions An array of actions to search.
   * @param {geo.actionRecord|string} action Either an action object or the
   *    name of an action.
   * @param {string} [name] Optional name associated with the action.
   * @param {string} [owner] Optional owner associated with the action.
   * @returns {geo.actionRecord?} The first matching action or `null`.
   * @memberof geo.util
   */
  hasAction: function (actions, action, name, owner) {
    if (action && action.action) {
      name = action.name;
      owner = action.owner;
      action = action.action;
    }
    for (var i = 0; i < actions.length; i += 1) {
      if ((!action || actions[i].action === action) &&
          (!name || actions[i].name === name) &&
          (!owner || actions[i].owner === owner)) {
        return actions[i];
      }
    }
    return null;
  },

  /**
   * Remove all matching actions.  Actions are matched as with `hasAction`.
   *
   * @param {geo.actionRecord[]} actions An array of actions to adjust as
   *    needed.
   * @param {geo.actionRecord|string} action Either an action object or the
   *    name of an action.
   * @param {string} [name] Optional name associated with the action.
   * @param {string} [owner] Optional owner associated with the action.
   * @returns {number} The number of actions that were removed.
   * @memberof geo.util
   */
  removeAction: function (actions, action, name, owner) {
    var found, removed = 0;

    do {
      found = util.hasAction(actions, action, name, owner);
      if (found) {
        actions.splice(actions.indexOf(found), 1);
        removed += 1;
      }
    } while (found);
    return removed;
  },

  /**
   * Determine if the current inputs and modifiers match a known action.
   *
   * @param {object} inputs Aan object where each input that is currently
   *    active is truthy.  Common inputs are `left`, `right`, `middle`,
   *    `wheel`, `pan`, `rotate`.
   * @param {object} modifiers An object where each currently applied
   *    modifier is truthy.  Common modifiers are `shift`, `ctrl`, `alt`,
   *    `meta`.
   * @param {geo.actionRecord[]} actions A list of actions to compare to the
   *    inputs and modifiers.  The first action that matches will be
   *    returned.
   * @returns {geo.actionRecord} A matching action or `undefined`.
   * @memberof geo.util
   */
  actionMatch: function (inputs, modifiers, actions) {
    var matched;

    /* actions must have already been processed by adjustActions */
    if (actions.some(function (action) {
      for (var input in action.input) {
        if (action.input.hasOwnProperty(input)) {
          if ((action.input[input] === false && inputs[input]) ||
              (action.input[input] && !inputs[input])) {
            return false;
          }
        }
      }
      for (var modifier in action.modifiers) {
        if (action.modifiers.hasOwnProperty(modifier)) {
          if ((action.modifiers[modifier] === false && modifiers[modifier]) ||
              (action.modifiers[modifier] && !modifiers[modifier])) {
            return false;
          }
        }
      }
      matched = action;
      return true;
    })) {
      return matched;
    }
  },

  /**
   * Return recommended defaults for map parameters and osm or tile layer
   * parameters where the expected intent is to use the map in pixel
   * coordinates (upper left is (0, 0), lower right is (`width`, `height`).
   *
   * @example <caption>The returned objects can be modified or
   *    extended.</caption>
   * var results = pixelCoordinateParams('#map', 10000, 9000);
   * var map = geo.map(Object.assign(results.map, {clampZoom: false}));
   * map.createLayer('osm', results.layer);
   *
   * @param {string?} node DOM selector for the map container.
   * @param {number} width Width of the whole map contents in pixels.
   * @param {number} height Height of the whole map contents in pixels.
   * @param {number} [tileWidth] If an osm or tile layer is going to be used,
   *    the width of a tile.
   * @param {number} [tileHeight] If an osm or tile layer is going to be used,
   *    the height of a tile.
   * @returns {object} An object with `map` and `layer` properties.  `map` is
   *    an object that can be passed to {@link geo.map}, and `layer` is an
   *    object that can be passed to `map.createLayer`.
   * @memberof geo.util
   */
  pixelCoordinateParams: function (node, width, height, tileWidth, tileHeight) {
    var mapW, mapH, tiled;
    if (node) {
      node = $(node);
      mapW = node.innerWidth();
      mapH = node.innerHeight();
    }
    tileWidth = tileWidth || width;
    tileHeight = tileHeight || height;
    tiled = (tileWidth !== width || tileHeight !== height);
    var minLevel = Math.min(0, Math.floor(Math.log(Math.min(
          (mapW || tileWidth) / tileWidth,
          (mapH || tileHeight) / tileHeight)) / Math.log(2))),
        maxLevel = Math.ceil(Math.log(Math.max(
          width / tileWidth,
          height / tileHeight)) / Math.log(2));
    var mapParams = {
      node: node,
      ingcs: '+proj=longlat +axis=esu',
      gcs: '+proj=longlat +axis=enu',
      maxBounds: {left: 0, top: 0, right: width, bottom: height},
      unitsPerPixel: Math.pow(2, maxLevel),
      center: {x: width / 2, y: height / 2},
      min: minLevel,
      max: maxLevel,
      zoom: minLevel,
      clampBoundsX: true,
      clampBoundsY: true,
      clampZoom: true
    };
    var layerParams = {
      maxLevel: maxLevel,
      wrapX: false,
      wrapY: false,
      tileOffset: function () {
        return {x: 0, y: 0};
      },
      attribution: '',
      tileWidth: tileWidth,
      tileHeight: tileHeight,
      tileRounding: Math.ceil,
      tilesAtZoom: tiled ? function (level) {
        var scale = Math.pow(2, maxLevel - level);
        return {
          x: Math.ceil(width / tileWidth / scale),
          y: Math.ceil(height / tileHeight / scale)
        };
      } : undefined,
      tilesMaxBounds: tiled ? function (level) {
        var scale = Math.pow(2, maxLevel - level);
        return {
          x: Math.floor(width / scale),
          y: Math.floor(height / scale)
        };
      } : undefined
    };
    return {map: mapParams, layer: layerParams};
  },

  /**
   * Return the coordinate associated with the center of the perimeter formed
   * from a list of points.  This averages all of the vertices in the perimeter
   * weighted by the line length on either side of each point.  Functionally,
   * this is the same as the average of all the points of the lines of the
   * perimeter.
   *
   * @param {geo.geoPolygon} coor An array of coordinates.  This can also be
   *    a polygon object with an ``outer`` element with an array of
   *    coordinates.
   * @returns {geo.geoPosition|undefined} The position for the center, or
   *    `undefined` if no such position exists.
   * @memberof geo.util
   */
  centerFromPerimeter: function (coor) {
    var position, p0, p1, w, sumw, i;
    if (coor && coor.outer) {
      coor = coor.outer;
    }
    if (!coor || !coor.length) {
      return;
    }
    if (coor.length === 1) {
      return {x: coor[0].x, y: coor[0].y};
    }
    position = {x: 0, y: 0};
    sumw = 0;
    p0 = coor[coor.length - 1];
    for (i = 0; i < coor.length; i += 1) {
      p1 = p0;
      p0 = coor[i];
      w = Math.sqrt(Math.pow(p1.x - p0.x, 2) + Math.pow(p1.y - p0.y, 2));
      position.x += (p0.x + p1.x) * w;
      position.y += (p0.y + p1.y) * w;
      sumw += 2 * w;
    }
    position.x /= sumw;
    position.y /= sumw;
    // return a copy of p0 if all points are the same
    return sumw ? position : {x: p0.x, y: p0.y};
  },

  /**
   * Get the square of the Euclidean 2D distance between two points.
   *
   * @param {geo.geoPosition} pt1 The first point.
   * @param {geo.geoPosition} pt2 The second point.
   * @returns {number} The distance squared.
   */
  distance2dSquared: function (pt1, pt2) {
    var dx = pt1.x - pt2.x,
        dy = pt1.y - pt2.y;
    return dx * dx + dy * dy;
  },

  /**
   * Get the square of the Euclidean 2D distance between a point and a line
   * segment.
   *
   * @param {geo.geoPosition} pt The point.
   * @param {geo.geoPosition} line1 One end of the line.
   * @param {geo.geoPosition} line2 The other end of the line.
   * @returns {number} The distance squared.
   * @memberof geo.util
   */
  distance2dToLineSquared: function (pt, line1, line2) {
    var dx = line2.x - line1.x,
        dy = line2.y - line1.y,
        // we could get the line length from the distance2dSquared function,
        // but since we need dx and dy in this function, it is faster to just
        // compute it here.
        lengthSquared = dx * dx + dy * dy,
        t = 0;
    if (lengthSquared) {
      t = ((pt.x - line1.x) * dx + (pt.y - line1.y) * dy) / lengthSquared;
      t = Math.max(0, Math.min(1, t));
    }
    return util.distance2dSquared(pt, {
      x: line1.x + t * dx,
      y: line1.y + t * dy
    });
  },

  /**
   * Get the signed Euclidean 2D distance between a point and a polygon.  The
   * distance is positive if the point is inside of the polygon.
   *
   * @param {geo.geoPosition} pt The point.
   * @param {geo.polygonObject} poly The polygon.
   * @param {boolean} [onlySign] If truthy, only the sign of the answer is
   *    significant.
   * @returns {number} The signed distance.
   * @memberof geo.util
   */
  distanceToPolygon2d: function (pt, poly, onlySign) {
    const outer = poly.outer || poly;
    let inside = false,
        minDistSq, distSq;
    for (let i = 0, len = outer.length, j = len - 1; i < len; j = i, i += 1) {
      const p0 = outer[i],
          p1 = outer[j];
      if (((p0.y > pt.y) !== (p1.y > pt.y)) && (pt.x < (p1.x - p0.x) * (pt.y - p0.y) / (p1.y - p0.y) + p0.x)) {
        inside = !inside;
      }
      distSq = onlySign ? 1 : util.distance2dToLineSquared(pt, p0, p1);
      if (minDistSq === undefined || distSq < minDistSq) {
        minDistSq = distSq;
      }
    }
    if (poly.inner) {
      poly.inner.forEach(inner => {
        const innerDist = util.distanceToPolygon2d(pt, inner, onlySign);
        if (innerDist * innerDist < minDistSq) {
          minDistSq = innerDist * innerDist;
        }
        if (innerDist > 0) {
          inside = !inside;
        }
      });
    }
    const dist = (inside ? 1 : -1) * Math.sqrt(minDistSq);
    return dist;
  },

  /**
   * Get twice the signed area of a 2d triangle.
   *
   * @param {geo.geoPosition} pt1 A vertex.
   * @param {geo.geoPosition} pt2 A vertex.
   * @param {geo.geoPosition} pt3 A vertex.
   * @returns {number} Twice the signed area.
   * @memberof geo.util
   */
  triangleTwiceSignedArea2d: function (pt1, pt2, pt3) {
    return (pt2.y - pt1.y) * (pt3.x - pt2.x) - (pt2.x - pt1.x) * (pt3.y - pt2.y);
  },

  /**
   * Determine if a line segment crosses any line segments of a polygon.
   *
   * @param {geo.geoPosition} pt1 One endpoint of the line.
   * @param {geo.geoPosition} pt2 The other endpoint of the line.
   * @param {geo.polygonObject} poly The polygon.
   * @returns {boolean} True if the segment cross any segment of the polygon.
   * @memberof geo.util
   */
  crossedLineSegmentPolygon2d: function (pt1, pt2, poly) {
    const outer = poly.outer || poly, len = outer.length;
    let i, j;
    for (i = 0, j = len - 1; i < len; j = i, i += 1) {
      if (util.crossedLineSegments2d(pt1, pt2, outer[i], outer[j])) {
        return true;
      }
    }
    if (poly.inner) {
      for (i = 0; i < poly.inner.length; i += 1) {
        if (util.crossedLineSegmentPolygon2d(pt1, pt2, poly.inner[i])) {
          return true;
        }
      }
    }
    return false;
  },

  /**
   * Determine if two line segments cross.  They are not considered crossing if
   * they share a vertex.  They are crossing if either of one segment's
   * vertices are collinear with the other segment.
   *
   * @param {geo.geoPosition} seg1pt1 One endpoint of the first segment.
   * @param {geo.geoPosition} seg1pt2 The other endpoint of the first segment.
   * @param {geo.geoPosition} seg2pt1 One endpoint of the second segment.
   * @param {geo.geoPosition} seg2pt2 The other endpoint of the second segment.
   * @returns {boolean} True if the segments cross.
   * @memberof geo.util
   */
  crossedLineSegments2d: function (seg1pt1, seg1pt2, seg2pt1, seg2pt2) {
    /* If the segments don't have any overlap in x or y, they can't cross */
    if ((seg1pt1.x > seg2pt1.x && seg1pt1.x > seg2pt2.x &&
         seg1pt2.x > seg2pt1.x && seg1pt2.x > seg2pt2.x) ||
        (seg1pt1.x < seg2pt1.x && seg1pt1.x < seg2pt2.x &&
         seg1pt2.x < seg2pt1.x && seg1pt2.x < seg2pt2.x) ||
        (seg1pt1.y > seg2pt1.y && seg1pt1.y > seg2pt2.y &&
         seg1pt2.y > seg2pt1.y && seg1pt2.y > seg2pt2.y) ||
        (seg1pt1.y < seg2pt1.y && seg1pt1.y < seg2pt2.y &&
         seg1pt2.y < seg2pt1.y && seg1pt2.y < seg2pt2.y)) {
      return false;
    }
    /* If any vertex is in common, it is not considered crossing */
    if ((seg1pt1.x === seg2pt1.x && seg1pt1.y === seg2pt1.y) ||
        (seg1pt1.x === seg2pt2.x && seg1pt1.y === seg2pt2.y) ||
        (seg1pt2.x === seg2pt1.x && seg1pt2.y === seg2pt1.y) ||
        (seg1pt2.x === seg2pt2.x && seg1pt2.y === seg2pt2.y)) {
      return false;
    }
    /* If the lines cross, the signed area of the triangles formed between one
     * segment and the other's vertices will have different signs.  By using
     * > 0, collinear points are crossing. */
    if (util.triangleTwiceSignedArea2d(seg1pt1, seg1pt2, seg2pt1) *
        util.triangleTwiceSignedArea2d(seg1pt1, seg1pt2, seg2pt2) > 0 ||
        util.triangleTwiceSignedArea2d(seg2pt1, seg2pt2, seg1pt1) *
        util.triangleTwiceSignedArea2d(seg2pt1, seg2pt2, seg1pt2) > 0) {
      return false;
    }
    return true;
  },

  /**
   * Check if a line segment crosses any segment from a list of lines.  The
   * segment is considered crossing it it touches a line segment, unless that
   * line segment shares a vertex with the segment.
   *
   * @param {geo.geoPosition} pt1 One end of the line segment.
   * @param {geo.geoPosition} pt2 The other end of the line segment.
   * @param {Array.<geo.geoPosition[]>} lineList A list of open lines.  Each
   *    line is a list of vertices.  The line segment is checked against each
   *    segment of each line in this list.
   * @returns {boolean} True if the segment crosses any line segment.
   * @memberof geo.util
   */
  segmentCrossesLineList2d: function (pt1, pt2, lineList) {
    var result = lineList.some(function (line) {
      return line.some(function (linePt, idx) {
        if (idx) {
          return util.crossedLineSegments2d(pt1, pt2, line[idx - 1], linePt);
        }
        return false;
      });
    });
    return result;
  },

  /**
   * Remove vertices from a chain of 2d line segments so that it is simpler but
   * is close to the original overall shape within some tolerance limit.  This
   * is the Ramer–Douglas–Peucker algorithm.  The first and last points will
   * always remain the same for open lines.  For closed lines (polygons), this
   * picks an point that likely to be significant and then reduces it, possibly
   * returning a single point.
   *
   * @param {geo.geoPosition[]} pts A list of points forming the line or
   *    polygon.
   * @param {number} tolerance The maximum variation allowed.  A value of zero
   *    will only remove perfectly collinear points.
   * @param {boolean} [closed] If true, this is a polygon rather than an open
   *    line.  In this case, it is possible to get back a single point.
   * @param {Array.<geo.geoPosition[]>?} [noCrossLines] A falsy value to allow
   *    the resultant line to cross itself, an empty array (`[]`) to prevent
   *    self-crossing, or an array of line segments to prevent self-crossing
   *    and disallow crossing any line segment in the list.  Each entry in the
   *    list is an open line (with one segment less than the number of
   *    vertices).  If self-crossing is prohibited, the resultant point set
   *    might not be as simplified as it could be.
   * @returns {geo.geoPosition[]} The new point set.
   * @memberof geo.util
   */
  rdpLineSimplify: function (pts, tolerance, closed, noCrossLines) {
    if (pts.length <= 2 || tolerance < 0) {
      return pts;
    }
    var i, distSq, maxDistSq = -1, index, toleranceSq = tolerance * tolerance;
    if (closed) {
      /* If this is closed, find the point that is furthest from the first
       * point.  ideally, one would find a point that is guaranteed to be on
       * the diameter of the convex hull, but doing so is an O(n^2) operation,
       * whereas this is sufficient and only O(n).  The chosen point is
       * duplicated at the start and end of the chain. */
      for (i = 1; i < pts.length; i += 1) {
        distSq = util.distance2dSquared(pts[0], pts[i]);
        if (distSq > maxDistSq) {
          maxDistSq = distSq;
          index = i;
        }
      }
      /* Points could be on any side of the start point, so if all points are
       * within 1/2 of the tolerance of the start point, we know all points are
       * within the tolerance of each other and therefore this polygon or
       * closed line can be simplified to a point. */
      if (maxDistSq * 4 <= toleranceSq) {
        return pts.slice(index, index + 1);
      }
      pts = pts.slice(index).concat(pts.slice(0, index + 1));
      pts = util.rdpLineSimplify(pts, tolerance, false, noCrossLines);
      /* Removed the duplicated first point */
      pts.splice(pts.length - 1);
      return pts;
    }
    for (i = 1; i < pts.length - 1; i += 1) {
      distSq = util.distance2dToLineSquared(pts[i], pts[0], pts[pts.length - 1]);
      if (distSq > maxDistSq) {
        maxDistSq = distSq;
        index = i;
      }
    }
    /* We can collapse this to a single line if it is within the tolerance and
     * we are either allowed to self-cross or it does not self-cross the rest
     * of the line. */
    if (maxDistSq <= toleranceSq && (!noCrossLines || !util.segmentCrossesLineList2d(
      pts[0], pts[pts.length - 1], noCrossLines))) {
      return [pts[0], pts[pts.length - 1]];
    }
    var left = pts.slice(0, index + 1),
        right = pts.slice(index),
        leftSide = util.rdpLineSimplify(
          left, tolerance, false,
          noCrossLines ? noCrossLines.concat([right]) : null),
        rightSide = util.rdpLineSimplify(
          right, tolerance, false,
          noCrossLines ? noCrossLines.concat([left]) : null);
    return leftSide.slice(0, leftSide.length - 1).concat(rightSide);
  },

  /**
   * Given an array, return the minimum and maximum values within the array.
   * If a numeric value is specified for one or the other, return that instead.
   *
   * @param {number[]} values An array of numeric values.
   * @param {number} [min] If specified, use this instead of calculating the
   *    minimum.
   * @param {number} [max] If specified, use this instead of calculating the
   *    maximum.
   * @param {boolean} [limit=false] If truthy, if `min` is specified, the
   *    returned `min` will be the larger of the specified value and the
   *    computed value, and if `max` is specified, the returned value will
   *    be the smaller of the specified value and the computed value.
   * @returns {object} An object with `min` and `max`, both numbers.  If the
   *    array is empty, `undefined` may be returned for the `min` and `max`.
   * @memberof geo.util
   */
  getMinMaxValues: function (values, min, max, limit) {
    if (values.length && (limit || !$.isNumeric(min) || !$.isNumeric(max))) {
      var minValue = values[0],
          maxValue = values[0],
          value, i;
      for (i = values.length - 1; i > 0; i -= 1) {
        value = values[i];
        if (value < minValue) { minValue = value; }
        if (value > maxValue) { maxValue = value; }
      }
      if (!$.isNumeric(min) || (limit && minValue > min)) {
        min = minValue;
      }
      if (!$.isNumeric(max) || (limit && maxValue < max)) {
        max = maxValue;
      }
    }
    return {min: min, max: max};
  },

  /**
   * Given a value in radians, return a value wrapped to the range [-PI, PI).
   *
   * @param {number} value A value in radians.
   * @returns {number} The wrapped value.
   * @memberof geo.util
   */
  wrapAngle: function (value) {
    /* Module will only ensure that this is between [-2 PI, 2 PI). */
    value = value % (Math.PI * 2);
    if (value < -Math.PI) {
      value += Math.PI * 2;
    } else if (value >= Math.PI) {
      value -= Math.PI * 2;
    }
    return value;
  },

  /**
   * Escape any character in a string that has a code point >= 127.
   *
   * @param {string} text The string to escape.
   * @returns {string} The escaped string.
   * @memberof geo.util
   */
  escapeUnicodeHTML: function (text) {
    return text.replace(/[^- 0-9A-Za-z~`!@#$%^&*()_+={}|[\]\\:";'<>?,./]/g, function (k, pos) {
      var code = k.charCodeAt(0);
      if (code < 127) {
        return k;
      }
      /* decode utf016 surrogate pairs */
      if (code >= 0xD800 && code <= 0xE000) {
        if (code < 0xDC00 || pos) {
          return '';
        }
        var code0 = text.charCodeAt(pos - 1);
        if (code0 < 0xD800 || code0 >= 0xDC00) {
          return '';
        }
        code = (code0 - 0xD800) * 0x400 + (code - 0xDC00);
      }
      return '&#' + code.toString(10) + ';';
    });
  },

  /**
   * Recursively merge two objects.  This is intended to replace
   * util.deepMerge(target, ...sources).
   *
   * @param {object} target target object to modify.
   * @param {object} sources object(s) to merge into the target.
   * @returns {object} The merged object.
   * @member geo.util
   */
  deepMerge: function (target, ...sources) {
    for (const source of sources) {
      if (source === null || source === undefined) {
        continue;
      }
      for (const key in source) {
        if (key === '__proto__' || target === source[key]) {
          continue;
        }
        const copy = source[key];
        if (copy && typeof copy === 'object' && (copy.constructor === Object || Array.isArray(copy))) {
          let value = target[key];
          if (!Array.isArray(copy)) {
            if (typeof value !== 'object' || Array.isArray(value)) {
              value = {};
            }
          } else if (!Array.isArray(value)) {
            value = [];
          }
          target[key] = util.deepMerge(value, copy);
        } else if (copy !== undefined) {
          target[key] = copy;
        }
      }
    }
    return target;
  },

  /**
   * Check svg image and html img tags.  If the source is set, load images
   * explicitly and convert them to local data:image references.
   *
   * @param {jQuery.selector} elem A jQuery selector or element set that may
   *    contain images.
   * @returns {jQuery.Deferred[]} A list of deferred objects that resolve
   *    when images are dereferenced.
   * @memberof geo.util
   */
  dereferenceElements: function (elem) {
    var deferList = [];

    $('img,image', elem).each(function () {
      var src = $(this);
      var key = src.is('image') ? 'href' : 'src';
      if (src.attr(key)) {
        var img = new Image();
        if (src.attr(key).substr(0, 4) === 'http' || src[0].crossOrigin) {
          img.crossOrigin = src[0].crossOrigin || 'anonymous';
        }
        var defer = $.Deferred();
        img.onload = function () {
          var cvs = document.createElement('canvas');
          cvs.width = img.naturalWidth;
          cvs.height = img.naturalHeight;
          cvs.getContext('2d').drawImage(img, 0, 0);
          src.attr(key, cvs.toDataURL('image/png'));
          if (src.attr(key).substr(0, 10) !== 'data:image') {
            src.remove();
          }
          defer.resolve();
        };
        img.onerror = function () {
          src.remove();
          defer.resolve();
        };
        img.src = src.attr(key);
        deferList.push(defer);
      }
    });
    return deferList;
  },

  dereferenceCssUrlsRegex: /url\(["']?(http[^)"']+|[^:)"']+)["']?\)/g,

  /**
   * Check css text.  Any url(http[s]...) references are dereferenced and
   * stored as local base64 urls.
   *
   * @param {string} css The css to parse for urls.
   * @param {jQuery.selector|HTMLElement} styleElem The element that receives
   *    the css text after dereferencing or the DOM element that has style
   *    that will be updated.
   * @param {jQuery.Deferred} styleDefer A Deferred to resolve once
   *    dereferencing is complete.
   * @param {string} [styleKey] If unset, styleElem is a header element.  If
   *    set, styleElem is a DOM element and the named style will be updated.
   * @param {string} [baseUrl] If present, this is the base for relative urls.
   * @memberof geo.util
   */
  dereferenceCssUrls: function (css, styleElem, styleDefer, styleKey, baseUrl) {
    var deferList = [],
        results = [];

    /* Remove comments to avoid dereferencing commented out sections.
     * To match across lines, use [^\0] rather than . */
    css = css.replace(/\/\*[^\0]*?\*\//g, '');
    /* reduce whitespace to make the css shorter */
    css = css.replace(/\r/g, '\n').replace(/\s+\n/g, '\n')
             .replace(/\n\s+/g, '\n').replace(/\n\n+/g, '\n');
    if (baseUrl) {
      var match = /(^[^?#]*)\/[^?#/]*([?#]|$)/g.exec(baseUrl);
      baseUrl = match && match[1] ? match[1] + '/' : null;
    }
    css.replace(util.dereferenceCssUrlsRegex, function (match, url) {
      var idx = deferList.length,
          defer = $.Deferred(),
          xhr = new XMLHttpRequest();
      deferList.push(defer);
      results.push('');

      if (/^[^/:][^:]*(\/|$)/g.exec(url) && baseUrl) {
        url = baseUrl + url;
      }
      xhr.open('GET', url, true);
      xhr.responseType = 'arraybuffer';
      xhr.onload = function () {
        if (this.status === 200) {
          var response = new Uint8Array(this.response),
              data = new Array(response.length),
              i;
          for (i = 0; i < response.length; i += 1) {
            data[i] = String.fromCharCode(response[i]);
          }
          data = data.join('');
          results[idx] = 'url(data:' + xhr.getResponseHeader('content-type') + ';base64,' + btoa(data) + ')';
        }
        // resolve regardless of what response we get
        defer.resolve();
      };
      // if this fails, resolve anyway
      xhr.onerror = defer.resolve;
      xhr.send();
      return match;
    });
    $.when.apply($, deferList).then(function () {
      var idx = 0;
      css = css.replace(util.dereferenceCssUrlsRegex, function (match, url) {
        idx += 1;
        return results[idx - 1];
      });
      if (styleKey === undefined) {
        styleElem.text(css);
      } else {
        styleElem.style[styleKey] = css;
      }
      styleDefer.resolve();
    });
  },

  /**
   * Check if the current browser supports converting html to an image via an
   * svg foreignObject and canvas.  If this has not been checked before, it
   * returns a Deferred that resolves to a boolean (never rejects).  If the
   * check has been done before, it returns a boolean.
   *
   * @returns {boolean|jQuery.Deferred}
   * @memberof geo.util
   */
  htmlToImageSupported: function () {
    if (m_htmlToImageSupport === undefined) {
      var defer = $.Deferred();
      var svg = $(svgForeignObject);
      svg.attr({
        width: '10px',
        height: '10px',
        'text-rendering': 'optimizeLegibility'
      });
      $('foreignObject', svg).append('<div/>');
      var img = new Image();
      img.onload = img.onerror = function () {
        var canvas = document.createElement('canvas');
        canvas.width = 10;
        canvas.height = 10;
        var context = canvas.getContext('2d');
        context.drawImage(img, 0, 0);
        try {
          canvas.toDataURL();
          m_htmlToImageSupport = true;
        } catch (err) {
          console.warn(
            'This browser does not support converting HTML to an image via ' +
            'SVG foreignObject.  Some functionality will be limited.', err);
          m_htmlToImageSupport = false;
        }
        defer.resolve(m_htmlToImageSupport);
      };
      img.src = 'data:image/svg+xml;base64,' + btoa(util.escapeUnicodeHTML(
        new XMLSerializer().serializeToString(svg[0])));
      return defer;
    }
    return m_htmlToImageSupport;
  },

  /**
   * Convert an html element to an image.  This attempts to localize any
   * images within the element.  If there are other external references, the
   * image may not work due to security considerations.
   *
   * @param {jQuery.selector} elem Either a jquery selector or an HTML
   *    element.  This may contain multiple elements.  The direct parent and
   *    grandparent of the element are used for class information.
   * @param {number} [parents] Number of layers up to travel to get class
   *    information.
   * @returns {jQuery.Deferred} A jquery deferred object which receives an
   *    HTML Image element when resolved.
   * @memberof geo.util
   */
  htmlToImage: function (elem, parents) {
    var defer = $.Deferred(),
        deferList = [util.htmlToImageSupported()],
        container;

    var parent = $(elem);
    elem = $(elem).clone();
    while (parents && parents > 0) {
      parent = parent.parent();
      if (parent.is('div')) {
        /* Create a containing div with the parent's class and id (so css
         * will be used), but override size and background. */
        container = $('<div>').attr({
          class: parent.attr('class'),
          id: parent.attr('id')
        }).css({
          width: '100%',
          height: '100%',
          background: 'none',
          margin: 0
        });
        container.append(elem);
        elem = container;
      }
      parents -= 1;
    }
    // canvas elements won't render properly here.
    $('canvas', elem).remove();
    /* Walk through all of the children of elem and check if any explicitly set
     * css property needs to be dereferenced. */
    $('*', elem).addBack().each(function () {
      var style = this.style;
      for (var idx = 0; idx < style.length; idx += 1) {
        var key = this.style[idx];
        if (this.style[key].match(util.dereferenceCssUrlsRegex)) {
          var styleDefer = $.Deferred();
          util.dereferenceCssUrls(this.style[key], this, styleDefer, key);
          deferList.push(styleDefer);
        }
      }
    });
    container = $('<div xmlns="http://www.w3.org/1999/xhtml">');
    container.css({
      width: parent.width() + 'px',
      height: parent.height() + 'px'
    });
    container.append($('<head>'));
    var body = $('<body>');
    container.append(body);
    /* We must specify the new body as having no background, or we'll clobber
     * other layers. */
    body.css({
      width: parent.width() + 'px',
      height: parent.height() + 'px',
      background: 'none',
      margin: 0
    });
    body.append(elem);
    deferList = deferList.concat(util.dereferenceElements(elem));
    /* Get styles and links in order, as order matters in css */
    $('style,link[rel="stylesheet"]').each(function () {
      var styleElem = $('<style type="text/css">'),
          styleDefer = $.Deferred();
      if ($(this).is('style')) {
        var css = $(this).text();
        util.dereferenceCssUrls(css, styleElem, styleDefer);
      } else {
        var href = $(this).attr('href');
        $.get(href).done(function (css) {
          util.dereferenceCssUrls(css, styleElem, styleDefer, undefined, href);
        }).fail(function (xhr, status, err) {
          console.warn('Failed to dereference ' + href, status, err);
          styleElem.remove();
          styleDefer.resolve();
        });
      }
      deferList.push(styleDefer);
      $('head', container).append(styleElem);
    });

    $.when.apply($, deferList).then(function () {
      var svg = $(svgForeignObject);
      svg.attr({
        width: parent.width() + 'px',
        height: parent.height() + 'px',
        // Adding this via the attr call works in Firefox headless, whereas if
        // it is part of the svgForeignObject string, it does not.
        'text-rendering': 'optimizeLegibility'
      });
      $('foreignObject', svg).append(container);

      var img = new Image();
      if (!util.htmlToImageSupported()) {
        defer.resolve(img);
      } else {
        img.onload = function () {
          defer.resolve(img);
        };
        img.onerror = function () {
          console.warn('Failed to render html to image');
          defer.reject();
        };
        // Firefox requires the HTML to be base64 encoded.  Chrome doesn't, but
        // doing so does no harm.
        img.src = 'data:image/svg+xml;base64,' + btoa(util.escapeUnicodeHTML(
          new XMLSerializer().serializeToString(svg[0])));
      }
    });
    return defer;
  },

  /**
   * Report on one or all of the tracked timings.
   *
   * @param {string} [name] A name to report on, or `undefined` to report all.
   * @returns {object} An object with timing information, or an object with
   *    properties for all tracked timings, each of which contains timing
   *    information.
   * @memberof geo.util
   */
  timeReport: function (name) {
    $.each(m_timingData, function (key, item) {
      /* calculate the standard deviation of each item. */
      if (item.count) {
        item.stddev = Math.sqrt(Math.abs((
          item.sum2 - item.sum * item.sum / item.count) / item.count));
        item.average = item.sum / item.count;
      } else {
        item.stddev = 0;
        item.average = 0;
      }
    });
    if (name) {
      return m_timingData[name];
    }
    return m_timingData;
  },

  /**
   * Note the start time of a function (or any other section of code).  This
   * should be paired with `timeFunctionStop`, which will collect statistics on
   * the amount of time spent in a function.
   *
   * @param {string} name Name to use for tracking the timing.
   * @param {boolean} reset If `true`, clear old tracking data for this named
   *    tracker.
   * @memberof geo.util
   */
  timeFunctionStart: function (name, reset) {
    if (!m_timingData[name] || reset) {
      m_timingData[name] = {
        count: 0, sum: 0, sum2: 0, max: 0, recent: []
      };
    }
    m_timingData[name].start = window.performance.now();
  },

  /**
   * Note the stop time of a function (or any other section of code).  This
   * should be paired with `timeFunctionStart`.
   *
   * @param {string} name Name to use for tracking the timing.
   * @memberof geo.util
   */
  timeFunctionStop: function (name) {
    if (!m_timingData[name] || !m_timingData[name].start) {
      return;
    }
    var duration = window.performance.now() - m_timingData[name].start;
    m_timingData[name].start = null;
    m_timingData[name].sum += duration;
    m_timingData[name].sum2 += duration * duration;
    m_timingData[name].count += 1;
    m_timingData[name].max = Math.max(
      m_timingData[name].max, duration);
    m_timingData[name].recent.push(duration);
    if (m_timingData[name].recent.length > m_timingKeepRecent) {
      m_timingData[name].recent.splice(
        0, m_timingData[name].recent.length - m_timingKeepRecent);
    }
  },

  /**
   * Start or stop tracking the time spent in `requestAnimationFrame`.  If
   * tracked, the results can be fetched via
   * `timeFunctionReport('requestAnimationFrame')`.
   *
   * @param {boolean} [stop] Falsy to start tracking, truthy to start tracking.
   * @param {boolean} [reset] If truthy, reset the statistics.
   * @param {number} [threshold=15] If present, set the threshold in
   *    milliseconds used in tracking slow callbacks.
   * @param {number} [keep=200] If present, set the number of recent frame
   *    times to track.
   * @memberof geo.util
   */
  timeRequestAnimationFrame: function (stop, reset, threshold, keep) {
    if (!m_timingData.requestAnimationFrame || reset) {
      m_timingData.requestAnimationFrame = {
        count: 0,
        sum: 0,
        sum2: 0,
        max: 0,
        above_threshold: 0,
        recent: [],
        recentsub: []
      };
    }
    if (threshold) {
      m_threshold = threshold;
    }
    if (keep) {
      m_timingKeepRecent = keep;
    }
    if (stop && m_originalRequestAnimationFrame) {
      window.requestAnimationFrame = m_originalRequestAnimationFrame;
      m_originalRequestAnimationFrame = null;
    } else if (!stop && !m_originalRequestAnimationFrame) {
      m_originalRequestAnimationFrame = window.requestAnimationFrame;
      window.requestAnimationFrame = function (callback) {
        return m_originalRequestAnimationFrame.call(window, function (timestamp) {
          var track = m_timingData.requestAnimationFrame, recent;
          /* Some environments have unsynchronized performance and time
           * counters.  The nowDelta factor compensates for this.  For
           * instance, our test environment has performance.now() values on
           * the order of ~3000 and timestamps approximating epoch. */
          if (track.timestamp !== timestamp) {
            track.nowDelta = window.performance.now() - timestamp;
            if (Math.abs(track.nowDelta) < 1000) {
              track.nowDelta = 0;
            }
            track.timestamp = timestamp;
            track.subcalls = track.subcalls || 0;
            track.start = {
              sum: track.sum,
              sum2: track.sum2,
              count: track.count,
              max: track.max,
              above_threshold: track.above_threshold
            };
            track.recent.push([0]);
            track.recentsub.push([]);
            if (track.recent.length > m_timingKeepRecent) {
              track.recent.splice(
                0, track.recent.length - m_timingKeepRecent);
              track.recentsub.splice(
                0, track.recentsub.length - m_timingKeepRecent);
            }
          }
          track.subcalls += 1;
          callback.apply(this, arguments);
          var duration = window.performance.now() - timestamp;
          duration -= track.nowDelta;
          track.sum = track.start.sum + duration;
          track.sum2 = track.start.sum2 + duration * duration;
          track.count = track.start.count + 1;
          track.max = Math.max(track.max, duration);
          track.above_threshold = track.start.above_threshold + (
            duration >= m_threshold ? 1 : 0);
          track.recent[track.recent.length - 1] = duration;
          recent = track.recentsub[track.recent.length - 1];
          recent.push({
            total_duration: duration,
            duration: duration - (recent.length ?
              recent[recent.length - 1].total_duration : 0),
            callback: callback.name || callback
          });
        });
      };
    }
  },

  /**
   * Test if an item is an object.  This uses typeof not instanceof, since
   * instanceof will return false for some things that we expect to be objects.
   *
   * @param {*} value The item to test.
   * @returns {boolean} True if the tested item is an object.
   * @memberof geo.util
   */
  isObject: function (value) {
    var type = typeof value;
    return value !== null && value !== undefined && (type === 'object' || type === 'function');
  },

  /**
   * Return the first value passed to the function.  Using this function for
   * identity calls can allow some code to bypass making such a call entirely.
   *
   * @param {*} d Any value.
   * @returns {*} The passed value.
   */
  identityFunction: function (d) {
    return d;
  },

  ///////////////////////////////////////////////////////////////////////////
  /*
   * Utility member properties.
   */
  ///////////////////////////////////////////////////////////////////////////

  /**
   * Radius of the earth in meters, from the equatorial radius of SRID 4326.
   * @memberof geo.util
   */
  radiusEarth: proj4.WGS84.a
};

module.exports = util;