UNPKG

boundingbox

Version:

Implements bounding boxes with usual functions like intersects. Can convert from/to Leaflet bounds and GeoJSON.

931 lines (807 loc) 25.8 kB
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ 'use strict' const GeoJSONBounds = require('geojson-bounds') const haversine = require('haversine') /* global L:false */ /** * create bounding box from input * @class * @param {object|Leaflet.latLngBounds|GeoJSON} bounds Input boundary. Can be an object with { minlat, minlon, maxlat, maxlon } or { lat, lon } or { lat, lng } or [ N (lat), N (lon) ] a GeoJSON object or a Leaflet object (latLng or latLngBounds). The boundary will automatically be wrapped at longitude -180 / 180. * @example * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 }) */ function BoundingBox (bounds) { let k if (bounds === null || typeof bounds === 'undefined') { this.minlat = -90 this.minlon = -180 this.maxlat = +90 this.maxlon = +180 return } // Leaflet.latLngBounds detected! if (typeof bounds.getSouthWest === 'function') { const sw = bounds.getSouthWest().wrap() const ne = bounds.getNorthEast().wrap() bounds = { minlat: sw.lat, minlon: sw.lng, maxlat: ne.lat, maxlon: ne.lng } } // GeoJSON Feature detected if (bounds.type === 'Feature') { let boxes if (bounds.geometry.type === 'GeometryCollection') { boxes = bounds.geometry.geometries.map( geometry => { const b = new BoundingBox({ type: 'Feature', geometry }) return [b.minlon, b.minlat, b.maxlon, b.maxlat] } ) } else if (['MultiPoint', 'MultiPolygon', 'MultiLineString'].includes(bounds.geometry.type)) { boxes = bounds.geometry.coordinates.map( geom => GeoJSONBounds.extent({ type: 'Feature', geometry: { type: bounds.geometry.type.substr(5), coordinates: geom } }) ) } else { boxes = [GeoJSONBounds.extent(bounds)] } const b = boxes.shift() this.minlat = b[1] this.minlon = b[0] this.maxlat = b[3] this.maxlon = b[2] boxes.forEach(b => this.extend({ minlat: b[1], minlon: b[0], maxlat: b[3], maxlon: b[2] })) this._wrap() return } else // GeoJSON FeatureCollection detected if (bounds.type === 'FeatureCollection') { if (bounds.features.length === 0) { return } const r = new BoundingBox(bounds.features[0]) for (let i = 1; i < bounds.features.length; i++) { r.extend(bounds.features[i]) } this.minlat = r.minlat this.minlon = r.minlon this.maxlat = r.maxlat this.maxlon = r.maxlon return } if ('bounds' in bounds) { bounds = bounds.bounds } if (bounds.lat) { this.minlat = +bounds.lat this.maxlat = +bounds.lat } if (bounds.lon) { this.minlon = +bounds.lon this.maxlon = +bounds.lon } if (bounds.latitude) { this.minlat = +bounds.latitude this.maxlat = +bounds.latitude this.minlon = +bounds.longitude this.maxlon = +bounds.longitude } if (Array.isArray(bounds)) { this.minlat = +bounds[0] this.maxlat = +bounds[0] this.minlon = +bounds[1] this.maxlon = +bounds[1] } // e.g. L.latLng object if (bounds.lng) { this.minlon = +bounds.lng this.maxlon = +bounds.lng } const props = ['minlon', 'minlat', 'maxlon', 'maxlat'] for (let i = 0; i < props.length; i++) { k = props[i] if (k in bounds) { this[k] = bounds[k] } } this._wrap() } BoundingBox.prototype.wrapMaxLon = function () { return (this.minlon > this.maxlon) ? this.maxlon + 360 : this.maxlon } BoundingBox.prototype.wrapMinLon = function () { return (this.minlon > this.maxlon) ? this.minlon - 360 : this.minlon } BoundingBox.prototype._wrap = function () { if (this.minlon < -180 || this.minlon > 180) { this.minlon = (this.minlon + 180) % 360 - 180 } if (this.maxlon < -180 || this.maxlon > 180) { this.maxlon = (this.maxlon + 180) % 360 - 180 } return this } /** * Checks whether the other bounding box intersects (shares any portion of space) the current object. * @param {BoundingBox} other Other boundingbox to check for * @return {boolean} true if the bounding boxes intersect * @example * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 }) * var bbox2 = new BoundingBox({ lat: 48.5, lon: 16.267 }) * console.log(bbox.intersects(bbox2)) // true */ BoundingBox.prototype.intersects = function (other) { if (!(other instanceof BoundingBox)) { other = new BoundingBox(other) } if (other.maxlat < this.minlat) { return false } if (other.minlat > this.maxlat) { return false } if (this.wrapMaxLon() >= other.minlon && this.minlon <= other.wrapMaxLon()) { return true } if (this.maxlon >= other.wrapMinLon() && this.wrapMinLon() <= other.maxlon) { return true } return false } /** * Checks whether the current object is fully within the other bounding box. * @param {BoundingBox} other Other boundingbox to check for * @return {boolean} true if the bounding boxes is within other * @example * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 }) * var bbox2 = new BoundingBox({ lat: 48.5, lon: 16.267 }) * console.log(bbox2.within(bbox)) // true */ BoundingBox.prototype.within = function (other) { if (!(other instanceof BoundingBox)) { other = new BoundingBox(other) } if (other.maxlat < this.maxlat) { return false } if (other.minlat > this.minlat) { return false } if (this.wrapMinLon() >= other.wrapMinLon() && this.maxlon <= other.maxlon) { return true } if (this.minlon >= other.minlon && this.wrapMaxLon() <= other.wrapMaxLon()) { return true } return false } BoundingBox.prototype.toTile = function () { return new BoundingBox({ minlat: Math.floor(this.minlat * 10) / 10, minlon: Math.floor(this.minlon * 10) / 10, maxlat: Math.ceil(this.maxlat * 10) / 10, maxlon: Math.ceil(this.maxlon * 10) / 10 }) } /** * return the bounding box as lon-lat string, e.g. '179.5,55,-179.5,56' * @return {string} * @example * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 }) * console.log(bbox.toLonLatString()) // '16.23,48.123,16.367,49.012' */ BoundingBox.prototype.toLonLatString = function () { return this.minlon + ',' + this.minlat + ',' + this.maxlon + ',' + this.maxlat } /** * return the bounding box as lon-lat string, e.g. '179.5,55,-179.5,56'. Useful for sending requests to web services that return geo data. * @return {string} * @example * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 }) * console.log(bbox.toBBoxString()) // '16.23,48.123,16.367,49.012' */ BoundingBox.prototype.toBBoxString = BoundingBox.prototype.toLonLatString /** * return the bounding box as lon-lat string, e.g. '55,179.5,56,-179.5'. Useful e.g. for Overpass API requests. * @return {string} * @example * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 }) * console.log(bbox.toLatLonString()) // '48.123,16.23,49.012,16.367' */ BoundingBox.prototype.toLatLonString = function () { return this.minlat + ',' + this.minlon + ',' + this.maxlat + ',' + this.maxlon } /** * return the diagonal length (length of hypothenuse). * @return {number} * @example * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 }) * console.log(bbox.diagonalLength()) // 0.8994943023721748 */ BoundingBox.prototype.diagonalLength = function () { const dlat = this.maxlat - this.minlat const dlon = this.wrapMaxLon() - this.minlon return Math.sqrt(dlat * dlat + dlon * dlon) } /** * return the diagonal distance (using the haversine function). See https://github.com/njj/haversine for further details. * @param {object} [options] Options * @param {string} [options.unit=km] Unit of measurement applied to result ('km', 'mile', 'meter', 'nmi') * @param {number} [options.threshold] If passed, will result in library returning boolean value of whether or not the start and end points are within that supplied threshold. * @return {number} * @example * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 }) * console.log(bbox.diagonalDistance({ unit: 'm' })) // 99.36491328576697 */ BoundingBox.prototype.diagonalDistance = function (options = {}) { return haversine( { latitude: this.minlat, longitude: this.minlon }, { latitude: this.maxlat, longitude: this.maxlon }, options ) } /** * Returns the center point of the bounding box as { lat, lon } * @return {object} * @example * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 }) * console.log(bbox.getCenter()) // { lat: 48.567499999999995, lon: 16.2985 } */ BoundingBox.prototype.getCenter = function () { const dlat = this.maxlat - this.minlat const dlon = this.wrapMaxLon() - this.minlon let lon = this.minlon + dlon / 2 if (lon < -180 || lon > 180) { lon = (lon + 180) % 360 - 180 } return { lat: this.minlat + dlat / 2, lon } } /** * get Northern boundary (latitude) * @param {number} */ BoundingBox.prototype.getNorth = function () { return this.maxlat } /** * get Southern boundary (latitude) * @param {number} */ BoundingBox.prototype.getSouth = function () { return this.minlat } /** * get Eastern boundary (longitude) * @param {number} */ BoundingBox.prototype.getEast = function () { return this.maxlon } /** * get Western boundary (longitude) * @param {number} */ BoundingBox.prototype.getWest = function () { return this.minlon } /** * extends current boundary by the other boundary * @param {BoundingBox} other * @example * var bbox1 = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 }) * var bbox2 = new BoundingBox({ minlat: 48.000, minlon: 16.23, maxlat: 49.012, maxlon: 16.789 }) * bbox1.extend(bbox2) * console.log(bbox1.bounds) // { minlat: 48, minlon: 16.23, maxlat: 49.012, maxlon: 16.789 } */ BoundingBox.prototype.extend = function (other) { if (!(other instanceof BoundingBox)) { other = new BoundingBox(other) } if (other.minlat < this.minlat) { this.minlat = other.minlat } if (other.maxlat > this.maxlat) { this.maxlat = other.maxlat } // does bounds intersect with other bounds in longitude? for (let shift = -360; shift <= 360; shift += 360) { if (other.wrapMaxLon() + shift > this.minlon && other.minlon + shift < this.wrapMaxLon()) { this.minlon = Math.min(this.minlon, other.minlon + shift) this.maxlon = Math.max(this.wrapMaxLon(), other.wrapMaxLon() + shift) this._wrap() return } } const min1 = Math.min(this.minlon, other.minlon) const min2 = Math.max(this.minlon, other.minlon) const max1 = Math.max(this.wrapMaxLon(), other.wrapMaxLon()) const max2 = Math.min(this.wrapMaxLon(), other.wrapMaxLon()) if (max1 - min1 < max2 - min2 + 360) { this.minlon = min1 this.maxlon = max1 } else { this.minlon = min2 this.maxlon = max2 } this._wrap() } /** * Returns the bounding box as GeoJSON feature. In case of bounding boxes crossing the antimeridian, this function will return a multipolygon with the parts on each side of the antimeridian (as specified in RFC 7946, section 3.1.9). * @return {object} * @example * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 }) * bbox.toGeoJSON() * // { * // "type": "Feature", * // "properties": {}, * // "geometry": { * // "type": "Polygon", * // "coordinates": [ * // [ * // [ 16.23, 48.123 ], * // [ 16.367, 48.123 ], * // [ 16.367, 49.012 ], * // [ 16.23, 49.012 ], * // [ 16.23, 48.123 ] * // ] * // ] * // } * // } */ BoundingBox.prototype.toGeoJSON = function () { if (this.minlon > this.maxlon) { return { type: 'Feature', properties: {}, geometry: { type: 'MultiPolygon', coordinates: [ [[ [this.minlon, this.minlat], [180, this.minlat], [180, this.maxlat], [this.minlon, this.maxlat], [this.minlon, this.minlat] ]], [[ [-180, this.minlat], [this.maxlon, this.minlat], [this.maxlon, this.maxlat], [-180, this.maxlat], [-180, this.minlat] ]] ] } } } return { type: 'Feature', properties: {}, geometry: { type: 'Polygon', coordinates: [[ [this.minlon, this.minlat], [this.maxlon, this.minlat], [this.maxlon, this.maxlat], [this.minlon, this.maxlat], [this.minlon, this.minlat] ]] } } } /** * Returns the bounding box as L.latLngBounds object. Leaflet must be included separately! * @param {object} [options] Options. * @param {number[]} [options.shiftWorld=[0, 0]] Shift the world by the first value for the Western hemisphere (lon < 0) or the second value for the Eastern hemisphere (lon >= 0). */ BoundingBox.prototype.toLeaflet = function (options = {}) { if (!('shiftWorld' in options)) { options.shiftWorld = [0, 0] } return L.latLngBounds( L.latLng(this.minlat, this.minlon + (this.minlon < 0 ? options.shiftWorld[0] : options.shiftWorld[1])), L.latLng(this.maxlat, this.maxlon + (this.maxlon < 0 ? options.shiftWorld[0] : options.shiftWorld[1])) ) } if (typeof module !== 'undefined' && module.exports) { module.exports = BoundingBox } if (typeof window !== 'undefined') { window.BoundingBox = BoundingBox } },{"geojson-bounds":2,"haversine":3}],2:[function(require,module,exports){ (function (process){(function (){ (function() { /* Modified version of underscore.js's flatten function https://github.com/jashkenas/underscore/blob/master/underscore.js#L501 */ function flatten(input, output) { output = output || []; var idx = output.length; for (var i = 0; i < input.length; i++) { if (Array.isArray(input[i]) && Array.isArray(input[i][0])) { flatten(input[i], output); idx = output.length; } else { output[idx++] = input[i]; } } return (Array.isArray(output[0])) ? output : [output]; }; function maxLat(coords) { return Math.max.apply(null, coords.map(function(d) { return d[1]; })); } function maxLng(coords) { return Math.max.apply(null, coords.map(function(d) { return d[0]; })); } function minLat(coords) { return Math.min.apply(null, coords.map(function(d) { return d[1]; })); } function minLng(coords) { return Math.min.apply(null, coords.map(function(d) { return d[0]; })); } function fetchEnvelope(coords) { var mmc = { "minLng": minLng(coords), "minLat": minLat(coords), "maxLng": maxLng(coords), "maxLat": maxLat(coords) } return { "type": "Feature", "properties": {}, "geometry": { "type": "Polygon", "coordinates": [[ [mmc.minLng, mmc.minLat], [mmc.minLng, mmc.maxLat], [mmc.maxLng, mmc.maxLat], [mmc.maxLng, mmc.minLat], [mmc.minLng, mmc.minLat] ]] } } } function fetchExtent(coords) { return [ minLng(coords), minLat(coords), maxLng(coords), maxLat(coords) ] } // Adapted from http://stackoverflow.com/questions/2792443/finding-the-centroid-of-a-polygon function fetchCentroid(vertices) { var centroid = { x: 0, y: 0 } var signedArea = 0; var x0 = 0; var y0 = 0; var x1 = 0; var y1 = 0; var a = 0; for (var i = 0; i < vertices.length - 1; i++) { x0 = vertices[i][0]; y0 = vertices[i][1]; x1 = vertices[i + 1][0]; y1 = vertices[i + 1][1]; a = (x0 * y1) - (x1 * y0); signedArea += a; centroid.x += (x0 + x1) * a; centroid.y += (y0 + y1) * a; } x0 = vertices[vertices.length - 1][0]; y0 = vertices[vertices.length - 1][1]; x1 = vertices[0][0]; y1 = vertices[0][1]; a = (x0 * y1) - (x1 * y0); signedArea += a; centroid.x += (x0 + x1) * a; centroid.y += (y0 + y1) * a; signedArea = signedArea * 0.5; centroid.x = centroid.x / (6.0*signedArea); centroid.y = centroid.y / (6.0*signedArea); return [centroid.x, centroid.y]; } function feature(obj) { return flatten(obj.geometry.coordinates); } function featureCollection(f) { return flatten(f.features.map(feature)); } function geometryCollection(g) { return flatten(g.geometries.map(process)); } function process(t) { if (!t) { return []; } switch (t.type) { case "Feature": return feature(t); case "GeometryCollection": return geometryCollection(t); case "FeatureCollection": return featureCollection(t); case "Point": case "LineString": case "Polygon": case "MultiPoint": case "MultiPolygon": case "MultiLineString": return flatten(t.coordinates); default: return []; } } function envelope(t) { return fetchEnvelope(process(t)); } function extent(t) { return fetchExtent(process(t)); } function centroid(t) { return fetchCentroid(process(t)); } function xMin(t) { return minLng(process(t)); } function xMax(t) { return maxLng(process(t)); } function yMin(t) { return minLat(process(t)); } function yMax(t) { return maxLat(process(t)); } module.exports = { "envelope": envelope, "extent": extent, "centroid": centroid, "xMin": xMin, "xMax": xMax, "yMin": yMin, "yMax": yMax } }()); }).call(this)}).call(this,require('_process')) },{"_process":4}],3:[function(require,module,exports){ var haversine = (function () { var RADII = { km: 6371, mile: 3960, meter: 6371000, nmi: 3440 } // convert to radians var toRad = function (num) { return num * Math.PI / 180 } // convert coordinates to standard format based on the passed format option var convertCoordinates = function (format, coordinates) { switch (format) { case '[lat,lon]': return { latitude: coordinates[0], longitude: coordinates[1] } case '[lon,lat]': return { latitude: coordinates[1], longitude: coordinates[0] } case '{lon,lat}': return { latitude: coordinates.lat, longitude: coordinates.lon } case '{lat,lng}': return { latitude: coordinates.lat, longitude: coordinates.lng } case 'geojson': return { latitude: coordinates.geometry.coordinates[1], longitude: coordinates.geometry.coordinates[0] } default: return coordinates } } return function haversine (startCoordinates, endCoordinates, options) { options = options || {} var R = options.unit in RADII ? RADII[options.unit] : RADII.km var start = convertCoordinates(options.format, startCoordinates) var end = convertCoordinates(options.format, endCoordinates) var dLat = toRad(end.latitude - start.latitude) var dLon = toRad(end.longitude - start.longitude) var lat1 = toRad(start.latitude) var lat2 = toRad(end.latitude) var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2) var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) if (options.threshold) { return options.threshold > (R * c) } return R * c } })() if (typeof module !== 'undefined' && module.exports) { module.exports = haversine } },{}],4:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {}; // cached from whatever global is present so that test runners that stub it // don't break things. But we need to wrap it in a try catch in case it is // wrapped in strict mode code which doesn't define any globals. It's inside a // function because try/catches deoptimize in certain engines. var cachedSetTimeout; var cachedClearTimeout; function defaultSetTimout() { throw new Error('setTimeout has not been defined'); } function defaultClearTimeout () { throw new Error('clearTimeout has not been defined'); } (function () { try { if (typeof setTimeout === 'function') { cachedSetTimeout = setTimeout; } else { cachedSetTimeout = defaultSetTimout; } } catch (e) { cachedSetTimeout = defaultSetTimout; } try { if (typeof clearTimeout === 'function') { cachedClearTimeout = clearTimeout; } else { cachedClearTimeout = defaultClearTimeout; } } catch (e) { cachedClearTimeout = defaultClearTimeout; } } ()) function runTimeout(fun) { if (cachedSetTimeout === setTimeout) { //normal enviroments in sane situations return setTimeout(fun, 0); } // if setTimeout wasn't available but was latter defined if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { cachedSetTimeout = setTimeout; return setTimeout(fun, 0); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedSetTimeout(fun, 0); } catch(e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedSetTimeout.call(null, fun, 0); } catch(e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error return cachedSetTimeout.call(this, fun, 0); } } } function runClearTimeout(marker) { if (cachedClearTimeout === clearTimeout) { //normal enviroments in sane situations return clearTimeout(marker); } // if clearTimeout wasn't available but was latter defined if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { cachedClearTimeout = clearTimeout; return clearTimeout(marker); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedClearTimeout(marker); } catch (e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedClearTimeout.call(null, marker); } catch (e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. // Some versions of I.E. have different rules for clearTimeout vs setTimeout return cachedClearTimeout.call(this, marker); } } } var queue = []; var draining = false; var currentQueue; var queueIndex = -1; function cleanUpNextTick() { if (!draining || !currentQueue) { return; } draining = false; if (currentQueue.length) { queue = currentQueue.concat(queue); } else { queueIndex = -1; } if (queue.length) { drainQueue(); } } function drainQueue() { if (draining) { return; } var timeout = runTimeout(cleanUpNextTick); draining = true; var len = queue.length; while(len) { currentQueue = queue; queue = []; while (++queueIndex < len) { if (currentQueue) { currentQueue[queueIndex].run(); } } queueIndex = -1; len = queue.length; } currentQueue = null; draining = false; runClearTimeout(timeout); } process.nextTick = function (fun) { var args = new Array(arguments.length - 1); if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i]; } } queue.push(new Item(fun, args)); if (queue.length === 1 && !draining) { runTimeout(drainQueue); } }; // v8 likes predictible objects function Item(fun, array) { this.fun = fun; this.array = array; } Item.prototype.run = function () { this.fun.apply(null, this.array); }; process.title = 'browser'; process.browser = true; process.env = {}; process.argv = []; process.version = ''; // empty string to avoid regexp issues process.versions = {}; function noop() {} process.on = noop; process.addListener = noop; process.once = noop; process.off = noop; process.removeListener = noop; process.removeAllListeners = noop; process.emit = noop; process.prependListener = noop; process.prependOnceListener = noop; process.listeners = function (name) { return [] } process.binding = function (name) { throw new Error('process.binding is not supported'); }; process.cwd = function () { return '/' }; process.chdir = function (dir) { throw new Error('process.chdir is not supported'); }; process.umask = function() { return 0; }; },{}]},{},[1]);