Skip to content

Commit

Permalink
feat: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
stepankuzmin committed May 14, 2017
0 parents commit 47d8903
Show file tree
Hide file tree
Showing 12 changed files with 5,223 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .eslintrc
@@ -0,0 +1,13 @@
{
"extends": "airbnb-base",
"plugins": [
"import"
],
"env": {
"es6": true,
"node": true
},
"rules": {
"comma-dangle": ["error", "never"]
}
}
8 changes: 8 additions & 0 deletions .gitignore
@@ -0,0 +1,8 @@
.DS_Store
node_modules/
npm-debug.log
yarn-error.log

test/data/*
!test/data/Makefile
!test/data/data.md5sum
28 changes: 28 additions & 0 deletions .travis.yml
@@ -0,0 +1,28 @@
language: node_js
dist: trusty
sudo: required

branches:
except:
- /^v[0-9]/

node_js:
- 4

addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

cache:
yarn: true
directories:
- node_modules

install:
- yarn

script:
- make test
27 changes: 27 additions & 0 deletions API.md
@@ -0,0 +1,27 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->

### Table of Contents

- [isochrone](#isochrone)

## isochrone

Build isochrone using start point and options

**Parameters**

- `startPoint` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)&lt;float>** start point [lng, lat]
- `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** object
- `options.osrm` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** [osrm](https://github.com/Project-OSRM/osrm-backend) instance
- `options.radius` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** distance to draw the buffer as in
[@turf/buffer](https://github.com/Turfjs/turf/tree/master/packages/turf-buffer)
- `options.cellSize` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** the distance across each cell as in
[@turf/point-grid](https://github.com/Turfjs/turf/tree/master/packages/turf-point-grid)
- `options.intervals` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)&lt;[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)>** intervals for isochrones in minutes
- `options.concavity` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** relative measure of concavity as in
[concaveman](https://github.com/mapbox/concaveman) (optional, default `2`)
- `options.lengthThreshold` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** length threshold as in
[concaveman](https://github.com/mapbox/concaveman) (optional, default `0`)
- `options.units` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** any of the options supported by turf units (optional, default `'kilometers'`)

Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** promise with GeoJSON when resolved
13 changes: 13 additions & 0 deletions Makefile
@@ -0,0 +1,13 @@
all:
yarn

clean:
rm -rf node_modules

shm:
$(MAKE) all -i -C ./test/data

test: shm
yarn test

.PHONY: test clean shm
40 changes: 40 additions & 0 deletions README.md
@@ -0,0 +1,40 @@
# Isochrone

[![npm version](https://img.shields.io/npm/v/isochrone.svg)](https://www.npmjs.com/package/isochrone)
[![Build Status](https://travis-ci.org/stepankuzmin/node-isochrone.svg?branch=master)](https://travis-ci.org/stepankuzmin/node-isochrone)
[![npm downloads](https://img.shields.io/npm/dt/isochrone.svg)](https://www.npmjs.com/package/galton)

Isochrone maps are commonly used to depict areas of equal travel time.
Build isochrones using [OSRM](http://project-osrm.org/), [Turf](http://turfjs.org/) and [concaveman](https://github.com/mapbox/concaveman).

![Screenshot](https://raw.githubusercontent.com/stepankuzmin/galton/master/example.png)

## Installation

```
npm install -g isochrone
```

## Usage

```js
const OSRM = require('osrm');
const isochrone = require('isochrone');

const osrm = new OSRM({ path: './monaco.osrm' });
const startPoint = [7.41337, 43.72956];

const options = {
osrm,
radius: 2,
cellSize: 0.1,
intervals: [5, 10, 15],
};

isochrone(startPoint, options)
.then((geojson) => {
console.log(JSON.stringify(geojson));
});
```

See [API](https://github.com/stepankuzmin/node-isochrone/blob/master/API.md) for more info.
105 changes: 105 additions & 0 deletions index.js
@@ -0,0 +1,105 @@
const buffer = require('@turf/buffer');
const concaveman = require('concaveman');
const pointGrid = require('@turf/point-grid');
const rewind = require('geojson-rewind');

const makeGrid = (startPoint, options) => {
const point = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: startPoint
}
};

const buffered = buffer(point, options.radius, options.unit);
const grid = pointGrid(buffered, options.cellSize, options.units);

return grid.features.map(feature => feature.geometry.coordinates);
};

const groupByInterval = (destinations, intervals, travelTime) => {
const intervalGroups = intervals.reduce((acc, interval) =>
Object.assign({}, acc, { [interval]: [] })
, {});

const pointsByInterval = travelTime.reduce((acc, time, index) => {
const timem = Math.round(time / 60);
const ceil = intervals.find(interval => timem <= interval);
if (ceil) {
acc[ceil].push(destinations[index].location);
}
return acc;
}, intervalGroups);

return pointsByInterval;
};

const makePolygon = (points, interval, options) => {
const concave = concaveman(points, options.concavity, options.lengthThreshold);

return {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [concave]
},
properties: {
time: parseFloat(interval)
}
};
};

const makePolygons = (pointsByInterval, options) =>
Object.keys(pointsByInterval).reduce((acc, interval) => {
const points = pointsByInterval[interval];
if (points.length >= 3) {
acc.push(makePolygon(points, interval, options));
}
return acc;
}, []);

/**
* Build isochrone using start point and options
*
* @name isochrone
* @param {Array.<float>} startPoint start point [lng, lat]
* @param {Object} options object
* @param {Object} options.osrm - [OSRM](https://github.com/Project-OSRM/osrm-backend) instance
* @param {number} options.radius - distance to draw the buffer as in
* [@turf/buffer](https://github.com/Turfjs/turf/tree/master/packages/turf-buffer)
* @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)
* @param {Array.<number>} options.intervals - intervals for isochrones in minutes
* @param {number} [options.concavity=2] - relative measure of concavity as in
* [concaveman](https://github.com/mapbox/concaveman)
* @param {number} [options.lengthThreshold=0] - length threshold as in
* [concaveman](https://github.com/mapbox/concaveman)
* @param {string} [options.units='kilometers'] - any of the options supported by turf units
* @returns {Promise} GeoJSON FeatureCollection of Polygons when resolved
*/
const isochrone = (startPoint, options) => {
const endPoints = makeGrid(startPoint, options);
const coordinates = [startPoint].concat(endPoints);

return new Promise((resolve, reject) => {
options.osrm.table({ sources: [0], coordinates }, (error, table) => {
if (error) {
reject(error);
}

try {
const travelTime = table.durations[0] || [];
const pointsByInterval = groupByInterval(table.destinations, options.intervals, travelTime);

const features = makePolygons(pointsByInterval, options);
const featureCollection = rewind({ type: 'FeatureCollection', features });
resolve(featureCollection);
} catch (e) {
reject(e);
}
});
});
};

module.exports = isochrone;
50 changes: 50 additions & 0 deletions package.json
@@ -0,0 +1,50 @@
{
"name": "isochrone",
"version": "5.7.0",
"description": "",
"author": "Stepan Kuzmin <to.stepan.kuzmin@gmail.com> (stepankuzmin.ru)",
"license": "MIT",
"main": "dist/bundle.js",
"jsnext:main": "src/server.js",
"scripts": {
"commit": "git-cz",
"release": "standard-version",
"docs": "documentation build index.js --format md --output API.md",
"lint": "eslint .",
"test": "node test/index.js"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"engine-strict": true,
"engines": {
"node": "4"
},
"repository": {
"type": "git",
"url": "git://github.com/stepankuzmin/node-isochrone.git"
},
"dependencies": {
"@turf/buffer": "4.3.1",
"@turf/point-grid": "4.3.0",
"concaveman": "1.1.1",
"geojson-rewind": "0.2.0"
},
"peerDependencies": {
"osrm": "5.7.0"
},
"devDependencies": {
"@mapbox/geojsonhint": "2.0.1",
"commitizen": "2.9.6",
"cz-conventional-changelog": "2.0.0",
"documentation": "4.0.0-rc.1",
"osrm": "5.7.0",
"eslint": "3.19.0",
"eslint-config-airbnb-base": "11.1.3",
"eslint-plugin-import": "2.2.0",
"standard-version": "4.0.0",
"tape": "4.6.3"
}
}
20 changes: 20 additions & 0 deletions test/data/Makefile
@@ -0,0 +1,20 @@
DATA_NAME:=monaco
DATA_URL:=https://s3.amazonaws.com/mapbox/osrm/testing/$(DATA_NAME).osm.pbf
DATA_POLY_URL:=https://s3.amazonaws.com/mapbox/osrm/testing/$(DATA_NAME).poly
OSRM_BUILD_DIR=../../node_modules/osrm/lib/binding
PROFILE_ROOT:=../../node_modules/osrm/profiles
OSRM_EXTRACT:=$(OSRM_BUILD_DIR)/osrm-extract
OSRM_CONTRACT:=$(OSRM_BUILD_DIR)/osrm-contract
PROFILE:=$(PROFILE_ROOT)/car.lua

all:
wget $(DATA_URL) -O $(DATA_NAME).osm.pbf
@echo "Running osrm-extract..."
$(OSRM_EXTRACT) -p $(PROFILE) $(DATA_NAME).osm.pbf
@echo "Running osrm-contract..."
$(OSRM_CONTRACT) $(DATA_NAME).osrm

clean:
-rm -r $(DATA_NAME).*

.PHONY: clean all
2 changes: 2 additions & 0 deletions test/data/data.md5sum
@@ -0,0 +1,2 @@
2b8dd9343d5e615afc9c67bcc7028a63 monaco.osm.pbf
b0788991ab3791d53c1c20b6281f81ad monaco.poly
39 changes: 39 additions & 0 deletions test/index.js
@@ -0,0 +1,39 @@
const path = require('path');
const test = require('tape');
const OSRM = require('osrm');
const geojsonhint = require('@mapbox/geojsonhint');
const isochrone = require('../index');

const points = [[7.41337, 43.72956],
[7.41546, 43.73077],
[7.41862, 43.73216]];

const osrmPath = path.join(__dirname, './data/monaco.osrm');
const osrm = new OSRM({ path: osrmPath });

const options = {
osrm,
radius: 2,
cellSize: 0.1,
concavity: 2,
intervals: [5, 10, 15],
lengthThreshold: 0,
units: 'kilometers'
};

test('isochrone', (t) => {
t.plan(3);
points.forEach(point =>
isochrone(point, options)
.then((geojson) => {
const errors = geojsonhint.hint(geojson);
if (errors.length > 0) {
errors.forEach(error => t.comment(error.message));
t.fail('Invalid GeoJSON');
} else {
t.pass('Valid GeoJSON');
}
})
.catch(error => t.error(error, 'No error'))
);
});

0 comments on commit 47d8903

Please sign in to comment.