├── CODEOWNERS ├── LICENSE ├── README.md ├── dist └── app.js ├── index.js ├── isochrone.js ├── lib └── conrec.js ├── package.json └── test ├── .DS_Store ├── app.js ├── fixtures └── parameters.json └── index.test.js /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mapbox/logistics-api -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mapbox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # isochrone 2 | 3 | Isochrone generator built atop the Mapbox Matrix API, with CONREC polygonization. Calculates isochrones up to 1 hour of travel time. 4 | 5 | **[Demo!](https://www.mapbox.com/bites/00156/)** 6 | 7 | 8 | ## Setup 9 | 10 | Clone the repository, and either 11 | 12 | - include `dist/app.js` in your markup directly, or 13 | - build your own via `browserify index.js -o ` 14 | 15 | For all pull requests that modify `isochrone.js`, please also rebuild `dist/app.js` to make sure your changes get applied. 16 | 17 | ## Usage 18 | 19 | `isochrone(origin, options, callback)` 20 | 21 | - `origin` 22 | Starting point for isochrone generation expressed as `[lng, lat]`. 23 | 24 | - `options` 25 | 26 | Parameter | Required? | Default | Limits | Description 27 | --- | --- | --- |--- | --- 28 | `token` | Yes | n/a | --- | 🔑 Mapbox user token 29 | `threshold` | Yes | n/a | min:1, max: 3600 | ⌛️ Time thresholds for the desired ischrone(s), expressed in seconds. Can be expressed as either a) an array of numbers (e.g. `[600, 1200, 1800]`) to calculate isochrones at those specific time cutoffs, or b) a single number (e.g. `1800`) to calculate isochrones at 60-second intervals, up to that time 30 | `mode` | No | `driving` | one of `driving`, `cycling`, or `walking` | 🚗 🚲 👟 Method of transportation desired, as defined in the Mapbox Matrix API [documentation](https://www.mapbox.com/api-documentation/navigation/#retrieve-a-matrix). 31 | `direction` | No | `divergent` | `divergent` or `convergent` | ⬇️ ⬆️ Direction of travel. `Divergent` isochrones are the total area reachable _from_ the origin within the given time, while `convergent` ones cover the total area that _can_ reach it. 32 | `resolution` | No | An `auto` value, computed from `mode` and maximum threshold | min:0.05, max: 5 | 📏 Granularity of the underlying sample grid, in units of kilometers. Lower values yield finer results, but are more expensive in both query time and API request load. Scaling this value with both time threshold and transport mode is recommended. 33 | `batchSize` | No | 25 | min: 2 | 👨 👬 👨‍👦‍👦 Number of coordinates per Matrix API request. The default value applies for most Mapbox starter plans. Higher values will speed up computation and avoid rate-limiting issues. 34 | 35 | 36 | - `callback` 37 | 38 | 39 | Function to execute once the calculation completes. 40 | 41 | 42 | 43 | ## Example 44 | This requests a set of isochrones at 1-minute intervals up to 30 minutes, from near Sacramento, CA: 45 | 46 | ```javascript 47 | isochrone([-121.4738,38.6194], {"token":, "threshold":1800}, function(err, output){ 48 | if (err) throw err; 49 | console.log(output); 50 | }) 51 | ``` 52 | 53 | ## Output 54 | 55 | Isochrones are returned as a GeoJSON featurecollection of polygon features. Each feature contains a `time` parameter that corresponds to its threshold in seconds. 56 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | isochrone = require('./isochrone.js'); -------------------------------------------------------------------------------- /isochrone.js: -------------------------------------------------------------------------------- 1 | var d3 = require('d3-request'); 2 | var cheapRuler = require('cheap-ruler'); 3 | var Conrec = require('./lib/conrec.js'); 4 | 5 | var turf = { 6 | polygon: require('@turf/helpers').polygon, 7 | point: require('@turf/helpers').point, 8 | featureCollection: require('@turf/helpers').featureCollection, 9 | inside: require('@turf/inside') 10 | }; 11 | 12 | function isochrone(startingPosition, parameters, cb){ 13 | 14 | var constants = { 15 | timeIncrement:60, 16 | retryDelay:10000, 17 | queryURL: { 18 | 'divergent': '?sources=0&destinations=all', 19 | 'convergent': '?sources=all&destinations=0' 20 | }, 21 | startingGrid: 2, 22 | relativeSpeeds:{ 23 | 'driving': 1, 24 | 'cycling':0.3, 25 | 'walking':0.1 26 | } 27 | }; 28 | 29 | //validate 30 | parameters = validate(startingPosition, parameters, cb); 31 | if (!parameters) return; 32 | 33 | startingPosition = startingPosition.map(function(coord){ 34 | return parseFloat(coord.toFixed(6)) 35 | }) 36 | 37 | 38 | var state = { 39 | travelTimes: {}, 40 | lngs:{}, 41 | lats:{}, 42 | snapTable:{}, 43 | outstandingRequests:0, 44 | startTime: Date.now(), 45 | timeMaximum: typeof parameters.threshold === 'number' ? parameters.threshold : Math.max.apply(null, parameters.threshold) 46 | } 47 | 48 | state.thresholds = typeof parameters.threshold === 'number' ? listOutIntegers(state.timeMaximum,constants.timeIncrement) : parameters.threshold; 49 | state.lngs[startingPosition[0]] = 0; 50 | state.lats[startingPosition[1]] = 0; 51 | 52 | ruler = cheapRuler(startingPosition[1], 'kilometers'); 53 | 54 | 55 | 56 | // kick off initial batch of queries 57 | extendBuffer([startingPosition], [constants.startingGrid]) 58 | 59 | function generateDiagonal(centerOffset, cellSize, dimensions){ 60 | 61 | var halfOffset = dimensions * 0.5-1; 62 | var output = []; 63 | 64 | 65 | for (var r = -halfOffset; r <= halfOffset+1; r++){ 66 | 67 | var xDelta = centerOffset[0] + r; 68 | var yDelta = centerOffset[1] + r; 69 | 70 | //first move left/right 71 | var horizontalMovement = ruler.destination(startingPosition, xDelta * cellSize, 90) 72 | 73 | //then move up/down 74 | var verticalMovement = 75 | ruler.destination(horizontalMovement, yDelta * cellSize, 0) 76 | .map(function(coord){ 77 | return parseFloat(coord.toFixed(6)) 78 | }) 79 | 80 | state.lngs[verticalMovement[0]] = xDelta; 81 | state.lats[verticalMovement[1]] = yDelta; 82 | 83 | output.push(verticalMovement) 84 | } 85 | 86 | return output 87 | } 88 | 89 | 90 | function generateBuffer(center, dimensions){ 91 | 92 | var centerIndex = [state.lngs[center[0]], state.lats[center[1]]]; 93 | var diagonal = generateDiagonal(centerIndex, parameters.resolution*Math.pow(2,0.5), dimensions) 94 | var grid = []; 95 | 96 | for (var r = 0; r < dimensions; r++){ 97 | for (var c = 0; cstate.resolution/2 ? state.timeMaximum : snapDistance * 1200; 210 | 211 | // write time to record 212 | var time = Math.ceil(parameters.fudgeFactor*durations[i]+snapPenalty); 213 | state.travelTimes[coords[i]] = time; 214 | 215 | // add to buffer list 216 | var timeLeft = state.timeMaximum - time 217 | if (timeLeft > 0 && !state.snapTable[snappedLocation]) { 218 | toBuffer.push(coords[i]) 219 | bufferRadii.push(calculateBufferRadius(timeLeft)) 220 | } 221 | 222 | state.snapTable[coords[i]] = snappedLocation; 223 | 224 | } 225 | 226 | if (toBuffer.length>0) extendBuffer(toBuffer, bufferRadii) 227 | 228 | // when all callbacks received 229 | else if (state.outstandingRequests === 0) polygonize() 230 | 231 | } 232 | 233 | function calculateBufferRadius(timeRemaining){ 234 | var radius = Math.round(Math.min(Math.max(2,timeRemaining/60),6))*2 235 | return 4// radius 236 | } 237 | 238 | function polygonize(){ 239 | 240 | snapTable = state.snapTable; 241 | internalState = state 242 | rawPoints = objectToArray(state.travelTimes, true); 243 | 244 | state.lngs = objectToArray(state.lngs, false).sort(function(a, b){return a-b}) 245 | state.lats = objectToArray(state.lats, false).sort(function(a, b){return a-b}).reverse() 246 | 247 | conrec(); 248 | 249 | function conrec(){ 250 | 251 | var points =[]; 252 | 253 | var twoDArray = []; 254 | 255 | var c = new Conrec.Conrec; 256 | 257 | for (r in state.lngs){ 258 | 259 | var row = []; 260 | 261 | for (d in state.lats){ 262 | var coord = [state.lngs[r]-0, state.lats[d]]; 263 | if(!state.travelTimes[coord]) state.travelTimes[coord] = [state.timeMaximum*10]; 264 | 265 | var time = state.travelTimes[coord]; 266 | 267 | points.push(turf.point(coord,{time:time})); 268 | 269 | row.push(time); 270 | } 271 | twoDArray.push(row) 272 | 273 | } 274 | postPoints = turf.featureCollection(points); 275 | 276 | // build conrec 277 | c.contour(twoDArray, 0, state.lngs.length-1, 0, state.lats.length-1, state.lngs, state.lats, state.thresholds.length, state.thresholds); 278 | 279 | var contours = c.contourList() 280 | polygons = []; 281 | 282 | //iterate through contour hulls 283 | for (c in contours){ 284 | 285 | // get the current level 286 | var level = contours[c].level; 287 | 288 | //if no level, create it (reserve a first position in array for outer ring) 289 | if (!polygons[level]) polygons[level] = []; 290 | 291 | // create a shape 292 | var shape = [] 293 | 294 | // map x-y to lng,lat array 295 | for (var k = 0; k0; p--){ 333 | var ring = vertices[p]; 334 | var r = 0; 335 | var soFarInside = true; 336 | while (r < ring.length && soFarInside) { 337 | var pointIsInside = turf.inside(turf.point(ring[r]), turf.polygon([vertices[0]])); 338 | 339 | if (!pointIsInside) soFarInside = false 340 | r++ 341 | } 342 | 343 | if (!soFarInside) vertices.splice(p,1) 344 | 345 | } 346 | 347 | var poly = vertices === null ? null : turf.polygon(vertices, { 348 | time: seconds 349 | }); 350 | 351 | return poly 352 | 353 | }) 354 | .filter(function(item){ 355 | return item !== null 356 | }) 357 | } 358 | 359 | 360 | hulls = turf.featureCollection(contours) 361 | travelTimes = state.travelTimes 362 | cb(null, hulls) 363 | } 364 | 365 | } 366 | 367 | 368 | function objectToArray(object, arrayOfArrays){ 369 | 370 | var keys = Object.keys(object); 371 | 372 | if (!arrayOfArrays) return toNumbers(keys); 373 | var commaDelimitedNums = keys.map(function(coords){ 374 | var commaDelimited = coords.split(','); 375 | 376 | commaDelimited = toNumbers(commaDelimited) 377 | return commaDelimited 378 | }); 379 | 380 | return commaDelimitedNums 381 | } 382 | 383 | function toNumbers(strings){ 384 | return strings.map( 385 | function(string){ 386 | return parseFloat(string) 387 | }) 388 | } 389 | 390 | function listOutIntegers(max, increment){ 391 | var array =[]; 392 | for (var v=increment; v<=max; v+=increment){ 393 | array.push(v) 394 | } 395 | return array 396 | } 397 | 398 | function validate(origin, parameters, cb){ 399 | 400 | var validator = { 401 | token: {format: 'type', values:['string'], required: true}, 402 | mode: {format: 'among', values:['driving', 'cycling', 'walking'], required:false, default: 'driving'}, 403 | direction: {format: 'among', values:['divergent', 'convergent'], required:false, default: 'divergent'}, 404 | threshold: {format: 'type', values:['number', 'object'], required:true}, 405 | batchSize: {format:'range', min:2, max: Infinity, required:false, default:25}, 406 | fudgeFactor: {format:'range', min:0.5, max: 2, required:false, default: 1}, 407 | keepIslands: {format:'type', values:['boolean'], required:false, default: false}, 408 | resolution: {format: 'range', min: 0.05, max: 5, required:false}, 409 | } 410 | 411 | function computeResolution(){ 412 | var timeMaximum = typeof parameters.threshold === 'number' ? parameters.threshold : Math.max.apply(null, parameters.threshold) 413 | var res = 4 * constants.relativeSpeeds[parameters.mode] * timeMaximum/3600 414 | res = Math.max(Math.min(res, validator.resolution.max), validator.resolution.min) 415 | return res 416 | } 417 | 418 | var error; 419 | 420 | // validate starting position 421 | if (!origin || typeof origin !=='object' || origin.length!== 2){ 422 | error = 'Starting position must be a longitude-latitude object, expressed as an array.' 423 | } 424 | 425 | else { 426 | Object.keys(validator).forEach(function(key){ 427 | var item = validator[key] 428 | // make sure required parameters are present. if optional, fill in with default value 429 | if (!parameters[key]) { 430 | if (item.required) error = (key+' required in query') 431 | else parameters[key] = key === 'resolution' ? computeResolution() : item.default 432 | } 433 | 434 | // ensure parameter is of right type 435 | if (item.format === 'type' && item.values.indexOf(typeof parameters[key]) === -1) { 436 | error = (key+' must be a '+ item.values.join(' or ')) 437 | } 438 | 439 | //ensure parameter holds a valid value 440 | else if (item.format === 'among' && item.values.indexOf(parameters[key]) ===-1) { 441 | error = (key+' must be '+ item.values.join(' or ')) 442 | } 443 | 444 | //ensure parameter falls within accepted range 445 | 446 | else if (item.format === 'range') { 447 | if (parameters[key]>item.max || parameters[key]=1 && item<=3600})){ 455 | error = ('thresholds must be an array of numbers between 1 and 3600') 456 | } 457 | } 458 | }); 459 | } 460 | 461 | if (error) return cb(new Error(error), null) 462 | 463 | 464 | else return parameters 465 | } 466 | } 467 | 468 | 469 | module.exports = exports = isochrone; 470 | -------------------------------------------------------------------------------- /lib/conrec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010, Jason Davies. 3 | * 4 | * All rights reserved. This code is based on Bradley White's Java version, 5 | * which is in turn based on Nicholas Yue's C++ version, which in turn is based 6 | * on Paul D. Bourke's original Fortran version. See below for the respective 7 | * copyright notices. 8 | * 9 | * See http://local.wasp.uwa.edu.au/~pbourke/papers/conrec/ for the original 10 | * paper by Paul D. Bourke. 11 | * 12 | * The vector conversion code is based on http://apptree.net/conrec.htm by 13 | * Graham Cox. 14 | * 15 | * Redistribution and use in source and binary forms, with or without 16 | * modification, are permitted provided that the following conditions are met: 17 | * * Redistributions of source code must retain the above copyright 18 | * notice, this list of conditions and the following disclaimer. 19 | * * Redistributions in binary form must reproduce the above copyright 20 | * notice, this list of conditions and the following disclaimer in the 21 | * documentation and/or other materials provided with the distribution. 22 | * * Neither the name of the nor the 23 | * names of its contributors may be used to endorse or promote products 24 | * derived from this software without specific prior written permission. 25 | * 26 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 27 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 28 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 29 | * ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 30 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 31 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 32 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 33 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 35 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | */ 37 | 38 | /* 39 | * Copyright (c) 1996-1997 Nicholas Yue 40 | * 41 | * This software is copyrighted by Nicholas Yue. This code is based on Paul D. 42 | * Bourke's CONREC.F routine. 43 | * 44 | * The authors hereby grant permission to use, copy, and distribute this 45 | * software and its documentation for any purpose, provided that existing 46 | * copyright notices are retained in all copies and that this notice is 47 | * included verbatim in any distributions. Additionally, the authors grant 48 | * permission to modify this software and its documentation for any purpose, 49 | * provided that such modifications are not distributed without the explicit 50 | * consent of the authors and that existing copyright notices are retained in 51 | * all copies. Some of the algorithms implemented by this software are 52 | * patented, observe all applicable patent law. 53 | * 54 | * IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY FOR 55 | * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT 56 | * OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, 57 | * EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 58 | * 59 | * THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, 60 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, 61 | * FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE IS 62 | * PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO 63 | * OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR 64 | * MODIFICATIONS. 65 | */ 66 | 67 | (function(exports) { 68 | exports.Conrec = Conrec; 69 | 70 | var EPSILON = 1e-10; 71 | 72 | function pointsEqual(a, b) { 73 | var x = a.x - b.x, y = a.y - b.y; 74 | return x * x + y * y < EPSILON; 75 | } 76 | 77 | function reverseList(list) { 78 | var pp = list.head; 79 | 80 | while (pp) { 81 | // swap prev/next pointers 82 | var temp = pp.next; 83 | pp.next = pp.prev; 84 | pp.prev = temp; 85 | 86 | // continue through the list 87 | pp = temp; 88 | } 89 | 90 | // swap head/tail pointers 91 | var temp = list.head; 92 | list.head = list.tail; 93 | list.tail = temp; 94 | } 95 | 96 | function ContourBuilder(level) { 97 | this.level = level; 98 | this.s = null; 99 | this.count = 0; 100 | } 101 | ContourBuilder.prototype.remove_seq = function(list) { 102 | // if list is the first item, static ptr s is updated 103 | if (list.prev) { 104 | list.prev.next = list.next; 105 | } else { 106 | this.s = list.next; 107 | } 108 | 109 | if (list.next) { 110 | list.next.prev = list.prev; 111 | } 112 | --this.count; 113 | } 114 | ContourBuilder.prototype.addSegment = function(a, b) { 115 | var ss = this.s; 116 | var ma = null; 117 | var mb = null; 118 | var prependA = false; 119 | var prependB = false; 120 | 121 | while (ss) { 122 | if (ma == null) { 123 | // no match for a yet 124 | if (pointsEqual(a, ss.head.p)) { 125 | ma = ss; 126 | prependA = true; 127 | } else if (pointsEqual(a, ss.tail.p)) { 128 | ma = ss; 129 | } 130 | } 131 | if (mb == null) { 132 | // no match for b yet 133 | if (pointsEqual(b, ss.head.p)) { 134 | mb = ss; 135 | prependB = true; 136 | } else if (pointsEqual(b, ss.tail.p)) { 137 | mb = ss; 138 | } 139 | } 140 | // if we matched both no need to continue searching 141 | if (mb != null && ma != null) { 142 | break; 143 | } else { 144 | ss = ss.next; 145 | } 146 | } 147 | 148 | // c is the case selector based on which of ma and/or mb are set 149 | var c = ((ma != null) ? 1 : 0) | ((mb != null) ? 2 : 0); 150 | 151 | switch(c) { 152 | case 0: // both unmatched, add as new sequence 153 | var aa = {p: a, prev: null}; 154 | var bb = {p: b, next: null}; 155 | aa.next = bb; 156 | bb.prev = aa; 157 | 158 | // create sequence element and push onto head of main list. The order 159 | // of items in this list is unimportant 160 | ma = {head: aa, tail: bb, next: this.s, prev: null, closed: false}; 161 | if (this.s) { 162 | this.s.prev = ma; 163 | } 164 | this.s = ma; 165 | 166 | ++this.count; // not essential - tracks number of unmerged sequences 167 | break; 168 | 169 | case 1: // a matched, b did not - thus b extends sequence ma 170 | var pp = {p: b}; 171 | 172 | if (prependA) { 173 | pp.next = ma.head; 174 | pp.prev = null; 175 | ma.head.prev = pp; 176 | ma.head = pp; 177 | } else { 178 | pp.next = null; 179 | pp.prev = ma.tail; 180 | ma.tail.next = pp; 181 | ma.tail = pp; 182 | } 183 | break; 184 | 185 | case 2: // b matched, a did not - thus a extends sequence mb 186 | var pp = {p: a}; 187 | 188 | if (prependB) { 189 | pp.next = mb.head; 190 | pp.prev = null; 191 | mb.head.prev = pp; 192 | mb.head = pp; 193 | } else { 194 | pp.next = null; 195 | pp.prev = mb.tail; 196 | mb.tail.next = pp; 197 | mb.tail = pp; 198 | } 199 | break; 200 | 201 | case 3: // both matched, can merge sequences 202 | // if the sequences are the same, do nothing, as we are simply closing this path (could set a flag) 203 | 204 | if (ma === mb) { 205 | var pp = {p: ma.tail.p, next: ma.head, prev: null}; 206 | ma.head.prev = pp; 207 | ma.head = pp; 208 | ma.closed = true; 209 | break; 210 | } 211 | 212 | // there are 4 ways the sequence pair can be joined. The current setting of prependA and 213 | // prependB will tell us which type of join is needed. For head/head and tail/tail joins 214 | // one sequence needs to be reversed 215 | switch((prependA ? 1 : 0) | (prependB ? 2 : 0)) { 216 | case 0: // tail-tail 217 | // reverse ma and append to mb 218 | reverseList(ma); 219 | // fall through to head/tail case 220 | case 1: // head-tail 221 | // ma is appended to mb and ma discarded 222 | mb.tail.next = ma.head; 223 | ma.head.prev = mb.tail; 224 | mb.tail = ma.tail; 225 | 226 | //discard ma sequence record 227 | this.remove_seq(ma); 228 | break; 229 | 230 | case 3: // head-head 231 | // reverse ma and append mb to it 232 | reverseList(ma); 233 | // fall through to tail/head case 234 | case 2: // tail-head 235 | // mb is appended to ma and mb is discarded 236 | ma.tail.next = mb.head; 237 | mb.head.prev = ma.tail; 238 | ma.tail = mb.tail; 239 | 240 | //discard mb sequence record 241 | this.remove_seq(mb); 242 | break; 243 | } 244 | } 245 | } 246 | 247 | /** 248 | * Implements CONREC. 249 | * 250 | * @param {function} drawContour function for drawing contour. Defaults to a 251 | * custom "contour builder", which populates the 252 | * contours property. 253 | */ 254 | function Conrec(drawContour) { 255 | if (!drawContour) { 256 | var c = this; 257 | c.contours = {}; 258 | /** 259 | * drawContour - interface for implementing the user supplied method to 260 | * render the countours. 261 | * 262 | * Draws a line between the start and end coordinates. 263 | * 264 | * @param startX - start coordinate for X 265 | * @param startY - start coordinate for Y 266 | * @param endX - end coordinate for X 267 | * @param endY - end coordinate for Y 268 | * @param contourLevel - Contour level for line. 269 | */ 270 | this.drawContour = function(startX, startY, endX, endY, contourLevel, k) { 271 | var cb = c.contours[k]; 272 | if (!cb) { 273 | cb = c.contours[k] = new ContourBuilder(contourLevel); 274 | } 275 | cb.addSegment({x: startX, y: startY}, {x: endX, y: endY}); 276 | } 277 | this.contourList = function() { 278 | var l = []; 279 | var a = c.contours; 280 | for (var k in a) { 281 | var s = a[k].s; 282 | var level = a[k].level; 283 | while (s) { 284 | var h = s.head; 285 | var l2 = []; 286 | l2.level = level; 287 | l2.k = k; 288 | while (h && h.p) { 289 | l2.push(h.p); 290 | h = h.next; 291 | } 292 | l.push(l2); 293 | s = s.next; 294 | } 295 | } 296 | l.sort(function(a, b) { return a.k - b.k }); 297 | return l; 298 | } 299 | } else { 300 | this.drawContour = drawContour; 301 | } 302 | this.h = new Array(5); 303 | this.sh = new Array(5); 304 | this.xh = new Array(5); 305 | this.yh = new Array(5); 306 | } 307 | 308 | /** 309 | * contour is a contouring subroutine for rectangularily spaced data 310 | * 311 | * It emits calls to a line drawing subroutine supplied by the user which 312 | * draws a contour map corresponding to real*4data on a randomly spaced 313 | * rectangular grid. The coordinates emitted are in the same units given in 314 | * the x() and y() arrays. 315 | * 316 | * Any number of contour levels may be specified but they must be in order of 317 | * increasing value. 318 | * 319 | * 320 | * @param {number[][]} d - matrix of data to contour 321 | * @param {number} ilb,iub,jlb,jub - index bounds of data matrix 322 | * 323 | * The following two, one dimensional arrays (x and y) contain 324 | * the horizontal and vertical coordinates of each sample points. 325 | * @param {number[]} x - data matrix column coordinates 326 | * @param {number[]} y - data matrix row coordinates 327 | * @param {number} nc - number of contour levels 328 | * @param {number[]} z - contour levels in increasing order. 329 | */ 330 | Conrec.prototype.contour = function(d, ilb, iub, jlb, jub, x, y, nc, z) { 331 | var h = this.h, sh = this.sh, xh = this.xh, yh = this.yh; 332 | var drawContour = this.drawContour; 333 | this.contours = {}; 334 | 335 | /** private */ 336 | var xsect = function(p1, p2){ 337 | return (h[p2]*xh[p1]-h[p1]*xh[p2])/(h[p2]-h[p1]); 338 | } 339 | 340 | var ysect = function(p1, p2){ 341 | return (h[p2]*yh[p1]-h[p1]*yh[p2])/(h[p2]-h[p1]); 342 | } 343 | var m1; 344 | var m2; 345 | var m3; 346 | var case_value; 347 | var dmin; 348 | var dmax; 349 | var x1 = 0.0; 350 | var x2 = 0.0; 351 | var y1 = 0.0; 352 | var y2 = 0.0; 353 | 354 | // The indexing of im and jm should be noted as it has to start from zero 355 | // unlike the fortran counter part 356 | var im = [0, 1, 1, 0]; 357 | var jm = [0, 0, 1, 1]; 358 | 359 | // Note that castab is arranged differently from the FORTRAN code because 360 | // Fortran and C/C++ arrays are transposed of each other, in this case 361 | // it is more tricky as castab is in 3 dimensions 362 | var castab = [ 363 | [ 364 | [0, 0, 8], [0, 2, 5], [7, 6, 9] 365 | ], 366 | [ 367 | [0, 3, 4], [1, 3, 1], [4, 3, 0] 368 | ], 369 | [ 370 | [9, 6, 7], [5, 2, 0], [8, 0, 0] 371 | ] 372 | ]; 373 | 374 | for (var j=(jub-1);j>=jlb;j--) { 375 | for (var i=ilb;i<=iub-1;i++) { 376 | var temp1, temp2; 377 | temp1 = Math.min(d[i][j],d[i][j+1]); 378 | temp2 = Math.min(d[i+1][j],d[i+1][j+1]); 379 | dmin = Math.min(temp1,temp2); 380 | temp1 = Math.max(d[i][j],d[i][j+1]); 381 | temp2 = Math.max(d[i+1][j],d[i+1][j+1]); 382 | dmax = Math.max(temp1,temp2); 383 | 384 | if (dmax>=z[0]&&dmin<=z[nc-1]) { 385 | for (var k=0;k=dmin&&z[k]<=dmax) { 387 | for (var m=4;m>=0;m--) { 388 | if (m>0) { 389 | // The indexing of im and jm should be noted as it has to 390 | // start from zero 391 | h[m] = d[i+im[m-1]][j+jm[m-1]]-z[k]; 392 | xh[m] = x[i+im[m-1]]; 393 | yh[m] = y[j+jm[m-1]]; 394 | } else { 395 | h[0] = 0.25*(h[1]+h[2]+h[3]+h[4]); 396 | xh[0]=0.5*(x[i]+x[i+1]); 397 | yh[0]=0.5*(y[j]+y[j+1]); 398 | } 399 | if (h[m]>EPSILON) { 400 | sh[m] = 1; 401 | } else if (h[m]<-EPSILON) { 402 | sh[m] = -1; 403 | } else 404 | sh[m] = 0; 405 | } 406 | // 407 | // Note: at this stage the relative heights of the corners and the 408 | // centre are in the h array, and the corresponding coordinates are 409 | // in the xh and yh arrays. The centre of the box is indexed by 0 410 | // and the 4 corners by 1 to 4 as shown below. 411 | // Each triangle is then indexed by the parameter m, and the 3 412 | // vertices of each triangle are indexed by parameters m1,m2,and 413 | // m3. 414 | // It is assumed that the centre of the box is always vertex 2 415 | // though this isimportant only when all 3 vertices lie exactly on 416 | // the same contour level, in which case only the side of the box 417 | // is drawn. 418 | // 419 | // 420 | // vertex 4 +-------------------+ vertex 3 421 | // | \ / | 422 | // | \ m-3 / | 423 | // | \ / | 424 | // | \ / | 425 | // | m=2 X m=2 | the centre is vertex 0 426 | // | / \ | 427 | // | / \ | 428 | // | / m=1 \ | 429 | // | / \ | 430 | // vertex 1 +-------------------+ vertex 2 431 | // 432 | // 433 | // 434 | // Scan each triangle in the box 435 | // 436 | for (m=1;m<=4;m++) { 437 | m1 = m; 438 | m2 = 0; 439 | if (m!=4) { 440 | m3 = m+1; 441 | } else { 442 | m3 = 1; 443 | } 444 | case_value = castab[sh[m1]+1][sh[m2]+1][sh[m3]+1]; 445 | if (case_value!=0) { 446 | switch (case_value) { 447 | case 1: // Line between vertices 1 and 2 448 | x1=xh[m1]; 449 | y1=yh[m1]; 450 | x2=xh[m2]; 451 | y2=yh[m2]; 452 | break; 453 | case 2: // Line between vertices 2 and 3 454 | x1=xh[m2]; 455 | y1=yh[m2]; 456 | x2=xh[m3]; 457 | y2=yh[m3]; 458 | break; 459 | case 3: // Line between vertices 3 and 1 460 | x1=xh[m3]; 461 | y1=yh[m3]; 462 | x2=xh[m1]; 463 | y2=yh[m1]; 464 | break; 465 | case 4: // Line between vertex 1 and side 2-3 466 | x1=xh[m1]; 467 | y1=yh[m1]; 468 | x2=xsect(m2,m3); 469 | y2=ysect(m2,m3); 470 | break; 471 | case 5: // Line between vertex 2 and side 3-1 472 | x1=xh[m2]; 473 | y1=yh[m2]; 474 | x2=xsect(m3,m1); 475 | y2=ysect(m3,m1); 476 | break; 477 | case 6: // Line between vertex 3 and side 1-2 478 | x1=xh[m3]; 479 | y1=yh[m3]; 480 | x2=xsect(m1,m2); 481 | y2=ysect(m1,m2); 482 | break; 483 | case 7: // Line between sides 1-2 and 2-3 484 | x1=xsect(m1,m2); 485 | y1=ysect(m1,m2); 486 | x2=xsect(m2,m3); 487 | y2=ysect(m2,m3); 488 | break; 489 | case 8: // Line between sides 2-3 and 3-1 490 | x1=xsect(m2,m3); 491 | y1=ysect(m2,m3); 492 | x2=xsect(m3,m1); 493 | y2=ysect(m3,m1); 494 | break; 495 | case 9: // Line between sides 3-1 and 1-2 496 | x1=xsect(m3,m1); 497 | y1=ysect(m3,m1); 498 | x2=xsect(m1,m2); 499 | y2=ysect(m1,m2); 500 | break; 501 | default: 502 | break; 503 | } 504 | // Put your processing code here and comment out the printf 505 | //printf("%f %f %f %f %f\n",x1,y1,x2,y2,z[k]); 506 | drawContour(x1,y1,x2,y2,z[k],k); 507 | } 508 | } 509 | } 510 | } 511 | } 512 | } 513 | } 514 | } 515 | })(typeof exports !== "undefined" ? exports : window); 516 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isochrone", 3 | "version": "0.1.0", 4 | "description": "Isochrone generator built atop the Mapbox Matrix API, with CONREC polygonization.", 5 | "main": "isochrone.js", 6 | "scripts": { 7 | "build": "browserify index.js -o app.js" 8 | }, 9 | "author": "Peter Liu peterqliu@gmail.com", 10 | "license": "MIT", 11 | "dependencies": { 12 | "d3-request": "^1.0.5", 13 | "cheap-ruler": "^2.5.0", 14 | "d3-request": "^1.0.5", 15 | "@turf/helpers": "^4.3.0", 16 | "@turf/inside": "^3.10.3" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/mapbox/isochrone.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/mapbox/isochrone/issues" 24 | }, 25 | "homepage": "https://github.com/mapbox/isochrone#readme" 26 | } -------------------------------------------------------------------------------- /test/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/mapbox-isochrone/a72117825b676688e053abc9f7a793c3e4029847/test/.DS_Store -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;ostate.resolution/2 ? state.timeMaximum : Math.ceil(snapDistance * 900); 169 | 170 | 171 | 172 | // write time to record 173 | var time = [durations[i]+snapPenalty] 174 | state.travelTimes[coords[i]] = time; 175 | 176 | 177 | if (time < state.timeMaximum) { 178 | toBuffer.push(coords[i]) 179 | } 180 | } 181 | 182 | outstandingRequests--; 183 | 184 | if (toBuffer.length>0) extendBuffer(toBuffer, 12) 185 | 186 | // when all callbacks received 187 | else if (outstandingRequests === 0) polygonize() 188 | }) 189 | } 190 | 191 | 192 | function polygonize(){ 193 | 194 | rawPoints = objectToArray(state.travelTimes, true); 195 | 196 | state.lngs = objectToArray(state.lngs, false).sort(function(a, b){return a-b}) 197 | state.lats = objectToArray(state.lats, false).sort(function(a, b){return a-b}).reverse() 198 | 199 | conrec() 200 | 201 | function conrec(){ 202 | 203 | var points =[]; 204 | 205 | var twoDArray = []; 206 | 207 | var c = new Conrec.Conrec; 208 | 209 | for (r in state.lngs){ 210 | 211 | var row = []; 212 | 213 | for (d in state.lats){ 214 | var coord = [state.lngs[r]-0, state.lats[d]]; 215 | if(!state.travelTimes[coord]) state.travelTimes[coord] = [state.timeMaximum*10]; 216 | 217 | var time = state.travelTimes[coord][0]; 218 | 219 | points.push(turf.point(coord,{time:time})); 220 | 221 | row.push(time); 222 | } 223 | twoDArray.push(row) 224 | 225 | } 226 | postPoints = turf.featureCollection(points); 227 | 228 | // build conrec 229 | c.contour(twoDArray, 0, state.lngs.length-1, 0, state.lats.length-1, state.lngs, state.lats, state.thresholds.length, state.thresholds); 230 | 231 | contours = c.contourList() 232 | polygons = []; 233 | 234 | //iterate through contour hulls 235 | for (c in contours){ 236 | 237 | // get the current level 238 | var level = contours[c].level; 239 | 240 | //if no level, create it (reserve a first position in array for outer ring) 241 | if (!polygons[level]) polygons[level] = []; 242 | 243 | // create a shape 244 | var shape = [] 245 | 246 | // map x-y to lng,lat array 247 | for (var k = 0; k0; p--){ 278 | var ring = vertices[p]; 279 | var r = 0; 280 | var soFarInside = true; 281 | while (r < ring.length && soFarInside) { 282 | var pointIsInside = turf.inside(turf.point(ring[r]), turf.polygon([vertices[0]])); 283 | 284 | if (!pointIsInside) soFarInside = false 285 | r++ 286 | } 287 | 288 | if (!soFarInside) vertices.splice(p,1) 289 | 290 | } 291 | 292 | var poly = vertices === null ? null : turf.polygon(vertices, { 293 | time: seconds 294 | }); 295 | 296 | return poly 297 | } 298 | 299 | }) 300 | .filter(function(item){ 301 | return item !== null 302 | }) 303 | 304 | hulls = turf.featureCollection(contours) 305 | travelTimes = state.travelTimes 306 | cb(hulls) 307 | } 308 | 309 | } 310 | 311 | 312 | function objectToArray(object, arrayOfArrays){ 313 | 314 | var keys = Object.keys(object); 315 | 316 | if (!arrayOfArrays) return toNumbers(keys); 317 | var commaDelimitedNums = keys.map(function(coords){ 318 | var commaDelimited = coords.split(','); 319 | 320 | commaDelimited = toNumbers(commaDelimited) 321 | return commaDelimited 322 | }); 323 | 324 | return commaDelimitedNums 325 | } 326 | 327 | function toNumbers(strings){ 328 | return strings.map( 329 | function(string){ 330 | return parseFloat(string) 331 | }) 332 | } 333 | 334 | function listOutIntegers(max, increment){ 335 | var array =[]; 336 | for (var v=increment; v<=max; v+=increment){ 337 | array.push(v) 338 | } 339 | return array 340 | } 341 | 342 | function validate(origin, parameters,cb){ 343 | 344 | var validator = { 345 | token: {format: 'type', values:['string'], required:true}, 346 | mode: {format: 'among', values:['driving', 'cycling', 'walking'], required:false, default: 'driving'}, 347 | direction: {format: 'among', values:['divergent', 'convergent'], required:false, default: 'divergent'}, 348 | threshold: {format: 'type', values:['number', 'object'], required:true}, 349 | resolution: {format: 'range', min: 0.05, max: 2, required:false, default: 0.5}, 350 | batchSize:{format:'range', min:2, max: Infinity, required:false, default:25}, 351 | clipCoasts: {format:'type', values:['boolean'], required:false, default: false} 352 | } 353 | 354 | var error; 355 | 356 | // validate starting position 357 | if (!origin || typeof origin !=='object' || origin.length!== 2){ 358 | error = 'Starting position must be a longitude-latitude object, expressed as an array.' 359 | } 360 | 361 | else { 362 | Object.keys(validator).forEach(function(key){ 363 | var item = validator[key] 364 | 365 | // make sure required parameters are present. if optional, fill in with default value 366 | if (!parameters[key]) { 367 | if(item.required) error = (key+' required in query') 368 | else parameters[key] = item.default 369 | } 370 | 371 | // ensure parameter is of right type 372 | else if (item.format === 'type' && item.values.indexOf(typeof parameters[key]) ===-1) { 373 | error = (key+' must be a '+ item.values.join(' or ')) 374 | } 375 | 376 | //ensure parameter holds a valid value 377 | else if (item.format === 'among' && item.values.indexOf(parameters[key]) ===-1) { 378 | error = (key+' must be '+ item.values.join(' or ')) 379 | } 380 | 381 | //ensure parameter falls within accepted range 382 | 383 | else if (item.format === 'range') { 384 | if (parameters[key]>item.max || parameters[key] nor the 432 | * names of its contributors may be used to endorse or promote products 433 | * derived from this software without specific prior written permission. 434 | * 435 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 436 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 437 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 438 | * ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 439 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 440 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 441 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 442 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 443 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 444 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 445 | */ 446 | 447 | /* 448 | * Copyright (c) 1996-1997 Nicholas Yue 449 | * 450 | * This software is copyrighted by Nicholas Yue. This code is based on Paul D. 451 | * Bourke's CONREC.F routine. 452 | * 453 | * The authors hereby grant permission to use, copy, and distribute this 454 | * software and its documentation for any purpose, provided that existing 455 | * copyright notices are retained in all copies and that this notice is 456 | * included verbatim in any distributions. Additionally, the authors grant 457 | * permission to modify this software and its documentation for any purpose, 458 | * provided that such modifications are not distributed without the explicit 459 | * consent of the authors and that existing copyright notices are retained in 460 | * all copies. Some of the algorithms implemented by this software are 461 | * patented, observe all applicable patent law. 462 | * 463 | * IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY FOR 464 | * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT 465 | * OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, 466 | * EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 467 | * 468 | * THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, 469 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, 470 | * FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE IS 471 | * PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO 472 | * OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR 473 | * MODIFICATIONS. 474 | */ 475 | 476 | (function(exports) { 477 | exports.Conrec = Conrec; 478 | 479 | var EPSILON = 1e-10; 480 | 481 | function pointsEqual(a, b) { 482 | var x = a.x - b.x, y = a.y - b.y; 483 | return x * x + y * y < EPSILON; 484 | } 485 | 486 | function reverseList(list) { 487 | var pp = list.head; 488 | 489 | while (pp) { 490 | // swap prev/next pointers 491 | var temp = pp.next; 492 | pp.next = pp.prev; 493 | pp.prev = temp; 494 | 495 | // continue through the list 496 | pp = temp; 497 | } 498 | 499 | // swap head/tail pointers 500 | var temp = list.head; 501 | list.head = list.tail; 502 | list.tail = temp; 503 | } 504 | 505 | function ContourBuilder(level) { 506 | this.level = level; 507 | this.s = null; 508 | this.count = 0; 509 | } 510 | ContourBuilder.prototype.remove_seq = function(list) { 511 | // if list is the first item, static ptr s is updated 512 | if (list.prev) { 513 | list.prev.next = list.next; 514 | } else { 515 | this.s = list.next; 516 | } 517 | 518 | if (list.next) { 519 | list.next.prev = list.prev; 520 | } 521 | --this.count; 522 | } 523 | ContourBuilder.prototype.addSegment = function(a, b) { 524 | var ss = this.s; 525 | var ma = null; 526 | var mb = null; 527 | var prependA = false; 528 | var prependB = false; 529 | 530 | while (ss) { 531 | if (ma == null) { 532 | // no match for a yet 533 | if (pointsEqual(a, ss.head.p)) { 534 | ma = ss; 535 | prependA = true; 536 | } else if (pointsEqual(a, ss.tail.p)) { 537 | ma = ss; 538 | } 539 | } 540 | if (mb == null) { 541 | // no match for b yet 542 | if (pointsEqual(b, ss.head.p)) { 543 | mb = ss; 544 | prependB = true; 545 | } else if (pointsEqual(b, ss.tail.p)) { 546 | mb = ss; 547 | } 548 | } 549 | // if we matched both no need to continue searching 550 | if (mb != null && ma != null) { 551 | break; 552 | } else { 553 | ss = ss.next; 554 | } 555 | } 556 | 557 | // c is the case selector based on which of ma and/or mb are set 558 | var c = ((ma != null) ? 1 : 0) | ((mb != null) ? 2 : 0); 559 | 560 | switch(c) { 561 | case 0: // both unmatched, add as new sequence 562 | var aa = {p: a, prev: null}; 563 | var bb = {p: b, next: null}; 564 | aa.next = bb; 565 | bb.prev = aa; 566 | 567 | // create sequence element and push onto head of main list. The order 568 | // of items in this list is unimportant 569 | ma = {head: aa, tail: bb, next: this.s, prev: null, closed: false}; 570 | if (this.s) { 571 | this.s.prev = ma; 572 | } 573 | this.s = ma; 574 | 575 | ++this.count; // not essential - tracks number of unmerged sequences 576 | break; 577 | 578 | case 1: // a matched, b did not - thus b extends sequence ma 579 | var pp = {p: b}; 580 | 581 | if (prependA) { 582 | pp.next = ma.head; 583 | pp.prev = null; 584 | ma.head.prev = pp; 585 | ma.head = pp; 586 | } else { 587 | pp.next = null; 588 | pp.prev = ma.tail; 589 | ma.tail.next = pp; 590 | ma.tail = pp; 591 | } 592 | break; 593 | 594 | case 2: // b matched, a did not - thus a extends sequence mb 595 | var pp = {p: a}; 596 | 597 | if (prependB) { 598 | pp.next = mb.head; 599 | pp.prev = null; 600 | mb.head.prev = pp; 601 | mb.head = pp; 602 | } else { 603 | pp.next = null; 604 | pp.prev = mb.tail; 605 | mb.tail.next = pp; 606 | mb.tail = pp; 607 | } 608 | break; 609 | 610 | case 3: // both matched, can merge sequences 611 | // if the sequences are the same, do nothing, as we are simply closing this path (could set a flag) 612 | 613 | if (ma === mb) { 614 | var pp = {p: ma.tail.p, next: ma.head, prev: null}; 615 | ma.head.prev = pp; 616 | ma.head = pp; 617 | ma.closed = true; 618 | break; 619 | } 620 | 621 | // there are 4 ways the sequence pair can be joined. The current setting of prependA and 622 | // prependB will tell us which type of join is needed. For head/head and tail/tail joins 623 | // one sequence needs to be reversed 624 | switch((prependA ? 1 : 0) | (prependB ? 2 : 0)) { 625 | case 0: // tail-tail 626 | // reverse ma and append to mb 627 | reverseList(ma); 628 | // fall through to head/tail case 629 | case 1: // head-tail 630 | // ma is appended to mb and ma discarded 631 | mb.tail.next = ma.head; 632 | ma.head.prev = mb.tail; 633 | mb.tail = ma.tail; 634 | 635 | //discard ma sequence record 636 | this.remove_seq(ma); 637 | break; 638 | 639 | case 3: // head-head 640 | // reverse ma and append mb to it 641 | reverseList(ma); 642 | // fall through to tail/head case 643 | case 2: // tail-head 644 | // mb is appended to ma and mb is discarded 645 | ma.tail.next = mb.head; 646 | mb.head.prev = ma.tail; 647 | ma.tail = mb.tail; 648 | 649 | //discard mb sequence record 650 | this.remove_seq(mb); 651 | break; 652 | } 653 | } 654 | } 655 | 656 | /** 657 | * Implements CONREC. 658 | * 659 | * @param {function} drawContour function for drawing contour. Defaults to a 660 | * custom "contour builder", which populates the 661 | * contours property. 662 | */ 663 | function Conrec(drawContour) { 664 | if (!drawContour) { 665 | var c = this; 666 | c.contours = {}; 667 | /** 668 | * drawContour - interface for implementing the user supplied method to 669 | * render the countours. 670 | * 671 | * Draws a line between the start and end coordinates. 672 | * 673 | * @param startX - start coordinate for X 674 | * @param startY - start coordinate for Y 675 | * @param endX - end coordinate for X 676 | * @param endY - end coordinate for Y 677 | * @param contourLevel - Contour level for line. 678 | */ 679 | this.drawContour = function(startX, startY, endX, endY, contourLevel, k) { 680 | var cb = c.contours[k]; 681 | if (!cb) { 682 | cb = c.contours[k] = new ContourBuilder(contourLevel); 683 | } 684 | cb.addSegment({x: startX, y: startY}, {x: endX, y: endY}); 685 | } 686 | this.contourList = function() { 687 | var l = []; 688 | var a = c.contours; 689 | for (var k in a) { 690 | var s = a[k].s; 691 | var level = a[k].level; 692 | while (s) { 693 | var h = s.head; 694 | var l2 = []; 695 | l2.level = level; 696 | l2.k = k; 697 | while (h && h.p) { 698 | l2.push(h.p); 699 | h = h.next; 700 | } 701 | l.push(l2); 702 | s = s.next; 703 | } 704 | } 705 | l.sort(function(a, b) { return a.k - b.k }); 706 | return l; 707 | } 708 | } else { 709 | this.drawContour = drawContour; 710 | } 711 | this.h = new Array(5); 712 | this.sh = new Array(5); 713 | this.xh = new Array(5); 714 | this.yh = new Array(5); 715 | } 716 | 717 | /** 718 | * contour is a contouring subroutine for rectangularily spaced data 719 | * 720 | * It emits calls to a line drawing subroutine supplied by the user which 721 | * draws a contour map corresponding to real*4data on a randomly spaced 722 | * rectangular grid. The coordinates emitted are in the same units given in 723 | * the x() and y() arrays. 724 | * 725 | * Any number of contour levels may be specified but they must be in order of 726 | * increasing value. 727 | * 728 | * 729 | * @param {number[][]} d - matrix of data to contour 730 | * @param {number} ilb,iub,jlb,jub - index bounds of data matrix 731 | * 732 | * The following two, one dimensional arrays (x and y) contain 733 | * the horizontal and vertical coordinates of each sample points. 734 | * @param {number[]} x - data matrix column coordinates 735 | * @param {number[]} y - data matrix row coordinates 736 | * @param {number} nc - number of contour levels 737 | * @param {number[]} z - contour levels in increasing order. 738 | */ 739 | Conrec.prototype.contour = function(d, ilb, iub, jlb, jub, x, y, nc, z) { 740 | var h = this.h, sh = this.sh, xh = this.xh, yh = this.yh; 741 | var drawContour = this.drawContour; 742 | this.contours = {}; 743 | 744 | /** private */ 745 | var xsect = function(p1, p2){ 746 | return (h[p2]*xh[p1]-h[p1]*xh[p2])/(h[p2]-h[p1]); 747 | } 748 | 749 | var ysect = function(p1, p2){ 750 | return (h[p2]*yh[p1]-h[p1]*yh[p2])/(h[p2]-h[p1]); 751 | } 752 | var m1; 753 | var m2; 754 | var m3; 755 | var case_value; 756 | var dmin; 757 | var dmax; 758 | var x1 = 0.0; 759 | var x2 = 0.0; 760 | var y1 = 0.0; 761 | var y2 = 0.0; 762 | 763 | // The indexing of im and jm should be noted as it has to start from zero 764 | // unlike the fortran counter part 765 | var im = [0, 1, 1, 0]; 766 | var jm = [0, 0, 1, 1]; 767 | 768 | // Note that castab is arranged differently from the FORTRAN code because 769 | // Fortran and C/C++ arrays are transposed of each other, in this case 770 | // it is more tricky as castab is in 3 dimensions 771 | var castab = [ 772 | [ 773 | [0, 0, 8], [0, 2, 5], [7, 6, 9] 774 | ], 775 | [ 776 | [0, 3, 4], [1, 3, 1], [4, 3, 0] 777 | ], 778 | [ 779 | [9, 6, 7], [5, 2, 0], [8, 0, 0] 780 | ] 781 | ]; 782 | 783 | for (var j=(jub-1);j>=jlb;j--) { 784 | for (var i=ilb;i<=iub-1;i++) { 785 | var temp1, temp2; 786 | temp1 = Math.min(d[i][j],d[i][j+1]); 787 | temp2 = Math.min(d[i+1][j],d[i+1][j+1]); 788 | dmin = Math.min(temp1,temp2); 789 | temp1 = Math.max(d[i][j],d[i][j+1]); 790 | temp2 = Math.max(d[i+1][j],d[i+1][j+1]); 791 | dmax = Math.max(temp1,temp2); 792 | 793 | if (dmax>=z[0]&&dmin<=z[nc-1]) { 794 | for (var k=0;k=dmin&&z[k]<=dmax) { 796 | for (var m=4;m>=0;m--) { 797 | if (m>0) { 798 | // The indexing of im and jm should be noted as it has to 799 | // start from zero 800 | h[m] = d[i+im[m-1]][j+jm[m-1]]-z[k]; 801 | xh[m] = x[i+im[m-1]]; 802 | yh[m] = y[j+jm[m-1]]; 803 | } else { 804 | h[0] = 0.25*(h[1]+h[2]+h[3]+h[4]); 805 | xh[0]=0.5*(x[i]+x[i+1]); 806 | yh[0]=0.5*(y[j]+y[j+1]); 807 | } 808 | if (h[m]>EPSILON) { 809 | sh[m] = 1; 810 | } else if (h[m]<-EPSILON) { 811 | sh[m] = -1; 812 | } else 813 | sh[m] = 0; 814 | } 815 | // 816 | // Note: at this stage the relative heights of the corners and the 817 | // centre are in the h array, and the corresponding coordinates are 818 | // in the xh and yh arrays. The centre of the box is indexed by 0 819 | // and the 4 corners by 1 to 4 as shown below. 820 | // Each triangle is then indexed by the parameter m, and the 3 821 | // vertices of each triangle are indexed by parameters m1,m2,and 822 | // m3. 823 | // It is assumed that the centre of the box is always vertex 2 824 | // though this isimportant only when all 3 vertices lie exactly on 825 | // the same contour level, in which case only the side of the box 826 | // is drawn. 827 | // 828 | // 829 | // vertex 4 +-------------------+ vertex 3 830 | // | \ / | 831 | // | \ m-3 / | 832 | // | \ / | 833 | // | \ / | 834 | // | m=2 X m=2 | the centre is vertex 0 835 | // | / \ | 836 | // | / \ | 837 | // | / m=1 \ | 838 | // | / \ | 839 | // vertex 1 +-------------------+ vertex 2 840 | // 841 | // 842 | // 843 | // Scan each triangle in the box 844 | // 845 | for (m=1;m<=4;m++) { 846 | m1 = m; 847 | m2 = 0; 848 | if (m!=4) { 849 | m3 = m+1; 850 | } else { 851 | m3 = 1; 852 | } 853 | case_value = castab[sh[m1]+1][sh[m2]+1][sh[m3]+1]; 854 | if (case_value!=0) { 855 | switch (case_value) { 856 | case 1: // Line between vertices 1 and 2 857 | x1=xh[m1]; 858 | y1=yh[m1]; 859 | x2=xh[m2]; 860 | y2=yh[m2]; 861 | break; 862 | case 2: // Line between vertices 2 and 3 863 | x1=xh[m2]; 864 | y1=yh[m2]; 865 | x2=xh[m3]; 866 | y2=yh[m3]; 867 | break; 868 | case 3: // Line between vertices 3 and 1 869 | x1=xh[m3]; 870 | y1=yh[m3]; 871 | x2=xh[m1]; 872 | y2=yh[m1]; 873 | break; 874 | case 4: // Line between vertex 1 and side 2-3 875 | x1=xh[m1]; 876 | y1=yh[m1]; 877 | x2=xsect(m2,m3); 878 | y2=ysect(m2,m3); 879 | break; 880 | case 5: // Line between vertex 2 and side 3-1 881 | x1=xh[m2]; 882 | y1=yh[m2]; 883 | x2=xsect(m3,m1); 884 | y2=ysect(m3,m1); 885 | break; 886 | case 6: // Line between vertex 3 and side 1-2 887 | x1=xh[m3]; 888 | y1=yh[m3]; 889 | x2=xsect(m1,m2); 890 | y2=ysect(m1,m2); 891 | break; 892 | case 7: // Line between sides 1-2 and 2-3 893 | x1=xsect(m1,m2); 894 | y1=ysect(m1,m2); 895 | x2=xsect(m2,m3); 896 | y2=ysect(m2,m3); 897 | break; 898 | case 8: // Line between sides 2-3 and 3-1 899 | x1=xsect(m2,m3); 900 | y1=ysect(m2,m3); 901 | x2=xsect(m3,m1); 902 | y2=ysect(m3,m1); 903 | break; 904 | case 9: // Line between sides 3-1 and 1-2 905 | x1=xsect(m3,m1); 906 | y1=ysect(m3,m1); 907 | x2=xsect(m1,m2); 908 | y2=ysect(m1,m2); 909 | break; 910 | default: 911 | break; 912 | } 913 | // Put your processing code here and comment out the printf 914 | //printf("%f %f %f %f %f\n",x1,y1,x2,y2,z[k]); 915 | drawContour(x1,y1,x2,y2,z[k],k); 916 | } 917 | } 918 | } 919 | } 920 | } 921 | } 922 | } 923 | } 924 | })(typeof exports !== "undefined" ? exports : window); 925 | 926 | },{}],4:[function(require,module,exports){ 927 | /** 928 | * Wraps a GeoJSON {@link Geometry} in a GeoJSON {@link Feature}. 929 | * 930 | * @name feature 931 | * @param {Geometry} geometry input geometry 932 | * @param {Object} properties properties 933 | * @returns {Feature} a GeoJSON Feature 934 | * @example 935 | * var geometry = { 936 | * "type": "Point", 937 | * "coordinates": [110, 50] 938 | * }; 939 | * 940 | * var feature = turf.feature(geometry); 941 | * 942 | * //=feature 943 | */ 944 | function feature(geometry, properties) { 945 | if (!geometry) throw new Error('No geometry passed'); 946 | 947 | return { 948 | type: 'Feature', 949 | properties: properties || {}, 950 | geometry: geometry 951 | }; 952 | } 953 | 954 | /** 955 | * Takes coordinates and properties (optional) and returns a new {@link Point} feature. 956 | * 957 | * @name point 958 | * @param {Array} coordinates longitude, latitude position (each in decimal degrees) 959 | * @param {Object=} properties an Object that is used as the {@link Feature}'s 960 | * properties 961 | * @returns {Feature} a Point feature 962 | * @example 963 | * var point = turf.point([-75.343, 39.984]); 964 | * 965 | * //=point 966 | */ 967 | function point(coordinates, properties) { 968 | if (!coordinates) throw new Error('No coordinates passed'); 969 | if (coordinates.length === undefined) throw new Error('Coordinates must be an array'); 970 | if (coordinates.length < 2) throw new Error('Coordinates must be at least 2 numbers long'); 971 | if (typeof coordinates[0] !== 'number' || typeof coordinates[1] !== 'number') throw new Error('Coordinates must numbers'); 972 | 973 | return feature({ 974 | type: 'Point', 975 | coordinates: coordinates 976 | }, properties); 977 | } 978 | 979 | /** 980 | * Takes an array of LinearRings and optionally an {@link Object} with properties and returns a {@link Polygon} feature. 981 | * 982 | * @name polygon 983 | * @param {Array>>} coordinates an array of LinearRings 984 | * @param {Object=} properties a properties object 985 | * @returns {Feature} a Polygon feature 986 | * @throws {Error} throw an error if a LinearRing of the polygon has too few positions 987 | * or if a LinearRing of the Polygon does not have matching Positions at the beginning & end. 988 | * @example 989 | * var polygon = turf.polygon([[ 990 | * [-2.275543, 53.464547], 991 | * [-2.275543, 53.489271], 992 | * [-2.215118, 53.489271], 993 | * [-2.215118, 53.464547], 994 | * [-2.275543, 53.464547] 995 | * ]], { name: 'poly1', population: 400}); 996 | * 997 | * //=polygon 998 | */ 999 | function polygon(coordinates, properties) { 1000 | if (!coordinates) throw new Error('No coordinates passed'); 1001 | 1002 | for (var i = 0; i < coordinates.length; i++) { 1003 | var ring = coordinates[i]; 1004 | if (ring.length < 4) { 1005 | throw new Error('Each LinearRing of a Polygon must have 4 or more Positions.'); 1006 | } 1007 | for (var j = 0; j < ring[ring.length - 1].length; j++) { 1008 | if (ring[ring.length - 1][j] !== ring[0][j]) { 1009 | throw new Error('First and last Position are not equivalent.'); 1010 | } 1011 | } 1012 | } 1013 | 1014 | return feature({ 1015 | type: 'Polygon', 1016 | coordinates: coordinates 1017 | }, properties); 1018 | } 1019 | 1020 | /** 1021 | * Creates a {@link LineString} based on a 1022 | * coordinate array. Properties can be added optionally. 1023 | * 1024 | * @name lineString 1025 | * @param {Array>} coordinates an array of Positions 1026 | * @param {Object=} properties an Object of key-value pairs to add as properties 1027 | * @returns {Feature} a LineString feature 1028 | * @throws {Error} if no coordinates are passed 1029 | * @example 1030 | * var linestring1 = turf.lineString([ 1031 | * [-21.964416, 64.148203], 1032 | * [-21.956176, 64.141316], 1033 | * [-21.93901, 64.135924], 1034 | * [-21.927337, 64.136673] 1035 | * ]); 1036 | * var linestring2 = turf.lineString([ 1037 | * [-21.929054, 64.127985], 1038 | * [-21.912918, 64.134726], 1039 | * [-21.916007, 64.141016], 1040 | * [-21.930084, 64.14446] 1041 | * ], {name: 'line 1', distance: 145}); 1042 | * 1043 | * //=linestring1 1044 | * 1045 | * //=linestring2 1046 | */ 1047 | function lineString(coordinates, properties) { 1048 | if (!coordinates) throw new Error('No coordinates passed'); 1049 | if (coordinates.length < 2) throw new Error('Coordinates must be an array of two or more positions'); 1050 | 1051 | return feature({ 1052 | type: 'LineString', 1053 | coordinates: coordinates 1054 | }, properties); 1055 | } 1056 | 1057 | /** 1058 | * Takes one or more {@link Feature|Features} and creates a {@link FeatureCollection}. 1059 | * 1060 | * @name featureCollection 1061 | * @param {Feature[]} features input features 1062 | * @returns {FeatureCollection} a FeatureCollection of input features 1063 | * @example 1064 | * var features = [ 1065 | * turf.point([-75.343, 39.984], {name: 'Location A'}), 1066 | * turf.point([-75.833, 39.284], {name: 'Location B'}), 1067 | * turf.point([-75.534, 39.123], {name: 'Location C'}) 1068 | * ]; 1069 | * 1070 | * var collection = turf.featureCollection(features); 1071 | * 1072 | * //=collection 1073 | */ 1074 | function featureCollection(features) { 1075 | if (!features) throw new Error('No features passed'); 1076 | 1077 | return { 1078 | type: 'FeatureCollection', 1079 | features: features 1080 | }; 1081 | } 1082 | 1083 | /** 1084 | * Creates a {@link Feature} based on a 1085 | * coordinate array. Properties can be added optionally. 1086 | * 1087 | * @name multiLineString 1088 | * @param {Array>>} coordinates an array of LineStrings 1089 | * @param {Object=} properties an Object of key-value pairs to add as properties 1090 | * @returns {Feature} a MultiLineString feature 1091 | * @throws {Error} if no coordinates are passed 1092 | * @example 1093 | * var multiLine = turf.multiLineString([[[0,0],[10,10]]]); 1094 | * 1095 | * //=multiLine 1096 | */ 1097 | function multiLineString(coordinates, properties) { 1098 | if (!coordinates) throw new Error('No coordinates passed'); 1099 | 1100 | return feature({ 1101 | type: 'MultiLineString', 1102 | coordinates: coordinates 1103 | }, properties); 1104 | } 1105 | 1106 | /** 1107 | * Creates a {@link Feature} based on a 1108 | * coordinate array. Properties can be added optionally. 1109 | * 1110 | * @name multiPoint 1111 | * @param {Array>} coordinates an array of Positions 1112 | * @param {Object=} properties an Object of key-value pairs to add as properties 1113 | * @returns {Feature} a MultiPoint feature 1114 | * @throws {Error} if no coordinates are passed 1115 | * @example 1116 | * var multiPt = turf.multiPoint([[0,0],[10,10]]); 1117 | * 1118 | * //=multiPt 1119 | */ 1120 | function multiPoint(coordinates, properties) { 1121 | if (!coordinates) throw new Error('No coordinates passed'); 1122 | 1123 | return feature({ 1124 | type: 'MultiPoint', 1125 | coordinates: coordinates 1126 | }, properties); 1127 | } 1128 | 1129 | /** 1130 | * Creates a {@link Feature} based on a 1131 | * coordinate array. Properties can be added optionally. 1132 | * 1133 | * @name multiPolygon 1134 | * @param {Array>>>} coordinates an array of Polygons 1135 | * @param {Object=} properties an Object of key-value pairs to add as properties 1136 | * @returns {Feature} a multipolygon feature 1137 | * @throws {Error} if no coordinates are passed 1138 | * @example 1139 | * var multiPoly = turf.multiPolygon([[[[0,0],[0,10],[10,10],[10,0],[0,0]]]]); 1140 | * 1141 | * //=multiPoly 1142 | * 1143 | */ 1144 | function multiPolygon(coordinates, properties) { 1145 | if (!coordinates) throw new Error('No coordinates passed'); 1146 | 1147 | return feature({ 1148 | type: 'MultiPolygon', 1149 | coordinates: coordinates 1150 | }, properties); 1151 | } 1152 | 1153 | /** 1154 | * Creates a {@link Feature} based on a 1155 | * coordinate array. Properties can be added optionally. 1156 | * 1157 | * @name geometryCollection 1158 | * @param {Array<{Geometry}>} geometries an array of GeoJSON Geometries 1159 | * @param {Object=} properties an Object of key-value pairs to add as properties 1160 | * @returns {Feature} a GeoJSON GeometryCollection Feature 1161 | * @example 1162 | * var pt = { 1163 | * "type": "Point", 1164 | * "coordinates": [100, 0] 1165 | * }; 1166 | * var line = { 1167 | * "type": "LineString", 1168 | * "coordinates": [ [101, 0], [102, 1] ] 1169 | * }; 1170 | * var collection = turf.geometryCollection([pt, line]); 1171 | * 1172 | * //=collection 1173 | */ 1174 | function geometryCollection(geometries, properties) { 1175 | if (!geometries) throw new Error('geometries is required'); 1176 | 1177 | return feature({ 1178 | type: 'GeometryCollection', 1179 | geometries: geometries 1180 | }, properties); 1181 | } 1182 | 1183 | // https://en.wikipedia.org/wiki/Great-circle_distance#Radius_for_spherical_Earth 1184 | var factors = { 1185 | miles: 3960, 1186 | nauticalmiles: 3441.145, 1187 | degrees: 57.2957795, 1188 | radians: 1, 1189 | inches: 250905600, 1190 | yards: 6969600, 1191 | meters: 6373000, 1192 | metres: 6373000, 1193 | centimeters: 6.373e+8, 1194 | centimetres: 6.373e+8, 1195 | kilometers: 6373, 1196 | kilometres: 6373, 1197 | feet: 20908792.65 1198 | }; 1199 | 1200 | /** 1201 | * Round number to precision 1202 | * 1203 | * @param {number} num Number 1204 | * @param {number} [precision=0] Precision 1205 | * @returns {number} rounded number 1206 | * @example 1207 | * round(120.4321) 1208 | * //=120 1209 | * 1210 | * round(120.4321, 2) 1211 | * //=120.43 1212 | */ 1213 | function round(num, precision) { 1214 | if (num === undefined || num === null || isNaN(num)) throw new Error('num is required'); 1215 | if (precision && !(precision >= 0)) throw new Error('precision must be a positive number'); 1216 | var multiplier = Math.pow(10, precision || 0); 1217 | return Math.round(num * multiplier) / multiplier; 1218 | } 1219 | 1220 | /** 1221 | * Convert a distance measurement (assuming a spherical Earth) from radians to a more friendly unit. 1222 | * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet 1223 | * 1224 | * @name radiansToDistance 1225 | * @param {number} radians in radians across the sphere 1226 | * @param {string} [units=kilometers] can be degrees, radians, miles, or kilometers inches, yards, metres, meters, kilometres, kilometers. 1227 | * @returns {number} distance 1228 | */ 1229 | function radiansToDistance(radians, units) { 1230 | if (radians === undefined || radians === null) throw new Error('radians is required'); 1231 | 1232 | var factor = factors[units || 'kilometers']; 1233 | if (!factor) throw new Error('units is invalid'); 1234 | return radians * factor; 1235 | } 1236 | 1237 | /** 1238 | * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into radians 1239 | * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet 1240 | * 1241 | * @name distanceToRadians 1242 | * @param {number} distance in real units 1243 | * @param {string} [units=kilometers] can be degrees, radians, miles, or kilometers inches, yards, metres, meters, kilometres, kilometers. 1244 | * @returns {number} radians 1245 | */ 1246 | function distanceToRadians(distance, units) { 1247 | if (distance === undefined || distance === null) throw new Error('distance is required'); 1248 | 1249 | var factor = factors[units || 'kilometers']; 1250 | if (!factor) throw new Error('units is invalid'); 1251 | return distance / factor; 1252 | } 1253 | 1254 | /** 1255 | * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into degrees 1256 | * Valid units: miles, nauticalmiles, inches, yards, meters, metres, centimeters, kilometres, feet 1257 | * 1258 | * @name distanceToDegrees 1259 | * @param {number} distance in real units 1260 | * @param {string} [units=kilometers] can be degrees, radians, miles, or kilometers inches, yards, metres, meters, kilometres, kilometers. 1261 | * @returns {number} degrees 1262 | */ 1263 | function distanceToDegrees(distance, units) { 1264 | return radians2degrees(distanceToRadians(distance, units)); 1265 | } 1266 | 1267 | /** 1268 | * Converts any bearing angle from the north line direction (positive clockwise) 1269 | * and returns an angle between 0-360 degrees (positive clockwise), 0 being the north line 1270 | * 1271 | * @name bearingToAngle 1272 | * @param {number} bearing angle, between -180 and +180 degrees 1273 | * @returns {number} angle between 0 and 360 degrees 1274 | */ 1275 | function bearingToAngle(bearing) { 1276 | if (bearing === null || bearing === undefined) throw new Error('bearing is required'); 1277 | 1278 | var angle = bearing % 360; 1279 | if (angle < 0) angle += 360; 1280 | return angle; 1281 | } 1282 | 1283 | /** 1284 | * Converts an angle in radians to degrees 1285 | * 1286 | * @name radians2degrees 1287 | * @param {number} radians angle in radians 1288 | * @returns {number} degrees between 0 and 360 degrees 1289 | */ 1290 | function radians2degrees(radians) { 1291 | if (radians === null || radians === undefined) throw new Error('radians is required'); 1292 | 1293 | var degrees = radians % (2 * Math.PI); 1294 | return degrees * 180 / Math.PI; 1295 | } 1296 | 1297 | /** 1298 | * Converts an angle in degrees to radians 1299 | * 1300 | * @name degrees2radians 1301 | * @param {number} degrees angle between 0 and 360 degrees 1302 | * @returns {number} angle in radians 1303 | */ 1304 | function degrees2radians(degrees) { 1305 | if (degrees === null || degrees === undefined) throw new Error('degrees is required'); 1306 | 1307 | var radians = degrees % 360; 1308 | return radians * Math.PI / 180; 1309 | } 1310 | 1311 | 1312 | /** 1313 | * Converts a distance to the requested unit. 1314 | * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet 1315 | * 1316 | * @param {number} distance to be converted 1317 | * @param {string} originalUnit of the distance 1318 | * @param {string} [finalUnit=kilometers] returned unit 1319 | * @returns {number} the converted distance 1320 | */ 1321 | function convertDistance(distance, originalUnit, finalUnit) { 1322 | if (distance === null || distance === undefined) throw new Error('distance is required'); 1323 | if (!(distance >= 0)) throw new Error('distance must be a positive number'); 1324 | 1325 | var convertedDistance = radiansToDistance(distanceToRadians(distance, originalUnit), finalUnit || 'kilometers'); 1326 | return convertedDistance; 1327 | } 1328 | 1329 | 1330 | module.exports = { 1331 | feature: feature, 1332 | featureCollection: featureCollection, 1333 | geometryCollection: geometryCollection, 1334 | point: point, 1335 | multiPoint: multiPoint, 1336 | lineString: lineString, 1337 | multiLineString: multiLineString, 1338 | polygon: polygon, 1339 | multiPolygon: multiPolygon, 1340 | radiansToDistance: radiansToDistance, 1341 | distanceToRadians: distanceToRadians, 1342 | distanceToDegrees: distanceToDegrees, 1343 | radians2degrees: radians2degrees, 1344 | degrees2radians: degrees2radians, 1345 | bearingToAngle: bearingToAngle, 1346 | convertDistance: convertDistance, 1347 | round: round 1348 | }; 1349 | 1350 | },{}],5:[function(require,module,exports){ 1351 | var invariant = require('@turf/invariant'); 1352 | 1353 | // http://en.wikipedia.org/wiki/Even%E2%80%93odd_rule 1354 | // modified from: https://github.com/substack/point-in-polygon/blob/master/index.js 1355 | // which was modified from http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html 1356 | 1357 | /** 1358 | * Takes a {@link Point} and a {@link Polygon} or {@link MultiPolygon} and determines if the point resides inside the polygon. The polygon can 1359 | * be convex or concave. The function accounts for holes. 1360 | * 1361 | * @name inside 1362 | * @param {Feature} point input point 1363 | * @param {Feature<(Polygon|MultiPolygon)>} polygon input polygon or multipolygon 1364 | * @returns {boolean} `true` if the Point is inside the Polygon; `false` if the Point is not inside the Polygon 1365 | * @example 1366 | * var pt = turf.point([-77, 44]); 1367 | * var poly = turf.polygon([[ 1368 | * [-81, 41], 1369 | * [-81, 47], 1370 | * [-72, 47], 1371 | * [-72, 41], 1372 | * [-81, 41] 1373 | * ]]); 1374 | * 1375 | * var isInside = turf.inside(pt, poly); 1376 | * 1377 | * //=isInside 1378 | */ 1379 | module.exports = function (point, polygon) { 1380 | var pt = invariant.getCoord(point); 1381 | var polys = polygon.geometry.coordinates; 1382 | // normalize to multipolygon 1383 | if (polygon.geometry.type === 'Polygon') polys = [polys]; 1384 | 1385 | for (var i = 0, insidePoly = false; i < polys.length && !insidePoly; i++) { 1386 | // check if it is in the outer ring first 1387 | if (inRing(pt, polys[i][0])) { 1388 | var inHole = false; 1389 | var k = 1; 1390 | // check for the point in any of the holes 1391 | while (k < polys[i].length && !inHole) { 1392 | if (inRing(pt, polys[i][k], true)) { 1393 | inHole = true; 1394 | } 1395 | k++; 1396 | } 1397 | if (!inHole) insidePoly = true; 1398 | } 1399 | } 1400 | return insidePoly; 1401 | }; 1402 | 1403 | // pt is [x,y] and ring is [[x,y], [x,y],..] 1404 | function inRing(pt, ring, ignoreBoundary) { 1405 | var isInside = false; 1406 | if (ring[0][0] === ring[ring.length - 1][0] && ring[0][1] === ring[ring.length - 1][1]) ring = ring.slice(0, ring.length - 1); 1407 | 1408 | for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) { 1409 | var xi = ring[i][0], yi = ring[i][1]; 1410 | var xj = ring[j][0], yj = ring[j][1]; 1411 | var onBoundary = (pt[1] * (xi - xj) + yi * (xj - pt[0]) + yj * (pt[0] - xi) === 0) && 1412 | ((xi - pt[0]) * (xj - pt[0]) <= 0) && ((yi - pt[1]) * (yj - pt[1]) <= 0); 1413 | if (onBoundary) return !ignoreBoundary; 1414 | var intersect = ((yi > pt[1]) !== (yj > pt[1])) && 1415 | (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi); 1416 | if (intersect) isInside = !isInside; 1417 | } 1418 | return isInside; 1419 | } 1420 | 1421 | },{"@turf/invariant":6}],6:[function(require,module,exports){ 1422 | /** 1423 | * Unwrap a coordinate from a Point Feature, Geometry or a single coordinate. 1424 | * 1425 | * @param {Array|Geometry|Feature} obj any value 1426 | * @returns {Array} coordinates 1427 | */ 1428 | function getCoord(obj) { 1429 | if (!obj) throw new Error('No obj passed'); 1430 | 1431 | var coordinates = getCoords(obj); 1432 | 1433 | // getCoord() must contain at least two numbers (Point) 1434 | if (coordinates.length > 1 && 1435 | typeof coordinates[0] === 'number' && 1436 | typeof coordinates[1] === 'number') { 1437 | return coordinates; 1438 | } else { 1439 | throw new Error('Coordinate is not a valid Point'); 1440 | } 1441 | } 1442 | 1443 | /** 1444 | * Unwrap coordinates from a Feature, Geometry Object or an Array of numbers 1445 | * 1446 | * @param {Array|Geometry|Feature} obj any value 1447 | * @returns {Array} coordinates 1448 | */ 1449 | function getCoords(obj) { 1450 | if (!obj) throw new Error('No obj passed'); 1451 | var coordinates; 1452 | 1453 | // Array of numbers 1454 | if (obj.length) { 1455 | coordinates = obj; 1456 | 1457 | // Geometry Object 1458 | } else if (obj.coordinates) { 1459 | coordinates = obj.coordinates; 1460 | 1461 | // Feature 1462 | } else if (obj.geometry && obj.geometry.coordinates) { 1463 | coordinates = obj.geometry.coordinates; 1464 | } 1465 | // Checks if coordinates contains a number 1466 | if (coordinates) { 1467 | containsNumber(coordinates); 1468 | return coordinates; 1469 | } 1470 | throw new Error('No valid coordinates'); 1471 | } 1472 | 1473 | /** 1474 | * Checks if coordinates contains a number 1475 | * 1476 | * @private 1477 | * @param {Array} coordinates GeoJSON Coordinates 1478 | * @returns {boolean} true if Array contains a number 1479 | */ 1480 | function containsNumber(coordinates) { 1481 | if (coordinates.length > 1 && 1482 | typeof coordinates[0] === 'number' && 1483 | typeof coordinates[1] === 'number') { 1484 | return true; 1485 | } 1486 | if (coordinates[0].length) { 1487 | return containsNumber(coordinates[0]); 1488 | } 1489 | throw new Error('coordinates must only contain numbers'); 1490 | } 1491 | 1492 | /** 1493 | * Enforce expectations about types of GeoJSON objects for Turf. 1494 | * 1495 | * @alias geojsonType 1496 | * @param {GeoJSON} value any GeoJSON object 1497 | * @param {string} type expected GeoJSON type 1498 | * @param {string} name name of calling function 1499 | * @throws {Error} if value is not the expected type. 1500 | */ 1501 | function geojsonType(value, type, name) { 1502 | if (!type || !name) throw new Error('type and name required'); 1503 | 1504 | if (!value || value.type !== type) { 1505 | throw new Error('Invalid input to ' + name + ': must be a ' + type + ', given ' + value.type); 1506 | } 1507 | } 1508 | 1509 | /** 1510 | * Enforce expectations about types of {@link Feature} inputs for Turf. 1511 | * Internally this uses {@link geojsonType} to judge geometry types. 1512 | * 1513 | * @alias featureOf 1514 | * @param {Feature} feature a feature with an expected geometry type 1515 | * @param {string} type expected GeoJSON type 1516 | * @param {string} name name of calling function 1517 | * @throws {Error} error if value is not the expected type. 1518 | */ 1519 | function featureOf(feature, type, name) { 1520 | if (!feature) throw new Error('No feature passed'); 1521 | if (!name) throw new Error('.featureOf() requires a name'); 1522 | if (!feature || feature.type !== 'Feature' || !feature.geometry) { 1523 | throw new Error('Invalid input to ' + name + ', Feature with geometry required'); 1524 | } 1525 | if (!feature.geometry || feature.geometry.type !== type) { 1526 | throw new Error('Invalid input to ' + name + ': must be a ' + type + ', given ' + feature.geometry.type); 1527 | } 1528 | } 1529 | 1530 | /** 1531 | * Enforce expectations about types of {@link FeatureCollection} inputs for Turf. 1532 | * Internally this uses {@link geojsonType} to judge geometry types. 1533 | * 1534 | * @alias collectionOf 1535 | * @param {FeatureCollection} featureCollection a FeatureCollection for which features will be judged 1536 | * @param {string} type expected GeoJSON type 1537 | * @param {string} name name of calling function 1538 | * @throws {Error} if value is not the expected type. 1539 | */ 1540 | function collectionOf(featureCollection, type, name) { 1541 | if (!featureCollection) throw new Error('No featureCollection passed'); 1542 | if (!name) throw new Error('.collectionOf() requires a name'); 1543 | if (!featureCollection || featureCollection.type !== 'FeatureCollection') { 1544 | throw new Error('Invalid input to ' + name + ', FeatureCollection required'); 1545 | } 1546 | for (var i = 0; i < featureCollection.features.length; i++) { 1547 | var feature = featureCollection.features[i]; 1548 | if (!feature || feature.type !== 'Feature' || !feature.geometry) { 1549 | throw new Error('Invalid input to ' + name + ', Feature with geometry required'); 1550 | } 1551 | if (!feature.geometry || feature.geometry.type !== type) { 1552 | throw new Error('Invalid input to ' + name + ': must be a ' + type + ', given ' + feature.geometry.type); 1553 | } 1554 | } 1555 | } 1556 | 1557 | module.exports.geojsonType = geojsonType; 1558 | module.exports.collectionOf = collectionOf; 1559 | module.exports.featureOf = featureOf; 1560 | module.exports.getCoord = getCoord; 1561 | module.exports.getCoords = getCoords; 1562 | 1563 | },{}],7:[function(require,module,exports){ 1564 | 'use strict'; /* @flow */ 1565 | 1566 | module.exports = cheapRuler; 1567 | 1568 | /** 1569 | * A collection of very fast approximations to common geodesic measurements. Useful for performance-sensitive code that measures things on a city scale. 1570 | * 1571 | * @param {number} lat latitude 1572 | * @param {string} [units='kilometers'] 1573 | * @returns {CheapRuler} 1574 | * @example 1575 | * var ruler = cheapRuler(35.05, 'miles'); 1576 | * //=ruler 1577 | */ 1578 | function cheapRuler(lat /*: number */, units /*: ?string */) { 1579 | return new CheapRuler(lat, units); 1580 | } 1581 | 1582 | /** 1583 | * Multipliers for converting between units. 1584 | * 1585 | * @example 1586 | * // convert 50 meters to yards 1587 | * 50 * cheapRuler.units.yards / cheapRuler.units.meters; 1588 | */ 1589 | var factors = cheapRuler.units = { 1590 | kilometers: 1, 1591 | miles: 1000 / 1609.344, 1592 | nauticalmiles: 1000 / 1852, 1593 | meters: 1000, 1594 | metres: 1000, 1595 | yards: 1000 / 0.9144, 1596 | feet: 1000 / 0.3048, 1597 | inches: 1000 / 0.0254 1598 | }; 1599 | 1600 | /** 1601 | * Creates a ruler object from tile coordinates (y and z). Convenient in tile-reduce scripts. 1602 | * 1603 | * @param {number} y 1604 | * @param {number} z 1605 | * @param {string} [units='kilometers'] 1606 | * @returns {CheapRuler} 1607 | * @example 1608 | * var ruler = cheapRuler.fromTile(1567, 12); 1609 | * //=ruler 1610 | */ 1611 | cheapRuler.fromTile = function (y, z, units) { 1612 | var n = Math.PI * (1 - 2 * (y + 0.5) / Math.pow(2, z)); 1613 | var lat = Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))) * 180 / Math.PI; 1614 | return new CheapRuler(lat, units); 1615 | }; 1616 | 1617 | function CheapRuler(lat, units) { 1618 | if (lat === undefined) throw new Error('No latitude given.'); 1619 | if (units && !factors[units]) throw new Error('Unknown unit ' + units + '. Use one of: ' + Object.keys(factors)); 1620 | 1621 | var m = units ? factors[units] : 1; 1622 | 1623 | var cos = Math.cos(lat * Math.PI / 180); 1624 | var cos2 = 2 * cos * cos - 1; 1625 | var cos3 = 2 * cos * cos2 - cos; 1626 | var cos4 = 2 * cos * cos3 - cos2; 1627 | var cos5 = 2 * cos * cos4 - cos3; 1628 | 1629 | // multipliers for converting longitude and latitude degrees into distance (http://1.usa.gov/1Wb1bv7) 1630 | this.kx = m * (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5); 1631 | this.ky = m * (111.13209 - 0.56605 * cos2 + 0.0012 * cos4); 1632 | } 1633 | 1634 | CheapRuler.prototype = { 1635 | /** 1636 | * Given two points of the form [longitude, latitude], returns the distance. 1637 | * 1638 | * @param {Array} a point [longitude, latitude] 1639 | * @param {Array} b point [longitude, latitude] 1640 | * @returns {number} distance 1641 | * @example 1642 | * var distance = ruler.distance([30.5, 50.5], [30.51, 50.49]); 1643 | * //=distance 1644 | */ 1645 | distance: function (a, b) { 1646 | var dx = (a[0] - b[0]) * this.kx; 1647 | var dy = (a[1] - b[1]) * this.ky; 1648 | return Math.sqrt(dx * dx + dy * dy); 1649 | }, 1650 | 1651 | /** 1652 | * Returns the bearing between two points in angles. 1653 | * 1654 | * @param {Array} a point [longitude, latitude] 1655 | * @param {Array} b point [longitude, latitude] 1656 | * @returns {number} bearing 1657 | * @example 1658 | * var bearing = ruler.bearing([30.5, 50.5], [30.51, 50.49]); 1659 | * //=bearing 1660 | */ 1661 | bearing: function (a, b) { 1662 | var dx = (b[0] - a[0]) * this.kx; 1663 | var dy = (b[1] - a[1]) * this.ky; 1664 | if (!dx && !dy) return 0; 1665 | var bearing = Math.atan2(-dy, dx) * 180 / Math.PI + 90; 1666 | if (bearing > 180) bearing -= 360; 1667 | return bearing; 1668 | }, 1669 | 1670 | /** 1671 | * Returns a new point given distance and bearing from the starting point. 1672 | * 1673 | * @param {Array} p point [longitude, latitude] 1674 | * @param {number} dist distance 1675 | * @param {number} bearing 1676 | * @returns {Array} point [longitude, latitude] 1677 | * @example 1678 | * var point = ruler.destination([30.5, 50.5], 0.1, 90); 1679 | * //=point 1680 | */ 1681 | destination: function (p, dist, bearing) { 1682 | var a = (90 - bearing) * Math.PI / 180; 1683 | return this.offset(p, 1684 | Math.cos(a) * dist, 1685 | Math.sin(a) * dist); 1686 | }, 1687 | 1688 | /** 1689 | * Returns a new point given easting and northing offsets (in ruler units) from the starting point. 1690 | * 1691 | * @param {Array} p point [longitude, latitude] 1692 | * @param {number} dx easting 1693 | * @param {number} dy northing 1694 | * @returns {Array} point [longitude, latitude] 1695 | * @example 1696 | * var point = ruler.offset([30.5, 50.5], 10, 10); 1697 | * //=point 1698 | */ 1699 | offset: function (p, dx, dy) { 1700 | return [ 1701 | p[0] + dx / this.kx, 1702 | p[1] + dy / this.ky 1703 | ]; 1704 | }, 1705 | 1706 | /** 1707 | * Given a line (an array of points), returns the total line distance. 1708 | * 1709 | * @param {Array>} points [longitude, latitude] 1710 | * @returns {number} total line distance 1711 | * @example 1712 | * var length = ruler.lineDistance([ 1713 | * [-67.031, 50.458], [-67.031, 50.534], 1714 | * [-66.929, 50.534], [-66.929, 50.458] 1715 | * ]); 1716 | * //=length 1717 | */ 1718 | lineDistance: function (points) { 1719 | var total = 0; 1720 | for (var i = 0; i < points.length - 1; i++) { 1721 | total += this.distance(points[i], points[i + 1]); 1722 | } 1723 | return total; 1724 | }, 1725 | 1726 | /** 1727 | * Given a polygon (an array of rings, where each ring is an array of points), returns the area. 1728 | * 1729 | * @param {Array>>} polygon 1730 | * @returns {number} area value in the specified units (square kilometers by default) 1731 | * @example 1732 | * var area = ruler.area([[ 1733 | * [-67.031, 50.458], [-67.031, 50.534], [-66.929, 50.534], 1734 | * [-66.929, 50.458], [-67.031, 50.458] 1735 | * ]]); 1736 | * //=area 1737 | */ 1738 | area: function (polygon) { 1739 | var sum = 0; 1740 | 1741 | for (var i = 0; i < polygon.length; i++) { 1742 | var ring = polygon[i]; 1743 | 1744 | for (var j = 0, len = ring.length, k = len - 1; j < len; k = j++) { 1745 | sum += (ring[j][0] - ring[k][0]) * (ring[j][1] + ring[k][1]) * (i ? -1 : 1); 1746 | } 1747 | } 1748 | 1749 | return (Math.abs(sum) / 2) * this.kx * this.ky; 1750 | }, 1751 | 1752 | /** 1753 | * Returns the point at a specified distance along the line. 1754 | * 1755 | * @param {Array>} line 1756 | * @param {number} dist distance 1757 | * @returns {Array} point [longitude, latitude] 1758 | * @example 1759 | * var point = ruler.along(line, 2.5); 1760 | * //=point 1761 | */ 1762 | along: function (line, dist) { 1763 | var sum = 0; 1764 | 1765 | if (dist <= 0) return line[0]; 1766 | 1767 | for (var i = 0; i < line.length - 1; i++) { 1768 | var p0 = line[i]; 1769 | var p1 = line[i + 1]; 1770 | var d = this.distance(p0, p1); 1771 | sum += d; 1772 | if (sum > dist) return interpolate(p0, p1, (dist - (sum - d)) / d); 1773 | } 1774 | 1775 | return line[line.length - 1]; 1776 | }, 1777 | 1778 | /** 1779 | * Returns an object of the form {point, index} where point is closest point on the line from the given point, and index is the start index of the segment with the closest point. 1780 | * 1781 | * @pointOnLine 1782 | * @param {Array>} line 1783 | * @param {Array} p point [longitude, latitude] 1784 | * @returns {Object} {point, index} 1785 | * @example 1786 | * var point = ruler.pointOnLine(line, [-67.04, 50.5]).point; 1787 | * //=point 1788 | */ 1789 | pointOnLine: function (line, p) { 1790 | var minDist = Infinity; 1791 | var minX, minY, minI, minT; 1792 | 1793 | for (var i = 0; i < line.length - 1; i++) { 1794 | 1795 | var x = line[i][0]; 1796 | var y = line[i][1]; 1797 | var dx = (line[i + 1][0] - x) * this.kx; 1798 | var dy = (line[i + 1][1] - y) * this.ky; 1799 | 1800 | if (dx !== 0 || dy !== 0) { 1801 | 1802 | var t = ((p[0] - x) * this.kx * dx + (p[1] - y) * this.ky * dy) / (dx * dx + dy * dy); 1803 | 1804 | if (t > 1) { 1805 | x = line[i + 1][0]; 1806 | y = line[i + 1][1]; 1807 | 1808 | } else if (t > 0) { 1809 | x += (dx / this.kx) * t; 1810 | y += (dy / this.ky) * t; 1811 | } 1812 | } 1813 | 1814 | dx = (p[0] - x) * this.kx; 1815 | dy = (p[1] - y) * this.ky; 1816 | 1817 | var sqDist = dx * dx + dy * dy; 1818 | if (sqDist < minDist) { 1819 | minDist = sqDist; 1820 | minX = x; 1821 | minY = y; 1822 | minI = i; 1823 | minT = t; 1824 | } 1825 | } 1826 | 1827 | return { 1828 | point: [minX, minY], 1829 | index: minI, 1830 | t: minT 1831 | }; 1832 | }, 1833 | 1834 | /** 1835 | * Returns a part of the given line between the start and the stop points (or their closest points on the line). 1836 | * 1837 | * @param {Array} start point [longitude, latitude] 1838 | * @param {Array} stop point [longitude, latitude] 1839 | * @param {Array>} line 1840 | * @returns {Array>} line part of a line 1841 | * @example 1842 | * var line2 = ruler.lineSlice([-67.04, 50.5], [-67.05, 50.56], line1); 1843 | * //=line2 1844 | */ 1845 | lineSlice: function (start, stop, line) { 1846 | var p1 = this.pointOnLine(line, start); 1847 | var p2 = this.pointOnLine(line, stop); 1848 | 1849 | if (p1.index > p2.index || (p1.index === p2.index && p1.t > p2.t)) { 1850 | var tmp = p1; 1851 | p1 = p2; 1852 | p2 = tmp; 1853 | } 1854 | 1855 | var slice = [p1.point]; 1856 | 1857 | var l = p1.index + 1; 1858 | var r = p2.index; 1859 | 1860 | if (!equals(line[l], slice[0]) && l <= r) 1861 | slice.push(line[l]); 1862 | 1863 | for (var i = l + 1; i <= r; i++) { 1864 | slice.push(line[i]); 1865 | } 1866 | 1867 | if (!equals(line[r], p2.point)) 1868 | slice.push(p2.point); 1869 | 1870 | return slice; 1871 | }, 1872 | 1873 | /** 1874 | * Returns a part of the given line between the start and the stop points indicated by distance along the line. 1875 | * 1876 | * @param {number} start distance 1877 | * @param {number} stop distance 1878 | * @param {Array>} line 1879 | * @returns {Array>} line part of a line 1880 | * @example 1881 | * var line2 = ruler.lineSliceAlong(10, 20, line1); 1882 | * //=line2 1883 | */ 1884 | lineSliceAlong: function (start, stop, line) { 1885 | var sum = 0; 1886 | var slice = []; 1887 | 1888 | for (var i = 0; i < line.length - 1; i++) { 1889 | var p0 = line[i]; 1890 | var p1 = line[i + 1]; 1891 | var d = this.distance(p0, p1); 1892 | 1893 | sum += d; 1894 | 1895 | if (sum > start && slice.length === 0) { 1896 | slice.push(interpolate(p0, p1, (start - (sum - d)) / d)); 1897 | } 1898 | 1899 | if (sum >= stop) { 1900 | slice.push(interpolate(p0, p1, (stop - (sum - d)) / d)); 1901 | return slice; 1902 | } 1903 | 1904 | if (sum > start) slice.push(p1); 1905 | } 1906 | 1907 | return slice; 1908 | }, 1909 | 1910 | /** 1911 | * Given a point, returns a bounding box object ([w, s, e, n]) created from the given point buffered by a given distance. 1912 | * 1913 | * @param {Array} p point [longitude, latitude] 1914 | * @param {number} buffer 1915 | * @returns {Array} box object ([w, s, e, n]) 1916 | * @example 1917 | * var bbox = ruler.bufferPoint([30.5, 50.5], 0.01); 1918 | * //=bbox 1919 | */ 1920 | bufferPoint: function (p, buffer) { 1921 | var v = buffer / this.ky; 1922 | var h = buffer / this.kx; 1923 | return [ 1924 | p[0] - h, 1925 | p[1] - v, 1926 | p[0] + h, 1927 | p[1] + v 1928 | ]; 1929 | }, 1930 | 1931 | /** 1932 | * Given a bounding box, returns the box buffered by a given distance. 1933 | * 1934 | * @param {Array} box object ([w, s, e, n]) 1935 | * @param {number} buffer 1936 | * @returns {Array} box object ([w, s, e, n]) 1937 | * @example 1938 | * var bbox = ruler.bufferBBox([30.5, 50.5, 31, 51], 0.2); 1939 | * //=bbox 1940 | */ 1941 | bufferBBox: function (bbox, buffer) { 1942 | var v = buffer / this.ky; 1943 | var h = buffer / this.kx; 1944 | return [ 1945 | bbox[0] - h, 1946 | bbox[1] - v, 1947 | bbox[2] + h, 1948 | bbox[3] + v 1949 | ]; 1950 | }, 1951 | 1952 | /** 1953 | * Returns true if the given point is inside in the given bounding box, otherwise false. 1954 | * 1955 | * @param {Array} p point [longitude, latitude] 1956 | * @param {Array} box object ([w, s, e, n]) 1957 | * @returns {boolean} 1958 | * @example 1959 | * var inside = ruler.insideBBox([30.5, 50.5], [30, 50, 31, 51]); 1960 | * //=inside 1961 | */ 1962 | insideBBox: function (p, bbox) { 1963 | return p[0] >= bbox[0] && 1964 | p[0] <= bbox[2] && 1965 | p[1] >= bbox[1] && 1966 | p[1] <= bbox[3]; 1967 | } 1968 | }; 1969 | 1970 | function equals(a, b) { 1971 | return a[0] === b[0] && a[1] === b[1]; 1972 | } 1973 | 1974 | function interpolate(a, b, t) { 1975 | var dx = b[0] - a[0]; 1976 | var dy = b[1] - a[1]; 1977 | return [ 1978 | a[0] + dx * t, 1979 | a[1] + dy * t 1980 | ]; 1981 | } 1982 | 1983 | },{}],8:[function(require,module,exports){ 1984 | // https://d3js.org/d3-collection/ Version 1.0.3. Copyright 2017 Mike Bostock. 1985 | (function (global, factory) { 1986 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 1987 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 1988 | (factory((global.d3 = global.d3 || {}))); 1989 | }(this, (function (exports) { 'use strict'; 1990 | 1991 | var prefix = "$"; 1992 | 1993 | function Map() {} 1994 | 1995 | Map.prototype = map.prototype = { 1996 | constructor: Map, 1997 | has: function(key) { 1998 | return (prefix + key) in this; 1999 | }, 2000 | get: function(key) { 2001 | return this[prefix + key]; 2002 | }, 2003 | set: function(key, value) { 2004 | this[prefix + key] = value; 2005 | return this; 2006 | }, 2007 | remove: function(key) { 2008 | var property = prefix + key; 2009 | return property in this && delete this[property]; 2010 | }, 2011 | clear: function() { 2012 | for (var property in this) if (property[0] === prefix) delete this[property]; 2013 | }, 2014 | keys: function() { 2015 | var keys = []; 2016 | for (var property in this) if (property[0] === prefix) keys.push(property.slice(1)); 2017 | return keys; 2018 | }, 2019 | values: function() { 2020 | var values = []; 2021 | for (var property in this) if (property[0] === prefix) values.push(this[property]); 2022 | return values; 2023 | }, 2024 | entries: function() { 2025 | var entries = []; 2026 | for (var property in this) if (property[0] === prefix) entries.push({key: property.slice(1), value: this[property]}); 2027 | return entries; 2028 | }, 2029 | size: function() { 2030 | var size = 0; 2031 | for (var property in this) if (property[0] === prefix) ++size; 2032 | return size; 2033 | }, 2034 | empty: function() { 2035 | for (var property in this) if (property[0] === prefix) return false; 2036 | return true; 2037 | }, 2038 | each: function(f) { 2039 | for (var property in this) if (property[0] === prefix) f(this[property], property.slice(1), this); 2040 | } 2041 | }; 2042 | 2043 | function map(object, f) { 2044 | var map = new Map; 2045 | 2046 | // Copy constructor. 2047 | if (object instanceof Map) object.each(function(value, key) { map.set(key, value); }); 2048 | 2049 | // Index array by numeric index or specified key function. 2050 | else if (Array.isArray(object)) { 2051 | var i = -1, 2052 | n = object.length, 2053 | o; 2054 | 2055 | if (f == null) while (++i < n) map.set(i, object[i]); 2056 | else while (++i < n) map.set(f(o = object[i], i, object), o); 2057 | } 2058 | 2059 | // Convert object to map. 2060 | else if (object) for (var key in object) map.set(key, object[key]); 2061 | 2062 | return map; 2063 | } 2064 | 2065 | var nest = function() { 2066 | var keys = [], 2067 | sortKeys = [], 2068 | sortValues, 2069 | rollup, 2070 | nest; 2071 | 2072 | function apply(array, depth, createResult, setResult) { 2073 | if (depth >= keys.length) return rollup != null 2074 | ? rollup(array) : (sortValues != null 2075 | ? array.sort(sortValues) 2076 | : array); 2077 | 2078 | var i = -1, 2079 | n = array.length, 2080 | key = keys[depth++], 2081 | keyValue, 2082 | value, 2083 | valuesByKey = map(), 2084 | values, 2085 | result = createResult(); 2086 | 2087 | while (++i < n) { 2088 | if (values = valuesByKey.get(keyValue = key(value = array[i]) + "")) { 2089 | values.push(value); 2090 | } else { 2091 | valuesByKey.set(keyValue, [value]); 2092 | } 2093 | } 2094 | 2095 | valuesByKey.each(function(values, key) { 2096 | setResult(result, key, apply(values, depth, createResult, setResult)); 2097 | }); 2098 | 2099 | return result; 2100 | } 2101 | 2102 | function entries(map$$1, depth) { 2103 | if (++depth > keys.length) return map$$1; 2104 | var array, sortKey = sortKeys[depth - 1]; 2105 | if (rollup != null && depth >= keys.length) array = map$$1.entries(); 2106 | else array = [], map$$1.each(function(v, k) { array.push({key: k, values: entries(v, depth)}); }); 2107 | return sortKey != null ? array.sort(function(a, b) { return sortKey(a.key, b.key); }) : array; 2108 | } 2109 | 2110 | return nest = { 2111 | object: function(array) { return apply(array, 0, createObject, setObject); }, 2112 | map: function(array) { return apply(array, 0, createMap, setMap); }, 2113 | entries: function(array) { return entries(apply(array, 0, createMap, setMap), 0); }, 2114 | key: function(d) { keys.push(d); return nest; }, 2115 | sortKeys: function(order) { sortKeys[keys.length - 1] = order; return nest; }, 2116 | sortValues: function(order) { sortValues = order; return nest; }, 2117 | rollup: function(f) { rollup = f; return nest; } 2118 | }; 2119 | }; 2120 | 2121 | function createObject() { 2122 | return {}; 2123 | } 2124 | 2125 | function setObject(object, key, value) { 2126 | object[key] = value; 2127 | } 2128 | 2129 | function createMap() { 2130 | return map(); 2131 | } 2132 | 2133 | function setMap(map$$1, key, value) { 2134 | map$$1.set(key, value); 2135 | } 2136 | 2137 | function Set() {} 2138 | 2139 | var proto = map.prototype; 2140 | 2141 | Set.prototype = set.prototype = { 2142 | constructor: Set, 2143 | has: proto.has, 2144 | add: function(value) { 2145 | value += ""; 2146 | this[prefix + value] = value; 2147 | return this; 2148 | }, 2149 | remove: proto.remove, 2150 | clear: proto.clear, 2151 | values: proto.keys, 2152 | size: proto.size, 2153 | empty: proto.empty, 2154 | each: proto.each 2155 | }; 2156 | 2157 | function set(object, f) { 2158 | var set = new Set; 2159 | 2160 | // Copy constructor. 2161 | if (object instanceof Set) object.each(function(value) { set.add(value); }); 2162 | 2163 | // Otherwise, assume it’s an array. 2164 | else if (object) { 2165 | var i = -1, n = object.length; 2166 | if (f == null) while (++i < n) set.add(object[i]); 2167 | else while (++i < n) set.add(f(object[i], i, object)); 2168 | } 2169 | 2170 | return set; 2171 | } 2172 | 2173 | var keys = function(map) { 2174 | var keys = []; 2175 | for (var key in map) keys.push(key); 2176 | return keys; 2177 | }; 2178 | 2179 | var values = function(map) { 2180 | var values = []; 2181 | for (var key in map) values.push(map[key]); 2182 | return values; 2183 | }; 2184 | 2185 | var entries = function(map) { 2186 | var entries = []; 2187 | for (var key in map) entries.push({key: key, value: map[key]}); 2188 | return entries; 2189 | }; 2190 | 2191 | exports.nest = nest; 2192 | exports.set = set; 2193 | exports.map = map; 2194 | exports.keys = keys; 2195 | exports.values = values; 2196 | exports.entries = entries; 2197 | 2198 | Object.defineProperty(exports, '__esModule', { value: true }); 2199 | 2200 | }))); 2201 | 2202 | },{}],9:[function(require,module,exports){ 2203 | // https://d3js.org/d3-dispatch/ Version 1.0.3. Copyright 2017 Mike Bostock. 2204 | (function (global, factory) { 2205 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 2206 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 2207 | (factory((global.d3 = global.d3 || {}))); 2208 | }(this, (function (exports) { 'use strict'; 2209 | 2210 | var noop = {value: function() {}}; 2211 | 2212 | function dispatch() { 2213 | for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) { 2214 | if (!(t = arguments[i] + "") || (t in _)) throw new Error("illegal type: " + t); 2215 | _[t] = []; 2216 | } 2217 | return new Dispatch(_); 2218 | } 2219 | 2220 | function Dispatch(_) { 2221 | this._ = _; 2222 | } 2223 | 2224 | function parseTypenames(typenames, types) { 2225 | return typenames.trim().split(/^|\s+/).map(function(t) { 2226 | var name = "", i = t.indexOf("."); 2227 | if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i); 2228 | if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t); 2229 | return {type: t, name: name}; 2230 | }); 2231 | } 2232 | 2233 | Dispatch.prototype = dispatch.prototype = { 2234 | constructor: Dispatch, 2235 | on: function(typename, callback) { 2236 | var _ = this._, 2237 | T = parseTypenames(typename + "", _), 2238 | t, 2239 | i = -1, 2240 | n = T.length; 2241 | 2242 | // If no callback was specified, return the callback of the given type and name. 2243 | if (arguments.length < 2) { 2244 | while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t; 2245 | return; 2246 | } 2247 | 2248 | // If a type was specified, set the callback for the given type and name. 2249 | // Otherwise, if a null callback was specified, remove callbacks of the given name. 2250 | if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback); 2251 | while (++i < n) { 2252 | if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback); 2253 | else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null); 2254 | } 2255 | 2256 | return this; 2257 | }, 2258 | copy: function() { 2259 | var copy = {}, _ = this._; 2260 | for (var t in _) copy[t] = _[t].slice(); 2261 | return new Dispatch(copy); 2262 | }, 2263 | call: function(type, that) { 2264 | if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2]; 2265 | if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type); 2266 | for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args); 2267 | }, 2268 | apply: function(type, that, args) { 2269 | if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type); 2270 | for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args); 2271 | } 2272 | }; 2273 | 2274 | function get(type, name) { 2275 | for (var i = 0, n = type.length, c; i < n; ++i) { 2276 | if ((c = type[i]).name === name) { 2277 | return c.value; 2278 | } 2279 | } 2280 | } 2281 | 2282 | function set(type, name, callback) { 2283 | for (var i = 0, n = type.length; i < n; ++i) { 2284 | if (type[i].name === name) { 2285 | type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1)); 2286 | break; 2287 | } 2288 | } 2289 | if (callback != null) type.push({name: name, value: callback}); 2290 | return type; 2291 | } 2292 | 2293 | exports.dispatch = dispatch; 2294 | 2295 | Object.defineProperty(exports, '__esModule', { value: true }); 2296 | 2297 | }))); 2298 | 2299 | },{}],10:[function(require,module,exports){ 2300 | // https://d3js.org/d3-dsv/ Version 1.0.5. Copyright 2017 Mike Bostock. 2301 | (function (global, factory) { 2302 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 2303 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 2304 | (factory((global.d3 = global.d3 || {}))); 2305 | }(this, (function (exports) { 'use strict'; 2306 | 2307 | function objectConverter(columns) { 2308 | return new Function("d", "return {" + columns.map(function(name, i) { 2309 | return JSON.stringify(name) + ": d[" + i + "]"; 2310 | }).join(",") + "}"); 2311 | } 2312 | 2313 | function customConverter(columns, f) { 2314 | var object = objectConverter(columns); 2315 | return function(row, i) { 2316 | return f(object(row), i, columns); 2317 | }; 2318 | } 2319 | 2320 | // Compute unique columns in order of discovery. 2321 | function inferColumns(rows) { 2322 | var columnSet = Object.create(null), 2323 | columns = []; 2324 | 2325 | rows.forEach(function(row) { 2326 | for (var column in row) { 2327 | if (!(column in columnSet)) { 2328 | columns.push(columnSet[column] = column); 2329 | } 2330 | } 2331 | }); 2332 | 2333 | return columns; 2334 | } 2335 | 2336 | var dsv = function(delimiter) { 2337 | var reFormat = new RegExp("[\"" + delimiter + "\n\r]"), 2338 | delimiterCode = delimiter.charCodeAt(0); 2339 | 2340 | function parse(text, f) { 2341 | var convert, columns, rows = parseRows(text, function(row, i) { 2342 | if (convert) return convert(row, i - 1); 2343 | columns = row, convert = f ? customConverter(row, f) : objectConverter(row); 2344 | }); 2345 | rows.columns = columns; 2346 | return rows; 2347 | } 2348 | 2349 | function parseRows(text, f) { 2350 | var EOL = {}, // sentinel value for end-of-line 2351 | EOF = {}, // sentinel value for end-of-file 2352 | rows = [], // output rows 2353 | N = text.length, 2354 | I = 0, // current character index 2355 | n = 0, // the current line number 2356 | t, // the current token 2357 | eol; // is the current token followed by EOL? 2358 | 2359 | function token() { 2360 | if (I >= N) return EOF; // special case: end of file 2361 | if (eol) return eol = false, EOL; // special case: end of line 2362 | 2363 | // special case: quotes 2364 | var j = I, c; 2365 | if (text.charCodeAt(j) === 34) { 2366 | var i = j; 2367 | while (i++ < N) { 2368 | if (text.charCodeAt(i) === 34) { 2369 | if (text.charCodeAt(i + 1) !== 34) break; 2370 | ++i; 2371 | } 2372 | } 2373 | I = i + 2; 2374 | c = text.charCodeAt(i + 1); 2375 | if (c === 13) { 2376 | eol = true; 2377 | if (text.charCodeAt(i + 2) === 10) ++I; 2378 | } else if (c === 10) { 2379 | eol = true; 2380 | } 2381 | return text.slice(j + 1, i).replace(/""/g, "\""); 2382 | } 2383 | 2384 | // common case: find next delimiter or newline 2385 | while (I < N) { 2386 | var k = 1; 2387 | c = text.charCodeAt(I++); 2388 | if (c === 10) eol = true; // \n 2389 | else if (c === 13) { eol = true; if (text.charCodeAt(I) === 10) ++I, ++k; } // \r|\r\n 2390 | else if (c !== delimiterCode) continue; 2391 | return text.slice(j, I - k); 2392 | } 2393 | 2394 | // special case: last token before EOF 2395 | return text.slice(j); 2396 | } 2397 | 2398 | while ((t = token()) !== EOF) { 2399 | var a = []; 2400 | while (t !== EOL && t !== EOF) { 2401 | a.push(t); 2402 | t = token(); 2403 | } 2404 | if (f && (a = f(a, n++)) == null) continue; 2405 | rows.push(a); 2406 | } 2407 | 2408 | return rows; 2409 | } 2410 | 2411 | function format(rows, columns) { 2412 | if (columns == null) columns = inferColumns(rows); 2413 | return [columns.map(formatValue).join(delimiter)].concat(rows.map(function(row) { 2414 | return columns.map(function(column) { 2415 | return formatValue(row[column]); 2416 | }).join(delimiter); 2417 | })).join("\n"); 2418 | } 2419 | 2420 | function formatRows(rows) { 2421 | return rows.map(formatRow).join("\n"); 2422 | } 2423 | 2424 | function formatRow(row) { 2425 | return row.map(formatValue).join(delimiter); 2426 | } 2427 | 2428 | function formatValue(text) { 2429 | return text == null ? "" 2430 | : reFormat.test(text += "") ? "\"" + text.replace(/\"/g, "\"\"") + "\"" 2431 | : text; 2432 | } 2433 | 2434 | return { 2435 | parse: parse, 2436 | parseRows: parseRows, 2437 | format: format, 2438 | formatRows: formatRows 2439 | }; 2440 | }; 2441 | 2442 | var csv = dsv(","); 2443 | 2444 | var csvParse = csv.parse; 2445 | var csvParseRows = csv.parseRows; 2446 | var csvFormat = csv.format; 2447 | var csvFormatRows = csv.formatRows; 2448 | 2449 | var tsv = dsv("\t"); 2450 | 2451 | var tsvParse = tsv.parse; 2452 | var tsvParseRows = tsv.parseRows; 2453 | var tsvFormat = tsv.format; 2454 | var tsvFormatRows = tsv.formatRows; 2455 | 2456 | exports.dsvFormat = dsv; 2457 | exports.csvParse = csvParse; 2458 | exports.csvParseRows = csvParseRows; 2459 | exports.csvFormat = csvFormat; 2460 | exports.csvFormatRows = csvFormatRows; 2461 | exports.tsvParse = tsvParse; 2462 | exports.tsvParseRows = tsvParseRows; 2463 | exports.tsvFormat = tsvFormat; 2464 | exports.tsvFormatRows = tsvFormatRows; 2465 | 2466 | Object.defineProperty(exports, '__esModule', { value: true }); 2467 | 2468 | }))); 2469 | 2470 | },{}],11:[function(require,module,exports){ 2471 | // https://d3js.org/d3-request/ Version 1.0.5. Copyright 2017 Mike Bostock. 2472 | (function (global, factory) { 2473 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-collection'), require('d3-dispatch'), require('d3-dsv')) : 2474 | typeof define === 'function' && define.amd ? define(['exports', 'd3-collection', 'd3-dispatch', 'd3-dsv'], factory) : 2475 | (factory((global.d3 = global.d3 || {}),global.d3,global.d3,global.d3)); 2476 | }(this, (function (exports,d3Collection,d3Dispatch,d3Dsv) { 'use strict'; 2477 | 2478 | var request = function(url, callback) { 2479 | var request, 2480 | event = d3Dispatch.dispatch("beforesend", "progress", "load", "error"), 2481 | mimeType, 2482 | headers = d3Collection.map(), 2483 | xhr = new XMLHttpRequest, 2484 | user = null, 2485 | password = null, 2486 | response, 2487 | responseType, 2488 | timeout = 0; 2489 | 2490 | // If IE does not support CORS, use XDomainRequest. 2491 | if (typeof XDomainRequest !== "undefined" 2492 | && !("withCredentials" in xhr) 2493 | && /^(http(s)?:)?\/\//.test(url)) xhr = new XDomainRequest; 2494 | 2495 | "onload" in xhr 2496 | ? xhr.onload = xhr.onerror = xhr.ontimeout = respond 2497 | : xhr.onreadystatechange = function(o) { xhr.readyState > 3 && respond(o); }; 2498 | 2499 | function respond(o) { 2500 | var status = xhr.status, result; 2501 | if (!status && hasResponse(xhr) 2502 | || status >= 200 && status < 300 2503 | || status === 304) { 2504 | if (response) { 2505 | try { 2506 | result = response.call(request, xhr); 2507 | } catch (e) { 2508 | event.call("error", request, e); 2509 | return; 2510 | } 2511 | } else { 2512 | result = xhr; 2513 | } 2514 | event.call("load", request, result); 2515 | } else { 2516 | event.call("error", request, o); 2517 | } 2518 | } 2519 | 2520 | xhr.onprogress = function(e) { 2521 | event.call("progress", request, e); 2522 | }; 2523 | 2524 | request = { 2525 | header: function(name, value) { 2526 | name = (name + "").toLowerCase(); 2527 | if (arguments.length < 2) return headers.get(name); 2528 | if (value == null) headers.remove(name); 2529 | else headers.set(name, value + ""); 2530 | return request; 2531 | }, 2532 | 2533 | // If mimeType is non-null and no Accept header is set, a default is used. 2534 | mimeType: function(value) { 2535 | if (!arguments.length) return mimeType; 2536 | mimeType = value == null ? null : value + ""; 2537 | return request; 2538 | }, 2539 | 2540 | // Specifies what type the response value should take; 2541 | // for instance, arraybuffer, blob, document, or text. 2542 | responseType: function(value) { 2543 | if (!arguments.length) return responseType; 2544 | responseType = value; 2545 | return request; 2546 | }, 2547 | 2548 | timeout: function(value) { 2549 | if (!arguments.length) return timeout; 2550 | timeout = +value; 2551 | return request; 2552 | }, 2553 | 2554 | user: function(value) { 2555 | return arguments.length < 1 ? user : (user = value == null ? null : value + "", request); 2556 | }, 2557 | 2558 | password: function(value) { 2559 | return arguments.length < 1 ? password : (password = value == null ? null : value + "", request); 2560 | }, 2561 | 2562 | // Specify how to convert the response content to a specific type; 2563 | // changes the callback value on "load" events. 2564 | response: function(value) { 2565 | response = value; 2566 | return request; 2567 | }, 2568 | 2569 | // Alias for send("GET", …). 2570 | get: function(data, callback) { 2571 | return request.send("GET", data, callback); 2572 | }, 2573 | 2574 | // Alias for send("POST", …). 2575 | post: function(data, callback) { 2576 | return request.send("POST", data, callback); 2577 | }, 2578 | 2579 | // If callback is non-null, it will be used for error and load events. 2580 | send: function(method, data, callback) { 2581 | xhr.open(method, url, true, user, password); 2582 | if (mimeType != null && !headers.has("accept")) headers.set("accept", mimeType + ",*/*"); 2583 | if (xhr.setRequestHeader) headers.each(function(value, name) { xhr.setRequestHeader(name, value); }); 2584 | if (mimeType != null && xhr.overrideMimeType) xhr.overrideMimeType(mimeType); 2585 | if (responseType != null) xhr.responseType = responseType; 2586 | if (timeout > 0) xhr.timeout = timeout; 2587 | if (callback == null && typeof data === "function") callback = data, data = null; 2588 | if (callback != null && callback.length === 1) callback = fixCallback(callback); 2589 | if (callback != null) request.on("error", callback).on("load", function(xhr) { callback(null, xhr); }); 2590 | event.call("beforesend", request, xhr); 2591 | xhr.send(data == null ? null : data); 2592 | return request; 2593 | }, 2594 | 2595 | abort: function() { 2596 | xhr.abort(); 2597 | return request; 2598 | }, 2599 | 2600 | on: function() { 2601 | var value = event.on.apply(event, arguments); 2602 | return value === event ? request : value; 2603 | } 2604 | }; 2605 | 2606 | if (callback != null) { 2607 | if (typeof callback !== "function") throw new Error("invalid callback: " + callback); 2608 | return request.get(callback); 2609 | } 2610 | 2611 | return request; 2612 | }; 2613 | 2614 | function fixCallback(callback) { 2615 | return function(error, xhr) { 2616 | callback(error == null ? xhr : null); 2617 | }; 2618 | } 2619 | 2620 | function hasResponse(xhr) { 2621 | var type = xhr.responseType; 2622 | return type && type !== "text" 2623 | ? xhr.response // null on error 2624 | : xhr.responseText; // "" on error 2625 | } 2626 | 2627 | var type = function(defaultMimeType, response) { 2628 | return function(url, callback) { 2629 | var r = request(url).mimeType(defaultMimeType).response(response); 2630 | if (callback != null) { 2631 | if (typeof callback !== "function") throw new Error("invalid callback: " + callback); 2632 | return r.get(callback); 2633 | } 2634 | return r; 2635 | }; 2636 | }; 2637 | 2638 | var html = type("text/html", function(xhr) { 2639 | return document.createRange().createContextualFragment(xhr.responseText); 2640 | }); 2641 | 2642 | var json = type("application/json", function(xhr) { 2643 | return JSON.parse(xhr.responseText); 2644 | }); 2645 | 2646 | var text = type("text/plain", function(xhr) { 2647 | return xhr.responseText; 2648 | }); 2649 | 2650 | var xml = type("application/xml", function(xhr) { 2651 | var xml = xhr.responseXML; 2652 | if (!xml) throw new Error("parse error"); 2653 | return xml; 2654 | }); 2655 | 2656 | var dsv = function(defaultMimeType, parse) { 2657 | return function(url, row, callback) { 2658 | if (arguments.length < 3) callback = row, row = null; 2659 | var r = request(url).mimeType(defaultMimeType); 2660 | r.row = function(_) { return arguments.length ? r.response(responseOf(parse, row = _)) : row; }; 2661 | r.row(row); 2662 | return callback ? r.get(callback) : r; 2663 | }; 2664 | }; 2665 | 2666 | function responseOf(parse, row) { 2667 | return function(request$$1) { 2668 | return parse(request$$1.responseText, row); 2669 | }; 2670 | } 2671 | 2672 | var csv = dsv("text/csv", d3Dsv.csvParse); 2673 | 2674 | var tsv = dsv("text/tab-separated-values", d3Dsv.tsvParse); 2675 | 2676 | exports.request = request; 2677 | exports.html = html; 2678 | exports.json = json; 2679 | exports.text = text; 2680 | exports.xml = xml; 2681 | exports.csv = csv; 2682 | exports.tsv = tsv; 2683 | 2684 | Object.defineProperty(exports, '__esModule', { value: true }); 2685 | 2686 | }))); 2687 | 2688 | },{"d3-collection":8,"d3-dispatch":9,"d3-dsv":10}]},{},[1]); 2689 | -------------------------------------------------------------------------------- /test/fixtures/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "badToken": { 3 | "token":null, 4 | "threshold":60 5 | }, 6 | "noThreshold": { 7 | "token":"tokenString", 8 | "threshold":null 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | var fs = require('fs') 3 | var isochrone = require('../isochrone.js'); 4 | 5 | var parameters = JSON.parse(fs.readFileSync('fixtures/parameters.json')) 6 | 7 | tape('can require isochrone', function(assert) { 8 | // code you would have run manually in the node console to test this 9 | // what you'd check in the node console -- that you have a function now 10 | assert.equal(typeof isochrone, 'function', 'got a function by requiring isochrone'); 11 | // ends the test 12 | assert.end(); 13 | }); 14 | 15 | // validation testing 16 | 17 | tape('errors when starting position absent', function(assert){ 18 | isochrone(null, {}, function(err, data){ 19 | console.log(err) 20 | assert.ok(err, 'starting position error works properly'); 21 | assert.end(); 22 | }) 23 | }); 24 | 25 | tape('errors when token is invalid', function(assert){ 26 | isochrone([0,0], parameters.badToken, function(err, data){ 27 | console.log(err) 28 | assert.ok(err, 'token error works properly'); 29 | assert.end(); 30 | }) 31 | }); 32 | 33 | tape('errors when threshold is invalid', function(assert){ 34 | isochrone([0,0], parameters.noThreshold, function(err, data){ 35 | console.log(err) 36 | assert.ok(err, 'threshold error works properly'); 37 | assert.end(); 38 | }) 39 | }); 40 | 41 | --------------------------------------------------------------------------------