├── .eslintrc ├── .gitignore ├── .travis.yml ├── API.md ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── test ├── data │ ├── Makefile │ └── data.md5sum └── index.js └── utils.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb-base", "prettier"], 3 | "plugins": ["import"], 4 | "rules": { 5 | "arrow-parens": ["warn", "as-needed", { "requireForBlockBody": true }], 6 | "comma-dangle": ["error", "never"], 7 | "max-len": ["error", { "code": 100, "ignoreUrls": true }], 8 | "no-param-reassign": "off", 9 | "quotes": ["error", "single"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | yarn-error.log 5 | 6 | test/data/* 7 | !test/data/Makefile 8 | !test/data/data.md5sum 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | dist: trusty 4 | 5 | sudo: required 6 | 7 | cache: 8 | apt: true 9 | npm: true 10 | ccache: true 11 | 12 | addons: 13 | apt: 14 | sources: ["ubuntu-toolchain-r-test"] 15 | packages: 16 | [ 17 | "g++-6", 18 | "gcc-6", 19 | "build-essential", 20 | "git", 21 | "wget", 22 | "cmake3", 23 | "pkg-config", 24 | "libbz2-dev", 25 | "libstxxl-dev", 26 | "libstxxl1", 27 | "libxml2-dev", 28 | "libzip-dev", 29 | "libboost-all-dev", 30 | "lua5.2", 31 | "liblua5.2-dev", 32 | "libtbb-dev", 33 | ] 34 | 35 | env: CPP=cpp-6 CC=gcc-6 CXX=g++-6 36 | 37 | branches: 38 | except: 39 | - /^v[0-9]/ 40 | 41 | node_js: 42 | - "10" 43 | - "8" 44 | 45 | install: npm install 46 | 47 | script: 48 | - npm run lint 49 | - make test 50 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## isochrone 4 | 5 | [index.js:91-119][1] 6 | 7 | Build isochrone using start point and options 8 | 9 | ### Parameters 10 | 11 | - `startPoint` **[Array][2]<float>** start point [lng, lat] 12 | - `options` **[Object][3]** object 13 | - `options.osrm` **[Object][3]** [OSRM][4] instance 14 | - `options.radius` **[number][5]** distance to draw the buffer as in 15 | [@turf/buffer][6] 16 | - `options.cellSize` **[number][5]** the distance across each cell as in 17 | [@turf/point-grid][7] 18 | - `options.intervals` **[Array][2]<[number][5]>** intervals for isochrones in minutes 19 | - `options.deintersect` **[boolean][8]** whether or not to deintersect the isochrones. 20 | If this is true, then the isochrones will be mutually exclusive 21 | - `options.concavity` **[number][5]** relative measure of concavity as in 22 | [concaveman][9] (optional, default `2`) 23 | - `options.lengthThreshold` **[number][5]** length threshold as in 24 | [concaveman][9] (optional, default `0`) 25 | - `options.units` **[string][10]** any of the options supported by turf units (optional, default `'kilometers'`) 26 | 27 | ### Examples 28 | 29 | ```javascript 30 | const OSRM = require('osrm'); 31 | const isochrone = require('isochrone'); 32 | 33 | const osrm = new OSRM({ path: './monaco.osrm' }); 34 | const startPoint = [7.41337, 43.72956]; 35 | 36 | const options = { 37 | osrm, 38 | radius: 2, 39 | cellSize: 0.1, 40 | intervals: [5, 10, 15], 41 | deintersect: true 42 | }; 43 | 44 | isochrone(startPoint, options) 45 | .then((geojson) => { 46 | console.log(JSON.stringify(geojson)); 47 | }); 48 | ``` 49 | 50 | Returns **[Promise][11]** GeoJSON FeatureCollection of Polygons when resolved 51 | 52 | [1]: https://github.com/stepankuzmin/node-isochrone/blob/3dc588a2f3a3b21db10e751150d41e318866fa27/index.js#L91-L119 "Source code on GitHub" 53 | 54 | [2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array 55 | 56 | [3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object 57 | 58 | [4]: https://github.com/Project-OSRM/osrm-backend 59 | 60 | [5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number 61 | 62 | [6]: https://github.com/Turfjs/turf/tree/master/packages/turf-buffer 63 | 64 | [7]: https://github.com/Turfjs/turf/tree/master/packages/turf-point-grid 65 | 66 | [8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean 67 | 68 | [9]: https://github.com/mapbox/concaveman 69 | 70 | [10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String 71 | 72 | [11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise 73 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [3.0.2](https://github.com/stepankuzmin/node-isochrone/compare/v3.0.1...v3.0.2) (2018-09-20) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * fix polygon deintersection ([900e59c](https://github.com/stepankuzmin/node-isochrone/commit/900e59c)) 12 | 13 | 14 | 15 | 16 | ## [3.0.1](https://github.com/stepankuzmin/node-isochrone/compare/v3.0.0...v3.0.1) (2018-05-25) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * Add extra catch for empty polygons ([1f4e521](https://github.com/stepankuzmin/node-isochrone/commit/1f4e521)) 22 | 23 | 24 | 25 | 26 | # [3.0.0](https://github.com/stepankuzmin/node-isochrone/compare/v2.0.2...v3.0.0) (2018-04-02) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * **package:** update [@turf](https://github.com/turf)/helpers to version 6.0.1 ([907a4b4](https://github.com/stepankuzmin/node-isochrone/commit/907a4b4)) 32 | * **package:** update [@turf](https://github.com/turf)/helpers to version 6.1.0 ([30d51c2](https://github.com/stepankuzmin/node-isochrone/commit/30d51c2)) 33 | * **package:** update geojson-rewind to version 0.3.1 ([67efc27](https://github.com/stepankuzmin/node-isochrone/commit/67efc27)) 34 | 35 | 36 | ### Features 37 | 38 | * update OSRM, drop node v6 support ([d3d05c3](https://github.com/stepankuzmin/node-isochrone/commit/d3d05c3)) 39 | 40 | 41 | ### BREAKING CHANGES 42 | 43 | * drop node v6 support 44 | 45 | 46 | 47 | 48 | ## [2.0.2](https://github.com/stepankuzmin/node-isochrone/compare/v2.0.1...v2.0.2) (2018-02-05) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * **package:** update [@turf](https://github.com/turf)/bbox to version 4.6.0 ([12efd8c](https://github.com/stepankuzmin/node-isochrone/commit/12efd8c)) 54 | * **package:** update [@turf](https://github.com/turf)/bbox to version 4.7.3 ([3365751](https://github.com/stepankuzmin/node-isochrone/commit/3365751)) 55 | * **package:** update [@turf](https://github.com/turf)/bbox to version 5.0.4 ([b9171aa](https://github.com/stepankuzmin/node-isochrone/commit/b9171aa)) 56 | * **package:** update [@turf](https://github.com/turf)/bbox to version 5.1.0 ([b53faf6](https://github.com/stepankuzmin/node-isochrone/commit/b53faf6)) 57 | * **package:** update [@turf](https://github.com/turf)/bbox to version 5.1.5 ([04fed0b](https://github.com/stepankuzmin/node-isochrone/commit/04fed0b)) 58 | * **package:** update [@turf](https://github.com/turf)/destination to version 4.6.0 ([caf0fed](https://github.com/stepankuzmin/node-isochrone/commit/caf0fed)) 59 | * **package:** update [@turf](https://github.com/turf)/destination to version 4.7.3 ([40c8186](https://github.com/stepankuzmin/node-isochrone/commit/40c8186)) 60 | * **package:** update [@turf](https://github.com/turf)/destination to version 5.0.4 ([c0c1ee8](https://github.com/stepankuzmin/node-isochrone/commit/c0c1ee8)) 61 | * **package:** update [@turf](https://github.com/turf)/destination to version 5.1.0 ([db65b78](https://github.com/stepankuzmin/node-isochrone/commit/db65b78)) 62 | * **package:** update [@turf](https://github.com/turf)/destination to version 5.1.5 ([a866ce1](https://github.com/stepankuzmin/node-isochrone/commit/a866ce1)) 63 | * **package:** update [@turf](https://github.com/turf)/helpers to version 4.6.0 ([099e56d](https://github.com/stepankuzmin/node-isochrone/commit/099e56d)) 64 | * **package:** update [@turf](https://github.com/turf)/point-grid to version 5.1.0 ([9da0702](https://github.com/stepankuzmin/node-isochrone/commit/9da0702)) 65 | * properly handle errors ([43f8532](https://github.com/stepankuzmin/node-isochrone/commit/43f8532)) 66 | * **package:** update [@turf](https://github.com/turf)/helpers to version 4.7.3 ([4e27f8c](https://github.com/stepankuzmin/node-isochrone/commit/4e27f8c)) 67 | * **package:** update [@turf](https://github.com/turf)/helpers to version 5.0.4 ([38dc2ec](https://github.com/stepankuzmin/node-isochrone/commit/38dc2ec)) 68 | * **package:** update [@turf](https://github.com/turf)/helpers to version 5.1.0 ([0f2674f](https://github.com/stepankuzmin/node-isochrone/commit/0f2674f)) 69 | * **package:** update [@turf](https://github.com/turf)/helpers to version 5.1.4 ([14def13](https://github.com/stepankuzmin/node-isochrone/commit/14def13)) 70 | * **package:** update [@turf](https://github.com/turf)/helpers to version 6.0.0-beta.1 ([2f47fa8](https://github.com/stepankuzmin/node-isochrone/commit/2f47fa8)) 71 | * **package:** update [@turf](https://github.com/turf)/helpers to version 6.0.0-beta.4 ([fc1139c](https://github.com/stepankuzmin/node-isochrone/commit/fc1139c)) 72 | * **package:** update [@turf](https://github.com/turf)/point-grid to version 4.6.0 ([197ad3f](https://github.com/stepankuzmin/node-isochrone/commit/197ad3f)) 73 | * **package:** update [@turf](https://github.com/turf)/point-grid to version 4.7.3 ([9d8c894](https://github.com/stepankuzmin/node-isochrone/commit/9d8c894)) 74 | * **package:** update [@turf](https://github.com/turf)/point-grid to version 5.0.0 ([0505a3d](https://github.com/stepankuzmin/node-isochrone/commit/0505a3d)) 75 | * **package:** update [@turf](https://github.com/turf)/point-grid to version 5.0.4 ([39407a0](https://github.com/stepankuzmin/node-isochrone/commit/39407a0)) 76 | * **package:** update [@turf](https://github.com/turf)/point-grid to version 5.1.5 ([aca7b85](https://github.com/stepankuzmin/node-isochrone/commit/aca7b85)) 77 | 78 | 79 | 80 | 81 | ## [2.0.1](https://github.com/stepankuzmin/node-isochrone/compare/v2.0.0...v2.0.1) (2017-07-17) 82 | 83 | 84 | 85 | 86 | # [2.0.0](https://github.com/stepankuzmin/node-isochrone/compare/v1.0.2...v2.0.0) (2017-07-12) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * **package:** update [@turf](https://github.com/turf)/bbox to version 4.5.1 ([0318cce](https://github.com/stepankuzmin/node-isochrone/commit/0318cce)) 92 | * **package:** update [@turf](https://github.com/turf)/destination to version 4.5.1 ([8518f8b](https://github.com/stepankuzmin/node-isochrone/commit/8518f8b)) 93 | * **package:** update [@turf](https://github.com/turf)/helpers to version 4.5.1 ([b5fe981](https://github.com/stepankuzmin/node-isochrone/commit/b5fe981)) 94 | * **package:** update [@turf](https://github.com/turf)/point-grid to version 4.5.1 ([d22e253](https://github.com/stepankuzmin/node-isochrone/commit/d22e253)) 95 | 96 | 97 | ### build 98 | 99 | * add node v8 to travis ([3a79eab](https://github.com/stepankuzmin/node-isochrone/commit/3a79eab)) 100 | 101 | 102 | ### Features 103 | 104 | * implement deintersect as a configurable option (by [@glifchits](https://github.com/glifchits)) ([21739f1](https://github.com/stepankuzmin/node-isochrone/commit/21739f1)) 105 | 106 | 107 | ### BREAKING CHANGES 108 | 109 | * drop node v4 support 110 | 111 | 112 | 113 | 114 | ## [1.0.2](https://github.com/stepankuzmin/node-isochrone/compare/v1.0.1...v1.0.2) (2017-06-11) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * **package:** update [@turf](https://github.com/turf)/bbox to version 4.4.0 ([74976ac](https://github.com/stepankuzmin/node-isochrone/commit/74976ac)) 120 | * **package:** update [@turf](https://github.com/turf)/destination to version 4.4.0 ([44e4560](https://github.com/stepankuzmin/node-isochrone/commit/44e4560)) 121 | * **package:** update [@turf](https://github.com/turf)/helpers to version 4.4.0 ([28295e4](https://github.com/stepankuzmin/node-isochrone/commit/28295e4)) 122 | * **package:** update [@turf](https://github.com/turf)/point-grid to version 4.4.0 ([55b5cc4](https://github.com/stepankuzmin/node-isochrone/commit/55b5cc4)) 123 | 124 | 125 | 126 | 127 | ## [1.0.1](https://github.com/stepankuzmin/node-isochrone/compare/v1.0.0...v1.0.1) (2017-05-30) 128 | 129 | 130 | 131 | 132 | # 1.0.0 (2017-05-29) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * deintersect isochrones with different time ([01b415a](https://github.com/stepankuzmin/node-isochrone/commit/01b415a)) 138 | * fix turf-deintersect version ([d25c565](https://github.com/stepankuzmin/node-isochrone/commit/d25c565)) 139 | 140 | 141 | ### Features 142 | 143 | * initial commit ([47d8903](https://github.com/stepankuzmin/node-isochrone/commit/47d8903)) 144 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Stepan Kuzmin 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | npm i 3 | 4 | clean: 5 | rm -rf node_modules 6 | 7 | shm: 8 | $(MAKE) all -i -C ./test/data 9 | 10 | test: shm 11 | npm test 12 | 13 | .PHONY: test clean shm 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Isochrone 2 | 3 | [![npm version](https://img.shields.io/npm/v/isochrone.svg)](https://www.npmjs.com/package/isochrone) 4 | [![npm downloads](https://img.shields.io/npm/dt/isochrone.svg)](https://www.npmjs.com/package/isochrone) 5 | [![Build Status](https://travis-ci.com/stepankuzmin/node-isochrone.svg?branch=master)](https://travis-ci.com/stepankuzmin/node-isochrone) 6 | 7 | Isochrone maps are commonly used to depict areas of equal travel time. 8 | Build isochrones using [OSRM](http://project-osrm.org/), [Turf](http://turfjs.org/) and [concaveman](https://github.com/mapbox/concaveman). 9 | 10 | ![screenshot](https://raw.githubusercontent.com/stepankuzmin/galton/master/example.png) 11 | 12 | ## Installation 13 | 14 | ```sh 15 | npm install osrm isochrone 16 | ``` 17 | 18 | ## Usage 19 | 20 | ### Building OSRM graph 21 | 22 | This package consumes preprocessed [OSRM](http://project-osrm.org/) graph as an input. To build such a graph you have to extract it from your OSM file with one of [profiles](https://github.com/Project-OSRM/osrm-backend/wiki/Profiles) and build it using one of the algorithms (Contraction Hierarchies or Multi-Level Dijkstra). 23 | 24 | To build OSRM graph using `isochrone` package, you can clone the source code and install dependencies 25 | 26 | ```sh 27 | git clone https://github.com/stepankuzmin/node-isochrone.git 28 | cd node-isochrone 29 | npm i 30 | ``` 31 | 32 | Here is an example of how to extract graph using `foot` profile and build it using contraction hierarchies algorithm. 33 | 34 | ```sh 35 | wget https://s3.amazonaws.com/mapbox/osrm/testing/monaco.osm.pbf 36 | ./node_modules/osrm/lib/binding/osrm-extract -p ./node_modules/osrm/profiles/foot.lua monaco.osm.pbf 37 | ./node_modules/osrm/lib/binding/osrm-contract monaco.osrm 38 | ``` 39 | 40 | ### Example 41 | 42 | See [API](https://github.com/stepankuzmin/node-isochrone/blob/master/API.md) for more info. 43 | 44 | ```js 45 | const OSRM = require("osrm"); 46 | const isochrone = require("isochrone"); 47 | 48 | const osrm = new OSRM({ path: "./monaco.osrm" }); 49 | 50 | const startPoint = [7.42063, 43.73104]; 51 | 52 | const options = { 53 | osrm, 54 | radius: 2, 55 | cellSize: 0.1, 56 | intervals: [5, 10, 15] 57 | }; 58 | 59 | isochrone(startPoint, options).then(geojson => { 60 | console.log(JSON.stringify(geojson, null, 2)); 61 | }); 62 | ``` 63 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const deintersect = require('turf-deintersect'); 2 | const helpers = require('@turf/helpers'); 3 | const rewind = require('geojson-rewind'); 4 | const { makeGrid, groupByInterval, makePolygons } = require('./utils'); 5 | 6 | /** 7 | * Build isochrone using start point and options 8 | * 9 | * @param {Array.} startPoint start point [lng, lat] 10 | * @param {Object} options object 11 | * @param {Object} options.osrm - [OSRM](https://github.com/Project-OSRM/osrm-backend) instance 12 | * @param {number} options.radius - distance to draw the buffer as in [@turf/buffer](https://github.com/Turfjs/turf/tree/master/packages/turf-buffer) 13 | * @param {number} options.cellSize - the distance across each cell as in [@turf/point-grid](https://github.com/Turfjs/turf/tree/master/packages/turf-point-grid) 14 | * @param {Array.} options.intervals - intervals for isochrones in minutes 15 | * @param {boolean} options.deintersect - whether or not to deintersect the isochrones. 16 | * If this is true, then the isochrones will be mutually exclusive 17 | * @param {number} [options.concavity=2] - relative measure of concavity as in [concaveman](https://github.com/mapbox/concaveman) 18 | * @param {number} [options.lengthThreshold=0] - length threshold as in [concaveman](https://github.com/mapbox/concaveman) 19 | * @param {string} [options.units='kilometers'] - any of the options supported by turf units 20 | * @returns {Promise} GeoJSON FeatureCollection of Polygons when resolved 21 | * 22 | * @example 23 | * const OSRM = require('osrm'); 24 | * const isochrone = require('isochrone'); 25 | * 26 | * const osrm = new OSRM({ path: './monaco.osrm' }); 27 | * const startPoint = [7.41337, 43.72956]; 28 | * 29 | * const options = { 30 | * osrm, 31 | * radius: 2, 32 | * cellSize: 0.1, 33 | * intervals: [5, 10, 15] 34 | * }; 35 | * 36 | * isochrone(startPoint, options) 37 | * .then((geojson) => { 38 | * console.log(JSON.stringify(geojson)); 39 | * }); 40 | */ 41 | const isochrone = (startPoint, options) => 42 | new Promise((resolve, reject) => { 43 | try { 44 | const endPoints = makeGrid(startPoint, options); 45 | const coordinates = [startPoint].concat(endPoints); 46 | options.osrm.table({ sources: [0], coordinates }, (error, table) => { 47 | if (error) { 48 | reject(error); 49 | } 50 | 51 | const travelTime = table.durations[0] || []; 52 | 53 | const pointsByInterval = groupByInterval(table.destinations, options.intervals, travelTime); 54 | 55 | const polygons = makePolygons(pointsByInterval, options); 56 | 57 | const features = options.deintersect ? deintersect(polygons) : polygons; 58 | const featureCollection = rewind(helpers.featureCollection(features)); 59 | 60 | resolve(featureCollection); 61 | }); 62 | } catch (error) { 63 | reject(error); 64 | } 65 | }); 66 | 67 | module.exports = isochrone; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isochrone", 3 | "version": "3.0.2", 4 | "description": "isochrone map library", 5 | "author": "Stepan Kuzmin (stepankuzmin.ru)", 6 | "homepage": "https://github.com/stepankuzmin/node-isochrone", 7 | "license": "MIT", 8 | "main": "index.js", 9 | "scripts": { 10 | "extract": "./node_modules/osrm/lib/binding/osrm-extract", 11 | "contract": "./node_modules/osrm/lib/binding/osrm-contract", 12 | "cz": "git-cz", 13 | "docs": "documentation build index.js -g --markdown-toc false -f md -o API.md", 14 | "lint": "eslint .", 15 | "release": "standard-version", 16 | "test": "node test/index.js" 17 | }, 18 | "config": { 19 | "commitizen": { 20 | "path": "cz-conventional-changelog" 21 | } 22 | }, 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": "lint-staged" 26 | } 27 | }, 28 | "lint-staged": { 29 | "*.js": [ 30 | "prettier-eslint --write", 31 | "npm run lint", 32 | "git add" 33 | ] 34 | }, 35 | "engine-strict": true, 36 | "engines": { 37 | "node": ">=8" 38 | }, 39 | "keywords": [ 40 | "isochrone", 41 | "drivetime", 42 | "walktime", 43 | "geojson", 44 | "polygon", 45 | "osrm" 46 | ], 47 | "repository": { 48 | "type": "git", 49 | "url": "git://github.com/stepankuzmin/node-isochrone.git" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/stepankuzmin/node-isochrone/issues" 53 | }, 54 | "dependencies": { 55 | "@turf/bbox": "6.0.1", 56 | "@turf/destination": "6.0.1", 57 | "@turf/helpers": "6.1.4", 58 | "@turf/point-grid": "6.0.1", 59 | "concaveman": "1.1.1", 60 | "geojson-rewind": "0.3.1", 61 | "turf-deintersect": "1.0.4" 62 | }, 63 | "peerDependencies": { 64 | "osrm": "^5.16.4" 65 | }, 66 | "devDependencies": { 67 | "@mapbox/geojsonhint": "^3.0.0", 68 | "@turf/area": "6.0.1", 69 | "@turf/intersect": "6.1.3", 70 | "commitizen": "^4.0.3", 71 | "cz-conventional-changelog": "^3.0.2", 72 | "documentation": "^12.1.4", 73 | "eslint": "^6.6.0", 74 | "eslint-config-airbnb-base": "^14.0.0", 75 | "eslint-config-prettier": "^6.5.0", 76 | "eslint-plugin-import": "^2.18.2", 77 | "husky": "^3.0.9", 78 | "lint-staged": "^9.4.3", 79 | "osrm": "^5.22.0", 80 | "prettier": "^1.19.1", 81 | "prettier-eslint": "^9.0.0", 82 | "prettier-eslint-cli": "^5.0.0", 83 | "standard-version": "^7.0.0", 84 | "tape": "^4.11.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/data/Makefile: -------------------------------------------------------------------------------- 1 | DATA_NAME:=monaco 2 | DATA_URL:=https://s3.amazonaws.com/mapbox/osrm/testing/$(DATA_NAME).osm.pbf 3 | DATA_POLY_URL:=https://s3.amazonaws.com/mapbox/osrm/testing/$(DATA_NAME).poly 4 | OSRM_BUILD_DIR=../../node_modules/osrm/lib/binding 5 | PROFILE_ROOT:=../../node_modules/osrm/profiles 6 | OSRM_EXTRACT:=$(OSRM_BUILD_DIR)/osrm-extract 7 | OSRM_CONTRACT:=$(OSRM_BUILD_DIR)/osrm-contract 8 | PROFILE:=$(PROFILE_ROOT)/car.lua 9 | 10 | all: 11 | wget $(DATA_URL) -O $(DATA_NAME).osm.pbf 12 | @echo "Running osrm-extract..." 13 | $(OSRM_EXTRACT) -p $(PROFILE) $(DATA_NAME).osm.pbf 14 | @echo "Running osrm-contract..." 15 | $(OSRM_CONTRACT) $(DATA_NAME).osrm 16 | 17 | clean: 18 | -rm -r $(DATA_NAME).* 19 | 20 | .PHONY: clean all 21 | -------------------------------------------------------------------------------- /test/data/data.md5sum: -------------------------------------------------------------------------------- 1 | 2b8dd9343d5e615afc9c67bcc7028a63 monaco.osm.pbf 2 | b0788991ab3791d53c1c20b6281f81ad monaco.poly 3 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const test = require('tape'); 3 | const OSRM = require('osrm'); 4 | const area = require('@turf/area').default; 5 | const intersect = require('@turf/intersect').default; 6 | const geojsonhint = require('@mapbox/geojsonhint'); 7 | const isochrone = require('../index'); 8 | 9 | const points = [[7.41337, 43.72956], [7.41375, 43.73339], [7.41862, 43.73216]]; 10 | 11 | const osrmPath = path.join(__dirname, './data/monaco.osrm'); 12 | const osrm = new OSRM({ path: osrmPath }); 13 | 14 | const options = { 15 | osrm, 16 | radius: 2, 17 | cellSize: 0.1, 18 | concavity: 2, 19 | intervals: [5, 10, 15], 20 | lengthThreshold: 0, 21 | units: 'kilometers' 22 | }; 23 | 24 | const timeColors = { 25 | 5: '#0f0', 26 | 10: '#00f', 27 | 15: '#f00' 28 | }; 29 | 30 | // For argument array intervals: [5, 10, 15, 20] 31 | // Gives: [ [5,10], [5,15], [5,20], [10,15], [10,20], [15,20] ] 32 | const testPairs = options.intervals 33 | .map(i => ({ head: i, tail: options.intervals.filter(x => x > i) })) 34 | .map(z => z.tail.map(t => [z.head, t])) 35 | .reduce((x, y) => x.concat(y), []); 36 | 37 | const getIntersectionArea = (a, b) => { 38 | const intersection = intersect(a, b); 39 | return intersection ? area(intersection) : 0; 40 | }; 41 | 42 | test('deintersected isochrone', (t) => { 43 | t.plan(points.length * (1 + testPairs.length)); 44 | points.forEach(point => 45 | isochrone(point, { ...options, deintersect: true }) 46 | .then((geojson) => { 47 | geojson.features.forEach((feature) => { 48 | // eslint-disable-next-line 49 | feature.properties.fill = timeColors[feature.properties.time]; 50 | }); 51 | 52 | const errors = geojsonhint.hint(geojson); 53 | if (errors.length > 0) { 54 | errors.forEach(error => t.comment(error.message)); 55 | t.fail('Invalid GeoJSON'); 56 | } else { 57 | t.pass('Valid GeoJSON'); 58 | } 59 | // Gives { interval1: isochrone1, interval2: isochrone2, ... } 60 | const isochrones = options.intervals.reduce( 61 | (acc, interval) => ({ 62 | ...acc, 63 | [interval]: geojson.features.find(iso => iso.properties.time === interval) 64 | }), 65 | {} 66 | ); 67 | // test that every smaller isochrone is contained by a larger one 68 | testPairs.forEach((minutePair) => { 69 | const [minSmall, minLarge] = minutePair; 70 | const [small, large] = [isochrones[minSmall], isochrones[minLarge]]; 71 | const areaIntersection = getIntersectionArea(small, large); 72 | 73 | // assert that there is no intersection between isochrones 74 | if (areaIntersection < 300) { 75 | t.pass(`Isochrone ${minSmall} does not intersect ${minLarge}`); 76 | } else { 77 | t.fail( 78 | `Isochrone ${minSmall} intersects with ${minLarge} with area ${areaIntersection}` 79 | ); 80 | } 81 | }); 82 | }) 83 | .catch(error => t.error(error, 'No error')) 84 | ); 85 | }); 86 | 87 | test('isochrone', (t) => { 88 | t.plan(points.length * (1 + testPairs.length)); 89 | points.forEach(point => 90 | isochrone(point, { ...options, deintersect: false }) 91 | .then((geojson) => { 92 | const errors = geojsonhint.hint(geojson); 93 | if (errors.length > 0) { 94 | errors.forEach(error => t.comment(error.message)); 95 | t.fail('Invalid GeoJSON'); 96 | } else { 97 | t.pass('Valid GeoJSON'); 98 | } 99 | // Gives { interval1: isochrone1, interval2: isochrone2, ... } 100 | const isochrones = options.intervals.reduce( 101 | (acc, interval) => ({ 102 | ...acc, 103 | [interval]: geojson.features.find(iso => iso.properties.time === interval) 104 | }), 105 | {} 106 | ); 107 | // test that every smaller isochrone is contained by a larger one 108 | testPairs.forEach((minutePair) => { 109 | const [minSmall, minLarge] = minutePair; 110 | const [small, large] = [isochrones[minSmall], isochrones[minLarge]]; 111 | const intersection = intersect(small, large); 112 | const areaIntersection = intersection ? area(intersection) : 0; 113 | // assert that the small isochrone overlaps over 90% with the large 114 | if (areaIntersection / area(small) > 0.9) { 115 | t.pass(`Isochrone ${minSmall} is contained in ${minLarge}`); 116 | } else { 117 | t.fail(`Isochrone ${minSmall} is not contained in ${minLarge}`); 118 | } 119 | }); 120 | }) 121 | .catch(error => t.error(error, 'No error')) 122 | ); 123 | }); 124 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const bbox = require('@turf/bbox').default; 2 | const concaveman = require('concaveman'); 3 | const destination = require('@turf/destination').default; 4 | const helpers = require('@turf/helpers'); 5 | const pointGrid = require('@turf/point-grid').default; 6 | 7 | const makeGrid = (startPoint, options) => { 8 | const point = helpers.point(startPoint); 9 | 10 | const spokes = helpers.featureCollection([ 11 | destination(point, options.radius, 180, options.unit), 12 | destination(point, options.radius, 0, options.unit), 13 | destination(point, options.radius, 90, options.unit), 14 | destination(point, options.radius, -90, options.unit) 15 | ]); 16 | 17 | const bboxGrid = bbox(spokes); 18 | const grid = pointGrid(bboxGrid, options.cellSize, { units: options.units }); 19 | 20 | return grid.features.map(feature => feature.geometry.coordinates); 21 | }; 22 | 23 | const groupByInterval = (destinations, intervals, travelTime) => 24 | intervals.reduce((pointsByInterval, interval) => { 25 | const intervalInSeconds = interval * 60; 26 | pointsByInterval[interval] = destinations.reduce((acc, point, index) => { 27 | if (travelTime[index] !== null && travelTime[index] <= intervalInSeconds) { 28 | acc.push(point.location); 29 | } 30 | return acc; 31 | }, []); 32 | 33 | return pointsByInterval; 34 | }, {}); 35 | 36 | const makePolygon = (points, interval, options) => { 37 | const concave = concaveman(points, options.concavity, options.lengthThreshold); 38 | return helpers.polygon([concave], { time: parseFloat(interval) }); 39 | }; 40 | 41 | const makePolygons = (pointsByInterval, options) => 42 | Object.keys(pointsByInterval).reduce((acc, interval) => { 43 | const points = pointsByInterval[interval]; 44 | if (points.length >= 3) { 45 | acc.push(makePolygon(points, interval, options)); 46 | } 47 | return acc; 48 | }, []); 49 | 50 | module.exports = { makeGrid, groupByInterval, makePolygons }; 51 | --------------------------------------------------------------------------------