// https://github.com/topojson/topojson-server v3.0.1 Copyright 2019 Mike Bostock (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.topojson = global.topojson || {})); }(this, function (exports) { 'use strict'; var hasOwnProperty = Object.prototype.hasOwnProperty; // Computes the bounding box of the specified hash of GeoJSON objects. function bounds(objects) { var x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity; function boundGeometry(geometry) { if (geometry != null && hasOwnProperty.call(boundGeometryType, geometry.type)) boundGeometryType[geometry.type](geometry); } var boundGeometryType = { GeometryCollection: function(o) { o.geometries.forEach(boundGeometry); }, Point: function(o) { boundPoint(o.coordinates); }, MultiPoint: function(o) { o.coordinates.forEach(boundPoint); }, LineString: function(o) { boundLine(o.arcs); }, MultiLineString: function(o) { o.arcs.forEach(boundLine); }, Polygon: function(o) { o.arcs.forEach(boundLine); }, MultiPolygon: function(o) { o.arcs.forEach(boundMultiLine); } }; function boundPoint(coordinates) { var x = coordinates[0], y = coordinates[1]; if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; } function boundLine(coordinates) { coordinates.forEach(boundPoint); } function boundMultiLine(coordinates) { coordinates.forEach(boundLine); } for (var key in objects) { boundGeometry(objects[key]); } return x1 >= x0 && y1 >= y0 ? [x0, y0, x1, y1] : undefined; } function hashset(size, hash, equal, type, empty) { if (arguments.length === 3) { type = Array; empty = null; } var store = new type(size = 1 << Math.max(4, Math.ceil(Math.log(size) / Math.LN2))), mask = size - 1; for (var i = 0; i < size; ++i) { store[i] = empty; } function add(value) { var index = hash(value) & mask, match = store[index], collisions = 0; while (match != empty) { if (equal(match, value)) return true; if (++collisions >= size) throw new Error("full hashset"); match = store[index = (index + 1) & mask]; } store[index] = value; return true; } function has(value) { var index = hash(value) & mask, match = store[index], collisions = 0; while (match != empty) { if (equal(match, value)) return true; if (++collisions >= size) break; match = store[index = (index + 1) & mask]; } return false; } function values() { var values = []; for (var i = 0, n = store.length; i < n; ++i) { var match = store[i]; if (match != empty) values.push(match); } return values; } return { add: add, has: has, values: values }; } function hashmap(size, hash, equal, keyType, keyEmpty, valueType) { if (arguments.length === 3) { keyType = valueType = Array; keyEmpty = null; } var keystore = new keyType(size = 1 << Math.max(4, Math.ceil(Math.log(size) / Math.LN2))), valstore = new valueType(size), mask = size - 1; for (var i = 0; i < size; ++i) { keystore[i] = keyEmpty; } function set(key, value) { var index = hash(key) & mask, matchKey = keystore[index], collisions = 0; while (matchKey != keyEmpty) { if (equal(matchKey, key)) return valstore[index] = value; if (++collisions >= size) throw new Error("full hashmap"); matchKey = keystore[index = (index + 1) & mask]; } keystore[index] = key; valstore[index] = value; return value; } function maybeSet(key, value) { var index = hash(key) & mask, matchKey = keystore[index], collisions = 0; while (matchKey != keyEmpty) { if (equal(matchKey, key)) return valstore[index]; if (++collisions >= size) throw new Error("full hashmap"); matchKey = keystore[index = (index + 1) & mask]; } keystore[index] = key; valstore[index] = value; return value; } function get(key, missingValue) { var index = hash(key) & mask, matchKey = keystore[index], collisions = 0; while (matchKey != keyEmpty) { if (equal(matchKey, key)) return valstore[index]; if (++collisions >= size) break; matchKey = keystore[index = (index + 1) & mask]; } return missingValue; } function keys() { var keys = []; for (var i = 0, n = keystore.length; i < n; ++i) { var matchKey = keystore[i]; if (matchKey != keyEmpty) keys.push(matchKey); } return keys; } return { set: set, maybeSet: maybeSet, // set if unset get: get, keys: keys }; } function equalPoint(pointA, pointB) { return pointA[0] === pointB[0] && pointA[1] === pointB[1]; } // TODO if quantized, use simpler Int32 hashing? var buffer = new ArrayBuffer(16), floats = new Float64Array(buffer), uints = new Uint32Array(buffer); function hashPoint(point) { floats[0] = point[0]; floats[1] = point[1]; var hash = uints[0] ^ uints[1]; hash = hash << 5 ^ hash >> 7 ^ uints[2] ^ uints[3]; return hash & 0x7fffffff; } // Given an extracted (pre-)topology, identifies all of the junctions. These are // the points at which arcs (lines or rings) will need to be cut so that each // arc is represented uniquely. // // A junction is a point where at least one arc deviates from another arc going // through the same point. For example, consider the point B. If there is a arc // through ABC and another arc through CBA, then B is not a junction because in // both cases the adjacent point pairs are {A,C}. However, if there is an // additional arc ABD, then {A,D} != {A,C}, and thus B becomes a junction. // // For a closed ring ABCA, the first point A’s adjacent points are the second // and last point {B,C}. For a line, the first and last point are always // considered junctions, even if the line is closed; this ensures that a closed // line is never rotated. function join(topology) { var coordinates = topology.coordinates, lines = topology.lines, rings = topology.rings, indexes = index(), visitedByIndex = new Int32Array(coordinates.length), leftByIndex = new Int32Array(coordinates.length), rightByIndex = new Int32Array(coordinates.length), junctionByIndex = new Int8Array(coordinates.length), junctionCount = 0, // upper bound on number of junctions i, n, previousIndex, currentIndex, nextIndex; for (i = 0, n = coordinates.length; i < n; ++i) { visitedByIndex[i] = leftByIndex[i] = rightByIndex[i] = -1; } for (i = 0, n = lines.length; i < n; ++i) { var line = lines[i], lineStart = line[0], lineEnd = line[1]; currentIndex = indexes[lineStart]; nextIndex = indexes[++lineStart]; ++junctionCount, junctionByIndex[currentIndex] = 1; // start while (++lineStart <= lineEnd) { sequence(i, previousIndex = currentIndex, currentIndex = nextIndex, nextIndex = indexes[lineStart]); } ++junctionCount, junctionByIndex[nextIndex] = 1; // end } for (i = 0, n = coordinates.length; i < n; ++i) { visitedByIndex[i] = -1; } for (i = 0, n = rings.length; i < n; ++i) { var ring = rings[i], ringStart = ring[0] + 1, ringEnd = ring[1]; previousIndex = indexes[ringEnd - 1]; currentIndex = indexes[ringStart - 1]; nextIndex = indexes[ringStart]; sequence(i, previousIndex, currentIndex, nextIndex); while (++ringStart <= ringEnd) { sequence(i, previousIndex = currentIndex, currentIndex = nextIndex, nextIndex = indexes[ringStart]); } } function sequence(i, previousIndex, currentIndex, nextIndex) { if (visitedByIndex[currentIndex] === i) return; // ignore self-intersection visitedByIndex[currentIndex] = i; var leftIndex = leftByIndex[currentIndex]; if (leftIndex >= 0) { var rightIndex = rightByIndex[currentIndex]; if ((leftIndex !== previousIndex || rightIndex !== nextIndex) && (leftIndex !== nextIndex || rightIndex !== previousIndex)) { ++junctionCount, junctionByIndex[currentIndex] = 1; } } else { leftByIndex[currentIndex] = previousIndex; rightByIndex[currentIndex] = nextIndex; } } function index() { var indexByPoint = hashmap(coordinates.length * 1.4, hashIndex, equalIndex, Int32Array, -1, Int32Array), indexes = new Int32Array(coordinates.length); for (var i = 0, n = coordinates.length; i < n; ++i) { indexes[i] = indexByPoint.maybeSet(i, i); } return indexes; } function hashIndex(i) { return hashPoint(coordinates[i]); } function equalIndex(i, j) { return equalPoint(coordinates[i], coordinates[j]); } visitedByIndex = leftByIndex = rightByIndex = null; var junctionByPoint = hashset(junctionCount * 1.4, hashPoint, equalPoint), j; // Convert back to a standard hashset by point for caller convenience. for (i = 0, n = coordinates.length; i < n; ++i) { if (junctionByIndex[j = indexes[i]]) { junctionByPoint.add(coordinates[j]); } } return junctionByPoint; } // Given an extracted (pre-)topology, cuts (or rotates) arcs so that all shared // point sequences are identified. The topology can then be subsequently deduped // to remove exact duplicate arcs. function cut(topology) { var junctions = join(topology), coordinates = topology.coordinates, lines = topology.lines, rings = topology.rings, next, i, n; for (i = 0, n = lines.length; i < n; ++i) { var line = lines[i], lineMid = line[0], lineEnd = line[1]; while (++lineMid < lineEnd) { if (junctions.has(coordinates[lineMid])) { next = {0: lineMid, 1: line[1]}; line[1] = lineMid; line = line.next = next; } } } for (i = 0, n = rings.length; i < n; ++i) { var ring = rings[i], ringStart = ring[0], ringMid = ringStart, ringEnd = ring[1], ringFixed = junctions.has(coordinates[ringStart]); while (++ringMid < ringEnd) { if (junctions.has(coordinates[ringMid])) { if (ringFixed) { next = {0: ringMid, 1: ring[1]}; ring[1] = ringMid; ring = ring.next = next; } else { // For the first junction, we can rotate rather than cut. rotateArray(coordinates, ringStart, ringEnd, ringEnd - ringMid); coordinates[ringEnd] = coordinates[ringStart]; ringFixed = true; ringMid = ringStart; // restart; we may have skipped junctions } } } } return topology; } function rotateArray(array, start, end, offset) { reverse(array, start, end); reverse(array, start, start + offset); reverse(array, start + offset, end); } function reverse(array, start, end) { for (var mid = start + ((end-- - start) >> 1), t; start < mid; ++start, --end) { t = array[start], array[start] = array[end], array[end] = t; } } // Given a cut topology, combines duplicate arcs. function dedup(topology) { var coordinates = topology.coordinates, lines = topology.lines, line, rings = topology.rings, ring, arcCount = lines.length + rings.length, i, n; delete topology.lines; delete topology.rings; // Count the number of (non-unique) arcs to initialize the hashmap safely. for (i = 0, n = lines.length; i < n; ++i) { line = lines[i]; while (line = line.next) ++arcCount; } for (i = 0, n = rings.length; i < n; ++i) { ring = rings[i]; while (ring = ring.next) ++arcCount; } var arcsByEnd = hashmap(arcCount * 2 * 1.4, hashPoint, equalPoint), arcs = topology.arcs = []; for (i = 0, n = lines.length; i < n; ++i) { line = lines[i]; do { dedupLine(line); } while (line = line.next); } for (i = 0, n = rings.length; i < n; ++i) { ring = rings[i]; if (ring.next) { // arc is no longer closed do { dedupLine(ring); } while (ring = ring.next); } else { dedupRing(ring); } } function dedupLine(arc) { var startPoint, endPoint, startArcs, startArc, endArcs, endArc, i, n; // Does this arc match an existing arc in order? if (startArcs = arcsByEnd.get(startPoint = coordinates[arc[0]])) { for (i = 0, n = startArcs.length; i < n; ++i) { startArc = startArcs[i]; if (equalLine(startArc, arc)) { arc[0] = startArc[0]; arc[1] = startArc[1]; return; } } } // Does this arc match an existing arc in reverse order? if (endArcs = arcsByEnd.get(endPoint = coordinates[arc[1]])) { for (i = 0, n = endArcs.length; i < n; ++i) { endArc = endArcs[i]; if (reverseEqualLine(endArc, arc)) { arc[1] = endArc[0]; arc[0] = endArc[1]; return; } } } if (startArcs) startArcs.push(arc); else arcsByEnd.set(startPoint, [arc]); if (endArcs) endArcs.push(arc); else arcsByEnd.set(endPoint, [arc]); arcs.push(arc); } function dedupRing(arc) { var endPoint, endArcs, endArc, i, n; // Does this arc match an existing line in order, or reverse order? // Rings are closed, so their start point and end point is the same. if (endArcs = arcsByEnd.get(endPoint = coordinates[arc[0]])) { for (i = 0, n = endArcs.length; i < n; ++i) { endArc = endArcs[i]; if (equalRing(endArc, arc)) { arc[0] = endArc[0]; arc[1] = endArc[1]; return; } if (reverseEqualRing(endArc, arc)) { arc[0] = endArc[1]; arc[1] = endArc[0]; return; } } } // Otherwise, does this arc match an existing ring in order, or reverse order? if (endArcs = arcsByEnd.get(endPoint = coordinates[arc[0] + findMinimumOffset(arc)])) { for (i = 0, n = endArcs.length; i < n; ++i) { endArc = endArcs[i]; if (equalRing(endArc, arc)) { arc[0] = endArc[0]; arc[1] = endArc[1]; return; } if (reverseEqualRing(endArc, arc)) { arc[0] = endArc[1]; arc[1] = endArc[0]; return; } } } if (endArcs) endArcs.push(arc); else arcsByEnd.set(endPoint, [arc]); arcs.push(arc); } function equalLine(arcA, arcB) { var ia = arcA[0], ib = arcB[0], ja = arcA[1], jb = arcB[1]; if (ia - ja !== ib - jb) return false; for (; ia <= ja; ++ia, ++ib) if (!equalPoint(coordinates[ia], coordinates[ib])) return false; return true; } function reverseEqualLine(arcA, arcB) { var ia = arcA[0], ib = arcB[0], ja = arcA[1], jb = arcB[1]; if (ia - ja !== ib - jb) return false; for (; ia <= ja; ++ia, --jb) if (!equalPoint(coordinates[ia], coordinates[jb])) return false; return true; } function equalRing(arcA, arcB) { var ia = arcA[0], ib = arcB[0], ja = arcA[1], jb = arcB[1], n = ja - ia; if (n !== jb - ib) return false; var ka = findMinimumOffset(arcA), kb = findMinimumOffset(arcB); for (var i = 0; i < n; ++i) { if (!equalPoint(coordinates[ia + (i + ka) % n], coordinates[ib + (i + kb) % n])) return false; } return true; } function reverseEqualRing(arcA, arcB) { var ia = arcA[0], ib = arcB[0], ja = arcA[1], jb = arcB[1], n = ja - ia; if (n !== jb - ib) return false; var ka = findMinimumOffset(arcA), kb = n - findMinimumOffset(arcB); for (var i = 0; i < n; ++i) { if (!equalPoint(coordinates[ia + (i + ka) % n], coordinates[jb - (i + kb) % n])) return false; } return true; } // Rings are rotated to a consistent, but arbitrary, start point. // This is necessary to detect when a ring and a rotated copy are dupes. function findMinimumOffset(arc) { var start = arc[0], end = arc[1], mid = start, minimum = mid, minimumPoint = coordinates[mid]; while (++mid < end) { var point = coordinates[mid]; if (point[0] < minimumPoint[0] || point[0] === minimumPoint[0] && point[1] < minimumPoint[1]) { minimum = mid; minimumPoint = point; } } return minimum - start; } return topology; } // Given an array of arcs in absolute (but already quantized!) coordinates, // converts to fixed-point delta encoding. // This is a destructive operation that modifies the given arcs! function delta(arcs) { var i = -1, n = arcs.length; while (++i < n) { var arc = arcs[i], j = 0, k = 1, m = arc.length, point = arc[0], x0 = point[0], y0 = point[1], x1, y1; while (++j < m) { point = arc[j], x1 = point[0], y1 = point[1]; if (x1 !== x0 || y1 !== y0) arc[k++] = [x1 - x0, y1 - y0], x0 = x1, y0 = y1; } if (k === 1) arc[k++] = [0, 0]; // Each arc must be an array of two or more positions. arc.length = k; } return arcs; } // Extracts the lines and rings from the specified hash of geometry objects. // // Returns an object with three properties: // // * coordinates - shared buffer of [x, y] coordinates // * lines - lines extracted from the hash, of the form [start, end] // * rings - rings extracted from the hash, of the form [start, end] // // For each ring or line, start and end represent inclusive indexes into the // coordinates buffer. For rings (and closed lines), coordinates[start] equals // coordinates[end]. // // For each line or polygon geometry in the input hash, including nested // geometries as in geometry collections, the `coordinates` array is replaced // with an equivalent `arcs` array that, for each line (for line string // geometries) or ring (for polygon geometries), points to one of the above // lines or rings. function extract(objects) { var index = -1, lines = [], rings = [], coordinates = []; function extractGeometry(geometry) { if (geometry && hasOwnProperty.call(extractGeometryType, geometry.type)) extractGeometryType[geometry.type](geometry); } var extractGeometryType = { GeometryCollection: function(o) { o.geometries.forEach(extractGeometry); }, LineString: function(o) { o.arcs = extractLine(o.arcs); }, MultiLineString: function(o) { o.arcs = o.arcs.map(extractLine); }, Polygon: function(o) { o.arcs = o.arcs.map(extractRing); }, MultiPolygon: function(o) { o.arcs = o.arcs.map(extractMultiRing); } }; function extractLine(line) { for (var i = 0, n = line.length; i < n; ++i) coordinates[++index] = line[i]; var arc = {0: index - n + 1, 1: index}; lines.push(arc); return arc; } function extractRing(ring) { for (var i = 0, n = ring.length; i < n; ++i) coordinates[++index] = ring[i]; var arc = {0: index - n + 1, 1: index}; rings.push(arc); return arc; } function extractMultiRing(rings) { return rings.map(extractRing); } for (var key in objects) { extractGeometry(objects[key]); } return { type: "Topology", coordinates: coordinates, lines: lines, rings: rings, objects: objects }; } // Given a hash of GeoJSON objects, returns a hash of GeoJSON geometry objects. // Any null input geometry objects are represented as {type: null} in the output. // Any feature.{id,properties,bbox} are transferred to the output geometry object. // Each output geometry object is a shallow copy of the input (e.g., properties, coordinates)! function geometry(inputs) { var outputs = {}, key; for (key in inputs) outputs[key] = geomifyObject(inputs[key]); return outputs; } function geomifyObject(input) { return input == null ? {type: null} : (input.type === "FeatureCollection" ? geomifyFeatureCollection : input.type === "Feature" ? geomifyFeature : geomifyGeometry)(input); } function geomifyFeatureCollection(input) { var output = {type: "GeometryCollection", geometries: input.features.map(geomifyFeature)}; if (input.bbox != null) output.bbox = input.bbox; return output; } function geomifyFeature(input) { var output = geomifyGeometry(input.geometry), key; // eslint-disable-line no-unused-vars if (input.id != null) output.id = input.id; if (input.bbox != null) output.bbox = input.bbox; for (key in input.properties) { output.properties = input.properties; break; } return output; } function geomifyGeometry(input) { if (input == null) return {type: null}; var output = input.type === "GeometryCollection" ? {type: "GeometryCollection", geometries: input.geometries.map(geomifyGeometry)} : input.type === "Point" || input.type === "MultiPoint" ? {type: input.type, coordinates: input.coordinates} : {type: input.type, arcs: input.coordinates}; // TODO Check for unknown types? if (input.bbox != null) output.bbox = input.bbox; return output; } function prequantize(objects, bbox, n) { var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3], kx = x1 - x0 ? (n - 1) / (x1 - x0) : 1, ky = y1 - y0 ? (n - 1) / (y1 - y0) : 1; function quantizePoint(input) { return [Math.round((input[0] - x0) * kx), Math.round((input[1] - y0) * ky)]; } function quantizePoints(input, m) { var i = -1, j = 0, n = input.length, output = new Array(n), // pessimistic pi, px, py, x, y; while (++i < n) { pi = input[i]; x = Math.round((pi[0] - x0) * kx); y = Math.round((pi[1] - y0) * ky); if (x !== px || y !== py) output[j++] = [px = x, py = y]; // non-coincident points } output.length = j; while (j < m) j = output.push([output[0][0], output[0][1]]); return output; } function quantizeLine(input) { return quantizePoints(input, 2); } function quantizeRing(input) { return quantizePoints(input, 4); } function quantizePolygon(input) { return input.map(quantizeRing); } function quantizeGeometry(o) { if (o != null && hasOwnProperty.call(quantizeGeometryType, o.type)) quantizeGeometryType[o.type](o); } var quantizeGeometryType = { GeometryCollection: function(o) { o.geometries.forEach(quantizeGeometry); }, Point: function(o) { o.coordinates = quantizePoint(o.coordinates); }, MultiPoint: function(o) { o.coordinates = o.coordinates.map(quantizePoint); }, LineString: function(o) { o.arcs = quantizeLine(o.arcs); }, MultiLineString: function(o) { o.arcs = o.arcs.map(quantizeLine); }, Polygon: function(o) { o.arcs = quantizePolygon(o.arcs); }, MultiPolygon: function(o) { o.arcs = o.arcs.map(quantizePolygon); } }; for (var key in objects) { quantizeGeometry(objects[key]); } return { scale: [1 / kx, 1 / ky], translate: [x0, y0] }; } // Constructs the TopoJSON Topology for the specified hash of features. // Each object in the specified hash must be a GeoJSON object, // meaning FeatureCollection, a Feature or a geometry object. function topology(objects, quantization) { var bbox = bounds(objects = geometry(objects)), transform = quantization > 0 && bbox && prequantize(objects, bbox, quantization), topology = dedup(cut(extract(objects))), coordinates = topology.coordinates, indexByArc = hashmap(topology.arcs.length * 1.4, hashArc, equalArc); objects = topology.objects; // for garbage collection topology.bbox = bbox; topology.arcs = topology.arcs.map(function(arc, i) { indexByArc.set(arc, i); return coordinates.slice(arc[0], arc[1] + 1); }); delete topology.coordinates; coordinates = null; function indexGeometry(geometry) { if (geometry && hasOwnProperty.call(indexGeometryType, geometry.type)) indexGeometryType[geometry.type](geometry); } var indexGeometryType = { GeometryCollection: function(o) { o.geometries.forEach(indexGeometry); }, LineString: function(o) { o.arcs = indexArcs(o.arcs); }, MultiLineString: function(o) { o.arcs = o.arcs.map(indexArcs); }, Polygon: function(o) { o.arcs = o.arcs.map(indexArcs); }, MultiPolygon: function(o) { o.arcs = o.arcs.map(indexMultiArcs); } }; function indexArcs(arc) { var indexes = []; do { var index = indexByArc.get(arc); indexes.push(arc[0] < arc[1] ? index : ~index); } while (arc = arc.next); return indexes; } function indexMultiArcs(arcs) { return arcs.map(indexArcs); } for (var key in objects) { indexGeometry(objects[key]); } if (transform) { topology.transform = transform; topology.arcs = delta(topology.arcs); } return topology; } function hashArc(arc) { var i = arc[0], j = arc[1], t; if (j < i) t = i, i = j, j = t; return i + 31 * j; } function equalArc(arcA, arcB) { var ia = arcA[0], ja = arcA[1], ib = arcB[0], jb = arcB[1], t; if (ja < ia) t = ia, ia = ja, ja = t; if (jb < ib) t = ib, ib = jb, jb = t; return ia === ib && ja === jb; } exports.topology = topology; Object.defineProperty(exports, '__esModule', { value: true }); }));