UNPKG

contraction-hierarchy-js

Version:
2,929 lines 95.2 kB
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
    typeof define === 'function' && define.amd ? define(['exports'], factory) :
    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.contractionHierarchy = {}));
})(this, (function (exports) { 'use strict';

    const ARRAY_TYPES = [
        Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
        Int32Array, Uint32Array, Float32Array, Float64Array
    ];

    /** @typedef {Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */

    const VERSION = 1; // serialized format version
    const HEADER_SIZE = 8;

    class KDBush {

        /**
         * Creates an index from raw `ArrayBuffer` data.
         * @param {ArrayBuffer} data
         */
        static from(data) {
            if (!(data instanceof ArrayBuffer)) {
                throw new Error('Data must be an instance of ArrayBuffer.');
            }
            const [magic, versionAndType] = new Uint8Array(data, 0, 2);
            if (magic !== 0xdb) {
                throw new Error('Data does not appear to be in a KDBush format.');
            }
            const version = versionAndType >> 4;
            if (version !== VERSION) {
                throw new Error(`Got v${version} data when expected v${VERSION}.`);
            }
            const ArrayType = ARRAY_TYPES[versionAndType & 0x0f];
            if (!ArrayType) {
                throw new Error('Unrecognized array type.');
            }
            const [nodeSize] = new Uint16Array(data, 2, 1);
            const [numItems] = new Uint32Array(data, 4, 1);

            return new KDBush(numItems, nodeSize, ArrayType, data);
        }

        /**
         * Creates an index that will hold a given number of items.
         * @param {number} numItems
         * @param {number} [nodeSize=64] Size of the KD-tree node (64 by default).
         * @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default).
         * @param {ArrayBuffer} [data] (For internal use only)
         */
        constructor(numItems, nodeSize = 64, ArrayType = Float64Array, data) {
            if (isNaN(numItems) || numItems < 0) throw new Error(`Unpexpected numItems value: ${numItems}.`);

            this.numItems = +numItems;
            this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535);
            this.ArrayType = ArrayType;
            this.IndexArrayType = numItems < 65536 ? Uint16Array : Uint32Array;

            const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType);
            const coordsByteSize = numItems * 2 * this.ArrayType.BYTES_PER_ELEMENT;
            const idsByteSize = numItems * this.IndexArrayType.BYTES_PER_ELEMENT;
            const padCoords = (8 - idsByteSize % 8) % 8;

            if (arrayTypeIndex < 0) {
                throw new Error(`Unexpected typed array class: ${ArrayType}.`);
            }

            if (data && (data instanceof ArrayBuffer)) { // reconstruct an index from a buffer
                this.data = data;
                this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems);
                this.coords = new this.ArrayType(this.data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
                this._pos = numItems * 2;
                this._finished = true;
            } else { // initialize a new index
                this.data = new ArrayBuffer(HEADER_SIZE + coordsByteSize + idsByteSize + padCoords);
                this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems);
                this.coords = new this.ArrayType(this.data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
                this._pos = 0;
                this._finished = false;

                // set header
                new Uint8Array(this.data, 0, 2).set([0xdb, (VERSION << 4) + arrayTypeIndex]);
                new Uint16Array(this.data, 2, 1)[0] = nodeSize;
                new Uint32Array(this.data, 4, 1)[0] = numItems;
            }
        }

        /**
         * Add a point to the index.
         * @param {number} x
         * @param {number} y
         * @returns {number} An incremental index associated with the added item (starting from `0`).
         */
        add(x, y) {
            const index = this._pos >> 1;
            this.ids[index] = index;
            this.coords[this._pos++] = x;
            this.coords[this._pos++] = y;
            return index;
        }

        /**
         * Perform indexing of the added points.
         */
        finish() {
            const numAdded = this._pos >> 1;
            if (numAdded !== this.numItems) {
                throw new Error(`Added ${numAdded} items when expected ${this.numItems}.`);
            }
            // kd-sort both arrays for efficient search
            sort(this.ids, this.coords, this.nodeSize, 0, this.numItems - 1, 0);

            this._finished = true;
            return this;
        }

        /**
         * Search the index for items within a given bounding box.
         * @param {number} minX
         * @param {number} minY
         * @param {number} maxX
         * @param {number} maxY
         * @returns {number[]} An array of indices correponding to the found items.
         */
        range(minX, minY, maxX, maxY) {
            if (!this._finished) throw new Error('Data not yet indexed - call index.finish().');

            const {ids, coords, nodeSize} = this;
            const stack = [0, ids.length - 1, 0];
            const result = [];

            // recursively search for items in range in the kd-sorted arrays
            while (stack.length) {
                const axis = stack.pop() || 0;
                const right = stack.pop() || 0;
                const left = stack.pop() || 0;

                // if we reached "tree node", search linearly
                if (right - left <= nodeSize) {
                    for (let i = left; i <= right; i++) {
                        const x = coords[2 * i];
                        const y = coords[2 * i + 1];
                        if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[i]);
                    }
                    continue;
                }

                // otherwise find the middle index
                const m = (left + right) >> 1;

                // include the middle item if it's in range
                const x = coords[2 * m];
                const y = coords[2 * m + 1];
                if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[m]);

                // queue search in halves that intersect the query
                if (axis === 0 ? minX <= x : minY <= y) {
                    stack.push(left);
                    stack.push(m - 1);
                    stack.push(1 - axis);
                }
                if (axis === 0 ? maxX >= x : maxY >= y) {
                    stack.push(m + 1);
                    stack.push(right);
                    stack.push(1 - axis);
                }
            }

            return result;
        }

        /**
         * Search the index for items within a given radius.
         * @param {number} qx
         * @param {number} qy
         * @param {number} r Query radius.
         * @returns {number[]} An array of indices correponding to the found items.
         */
        within(qx, qy, r) {
            if (!this._finished) throw new Error('Data not yet indexed - call index.finish().');

            const {ids, coords, nodeSize} = this;
            const stack = [0, ids.length - 1, 0];
            const result = [];
            const r2 = r * r;

            // recursively search for items within radius in the kd-sorted arrays
            while (stack.length) {
                const axis = stack.pop() || 0;
                const right = stack.pop() || 0;
                const left = stack.pop() || 0;

                // if we reached "tree node", search linearly
                if (right - left <= nodeSize) {
                    for (let i = left; i <= right; i++) {
                        if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) result.push(ids[i]);
                    }
                    continue;
                }

                // otherwise find the middle index
                const m = (left + right) >> 1;

                // include the middle item if it's in range
                const x = coords[2 * m];
                const y = coords[2 * m + 1];
                if (sqDist(x, y, qx, qy) <= r2) result.push(ids[m]);

                // queue search in halves that intersect the query
                if (axis === 0 ? qx - r <= x : qy - r <= y) {
                    stack.push(left);
                    stack.push(m - 1);
                    stack.push(1 - axis);
                }
                if (axis === 0 ? qx + r >= x : qy + r >= y) {
                    stack.push(m + 1);
                    stack.push(right);
                    stack.push(1 - axis);
                }
            }

            return result;
        }
    }

    /**
     * @param {Uint16Array | Uint32Array} ids
     * @param {InstanceType<TypedArrayConstructor>} coords
     * @param {number} nodeSize
     * @param {number} left
     * @param {number} right
     * @param {number} axis
     */
    function sort(ids, coords, nodeSize, left, right, axis) {
        if (right - left <= nodeSize) return;

        const m = (left + right) >> 1; // middle index

        // sort ids and coords around the middle index so that the halves lie
        // either left/right or top/bottom correspondingly (taking turns)
        select(ids, coords, m, left, right, axis);

        // recursively kd-sort first half and second half on the opposite axis
        sort(ids, coords, nodeSize, left, m - 1, 1 - axis);
        sort(ids, coords, nodeSize, m + 1, right, 1 - axis);
    }

    /**
     * Custom Floyd-Rivest selection algorithm: sort ids and coords so that
     * [left..k-1] items are smaller than k-th item (on either x or y axis)
     * @param {Uint16Array | Uint32Array} ids
     * @param {InstanceType<TypedArrayConstructor>} coords
     * @param {number} k
     * @param {number} left
     * @param {number} right
     * @param {number} axis
     */
    function select(ids, coords, k, left, right, axis) {

        while (right > left) {
            if (right - left > 600) {
                const n = right - left + 1;
                const m = k - left + 1;
                const z = Math.log(n);
                const s = 0.5 * Math.exp(2 * z / 3);
                const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
                const newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
                const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
                select(ids, coords, k, newLeft, newRight, axis);
            }

            const t = coords[2 * k + axis];
            let i = left;
            let j = right;

            swapItem(ids, coords, left, k);
            if (coords[2 * right + axis] > t) swapItem(ids, coords, left, right);

            while (i < j) {
                swapItem(ids, coords, i, j);
                i++;
                j--;
                while (coords[2 * i + axis] < t) i++;
                while (coords[2 * j + axis] > t) j--;
            }

            if (coords[2 * left + axis] === t) swapItem(ids, coords, left, j);
            else {
                j++;
                swapItem(ids, coords, j, right);
            }

            if (j <= k) left = j + 1;
            if (k <= j) right = j - 1;
        }
    }

    /**
     * @param {Uint16Array | Uint32Array} ids
     * @param {InstanceType<TypedArrayConstructor>} coords
     * @param {number} i
     * @param {number} j
     */
    function swapItem(ids, coords, i, j) {
        swap(ids, i, j);
        swap(coords, 2 * i, 2 * j);
        swap(coords, 2 * i + 1, 2 * j + 1);
    }

    /**
     * @param {InstanceType<TypedArrayConstructor>} arr
     * @param {number} i
     * @param {number} j
     */
    function swap(arr, i, j) {
        const tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    /**
     * @param {number} ax
     * @param {number} ay
     * @param {number} bx
     * @param {number} by
     */
    function sqDist(ax, ay, bx, by) {
        const dx = ax - bx;
        const dy = ay - by;
        return dx * dx + dy * dy;
    }

    class TinyQueue {
        constructor(data = [], compare = defaultCompare) {
            this.data = data;
            this.length = this.data.length;
            this.compare = compare;

            if (this.length > 0) {
                for (let i = (this.length >> 1) - 1; i >= 0; i--) this._down(i);
            }
        }

        push(item) {
            this.data.push(item);
            this.length++;
            this._up(this.length - 1);
        }

        pop() {
            if (this.length === 0) return undefined;

            const top = this.data[0];
            const bottom = this.data.pop();
            this.length--;

            if (this.length > 0) {
                this.data[0] = bottom;
                this._down(0);
            }

            return top;
        }

        peek() {
            return this.data[0];
        }

        _up(pos) {
            const {data, compare} = this;
            const item = data[pos];

            while (pos > 0) {
                const parent = (pos - 1) >> 1;
                const current = data[parent];
                if (compare(item, current) >= 0) break;
                data[pos] = current;
                pos = parent;
            }

            data[pos] = item;
        }

        _down(pos) {
            const {data, compare} = this;
            const halfLength = this.length >> 1;
            const item = data[pos];

            while (pos < halfLength) {
                let left = (pos << 1) + 1;
                let best = data[left];
                const right = left + 1;

                if (right < this.length && compare(data[right], best) < 0) {
                    left = right;
                    best = data[right];
                }
                if (compare(best, item) >= 0) break;

                data[pos] = best;
                pos = left;
            }

            data[pos] = item;
        }
    }

    function defaultCompare(a, b) {
        return a < b ? -1 : a > b ? 1 : 0;
    }

    const earthRadius = 6371;
    const rad = Math.PI / 180;

    function around(index, lng, lat, maxResults = Infinity, maxDistance = Infinity, predicate) {
        let maxHaverSinDist = 1;
        const result = [];

        if (maxResults === undefined) maxResults = Infinity;
        if (maxDistance !== undefined) maxHaverSinDist = haverSin(maxDistance / earthRadius);

        // a distance-sorted priority queue that will contain both points and kd-tree nodes
        const q = new TinyQueue([], compareDist);

        // an object that represents the top kd-tree node (the whole Earth)
        let node = {
            left: 0, // left index in the kd-tree array
            right: index.ids.length - 1, // right index
            axis: 0, // 0 for longitude axis and 1 for latitude axis
            minLng: -180, // bounding box of the node
            minLat: -90,
            maxLng: 180,
            maxLat: 90
        };

        const cosLat = Math.cos(lat * rad);

        while (node) {
            const right = node.right;
            const left = node.left;

            if (right - left <= index.nodeSize) { // leaf node

                // add all points of the leaf node to the queue
                for (let i = left; i <= right; i++) {
                    const id = index.ids[i];
                    if (!predicate || predicate(id)) {
                        const dist = haverSinDist(lng, lat, index.coords[2 * i], index.coords[2 * i + 1], cosLat);
                        q.push({id, dist});
                    }
                }

            } else { // not a leaf node (has child nodes)

                const m = (left + right) >> 1; // middle index
                const midLng = index.coords[2 * m];
                const midLat = index.coords[2 * m + 1];

                // add middle point to the queue
                const id = index.ids[m];
                if (!predicate || predicate(id)) {
                    const dist = haverSinDist(lng, lat, midLng, midLat, cosLat);
                    q.push({id, dist});
                }

                const nextAxis = (node.axis + 1) % 2;

                // first half of the node
                const leftNode = {
                    left,
                    right: m - 1,
                    axis: nextAxis,
                    minLng: node.minLng,
                    minLat: node.minLat,
                    maxLng: node.axis === 0 ? midLng : node.maxLng,
                    maxLat: node.axis === 1 ? midLat : node.maxLat,
                    dist: 0
                };
                // second half of the node
                const rightNode = {
                    left: m + 1,
                    right,
                    axis: nextAxis,
                    minLng: node.axis === 0 ? midLng : node.minLng,
                    minLat: node.axis === 1 ? midLat : node.minLat,
                    maxLng: node.maxLng,
                    maxLat: node.maxLat,
                    dist: 0
                };

                leftNode.dist = boxDist(lng, lat, cosLat, leftNode);
                rightNode.dist = boxDist(lng, lat, cosLat, rightNode);

                // add child nodes to the queue
                q.push(leftNode);
                q.push(rightNode);
            }

            // fetch closest points from the queue; they're guaranteed to be closer
            // than all remaining points (both individual and those in kd-tree nodes),
            // since each node's distance is a lower bound of distances to its children
            while (q.length && q.peek().id != null) {
                const candidate = q.pop();
                if (candidate.dist > maxHaverSinDist) return result;
                result.push(candidate.id);
                if (result.length === maxResults) return result;
            }

            // the next closest kd-tree node
            node = q.pop();
        }

        return result;
    }

    // lower bound for distance from a location to points inside a bounding box
    function boxDist(lng, lat, cosLat, node) {
        const minLng = node.minLng;
        const maxLng = node.maxLng;
        const minLat = node.minLat;
        const maxLat = node.maxLat;

        // query point is between minimum and maximum longitudes
        if (lng >= minLng && lng <= maxLng) {
            if (lat < minLat) return haverSin((lat - minLat) * rad);
            if (lat > maxLat) return haverSin((lat - maxLat) * rad);
            return 0;
        }

        // query point is west or east of the bounding box;
        // calculate the extremum for great circle distance from query point to the closest longitude;
        const haverSinDLng = Math.min(haverSin((lng - minLng) * rad), haverSin((lng - maxLng) * rad));
        const extremumLat = vertexLat(lat, haverSinDLng);

        // if extremum is inside the box, return the distance to it
        if (extremumLat > minLat && extremumLat < maxLat) {
            return haverSinDistPartial(haverSinDLng, cosLat, lat, extremumLat);
        }
        // otherwise return the distan e to one of the bbox corners (whichever is closest)
        return Math.min(
            haverSinDistPartial(haverSinDLng, cosLat, lat, minLat),
            haverSinDistPartial(haverSinDLng, cosLat, lat, maxLat)
        );
    }

    function compareDist(a, b) {
        return a.dist - b.dist;
    }

    function haverSin(theta) {
        const s = Math.sin(theta / 2);
        return s * s;
    }

    function haverSinDistPartial(haverSinDLng, cosLat1, lat1, lat2) {
        return cosLat1 * Math.cos(lat2 * rad) * haverSinDLng + haverSin((lat1 - lat2) * rad);
    }

    function haverSinDist(lng1, lat1, lng2, lat2, cosLat1) {
        const haverSinDLng = haverSin((lng1 - lng2) * rad);
        return haverSinDistPartial(haverSinDLng, cosLat1, lat1, lat2);
    }

    function distance(lng1, lat1, lng2, lat2) {
        const h = haverSinDist(lng1, lat1, lng2, lat2, Math.cos(lat1 * rad));
        return 2 * earthRadius * Math.asin(Math.sqrt(h));
    }

    function vertexLat(lat, haverSinDLng) {
        const cosDLng = 1 - 2 * haverSinDLng;
        if (cosDLng <= 0) return lat > 0 ? 90 : -90;
        return Math.atan(Math.tan(lat * rad) / cosDLng) / rad;
    }

    var geokdbush = /*#__PURE__*/Object.freeze({
        __proto__: null,
        around: around,
        distance: distance
    });

    function CoordinateLookup$1(graph) {

      if (!graph._geoJsonFlag) {
        throw new Error('Cannot use Coordinate Lookup on a non-GeoJson network.');
      }

      const points_set = new Set();

      Object.keys(graph._nodeToIndexLookup).forEach(key => {
        points_set.add(key);
      });

      const coordinate_list = [];

      points_set.forEach(pt_str => {
        coordinate_list.push(pt_str.split(',').map(d => Number(d)));
      });

      this.coordinate_list = coordinate_list; // Store for lookup
      this.index = new KDBush(coordinate_list.length);
      for (const coord of coordinate_list) {
        this.index.add(coord[0], coord[1]);
      }
      this.index.finish();
    }

    CoordinateLookup$1.prototype.getClosestNetworkPt = function(lng, lat) {
      const closestIndex = around(this.index, lng, lat, 1)[0];
      return this.coordinate_list[closestIndex];
    };

    const __geoindex$1 = geokdbush;
    const __kdindex$1 = KDBush;

    // Helper function to reconstruct complete node sequence from edges
    function reconstructNodesFromEdges(edgeList, edgeProperties, indexToNodeLookup, startNodeIndex) {
      if (edgeList.length === 0) {
        return [indexToNodeLookup[startNodeIndex]];
      }
      
      const nodeSequence = [];
      let currentNode = startNodeIndex;
      
      // Add starting node
      nodeSequence.push(indexToNodeLookup[currentNode]);
      
      // Traverse edges to build complete node sequence
      for (const edgeIndex of edgeList) {
        const edge = edgeProperties[edgeIndex];
        
        // Determine which direction we're traversing this edge
        if (currentNode === edge._start_index) {
          // Going from start to end
          currentNode = edge._end_index;
        } else if (currentNode === edge._end_index) {
          // Going from end to start
          currentNode = edge._start_index;
        } else {
          // Edge doesn't connect to current node - this shouldn't happen in a valid path
          // but we'll handle it gracefully by using the edge's start node
          currentNode = edge._end_index;
        }
        
        nodeSequence.push(indexToNodeLookup[currentNode]);
      }
      
      return nodeSequence;
    }

    function buildIdList(options, edgeProperties, edgeGeometry, forward_nodeState, backward_nodeState, tentative_shortest_node, indexToNodeLookup, startNode) {

      const pathway = [];
      const node_list = [tentative_shortest_node];

      let current_forward_node = forward_nodeState[tentative_shortest_node];
      let current_backward_node = backward_nodeState[tentative_shortest_node];

      // first check necessary because may not be any nodes in forward or backward pathway
      // (occasionally entire pathway may be ONLY in the backward or forward directions)
      if (current_forward_node) {
        while (current_forward_node.attrs != null) {
          pathway.push({ id: current_forward_node.attrs, direction: 'f' });
          node_list.push(current_forward_node.prev);
          current_forward_node = forward_nodeState[current_forward_node.prev];
        }
      }

      pathway.reverse();
      node_list.reverse();

      if (current_backward_node) {
        while (current_backward_node.attrs != null) {
          pathway.push({ id: current_backward_node.attrs, direction: 'b' });
          node_list.push(current_backward_node.prev);
          current_backward_node = backward_nodeState[current_backward_node.prev];
        }
      }

      let node = startNode;

      const ordered = pathway.map(p => {
        const start = p.direction === 'f' ? edgeProperties[p.id]._start_index : edgeProperties[p.id]._end_index;
        const end = p.direction === 'f' ? edgeProperties[p.id]._end_index : edgeProperties[p.id]._start_index;
        const props = [...edgeProperties[p.id]._ordered];

        if (node !== start) {
          props.reverse();
          node = start;
        }
        else {
          node = end;
        }

        return props;
      });

      const flattened = [].concat(...ordered);


      const ids = flattened.map(d => edgeProperties[d]._id);



      let properties, property_list, path, nodes;


      if (options.nodes) {
        // Use edge-based reconstruction to ensure all nodes are included
        nodes = reconstructNodesFromEdges(flattened, edgeProperties, indexToNodeLookup, startNode);
      }


      if (options.properties || options.path) {

        property_list = flattened.map(f => {

          // remove internal properties
          const { _start_index, _end_index, _ordered, ...originalProperties } = edgeProperties[f];

          return originalProperties;

        });

      }


      if (options.path) {

        const features = flattened.map((f, i) => {

          return {
            "type": "Feature",
            "properties": property_list[i],
            "geometry": {
              "type": "LineString",
              "coordinates": edgeGeometry[f]
            }
          };

        });

        path = { "type": "FeatureCollection", "features": features };

      }

      if (options.properties) {
        properties = property_list;
      }

      return { ids, path, properties, nodes };

    }

    /**
     * Based on https://github.com/mourner/tinyqueue
     * Copyright (c) 2017, Vladimir Agafonkin https://github.com/mourner/tinyqueue/blob/master/LICENSE
     * 
     * Adapted for PathFinding needs by @anvaka
     * Copyright (c) 2017, Andrei Kashcha
     *
     * Additional inconsequential changes by @royhobbstn
     * 
     **/


    function NodeHeap(options) {
      if (!(this instanceof NodeHeap)) return new NodeHeap(options);

      options = options || {};

      if (!options.compare) {
        throw new Error("Please supply a comparison function to NodeHeap");
      }

      this.data = [];
      this.length = this.data.length;
      this.compare = options.compare;
      this.setNodeId = function(nodeSearchState, heapIndex) {
        nodeSearchState.heapIndex = heapIndex;
      };

      if (this.length > 0) {
        for (var i = (this.length >> 1); i >= 0; i--) this._down(i);
      }

      if (options.setNodeId) {
        for (var i = 0; i < this.length; ++i) {
          this.setNodeId(this.data[i], i);
        }
      }
    }

    NodeHeap.prototype = {

      push: function(item) {
        this.data.push(item);
        this.setNodeId(item, this.length);
        this.length++;
        this._up(this.length - 1);
      },

      pop: function() {
        if (this.length === 0) return undefined;

        var top = this.data[0];
        this.length--;

        if (this.length > 0) {
          this.data[0] = this.data[this.length];
          this.setNodeId(this.data[0], 0);
          this._down(0);
        }
        this.data.pop();

        return top;
      },

      peek: function() {
        return this.data[0];
      },

      updateItem: function(pos) {
        this._down(pos);
        this._up(pos);
      },

      _up: function(pos) {
        var data = this.data;
        var compare = this.compare;
        var setNodeId = this.setNodeId;
        var item = data[pos];

        while (pos > 0) {
          var parent = (pos - 1) >> 1;
          var current = data[parent];
          if (compare(item, current) >= 0) break;
          data[pos] = current;

          setNodeId(current, pos);
          pos = parent;
        }

        data[pos] = item;
        setNodeId(item, pos);
      },

      _down: function(pos) {
        var data = this.data;
        var compare = this.compare;
        var halfLength = this.length >> 1;
        var item = data[pos];
        var setNodeId = this.setNodeId;

        while (pos < halfLength) {
          var left = (pos << 1) + 1;
          var right = left + 1;
          var best = data[left];

          if (right < this.length && compare(data[right], best) < 0) {
            left = right;
            best = data[right];
          }
          if (compare(best, item) >= 0) break;

          data[pos] = best;
          setNodeId(best, pos);
          pos = left;
        }

        data[pos] = item;
        setNodeId(item, pos);
      }
    };

    const createPathfinder = function(options) {

      const adjacency_list = this.adjacency_list;
      const reverse_adjacency_list = this.reverse_adjacency_list;
      const edgeProperties = this._edgeProperties;
      const edgeGeometry = this._edgeGeometry;
      const pool = this._createNodePool();
      const nodeToIndexLookup = this._nodeToIndexLookup;
      const indexToNodeLookup = this._indexToNodeLookup;

      if (!options) {
        options = {};
      }

      return {
        queryContractionHierarchy
      };

      function queryContractionHierarchy(
        start,
        end
      ) {

        pool.reset();

        const start_index = nodeToIndexLookup[String(start)];
        const end_index = nodeToIndexLookup[String(end)];

        const forward_nodeState = [];
        const backward_nodeState = [];

        const forward_distances = {};
        const backward_distances = {};


        let current_start = pool.createNewState({ id: start_index, dist: 0 });
        forward_nodeState[start_index] = current_start;
        current_start.opened = 1;
        forward_distances[current_start.id] = 0;

        let current_end = pool.createNewState({ id: end_index, dist: 0 });
        backward_nodeState[end_index] = current_end;
        current_end.opened = 1;
        backward_distances[current_end.id] = 0;

        const searchForward = doDijkstra(
          adjacency_list,
          current_start,
          forward_nodeState,
          forward_distances,
          backward_nodeState,
          backward_distances
        );
        const searchBackward = doDijkstra(
          reverse_adjacency_list,
          current_end,
          backward_nodeState,
          backward_distances,
          forward_nodeState,
          forward_distances
        );

        let forward_done = false;
        let backward_done = false;
        let sf, sb;

        let tentative_shortest_path = Infinity;
        let tentative_shortest_node = null;

        if (start_index !== end_index) {
          do {
            if (!forward_done) {
              sf = searchForward.next();
              if (sf.done) {
                forward_done = true;
              }
            }
            if (!backward_done) {
              sb = searchBackward.next();
              if (sb.done) {
                backward_done = true;
              }
            }

          } while (
            forward_distances[sf.value.id] < tentative_shortest_path ||
            backward_distances[sb.value.id] < tentative_shortest_path
          );
        }
        else {
          tentative_shortest_path = 0;
        }

        let result = { total_cost: tentative_shortest_path !== Infinity ? tentative_shortest_path : 0 };


        let extra_attrs;

        if (options.ids || options.path || options.nodes || options.properties) {
          if (tentative_shortest_node != null) {
            // tentative_shortest_path as falsy indicates no path found.
            extra_attrs = buildIdList(options, edgeProperties, edgeGeometry, forward_nodeState, backward_nodeState, tentative_shortest_node, indexToNodeLookup, start_index);
          }
          else {

            let ids, path, properties, nodes;

            // fill in object to prevent errors in the case of no path found
            if (options.ids) {
              ids = [];
            }
            if (options.path) {
              path = {};
            }
            if (options.properties) {
              properties = [];
            }
            if (options.nodes) {
              nodes = [];
            }

            extra_attrs = { ids, path, properties, nodes };
          }

        }

        // the end.  results sent to user
        return Object.assign(result, { ...extra_attrs });


        //

        function* doDijkstra(
          adj,
          current,
          nodeState,
          distances,
          reverse_nodeState,
          reverse_distances
        ) {

          var openSet = new NodeHeap({
            compare(a, b) {
              return a.dist - b.dist;
            }
          });

          do {
            (adj[current.id] || []).forEach(edge => {

              let node = nodeState[edge.end];
              if (node === undefined) {
                node = pool.createNewState({ id: edge.end });
                node.attrs = edge.attrs;
                nodeState[edge.end] = node;
              }

              if (node.visited === true) {
                return;
              }

              if (!node.opened) {
                openSet.push(node);
                node.opened = true;
              }

              const proposed_distance = current.dist + edge.cost;
              if (proposed_distance >= node.dist) {
                return;
              }

              node.dist = proposed_distance;
              distances[node.id] = proposed_distance;
              node.attrs = edge.attrs;
              node.prev = current.id;

              openSet.updateItem(node.heapIndex);

              const reverse_dist = reverse_distances[edge.end];
              if (reverse_dist >= 0) {
                const path_len = proposed_distance + reverse_dist;
                if (tentative_shortest_path > path_len) {
                  tentative_shortest_path = path_len;
                  tentative_shortest_node = edge.end;
                }
              }

            });
            current.visited = true;

            // get lowest value from heap
            current = openSet.pop();

            if (!current) {
              return '';
            }

            yield current;

          } while (true);

        }

      }

    };

    // ES6 Map
    var map;
    try {
      map = Map;
    } catch (_) { }
    var set;

    // ES6 Set
    try {
      set = Set;
    } catch (_) { }

    function baseClone (src, circulars, clones) {
      // Null/undefined/functions/etc
      if (!src || typeof src !== 'object' || typeof src === 'function') {
        return src
      }

      // DOM Node
      if (src.nodeType && 'cloneNode' in src) {
        return src.cloneNode(true)
      }

      // Date
      if (src instanceof Date) {
        return new Date(src.getTime())
      }

      // RegExp
      if (src instanceof RegExp) {
        return new RegExp(src)
      }

      // Arrays
      if (Array.isArray(src)) {
        return src.map(clone)
      }

      // ES6 Maps
      if (map && src instanceof map) {
        return new Map(Array.from(src.entries()))
      }

      // ES6 Sets
      if (set && src instanceof set) {
        return new Set(Array.from(src.values()))
      }

      // Object
      if (src instanceof Object) {
        circulars.push(src);
        var obj = Object.create(src);
        clones.push(obj);
        for (var key in src) {
          var idx = circulars.findIndex(function (i) {
            return i === src[key]
          });
          obj[key] = idx > -1 ? clones[idx] : baseClone(src[key], circulars, clones);
        }
        return obj
      }

      // ???
      return src
    }

    function clone (src) {
      return baseClone(src, [], [])
    }

    const _loadFromGeoJson = function(filedata) {

      if (this._locked) {
        throw new Error('Cannot add GeoJSON to a contracted network');
      }

      if (this._geoJsonFlag) {
        throw new Error('Cannot load more than one GeoJSON file.');
      }

      if (this._manualAdd) {
        throw new Error('Cannot load GeoJSON file after adding Edges manually via the API.');
      }


      // make a copy
      const geo = clone(filedata);

      // cleans geojson (mutates in place)
      const features = this._cleanseGeoJsonNetwork(geo);

      features.forEach((feature, index) => {
        const coordinates = feature.geometry.coordinates;
        const properties = feature.properties;

        if (!properties || !coordinates || !properties._cost) {
          if (this.debugMode) {
            console.log('invalid feature detected.  skipping...');
          }
          return;
        }

        const start_vertex = coordinates[0];
        const end_vertex = coordinates[coordinates.length - 1];

        // add forward
        this._addEdge(start_vertex, end_vertex, properties, clone(coordinates));

        // add backward
        this._addEdge(end_vertex, start_vertex, properties, clone(coordinates).reverse());

      });

      // after loading a GeoJSON, no further edges can be added
      this._geoJsonFlag = true;


    };


    const _cleanseGeoJsonNetwork = function(file) {

      // get rid of duplicate edges (same origin to dest)
      const inventory = {};

      const features = file.features;

      features.forEach(feature => {
        const start = feature.geometry.coordinates[0].join(',');
        const end = feature.geometry.coordinates[feature.geometry.coordinates.length - 1].join(',');
        const id = `${start}|${end}`;

        const reverse_id = `${end}|${start}`;


        if (!inventory[id]) {
          // new segment
          inventory[id] = feature;
        }
        else {

          if (this.debugMode) {
            console.log('Duplicate feature found, choosing shortest.');
          }

          // a segment with the same origin/dest exists.  choose shortest.
          const old_cost = inventory[id].properties._cost;
          const new_cost = feature.properties._cost;
          if (new_cost < old_cost) {
            // mark old segment for deletion
            inventory[id].properties.__markDelete = true;
            // rewrite old segment because this one is shorter
            inventory[id] = feature;
          }
          else {
            // instead mark new feature for deletion
            feature.properties.__markDelete = true;
          }
        }


        // now reverse
        if (!inventory[reverse_id]) {
          // new segment
          inventory[reverse_id] = feature;
        }
        else {

          // In theory this error is already pointed out in the block above

          // a segment with the same origin/dest exists.  choose shortest.
          const old_cost = inventory[reverse_id].properties._cost;
          const new_cost = feature.properties._cost;
          if (new_cost < old_cost) {
            // mark old segment for deletion
            inventory[reverse_id].properties.__markDelete = true;
            // rewrite old segment because this one is shorter
            inventory[reverse_id] = feature;
          }
          else {
            // instead mark new feature for deletion
            feature.properties.__markDelete = true;
          }
        }
      });

      // filter out marked items
      return features.filter(feature => {
        return !feature.properties.__markDelete;
      });

    };

    // public API for adding edges
    const addEdge = function(start, end, edge_properties, edge_geometry, is_undirected) {

      if (this._locked) {
        throw new Error('Graph has been contracted.  No additional edges can be added.');
      }

      if (this._geoJsonFlag) {
        throw new Error('Can not add additional edges manually to a GeoJSON network.');
      }

      this._manualAdd = true;
      this._addEdge(start, end, edge_properties, edge_geometry, is_undirected);
    };

    const _addEdge = function(start, end, edge_properties, edge_geometry, is_undirected) {

      const start_node = String(start);
      const end_node = String(end);

      if (start_node === end_node) {
        if (this.debugMode) {
          console.log("Start and End Nodes are the same.  Ignoring.");
        }
        return;
      }

      if (this._nodeToIndexLookup[start_node] == null) {
        this._currentNodeIndex++;
        this._nodeToIndexLookup[start_node] = this._currentNodeIndex;
        this._indexToNodeLookup[this._currentNodeIndex] = start_node;
      }
      if (this._nodeToIndexLookup[end_node] == null) {
        this._currentNodeIndex++;
        this._nodeToIndexLookup[end_node] = this._currentNodeIndex;
        this._indexToNodeLookup[this._currentNodeIndex] = end_node;
      }

      let start_node_index = this._nodeToIndexLookup[start_node];
      let end_node_index = this._nodeToIndexLookup[end_node];

      // add to adjacency list
      this._currentEdgeIndex++;
      this._edgeProperties[this._currentEdgeIndex] = JSON.parse(JSON.stringify(edge_properties));
      this._edgeProperties[this._currentEdgeIndex]._start_index = start_node_index;
      this._edgeProperties[this._currentEdgeIndex]._end_index = end_node_index;

      if (edge_geometry) {
        this._edgeGeometry[this._currentEdgeIndex] = JSON.parse(JSON.stringify(edge_geometry));
      }

      // create object to push into adjacency list
      const obj = {
        end: end_node_index,
        cost: edge_properties._cost,
        attrs: this._currentEdgeIndex
      };

      if (this.adjacency_list[start_node_index]) {
        this.adjacency_list[start_node_index].push(obj);
      }
      else {
        this.adjacency_list[start_node_index] = [obj];
      }

      // add to reverse adjacency list
      const reverse_obj = {
        end: start_node_index,
        cost: edge_properties._cost,
        attrs: this._currentEdgeIndex
      };

      if (this.reverse_adjacency_list[end_node_index]) {
        this.reverse_adjacency_list[end_node_index].push(reverse_obj);
      }
      else {
        this.reverse_adjacency_list[end_node_index] = [reverse_obj];
      }


      // specifying is_undirected=true allows us to save space by not duplicating properties
      if (is_undirected) {
        if (this.adjacency_list[end_node_index]) {
          this.adjacency_list[end_node_index].push(reverse_obj);
        }
        else {
          this.adjacency_list[end_node_index] = [reverse_obj];
        }

        if (this.reverse_adjacency_list[start_node_index]) {
          this.reverse_adjacency_list[start_node_index].push(obj);
        }
        else {
          this.reverse_adjacency_list[start_node_index] = [obj];
        }
      }

    };


    const _addContractedEdge = function(start_index, end_index, properties) {

      // geometry not applicable here
      this._currentEdgeIndex++;
      this._edgeProperties[this._currentEdgeIndex] = properties;
      this._edgeProperties[this._currentEdgeIndex]._start_index = start_index;
      this._edgeProperties[this._currentEdgeIndex]._end_index = end_index;

      // create object to push into adjacency list
      const obj = {
        end: end_index,
        cost: properties._cost,
        attrs: this._currentEdgeIndex
      };

      if (this.adjacency_list[start_index]) {
        this.adjacency_list[start_index].push(obj);
      }
      else {
        this.adjacency_list[start_index] = [obj];
      }

      // add it to reverse adjacency list
      const reverse_obj = {
        end: start_index,
        cost: properties._cost,
        attrs: this._currentEdgeIndex
      };

      if (this.reverse_adjacency_list[end_index]) {
        this.reverse_adjacency_list[end_index].push(reverse_obj);
      }
      else {
        this.reverse_adjacency_list[end_index] = [reverse_obj];
      }

    };

    // ContractionHierarchy ========================================

    var ContractionHierarchy = {};

    ContractionHierarchy.read = function (pbf, end) {
        return pbf.readFields(ContractionHierarchy._readField, {_locked: false, _geoJsonFlag: false, adjacency_list: [], reverse_adjacency_list: [], _nodeToIndexLookup: {}, _edgeProperties: [], _edgeGeometry: []}, end);
    };
    ContractionHierarchy._readField = function (tag, obj, pbf) {
        if (tag === 1) obj._locked = pbf.readBoolean();
        else if (tag === 2) obj._geoJsonFlag = pbf.readBoolean();
        else if (tag === 3) obj.adjacency_list.push(ContractionHierarchy.AdjList.read(pbf, pbf.readVarint() + pbf.pos));
        else if (tag === 4) obj.reverse_adjacency_list.push(ContractionHierarchy.AdjList.read(pbf, pbf.readVarint() + pbf.pos));
        else if (tag === 5)  { var entry = ContractionHierarchy._FieldEntry5.read(pbf, pbf.readVarint() + pbf.pos); obj._nodeToIndexLookup[entry.key] = entry.value; }
        else if (tag === 6) obj._edgeProperties.push(pbf.readString());
        else if (tag === 7) obj._edgeGeometry.push(ContractionHierarchy.GeometryArray.read(pbf, pbf.readVarint() + pbf.pos));
    };
    ContractionHierarchy.write = function (obj, pbf) {
        if (obj._locked) pbf.writeBooleanField(1, obj._locked);
        if (obj._geoJsonFlag) pbf.writeBooleanField(2, obj._geoJsonFlag);
        if (obj.adjacency_list) for (var i = 0; i < obj.adjacency_list.length; i++) pbf.writeMessage(3, ContractionHierarchy.AdjList.write, obj.adjacency_list[i]);
        if (obj.reverse_adjacency_list) for (i = 0; i < obj.reverse_adjacency_list.length; i++) pbf.writeMessage(4, ContractionHierarchy.AdjList.write, obj.reverse_adjacency_list[i]);
        if (obj._nodeToIndexLookup) for (i in obj._nodeToIndexLookup) if (Object.prototype.hasOwnProperty.call(obj._nodeToIndexLookup, i)) pbf.writeMessage(5, ContractionHierarchy._FieldEntry5.write, { key: i, value: obj._nodeToIndexLookup[i] });
        if (obj._edgeProperties) for (i = 0; i < obj._edgeProperties.length; i++) pbf.writeStringField(6, obj._edgeProperties[i]);
        if (obj._edgeGeometry) for (i = 0; i < obj._edgeGeometry.length; i++) pbf.writeMessage(7, ContractionHierarchy.GeometryArray.write, obj._edgeGeometry[i]);
    };

    // ContractionHierarchy.EdgeAttrs ========================================

    ContractionHierarchy.EdgeAttrs = {};

    ContractionHierarchy.EdgeAttrs.read = function (pbf, end) {
        return pbf.readFields(ContractionHierarchy.EdgeAttrs._readField, {end: 0, cost: 0, attrs: 0}, end);
    };
    ContractionHierarchy.EdgeAttrs._readField = function (tag, obj, pbf) {
        if (tag === 1) obj.end = pbf.readVarint();
        else if (tag === 2) obj.cost = pbf.readDouble();
        else if (tag === 3) obj.attrs = pbf.readVarint();
    };
    ContractionHierarchy.EdgeAttrs.write = function (obj, pbf) {
        if (obj.end) pbf.writeVarintField(1, obj.end);
        if (obj.cost) pbf.writeDoubleField(2, obj.cost);
        if (obj.attrs) pbf.writeVarintField(3, obj.attrs);
    };

    // ContractionHierarchy.AdjList ========================================

    ContractionHierarchy.AdjList = {};

    ContractionHierarchy.AdjList.read = function (pbf, end) {
        return pbf.readFields(ContractionHierarchy.AdjList._readField, {edges: []}, end);
    };
    ContractionHierarchy.AdjList._readField = function (tag, obj, pbf) {
        if (tag === 1) obj.edges.push(ContractionHierarchy.EdgeAttrs.read(pbf, pbf.readVarint() + pbf.pos));
    };
    ContractionHierarchy.AdjList.write = function (obj, pbf) {
        if (obj.edges) for (var i = 0; i < obj.edges.length; i++) pbf.writeMessage(1, ContractionHierarchy.EdgeAttrs.write, obj.edges[i]);
    };

    // ContractionHierarchy.LineStringAray ========================================

    ContractionHierarchy.LineStringAray = {};

    ContractionHierarchy.LineStringAray.read = function (pbf, end) {
        return pbf.readFields(ContractionHierarchy.LineStringAray._readField, {coords: []}, end);
    };
    ContractionHierarchy.LineStringAray._readField = function (tag, obj, pbf) {
        if (tag === 1) pbf.readPackedDouble(obj.coords);
    };
    ContractionHierarchy.LineStringAray.write = function (obj, pbf) {
        if (obj.coords) pbf.writePackedDouble(1, obj.coords);
    };

    // ContractionHierarchy.GeometryArray ========================================

    ContractionHierarchy.GeometryArray = {};

    ContractionHierarchy.GeometryArray.read = function (pbf, end) {
        return pbf.readFields(ContractionHierarchy.GeometryArray._readField, {linestrings: []}, end);
    };
    ContractionHierarchy.GeometryArray._readField = function (tag, obj, pbf) {
        if (tag === 1) obj.linestrings.push(ContractionHierarchy.LineStringAray.read(pbf, pbf.readVarint() + pbf.pos));
    };
    ContractionHierarchy.GeometryArray.write = function (obj, pbf) {
        if (obj.linestrings) for (var i = 0; i < obj.linestrings.length; i++) pbf.writeMessage(1, ContractionHierarchy.LineStringAray.write, obj.linestrings[i]);
    };

    // ContractionHierarchy._FieldEntry5 ========================================

    ContractionHierarchy._FieldEntry5 = {};

    ContractionHierarchy._FieldEntry5.read = function (pbf, end) {
        return pbf.readFields(ContractionHierarchy._FieldEntry5._readField, {key: "", value: 0}, end);
    };
    ContractionHierarchy._FieldEntry5._readField = function (tag, obj, pbf) {
        if (tag === 1) obj.key = pbf.readString();
        else if (tag === 2) obj.value = pbf.readVarint();
    };
    ContractionHierarchy._FieldEntry5.write = function (obj, pbf) {
        if (obj.key) pbf.writeStringField(1, obj.key);
        if (obj.value) pbf.writeVarintField(2, obj.value);
    };

    function getDefaultExportFromCjs (x) {
    	return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
    }

    var ieee754$1 = {};

    /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */

    ieee754$1.read = function (buffer, offset, isLE, mLen, nBytes) {
      var e, m;
      var eLen = (nBytes * 8) - mLen - 1;
      var eMax = (1 << eLen) - 1;
      var eBias = eMax >> 1;
      var nBits = -7;
      var i = isLE ? (nBytes - 1) : 0;
      var d = isLE ? -1 : 1;
      var s = buffer[offset + i];

      i += d;

      e = s & ((1 << (-nBits)) - 1);
      s >>= (-nBits);
      nBits += eLen;
      for (; nBits > 0; e = (e * 256) + buffer[offset + i], i += d, nBits -= 8) {}

      m = e & ((1 << (-nBits)) - 1);
      e >>= (-nBits);
      nBits += mLen;
      for (; nBits > 0; m = (m * 256) + buffer[offset + i], i += d, nBits -= 8) {}

      if (e === 0) {
        e = 1 - eBias;
      } else if (e === eMax) {
        return m ? NaN : ((s ? -1 : 1) * Infinity)
      } else {
        m = m + Math.pow(2, mLen);
        e = e - eBias;
      }
      return (s ? -1 : 1) * m * Math.pow(2, e - mLen)
    };

    ieee754$1.write = function (buffer, value, offset, isLE, mLen, nBytes) {
      var e, m, c;
      var eLen = (nBytes * 8) - mLen - 1;
      var eMax = (1 << eLen) - 1;
      var eBias = eMax >> 1;
      var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0);
      var i = isLE ? 0 : (nBytes - 1);
      var d = isLE ? 1 : -1;
      var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0;

      value = Math.abs(value);

      if (isNaN(value) || value === Infinity) {
        m = isNaN(value) ? 1 : 0;
        e = eMax;
      } else {
        e = Math.floor(Math.log(value) / Math.LN2);
        if (value * (c = Math.pow(2, -e)) < 1) {
          e--;
          c *= 2;
        }
        if (e + eBias >= 1) {
          value += rt / c;
        } else {
          value += rt * Math.pow(2, 1 - eBias);
        }
        if (value * c >= 2) {
          e++;
          c /= 2;
        }

        if (e + eBias >= eMax) {
          m = 0;
          e = eMax;
        } else if (e + eBias >= 1) {
          m = ((value * c) - 1) * Math.pow(2, mLen);
          e = e + eBias;
        } else {
          m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen);
          e = 0;
        }
      }

      for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {}

      e = (e << mLen) | m;
      eLen += mLen;
      for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {}

      buffer[offset + i - d] |= s * 128;
    };

    var pbf = Pbf;

    var ieee754 = ieee754$1;

    function Pbf(buf) {
        this.buf = ArrayBuffer.isView && ArrayBuffer.isView(buf) ? buf : new Uint8Array(buf || 0);
        this.pos = 0;
        this.type = 0;
        this.length = this.buf.length;
    }

    Pbf.Varint  = 0; // varint: int32, int64, uint32, uint64, sint32, sint64, bool, enum
    Pbf.Fixed64 = 1; // 64-bit: double, fixed64, sfixed64
    Pbf.Bytes   = 2; // length-delimited: string, bytes, embedded messages, packed repeated fields
    Pbf.Fixed32 = 5; // 32-bit: float, fixed32, sfixed32

    var SHIFT_LEFT_32 = (1 << 16) * (1 << 16),
        SHIFT_RIGHT_32 = 1 / SHIFT_LEFT_32;

    // Threshold chosen based on both benchmarking and knowledge about browser string
    // data structures (which currently switch structure types at 12 bytes or more)
    var TEXT_DECODER_MIN_LENGTH = 12;
    var utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8');

    Pbf.prototype = {

        destroy: function() {
            this.buf = null;
        },

        // === READING =================================================================

        readFields: function(readField, result, end) {
            end = end || this.length;

            while (this.pos < end) {
                var val = this.readVarint(),
                    tag = val >> 3,
                    startPos = this.pos;

                this.type = val & 0x7;
                readField(tag, result, this);

                if (this.pos === startPos) this.skip(val);
            }
            return result;
        },

        readMessage: function(readField, result) {
            return this.readFields(readField, result, this.readVarint() + this.pos);
        },

        readFixed32: function() {
            var val = readUInt32(this.buf, this.pos);
            this.pos += 4;
            return val;
        },

        readSFixed32: function() {
            var val = readInt32(this.buf, this.pos);
            this.pos += 4;
            return val;
        },

        // 64-bit int handling is based on github.com/dpw/node-buffer-more-ints (MIT-licensed)

        readFixed64: function() {
            var val = readUInt32(this.buf, this.pos) + readUInt32(this.buf, this.pos + 4) * SHIFT_LEFT_32;
            this.pos += 8;
            return val;
        },

        readSFixed64: function() {
            var val = readUInt32(this.buf, this.pos) + readInt32(this.buf, this.pos + 4) * SHIFT_LEFT_32;
            this.pos += 8;
            return val;
        },

        readFloat: function() {
            var val = ieee754.read(this.buf, this.pos, true, 23, 4);
            this.pos += 4;
            return val;
        },

        readDouble: function() {
            var val = ieee754.read(this.buf, this.pos, true, 52, 8);
            this.pos += 8;
            return val;
        },

        readVarint: function(isSigned) {
            var buf = this.buf,
                val, b;

            b = buf[this.pos++]; val  =  b & 0x7f;        if (b < 0x80) return val;
            b = buf[this.pos++]; val |= (b & 0x7f) << 7;  if (b < 0x80) return val;
            b = buf[this.pos++]; val |= (b & 0x7f) << 14; if (b < 0x80) return val;
            b = buf[this.pos++]; val |= (b & 0x7f) << 21; if (b < 0x80) return val;
            b = buf[this.pos];   val |= (b & 0x0f) << 28;

            return readVarintRemainder(val, isSigned, this);
        },

        readVarint64: function() { // for compatibility with v2.0.1
            return this.readVarint(true);
        },

        readSVarint: function() {
            var num = this.readVarint();
            return num % 2 === 1 ? (num + 1) / -2 : num / 2; // zigzag encoding
        },

        readBoolean: function() {
            return Boolean(this.readVarint());
        },

        readString: function() {
            var end = this.readVarint() + this.pos;
            var pos = this.pos;
            this.pos = end;

            if (end - pos >= TEXT_DECODER_MIN_LENGTH && utf8TextDecoder) {
                // longer strings are fast with the built-in browser TextDecoder API
                return readUtf8TextDecoder(this.buf, pos, end);
            }
            // short strings are fast with our custom implementation
            return readUtf8(this.buf, pos, end);
        },

        readBytes: function() {
            var end = this.readVarint() + this.pos,
                buffer = this.buf.subarray(this.pos, end);
            this.pos = end;
            return buffer;
        },

        // verbose for performance reasons; doesn't affect gzipped size

        readPackedVarint: function(arr, isSigned) {
            if (this.type !== Pbf.Bytes) return arr.push(this.readVarint(isSigned));
            var end = readPackedEnd(this);
            arr = arr || [];
            while (this.pos < end) arr.push(this.readVarint(isSigned));
            return arr;
        },
        readPackedSVarint: function(arr) {
            if (this.type !== Pbf.Bytes) return arr.push(this.readSVarint());
            var end = readPackedEnd(this);
            arr = arr || [];
            while (this.pos < end) arr.push(this.readSVarint());
            return arr;
        },
        readPackedBoolean: function(arr) {
            if (this.type !== Pbf.Bytes) return arr.push(this.readBoolean());
            var end = readPackedEnd(this);
            arr = arr || [];
            while (this.pos < end) arr.push(this.readBoolean());
            return arr;
        },
        readPackedFloat: function(arr) {
            if (this.type !== Pbf.Bytes) return arr.push(this.readFloat());
            var end = readPackedEnd(this);
            arr = arr || [];
            while (this.pos < end) arr.push(this.readFloat());
            return arr;
        },
        readPackedDouble: function(arr) {
            if (this.type !== Pbf.Bytes) return arr.push(this.readDouble());
            var end = readPackedEnd(this);
            arr = arr || [];
            while (this.pos < end) arr.push(this.readDouble());
            return arr;
        },
        readPackedFixed32: function(arr) {
            if (this.type !== Pbf.Bytes) return arr.push(this.readFixed32());
            var end = readPackedEnd(this);
            arr = arr || [];
            while (this.pos < end) arr.push(this.readFixed32());
            return arr;
        },
        readPackedSFixed32: function(arr) {
            if (this.type !== Pbf.Bytes) return arr.push(this.readSFixed32());
            var end = readPackedEnd(this);
            arr = arr || [];
            while (this.pos < end) arr.push(this.readSFixed32());
            return arr;
        },
        readPackedFixed64: function(arr) {
            if (this.type !== Pbf.Bytes) return arr.push(this.readFixed64());
            var end = readPackedEnd(this);
            arr = arr || [];
            while (this.pos < end) arr.push(this.readFixed64());
            return arr;
        },
        readPackedSFixed64: function(arr) {
            if (this.type !== Pbf.Bytes) return arr.push(this.readSFixed64());
            var end = readPackedEnd(this);
            arr = arr || [];
            while (this.pos < end) arr.push(this.readSFixed64());
            return arr;
        },

        skip: function(val) {
            var type = val & 0x7;
            if (type === Pbf.Varint) while (this.buf[this.pos++] > 0x7f) {}
            else if (type === Pbf.Bytes) this.pos = this.readVarint() + this.pos;
            else if (type === Pbf.Fixed32) this.pos += 4;
            else if (type === Pbf.Fixed64) this.pos += 8;
            else throw new Error('Unimplemented type: ' + type);
        },

        // === WRITING =================================================================

        writeTag: function(tag, type) {
            this.writeVarint((tag << 3) | type);
        },

        realloc: function(min) {
            var length = this.length || 16;

            while (length < this.pos + min) length *= 2;

            if (length !== this.length) {
                var buf = new Uint8Array(length);
                buf.set(this.buf);
                this.buf = buf;
                this.length = length;
            }
        },

        finish: function() {
            this.length = this.pos;
            this.pos = 0;
            return this.buf.subarray(0, this.length);
        },

        writeFixed32: function(val) {
            this.realloc(4);
            writeInt32(this.buf, val, this.pos);
            this.pos += 4;
        },

        writeSFixed32: function(val) {
            this.realloc(4);
            writeInt32(this.buf, val, this.pos);
            this.pos += 4;
        },

        writeFixed64: function(val) {
            this.realloc(8);
            writeInt32(this.buf, val & -1, this.pos);
            writeInt32(this.buf, Math.floor(val * SHIFT_RIGHT_32), this.pos + 4);
            this.pos += 8;
        },

        writeSFixed64: function(val) {
            this.realloc(8);
            writeInt32(this.buf, val & -1, this.pos);
            writeInt32(this.buf, Math.floor(val * SHIFT_RIGHT_32), this.pos + 4);
            this.pos += 8;
        },

        writeVarint: function(val) {
            val = +val || 0;

            if (val > 0xfffffff || val < 0) {
                writeBigVarint(val, this);
                return;
            }

            this.realloc(4);

            this.buf[this.pos++] =           val & 0x7f  | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
            this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
            this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
            this.buf[this.pos++] =   (val >>> 7) & 0x7f;
        },

        writeSVarint: function(val) {
            this.writeVarint(val < 0 ? -val * 2 - 1 : val * 2);
        },

        writeBoolean: function(val) {
            this.writeVarint(Boolean(val));
        },

        writeString: function(str) {
            str = String(str);
            this.realloc(str.length * 4);

            this.pos++; // reserve 1 byte for short string length

            var startPos = this.pos;
            // write the string directly to the buffer and see how much was written
            this.pos = writeUtf8(this.buf, str, this.pos);
            var len = this.pos - startPos;

            if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);

            // finally, write the message length in the reserved place and restore the position
            this.pos = startPos - 1;
            this.writeVarint(len);
            this.pos += len;
        },

        writeFloat: function(val) {
            this.realloc(4);
            ieee754.write(this.buf, val, this.pos, true, 23, 4);
            this.pos += 4;
        },

        writeDouble: function(val) {
            this.realloc(8);
            ieee754.write(this.buf, val, this.pos, true, 52, 8);
            this.pos += 8;
        },

        writeBytes: function(buffer) {
            var len = buffer.length;
            this.writeVarint(len);
            this.realloc(len);
            for (var i = 0; i < len; i++) this.buf[this.pos++] = buffer[i];
        },

        writeRawMessage: function(fn, obj) {
            this.pos++; // reserve 1 byte for short message length

            // write the message directly to the buffer and see how much was written
            var startPos = this.pos;
            fn(obj, this);
            var len = this.pos - startPos;

            if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);

            // finally, write the message length in the reserved place and restore the position
            this.pos = startPos - 1;
            this.writeVarint(len);
            this.pos += len;
        },

        writeMessage: function(tag, fn, obj) {
            this.writeTag(tag, Pbf.Bytes);
            this.writeRawMessage(fn, obj);
        },

        writePackedVarint:   function(tag, arr) { if (arr.length) this.writeMessage(tag, writePackedVarint, arr);   },
        writePackedSVarint:  function(tag, arr) { if (arr.length) this.writeMessage(tag, writePackedSVarint, arr);  },
        writePackedBoolean:  function(tag, arr) { if (arr.length) this.writeMessage(tag, writePackedBoolean, arr);  },
        writePackedFloat:    function(tag, arr) { if (arr.length) this.writeMessage(tag, writePackedFloat, arr);    },
        writePackedDouble:   function(tag, arr) { if (arr.length) this.writeMessage(tag, writePackedDouble, arr);   },
        writePackedFixed32:  function(tag, arr) { if (arr.length) this.writeMessage(tag, writePackedFixed32, arr);  },
        writePackedSFixed32: function(tag, arr) { if (arr.length) this.writeMessage(tag, writePackedSFixed32, arr); },
        writePackedFixed64:  function(tag, arr) { if (arr.length) this.writeMessage(tag, writePackedFixed64, arr);  },
        writePackedSFixed64: function(tag, arr) { if (arr.length) this.writeMessage(tag, writePackedSFixed64, arr); },

        writeBytesField: function(tag, buffer) {
            this.writeTag(tag, Pbf.Bytes);
            this.writeBytes(buffer);
        },
        writeFixed32Field: function(tag, val) {
            this.writeTag(tag, Pbf.Fixed32);
            this.writeFixed32(val);
        },
        writeSFixed32Field: function(tag, val) {
            this.writeTag(tag, Pbf.Fixed32);
            this.writeSFixed32(val);
        },
        writeFixed64Field: function(tag, val) {
            this.writeTag(tag, Pbf.Fixed64);
            this.writeFixed64(val);
        },
        writeSFixed64Field: function(tag, val) {
            this.writeTag(tag, Pbf.Fixed64);
            this.writeSFixed64(val);
        },
        writeVarintField: function(tag, val) {
            this.writeTag(tag, Pbf.Varint);
            this.writeVarint(val);
        },
        writeSVarintField: function(tag, val) {
            this.writeTag(tag, Pbf.Varint);
            this.writeSVarint(val);
        },
        writeStringField: function(tag, str) {
            this.writeTag(tag, Pbf.Bytes);
            this.writeString(str);
        },
        writeFloatField: function(tag, val) {
            this.writeTag(tag, Pbf.Fixed32);
            this.writeFloat(val);
        },
        writeDoubleField: function(tag, val) {
            this.writeTag(tag, Pbf.Fixed64);
            this.writeDouble(val);
        },
        writeBooleanField: function(tag, val) {
            this.writeVarintField(tag, Boolean(val));
        }
    };

    function readVarintRemainder(l, s, p) {
        var buf = p.buf,
            h, b;

        b = buf[p.pos++]; h  = (b & 0x70) >> 4;  if (b < 0x80) return toNum(l, h, s);
        b = buf[p.pos++]; h |= (b & 0x7f) << 3;  if (b < 0x80) return toNum(l, h, s);
        b = buf[p.pos++]; h |= (b & 0x7f) << 10; if (b < 0x80) return toNum(l, h, s);
        b = buf[p.pos++]; h |= (b & 0x7f) << 17; if (b < 0x80) return toNum(l, h, s);
        b = buf[p.pos++]; h |= (b & 0x7f) << 24; if (b < 0x80) return toNum(l, h, s);
        b = buf[p.pos++]; h |= (b & 0x01) << 31; if (b < 0x80) return toNum(l, h, s);

        throw new Error('Expected varint not more than 10 bytes');
    }

    function readPackedEnd(pbf) {
        return pbf.type === Pbf.Bytes ?
            pbf.readVarint() + pbf.pos : pbf.pos + 1;
    }

    function toNum(low, high, isSigned) {
        if (isSigned) {
            return high * 0x100000000 + (low >>> 0);
        }

        return ((high >>> 0) * 0x100000000) + (low >>> 0);
    }

    function writeBigVarint(val, pbf) {
        var low, high;

        if (val >= 0) {
            low  = (val % 0x100000000) | 0;
            high = (val / 0x100000000) | 0;
        } else {
            low  = ~(-val % 0x100000000);
            high = ~(-val / 0x100000000);

            if (low ^ 0xffffffff) {
                low = (low + 1) | 0;
            } else {
                low = 0;
                high = (high + 1) | 0;
            }
        }

        if (val >= 0x10000000000000000 || val < -18446744073709552e3) {
            throw new Error('Given varint doesn\'t fit into 10 bytes');
        }

        pbf.realloc(10);

        writeBigVarintLow(low, high, pbf);
        writeBigVarintHigh(high, pbf);
    }

    function writeBigVarintLow(low, high, pbf) {
        pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
        pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
        pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
        pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
        pbf.buf[pbf.pos]   = low & 0x7f;
    }

    function writeBigVarintHigh(high, pbf) {
        var lsb = (high & 0x07) << 4;

        pbf.buf[pbf.pos++] |= lsb         | ((high >>>= 3) ? 0x80 : 0); if (!high) return;
        pbf.buf[pbf.pos++]  = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
        pbf.buf[pbf.pos++]  = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
        pbf.buf[pbf.pos++]  = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
        pbf.buf[pbf.pos++]  = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
        pbf.buf[pbf.pos++]  = high & 0x7f;
    }

    function makeRoomForExtraLength(startPos, len, pbf) {
        var extraLen =
            len <= 0x3fff ? 1 :
            len <= 0x1fffff ? 2 :
            len <= 0xfffffff ? 3 : Math.floor(Math.log(len) / (Math.LN2 * 7));

        // if 1 byte isn't enough for encoding message length, shift the data to the right
        pbf.realloc(extraLen);
        for (var i = pbf.pos - 1; i >= startPos; i--) pbf.buf[i + extraLen] = pbf.buf[i];
    }

    function writePackedVarint(arr, pbf)   { for (var i = 0; i < arr.length; i++) pbf.writeVarint(arr[i]);   }
    function writePackedSVarint(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeSVarint(arr[i]);  }
    function writePackedFloat(arr, pbf)    { for (var i = 0; i < arr.length; i++) pbf.writeFloat(arr[i]);    }
    function writePackedDouble(arr, pbf)   { for (var i = 0; i < arr.length; i++) pbf.writeDouble(arr[i]);   }
    function writePackedBoolean(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeBoolean(arr[i]);  }
    function writePackedFixed32(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeFixed32(arr[i]);  }
    function writePackedSFixed32(arr, pbf) { for (var i = 0; i < arr.length; i++) pbf.writeSFixed32(arr[i]); }
    function writePackedFixed64(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeFixed64(arr[i]);  }
    function writePackedSFixed64(arr, pbf) { for (var i = 0; i < arr.length; i++) pbf.writeSFixed64(arr[i]); }

    // Buffer code below from https://github.com/feross/buffer, MIT-licensed

    function readUInt32(buf, pos) {
        return ((buf[pos]) |
            (buf[pos + 1] << 8) |
            (buf[pos + 2] << 16)) +
            (buf[pos + 3] * 0x1000000);
    }

    function writeInt32(buf, val, pos) {
        buf[pos] = val;
        buf[pos + 1] = (val >>> 8);
        buf[pos + 2] = (val >>> 16);
        buf[pos + 3] = (val >>> 24);
    }

    function readInt32(buf, pos) {
        return ((buf[pos]) |
            (buf[pos + 1] << 8) |
            (buf[pos + 2] << 16)) +
            (buf[pos + 3] << 24);
    }

    function readUtf8(buf, pos, end) {
        var str = '';
        var i = pos;

        while (i < end) {
            var b0 = buf[i];
            var c = null; // codepoint
            var bytesPerSequence =
                b0 > 0xEF ? 4 :
                b0 > 0xDF ? 3 :
                b0 > 0xBF ? 2 : 1;

            if (i + bytesPerSequence > end) break;

            var b1, b2, b3;

            if (bytesPerSequence === 1) {
                if (b0 < 0x80) {
                    c = b0;
                }
            } else if (bytesPerSequence === 2) {
                b1 = buf[i + 1];
                if ((b1 & 0xC0) === 0x80) {
                    c = (b0 & 0x1F) << 0x6 | (b1 & 0x3F);
                    if (c <= 0x7F) {
                        c = null;
                    }
                }
            } else if (bytesPerSequence === 3) {
                b1 = buf[i + 1];
                b2 = buf[i + 2];
                if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80) {
                    c = (b0 & 0xF) << 0xC | (b1 & 0x3F) << 0x6 | (b2 & 0x3F);
                    if (c <= 0x7FF || (c >= 0xD800 && c <= 0xDFFF)) {
                        c = null;
                    }
                }
            } else if (bytesPerSequence === 4) {
                b1 = buf[i + 1];
                b2 = buf[i + 2];
                b3 = buf[i + 3];
                if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80 && (b3 & 0xC0) === 0x80) {
                    c = (b0 & 0xF) << 0x12 | (b1 & 0x3F) << 0xC | (b2 & 0x3F) << 0x6 | (b3 & 0x3F);
                    if (c <= 0xFFFF || c >= 0x110000) {
                        c = null;
                    }
                }
            }

            if (c === null) {
                c = 0xFFFD;
                bytesPerSequence = 1;

            } else if (c > 0xFFFF) {
                c -= 0x10000;
                str += String.fromCharCode(c >>> 10 & 0x3FF | 0xD800);
                c = 0xDC00 | c & 0x3FF;
            }

            str += String.fromCharCode(c);
            i += bytesPerSequence;
        }

        return str;
    }

    function readUtf8TextDecoder(buf, pos, end) {
        return utf8TextDecoder.decode(buf.subarray(pos, end));
    }

    function writeUtf8(buf, str, pos) {
        for (var i = 0, c, lead; i < str.length; i++) {
            c = str.charCodeAt(i); // code point

            if (c > 0xD7FF && c < 0xE000) {
                if (lead) {
                    if (c < 0xDC00) {
                        buf[pos++] = 0xEF;
                        buf[pos++] = 0xBF;
                        buf[pos++] = 0xBD;
                        lead = c;
                        continue;
                    } else {
                        c = lead - 0xD800 << 10 | c - 0xDC00 | 0x10000;
                        lead = null;
                    }
                } else {
                    if (c > 0xDBFF || (i + 1 === str.length)) {
                        buf[pos++] = 0xEF;
                        buf[pos++] = 0xBF;
                        buf[pos++] = 0xBD;
                    } else {
                        lead = c;
                    }
                    continue;
                }
            } else if (lead) {
                buf[pos++] = 0xEF;
                buf[pos++] = 0xBF;
                buf[pos++] = 0xBD;
                lead = null;
            }

            if (c < 0x80) {
                buf[pos++] = c;
            } else {
                if (c < 0x800) {
                    buf[pos++] = c >> 0x6 | 0xC0;
                } else {
                    if (c < 0x10000) {
                        buf[pos++] = c >> 0xC | 0xE0;
                    } else {
                        buf[pos++] = c >> 0x12 | 0xF0;
                        buf[pos++] = c >> 0xC & 0x3F | 0x80;
                    }
                    buf[pos++] = c >> 0x6 & 0x3F | 0x80;
                }
                buf[pos++] = c & 0x3F | 0x80;
            }
        }
        return pos;
    }

    var Pbf$1 = /*@__PURE__*/getDefaultExportFromCjs(pbf);

    const loadCH = function(ch) {
      const parsed = (typeof ch === 'object') ? ch : JSON.parse(ch);
      this._locked = parsed._locked;
      this._geoJsonFlag = parsed._geoJsonFlag;
      this.adjacency_list = parsed.adjacency_list;
      this.reverse_adjacency_list = parsed.reverse_adjacency_list;
      this._nodeToIndexLookup = parsed._nodeToIndexLookup;
      this._edgeProperties = parsed._edgeProperties;
      this._edgeGeometry = parsed._edgeGeometry;
    };

    const saveCH = function() {

      if (!this._locked) {
        throw new Error('No sense in saving network before it is contracted.');
      }

      return JSON.stringify({
        _locked: this._locked,
        _geoJsonFlag: this._geoJsonFlag,
        adjacency_list: this.adjacency_list,
        reverse_adjacency_list: this.reverse_adjacency_list,
        _nodeToIndexLookup: this._nodeToIndexLookup,
        _edgeProperties: this._edgeProperties,
        _edgeGeometry: this._edgeGeometry
      });
    };


    const loadPbfCH = function(buffer) {

      var readpbf = new Pbf$1(buffer);
      var obj = ContractionHierarchy.read(readpbf);

      // back to graph compatible structure
      obj.adjacency_list = obj.adjacency_list.map(list => {
        return list.edges;
      });

      obj.reverse_adjacency_list = obj.reverse_adjacency_list.map(list => {
        return list.edges;
      });

      obj._edgeGeometry = obj._edgeGeometry.map(l => {
        return l.linestrings.map(c => {
          return c.coords;
        });
      });

      obj._edgeProperties = obj._edgeProperties.map(props => {
        return JSON.parse(props);
      });


      this._locked = obj._locked;
      this._geoJsonFlag = obj._geoJsonFlag;
      this.adjacency_list = obj.adjacency_list;
      this.reverse_adjacency_list = obj.reverse_adjacency_list;
      this._nodeToIndexLookup = obj._nodeToIndexLookup;
      this._edgeProperties = obj._edgeProperties; // TODO... misc user properties
      this._edgeGeometry = obj._edgeGeometry;

      // Rebuild _indexToNodeLookup from _nodeToIndexLookup since it's not serialized in PBF
      this._indexToNodeLookup = {};
      for (const [node, index] of Object.entries(this._nodeToIndexLookup)) {
        this._indexToNodeLookup[index] = node;
      }

      console.log(`done loading pbf`);

    };

    const savePbfCH = async function(path) {

      if (!this._locked) {
        throw new Error('No sense in saving network before it is contracted.');
      }

      // Check if we're in Node.js environment
      let fs;
      try {
        // Use dynamic import for ES modules
        fs = await import('fs');
      } catch (e) {
        console.log('saving as PBF only works in NodeJS');
        return;
      }

      const data = {
        _locked: this._locked,
        _geoJsonFlag: this._geoJsonFlag,
        adjacency_list: this.adjacency_list,
        reverse_adjacency_list: this.reverse_adjacency_list,
        _nodeToIndexLookup: this._nodeToIndexLookup,
        _edgeProperties: this._edgeProperties,
        _edgeGeometry: this._edgeGeometry
      };

      // convert to protobuf compatible

      data.adjacency_list = data.adjacency_list.map(list => {
        return {
          edges: list.map(edge => {
            return edge;
          })
        };
      });

      data.reverse_adjacency_list = data.reverse_adjacency_list.map(list => {
        return {
          edges: list.map(edge => {
            return edge;
          })
        };
      });

      data._edgeGeometry = data._edgeGeometry.map(linestring => {
        return {
          linestrings: linestring.map(coords => {
            return { coords };
          })
        };
      });

      // a poor solution.  seek a better way to serialize arbitrary properties
      data._edgeProperties = data._edgeProperties.map(props => {
        return JSON.stringify(props);
      });

      // write
      var pbf = new Pbf$1();
      ContractionHierarchy.write(data, pbf);

      var buffer = pbf.finish();

      fs.writeFileSync(path, buffer);

      console.log(`done saving ${path}`);

    };

    function Node(node) {
      this.id = node.id;
      this.dist = node.dist !== undefined ? node.dist : Infinity;
      this.prev = undefined;
      this.visited = undefined;
      this.opened = false; // whether has been put in queue
      this.heapIndex = -1;
    }

    function createNodePool() {
      var currentInCache = 0;
      var nodeCache = [];

      return {
        createNewState: createNewState,
        reset: reset
      };

      function reset() {
        currentInCache = 0;
      }

      function createNewState(node) {
        var cached = nodeCache[currentInCache];
        if (cached) {
          cached.id = node.id;
          cached.dist = node.dist !== undefined ? node.dist : Infinity;
          cached.prev = undefined;
          cached.visited = undefined;
          cached.opened = false;
          cached.heapIndex = -1;
        }
        else {
          cached = new Node(node);
          nodeCache[currentInCache] = cached;
        }
        currentInCache++;
        return cached;
      }

    }

    const contractGraph = function() {

      if (this._locked) {
        throw new Error('Network has already been contracted');
      }

      // prevent more edges from being added
      this._locked = true;

      // new contracted edges will be added after this index
      this._maxUncontractedEdgeIndex = this._currentEdgeIndex;

      // initialize dijkstra shortcut/path finder
      const finder = this._createChShortcutter();

      const getVertexScore = (v) => {
        const shortcut_count = this._contract(v, true, finder); /**/
        const edge_count = (this.adjacency_list[v] || []).length;
        const edge_difference = shortcut_count - edge_count;
        const contracted_neighbors = getContractedNeighborCount(v);
        return edge_difference + contracted_neighbors;
      };

      const getContractedNeighborCount = (v) => {
        return (this.adjacency_list[v] || []).reduce((acc, node) => {
          const is_contracted = this.contracted_nodes[node.end] != null ? 1 : 0;
          return acc + is_contracted;
        }, 0);
      };

      const nh = new NodeHeap({
        compare(a, b) {
          return a.score - b.score;
        }
      });

      this.contracted_nodes = [];

      // create an additional node ordering
      Object.keys(this._nodeToIndexLookup).forEach(key => {
        const index = this._nodeToIndexLookup[key];
        const score = getVertexScore(index);
        const node = new OrderNode(score, index);
        nh.push(node);
      });

      let contraction_level = 1;

      const len = nh.length;

      // main contraction loop
      while (nh.length > 0) {

        const updated_len = nh.length;

        if (updated_len % 50 === 0) {
          if (this.debugMode) {
            console.log(updated_len / len);
          }
          // prune adj list of no longer valid paths occasionally
          // theres probably a better formula for determining how often this should run
          // (bigger networks = less often)
          this._cleanAdjList(this.adjacency_list);
          this._cleanAdjList(this.reverse_adjacency_list);
        }

        // recompute to make sure that first node in priority queue
        // is still best candidate to contract
        let found_lowest = false;
        let node_obj = nh.peek();
        const old_score = node_obj.score;

        do {
          const first_vertex = node_obj.id;
          const new_score = getVertexScore(first_vertex);

          if (new_score > old_score) {
            node_obj.score = new_score;
            nh.updateItem(node_obj.heapIndex);
          }
          node_obj = nh.peek();
          if (node_obj.id === first_vertex) {
            found_lowest = true;
          }

        } while (found_lowest === false);

        // lowest found, pop it off the queue and contract it
        const v = nh.pop();

        this._contract(v.id, false, finder);

        // keep a record of contraction level of each node
        this.contracted_nodes[v.id] = contraction_level;
        contraction_level++;

      }

      this._cleanAdjList(this.adjacency_list);
      this._cleanAdjList(this.reverse_adjacency_list);
      this._arrangeContractedPaths(this.adjacency_list);
      this._arrangeContractedPaths(this.reverse_adjacency_list);

      if (this.debugMode) {
        console.log('Contraction complete');
      }

      return;

    };

    // do as much edge arrangement as possible ahead of times so that the cost is
    // not incurred at runtime
    const _arrangeContractedPaths = function(adj_list) {

      adj_list.forEach((node, index) => {

        node.forEach(edge => {

          const start_node = index;

          let simpleIds = [];
          let ids = [];

          ids = [edge.attrs]; // edge.attrs is an edge ID

          while (ids.length) {
            const id = ids.pop();
            if (id <= this._maxUncontractedEdgeIndex) {
              // this is an original network edge
              simpleIds.push(id);
            }
            else {
              // these are shorcut edges (added during contraction process)
              // where _id is an array of two items: edges of [u to v, v to w]
              ids.push(...this._edgeProperties[id]._id);
            }
          }


          //  now with simpleIds, get start and end index and make connection object
          const links = {};
          simpleIds.forEach(id => {
            const properties = this._edgeProperties[id];
            const start_index = properties._start_index;
            const end_index = properties._end_index;

            if (!links[start_index]) {
              links[start_index] = [id];
            }
            else {
              links[start_index].push(id);
            }

            if (!links[end_index]) {
              links[end_index] = [id];
            }
            else {
              links[end_index].push(id);
            }
          });

          const ordered = [];

          let last_node = String(start_node);

          let current_edge_id = links[last_node][0];
          // this value represents the attribute id of the first segment

          while (current_edge_id != null) {

            ordered.push(current_edge_id);
            // put this in the ordered array of attribute segments

            // this represents the nodes of the first segment
            const props = this._edgeProperties[current_edge_id];
            const c1 = String(props._start_index);
            const c2 = String(props._end_index);

            // c1 and c2 represent the first and last nodes of the line string
            // these nodes can be out of order; in fact 50% chance
            // so check to see if the first node = start
            // if it is, use c2, if not, use c1
            const next_node = c1 === last_node ? c2 : c1;

            last_node = next_node;

            const arr = links[next_node];
            // receive an array of 2 attribute segments.  
            // we've already seen one of them, so grab the other

            if (arr.length === 1) {
              // if the length of this is 1, it means we're at the end
              break;
            }

            if (arr.length > 2) {
              console.error('too many edges in array. unexpected. unrecoverable.');
              process.exit();
            }

            current_edge_id = arr[0] === current_edge_id ? arr[1] : arr[0];
          }

          this._edgeProperties[edge.attrs]._ordered = ordered;

        });
      });

    };

    const _cleanAdjList = function(adj_list) {

      // remove links to lower ranked nodes
      adj_list.forEach((node, node_id) => {
        const from_rank = this.contracted_nodes[node_id];
        if (from_rank == null) {
          return;
        }
        adj_list[node_id] = adj_list[node_id].filter(
          edge => {
            const to_rank = this.contracted_nodes[edge.end];
            if (to_rank == null) {
              return true;
            }
            return from_rank < to_rank;
          }
        );
      });

    };

    // this function is multi-use:  actually contract a node  OR
    // with `get_count_only = true` find number of shortcuts added
    // if node were to be contracted
    const _contract = function(v, get_count_only, finder) {

      // all edges from anywhere to v
      const from_connections = (this.reverse_adjacency_list[v] || []).filter(c => {
        return !this.contracted_nodes[c.end];
      });


      // all edges from v to somewhere else
      const to_connections = (this.adjacency_list[v] || []).filter(c => {
        return !this.contracted_nodes[c.end];
      });

      let shortcut_count = 0;

      from_connections.forEach(u => {

        let max_total = 0;

        // dist u to v
        const dist1 = u.cost;

        to_connections.forEach(w => {

          // ignore node to itself
          if (u.end === w.end) {
            return;
          }

          // dist v to w
          const dist2 = w.cost;

          const total = dist1 + dist2;

          if (total > max_total) {
            max_total = total;
          }
        });


        if (!to_connections.length) {
          // no sense in running dijkstra
          return;
        }

        // run a dijkstra from u to anything less than the existing dijkstra distance
        const path = finder.runDijkstra(
          u.end,
          null,
          v,
          max_total
        );

        to_connections.forEach(w => {
          if (u.end === w.end) {
            return;
          }

          // dist v to w
          const dist2 = w.cost;
          const total = dist1 + dist2;

          const dijkstra = path.distances[w.end] || Infinity;

          if (total < dijkstra) {

            shortcut_count++;

            if (!get_count_only) {

              const props = {
                _cost: total,
                _id: [u.attrs, w.attrs],
                _start_index: u.end,
                _end_index: w.end
              };

              this._addContractedEdge(u.end, w.end, props);
            }
          }
        });

      });

      return shortcut_count;
    };




    // node containing contraction order score
    function OrderNode(score, id) {
      this.score = score;
      this.id = id;
    }

    const _createChShortcutter = function() {

      const pool = this._createNodePool();
      const adjacency_list = this.adjacency_list;

      return {
        runDijkstra
      };

      function runDijkstra(
        start_index,
        end_index,
        vertex,
        total
      ) {

        pool.reset();

        const nodeState = [];
        const distances = {};

        var openSet = new NodeHeap({
          compare(a, b) {
            return a.dist - b.dist;
          }
        });

        let current = pool.createNewState({ id: start_index, dist: 0 });
        nodeState[start_index] = current;
        current.opened = 1;
        distances[current.id] = 0;

        // quick exit for start === end	
        if (start_index === end_index) {
          current = '';
        }

        while (current) {

          (adjacency_list[current.id] || [])
          .filter(edge => {
              // this is a modification for contraction hierarchy
              // otherwise vertex===undefined
              return edge.end !== vertex;
            })
            .forEach(edge => {

              let node = nodeState[edge.end];
              if (node === undefined) {
                node = pool.createNewState({ id: edge.end });
                nodeState[edge.end] = node;
              }

              if (node.visited === true) {
                return;
              }


              if (!node.opened) {
                openSet.push(node);
                node.opened = true;
              }

              const proposed_distance = current.dist + edge.cost;
              if (proposed_distance >= node.dist) {
                return;
              }

              node.dist = proposed_distance;
              distances[node.id] = proposed_distance;
              node.prev = current.id;

              openSet.updateItem(node.heapIndex);
            });

          current.visited = true;
          const settled_amt = current.dist;

          // get lowest value from heap
          current = openSet.pop();

          // exit early if current node becomes end node
          if (current && (current.id === end_index)) {
            current = '';
          }

          // stopping condition
          if (settled_amt > total) {
            current = '';
          }
        }

        return { distances, nodeState };

      }

    };

    const CoordinateLookup = CoordinateLookup$1;

    // backdoor to export spatial indexing for custom solutions
    const __geoindex = __geoindex$1;
    const __kdindex = __kdindex$1;

    function Graph(geojson, opt) {
      const options = opt || {};
      this.debugMode = options.debugMode || false;

      this.adjacency_list = [];
      this.reverse_adjacency_list = [];

      this._createNodePool = createNodePool;

      this._currentNodeIndex = -1;
      this._nodeToIndexLookup = {};
      this._indexToNodeLookup = {};

      this._currentEdgeIndex = -1;
      this._edgeProperties = [];
      this._edgeGeometry = [];
      this._maxUncontractedEdgeIndex = 0;

      this._locked = false; // locked if contraction has already been run
      this._geoJsonFlag = false; // if data was loaded as geoJson
      this._manualAdd = false; // if the API was used directly to add edges

      if (geojson) {
        this._loadFromGeoJson(geojson);

        if (this.debugMode) {
          console.log('Nodes: ', this._currentNodeIndex);
          console.log('Edges: ', this._currentEdgeIndex);
        }
      }

    }

    Graph.prototype.createPathfinder = createPathfinder;

    Graph.prototype._loadFromGeoJson = _loadFromGeoJson;
    Graph.prototype._cleanseGeoJsonNetwork = _cleanseGeoJsonNetwork;

    Graph.prototype._addContractedEdge = _addContractedEdge;
    Graph.prototype.addEdge = addEdge;
    Graph.prototype._addEdge = _addEdge;

    Graph.prototype.loadCH = loadCH;
    Graph.prototype.saveCH = saveCH;
    Graph.prototype.loadPbfCH = loadPbfCH;
    Graph.prototype.savePbfCH = savePbfCH;

    Graph.prototype.contractGraph = contractGraph;
    Graph.prototype._arrangeContractedPaths = _arrangeContractedPaths;
    Graph.prototype._cleanAdjList = _cleanAdjList;
    Graph.prototype._contract = _contract;
    Graph.prototype._createChShortcutter = _createChShortcutter;

    exports.CoordinateLookup = CoordinateLookup;
    exports.Graph = Graph;
    exports.__geoindex = __geoindex;
    exports.__kdindex = __kdindex;

}));