From a55d0189c370305e90e18850acc6283cb9de39ed Mon Sep 17 00:00:00 2001 From: Matthew Wigginton Conway Date: Tue, 18 Oct 2016 16:19:20 -0400 Subject: [PATCH 1/7] feat(grid): save min/max in grids Grids now have a min and max attribute that contain the min and max value of that grid. --- lib/grid.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/grid.js b/lib/grid.js index de127c7..c79a368 100644 --- a/lib/grid.js +++ b/lib/grid.js @@ -6,8 +6,13 @@ export function create (data) { const array = new Int32Array(data, 4 * 5) const header = new Int32Array(data) + let min = Infinity + let max = -Infinity + for (let i = 0, prev = 0; i < array.length; i++) { array[i] = (prev += array[i]) + if (prev < min) min = prev + if (prev > max) max = prev } // parse header return { @@ -16,6 +21,8 @@ export function create (data) { north: header[2], width: header[3], height: header[4], - data: array + data: array, + min, + max } } From d1fa9447c149f5f2c81c44798564fd7e17eb99ed Mon Sep 17 00:00:00 2001 From: Matthew Wigginton Conway Date: Tue, 18 Oct 2016 16:20:21 -0400 Subject: [PATCH 2/7] feat(grid): export createGrid function for use outside of browsochrones. --- lib/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/index.js b/lib/index.js index ea30669..d46fad1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,6 @@ import WebWorkerPromiseInterface from 'web-worker-promise-interface' import {Map as LeafletMap} from 'leaflet' // NB raw leaflet map not react-leaflet +import {create as createGridFunc} from './grid' import workerHandlers from './worker-handlers' @@ -264,3 +265,5 @@ export default class Browsochrones extends WebWorkerPromiseInterface { }) } } + +export const createGrid = createGridFunc From be7efb2b4ea044f1ef164980a25ca87f2751dd12 Mon Sep 17 00:00:00 2001 From: Matthew Wigginton Conway Date: Wed, 19 Oct 2016 12:08:16 -0400 Subject: [PATCH 3/7] refactor(surface): Move code to do propagation to cells into its own module so it can be reused for --- lib/get-surface.js | 69 ++++++++++----------------------------- lib/propagation.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 53 deletions(-) create mode 100644 lib/propagation.js diff --git a/lib/get-surface.js b/lib/get-surface.js index 9099f18..377ee2e 100644 --- a/lib/get-surface.js +++ b/lib/get-surface.js @@ -1,9 +1,7 @@ -import fill from 'lodash.fill' import dbg from 'debug' +import propagate from './propagation' const debug = dbg('browsochrones:get-surface') -import {getNonTransitTime, ITERATION_WIDTH} from './origin' - /** * Get a travel time surface and accessibility results for a particular origin. * Pass in references to the query (the JS object stored in query.json), the stopTreeCache, the origin file, the @@ -21,63 +19,28 @@ export default function getSurface ({origin, query, stopTreeCache, which}) { // how many departure minutes are there. skip number of stops const nMinutes = origin.data[transitOffset + 1] - const travelTimesForDest = new Uint8Array(nMinutes) // the total travel time per iteration to reach a particular destination - const waitTimesForDest = new Uint8Array(nMinutes) // wait time per iteration for particular destination - const inVehicleTravelTimesForDest = new Uint8Array(nMinutes) // in-vehicle travel time per destination - const walkTimesForDest = new Uint8Array(nMinutes) - - // x and y refer to pixel not origins here - // loop over rows first - for (let y = 0, pixelIdx = 0, stcOffset = 0; y < query.height; y++) { - for (let x = 0; x < query.width; x++, pixelIdx++) { - const nStops = stopTreeCache.data[stcOffset++] - - // can we reach this pixel without riding transit? - const nonTransitTime = getNonTransitTime(origin, {x, y}) - - // fill with unreachable, or the walk distance - fill(travelTimesForDest, nonTransitTime) - fill(waitTimesForDest, 255) - fill(inVehicleTravelTimesForDest, 255) - fill(walkTimesForDest, nonTransitTime) // when the origin is within walking distance and - // walking is the fastest way to reach the destination, _everything_ is walk time - - for (let stopIdx = 0; stopIdx < nStops; stopIdx++) { - // read the stop ID - const stopId = stopTreeCache.data[stcOffset++] - - // read the time (minutes) - const time = stopTreeCache.data[stcOffset++] - - for (let minute = 0; minute < nMinutes; minute++) { - const offset = origin.index[stopId] + minute * ITERATION_WIDTH - const travelTimeToStop = origin.data[offset] - - if (travelTimeToStop !== -1) { - const travelTimeToPixel = travelTimeToStop + time - - // no need to check that travelTimeToPixel < 255 as travelTimesForDest[minute] is preinitialized to the nontransit time or 255 - if (travelTimesForDest[minute] > travelTimeToPixel) { - travelTimesForDest[minute] = travelTimeToPixel - let inVehicle = inVehicleTravelTimesForDest[minute] = origin.data[offset + 1] - let wait = waitTimesForDest[minute] = origin.data[offset + 2] - // NB when we're talking about a particular trip, then walk + wait + inVehicle == total - // However if you calculate summary statistics for each of these individually, that may - // not be true. So we need to calculate walk here and explicitly calculate summary stats about it. - let walkTime = travelTimeToPixel - wait - inVehicle - walkTimesForDest[minute] = walkTime - } - } - } - } + propagate({ + query, + stopTreeCache, + origin, + nMinutes, + callback: ({ + travelTimesForDest, + walkTimesForDest, + inVehicleTravelTimesForDest, + waitTimesForDest, + x, + y + }) => { + const pixelIdx = y * query.width + x // compute and set value for pixel surface[pixelIdx] = computePixelValue(which, travelTimesForDest) waitTimes[pixelIdx] = computePixelValue(which, waitTimesForDest) walkTimes[pixelIdx] = computePixelValue(which, walkTimesForDest) inVehicleTravelTimes[pixelIdx] = computePixelValue(which, inVehicleTravelTimesForDest) } - } + }) debug('generating surface complete') diff --git a/lib/propagation.js b/lib/propagation.js new file mode 100644 index 0000000..88a43b4 --- /dev/null +++ b/lib/propagation.js @@ -0,0 +1,80 @@ +/** + * Perform propagation from an origin to destination cells, calling a callback to summarize the data. + * + * This seems like it's overly complicated, but I don't think there's an easier way to have this code + * used both to create surfaces/isochrones and spectrogram data, which are different summaries of the data. + * + * The callback will be called with the following, once for each destination pixel + * travelTimesForDest: Travel times at each iteration + * walkTimesForDest: Walk times at each iteration + * inVehicleTravelTimesForDest: In vehicle travel times at each iteration + * waitTimesForDest: Wait times at each iteration + * x: X coordinate of grid for this destination + * y: Y coordinate of grid for this destination + */ + +import fill from 'lodash.fill' +import {getNonTransitTime, ITERATION_WIDTH} from './origin' + +export default function propagate ({ query, stopTreeCache, nMinutes, origin, callback }) { + const travelTimesForDest = new Uint8Array(nMinutes) // the total travel time per iteration to reach a particular destination + const waitTimesForDest = new Uint8Array(nMinutes) // wait time per iteration for particular destination + const inVehicleTravelTimesForDest = new Uint8Array(nMinutes) // in-vehicle travel time per destination + const walkTimesForDest = new Uint8Array(nMinutes) + + // x and y refer to pixel not origins here + // loop over rows first + for (let y = 0, pixelIdx = 0, stcOffset = 0; y < query.height; y++) { + for (let x = 0; x < query.width; x++, pixelIdx++) { + const nStops = stopTreeCache.data[stcOffset++] + + // can we reach this pixel without riding transit? + const nonTransitTime = getNonTransitTime(origin, {x, y}) + + // fill with unreachable, or the walk distance + fill(travelTimesForDest, nonTransitTime) + fill(waitTimesForDest, 255) + fill(inVehicleTravelTimesForDest, 255) + fill(walkTimesForDest, nonTransitTime) // when the origin is within walking distance and + // walking is the fastest way to reach the destination, _everything_ is walk time + + for (let stopIdx = 0; stopIdx < nStops; stopIdx++) { + // read the stop ID + const stopId = stopTreeCache.data[stcOffset++] + + // read the time (minutes) + const time = stopTreeCache.data[stcOffset++] + + for (let minute = 0; minute < nMinutes; minute++) { + const offset = origin.index[stopId] + minute * ITERATION_WIDTH + const travelTimeToStop = origin.data[offset] + + if (travelTimeToStop !== -1) { + const travelTimeToPixel = travelTimeToStop + time + + // no need to check that travelTimeToPixel < 255 as travelTimesForDest[minute] is preinitialized to the nontransit time or 255 + if (travelTimesForDest[minute] > travelTimeToPixel) { + travelTimesForDest[minute] = travelTimeToPixel + let inVehicle = inVehicleTravelTimesForDest[minute] = origin.data[offset + 1] + let wait = waitTimesForDest[minute] = origin.data[offset + 2] + // NB when we're talking about a particular trip, then walk + wait + inVehicle == total + // However if you calculate summary statistics for each of these individually, that may + // not be true. So we need to calculate walk here and explicitly calculate summary stats about it. + let walkTime = travelTimeToPixel - wait - inVehicle + walkTimesForDest[minute] = walkTime + } + } + } + } + + callback({ + travelTimesForDest, + walkTimesForDest, + inVehicleTravelTimesForDest, + waitTimesForDest, + x, + y + }) + } + } +} From 9b1073c716be498062ce818f336909a6695161da Mon Sep 17 00:00:00 2001 From: Matthew Wigginton Conway Date: Wed, 19 Oct 2016 12:26:37 -0400 Subject: [PATCH 4/7] feat(isochrones): allow disabling isochrone interpolation The interpolation can obscure issues with the isochrones, allow turning it off when desired. It also makes the isochrones snappier. --- lib/index.js | 5 +++-- lib/worker-handlers.js | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index d46fad1..834d634 100644 --- a/lib/index.js +++ b/lib/index.js @@ -256,11 +256,12 @@ export default class Browsochrones extends WebWorkerPromiseInterface { } /** Get a GeoJSON isochrone with the given cutoff (in minutes) */ - getIsochrone (cutoff = 60) { + getIsochrone (cutoff = 60, interpolation = true) { return this.work({ command: 'getIsochrone', message: { - cutoff + cutoff, + interpolation } }) } diff --git a/lib/worker-handlers.js b/lib/worker-handlers.js index e37772c..c47dd4f 100644 --- a/lib/worker-handlers.js +++ b/lib/worker-handlers.js @@ -108,6 +108,7 @@ module.exports = createHandler({ width: ctx.query.width, height: ctx.query.height, cutoff: message.cutoff, + interpolation: message.interpolation, // coords are at zoom level of query project: ([x, y]) => { return [mercator.pixelToLon(x + ctx.query.west, ctx.query.zoom), mercator.pixelToLat(y + ctx.query.north, ctx.query.zoom)] From 8dccd534aaf428a630db15912157d741fc297e99 Mon Sep 17 00:00:00 2001 From: Matthew Wigginton Conway Date: Wed, 19 Oct 2016 15:50:48 -0400 Subject: [PATCH 5/7] feat(spectrogram): Add spectrogram data. Add the ability to produce spectrogram data, i.e. accessibility curves for each iteration. --- lib/get-spectrogram-data.js | 50 +++++++++++++++++++++++++++++++++++++ lib/get-surface.js | 1 - lib/index.js | 13 ++++++++++ lib/propagation.js | 3 ++- lib/worker-handlers.js | 9 +++++++ package.json | 6 +++-- 6 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 lib/get-spectrogram-data.js diff --git a/lib/get-spectrogram-data.js b/lib/get-spectrogram-data.js new file mode 100644 index 0000000..b3ee634 --- /dev/null +++ b/lib/get-spectrogram-data.js @@ -0,0 +1,50 @@ +/** Get data to render a spectrogram plot from browsochrones data */ + +import propagate from './propagation' + +const MAX_TRIP_LENGTH = 120 // minutes + +/** + * Return data used to draw a spectrogram. + * This is basically just an array of curves like so: + * For each iteration you will have an array of + * [opportunities reachable in 1 minute, + * marginal opportunities reachable in 2 minutes, + * ... + * marginal opportunities reachable in 120 minutes] + */ +export default function getSpectrogramData ({origin, query, stopTreeCache, grid}) { + let output = [] + for (let i = 0; i < origin.nMinutes; i++) output.push(new Uint32Array(MAX_TRIP_LENGTH)) + + propagate({ + query, + stopTreeCache, + origin, + callback: ({ + travelTimesForDest, + x, + y + }) => { + let gridx = x + query.west - grid.west + let gridy = y + query.north - grid.north + + // off the grid, return + if (gridx < 0 || gridy < 0 || gridx >= grid.width || gridy >= grid.height) return + + const val = grid.data[gridy * grid.width + gridx] + + for (let i = 0; i < travelTimesForDest.length; i++) { + let time = travelTimesForDest[i] + if (time === 255) continue // unreachable + + if (time < MAX_TRIP_LENGTH) { + // time - 1 so areas reachable in 1 minute will be included in output[i][0] + // TODO audit all the places we're flooring things + output[i][time - 1] += val + } + } + }}) + + return output +} diff --git a/lib/get-surface.js b/lib/get-surface.js index 377ee2e..6b55614 100644 --- a/lib/get-surface.js +++ b/lib/get-surface.js @@ -24,7 +24,6 @@ export default function getSurface ({origin, query, stopTreeCache, which}) { query, stopTreeCache, origin, - nMinutes, callback: ({ travelTimesForDest, walkTimesForDest, diff --git a/lib/index.js b/lib/index.js index 834d634..550a706 100644 --- a/lib/index.js +++ b/lib/index.js @@ -74,6 +74,19 @@ export default class Browsochrones extends WebWorkerPromiseInterface { }) } + getSpectrogramData (grid) { + if (!this.isLoaded()) { + return Promise.reject(new Error('Accessibility cannot be computed before generating a surface.')) + } + + return this.work({ + command: 'getSpectrogramData', + message: { + grid + } + }) + } + generateDestinationData (point) { return this.work({ command: 'generateDestinationData', diff --git a/lib/propagation.js b/lib/propagation.js index 88a43b4..3a9ab74 100644 --- a/lib/propagation.js +++ b/lib/propagation.js @@ -16,7 +16,8 @@ import fill from 'lodash.fill' import {getNonTransitTime, ITERATION_WIDTH} from './origin' -export default function propagate ({ query, stopTreeCache, nMinutes, origin, callback }) { +export default function propagate ({ query, stopTreeCache, origin, callback }) { + let {nMinutes} = origin const travelTimesForDest = new Uint8Array(nMinutes) // the total travel time per iteration to reach a particular destination const waitTimesForDest = new Uint8Array(nMinutes) // wait time per iteration for particular destination const inVehicleTravelTimesForDest = new Uint8Array(nMinutes) // in-vehicle travel time per destination diff --git a/lib/worker-handlers.js b/lib/worker-handlers.js index c47dd4f..30d0c11 100644 --- a/lib/worker-handlers.js +++ b/lib/worker-handlers.js @@ -9,6 +9,7 @@ import {create as createGrid} from './grid' import * as mercator from './mercator' import {create as createOrigin} from './origin' import {create as createStopTreeCache} from './stop-tree-cache' +import getSpectrogramData from './get-spectrogram-data' module.exports = createHandler({ putGrid (ctx, message) { @@ -128,5 +129,13 @@ module.exports = createHandler({ stopTreeCache: ctx.stopTreeCache, to: message.point }) + }, + getSpectrogramData (ctx, message) { + return getSpectrogramData({ + origin: ctx.origin, + query: ctx.query, + stopTreeCache: ctx.stopTreeCache, + grid: ctx.grids.get(message.grid) + }) } }) diff --git a/package.json b/package.json index 360095f..9238a0b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "author": "Conveyal", "license": "MIT", "dependencies": { + "chroma-js": "^1.2.1", "color": "^0.11.3", + "d3": "^4.2.7", "debug": "^2.2.0", "jsolines": "^0.2.2", "json": "^9.0.3", @@ -35,9 +37,9 @@ "normalize.css": "^5.0.0", "react": "^15.3.1", "react-dom": "^15.3.1", + "semantic-release": "^4.3.5", "tape": "^4.2.2", - "transitive-js": "^0.8.2", - "semantic-release": "^4.3.5" + "transitive-js": "^0.8.2" }, "standard": { "parser": "babel-eslint" From c8533daa135bedd9ebdc73900bf83676f4c5c927 Mon Sep 17 00:00:00 2001 From: Matthew Wigginton Conway Date: Wed, 19 Oct 2016 16:14:32 -0400 Subject: [PATCH 6/7] build(deps): Remove unused dependencies. Remove dependencies accidentally installed here instead of in scenario editor. --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 9238a0b..1f57a87 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,7 @@ "author": "Conveyal", "license": "MIT", "dependencies": { - "chroma-js": "^1.2.1", "color": "^0.11.3", - "d3": "^4.2.7", "debug": "^2.2.0", "jsolines": "^0.2.2", "json": "^9.0.3", From 3456a5ddecae41ac9ed5851c2c9150a44e572f8e Mon Sep 17 00:00:00 2001 From: Matthew Wigginton Conway Date: Thu, 27 Oct 2016 11:44:10 -0400 Subject: [PATCH 7/7] refactor(propagation): Replace let with const where possible. --- lib/get-spectrogram-data.js | 4 ++-- lib/propagation.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/get-spectrogram-data.js b/lib/get-spectrogram-data.js index b3ee634..3a3d792 100644 --- a/lib/get-spectrogram-data.js +++ b/lib/get-spectrogram-data.js @@ -14,7 +14,7 @@ const MAX_TRIP_LENGTH = 120 // minutes * marginal opportunities reachable in 120 minutes] */ export default function getSpectrogramData ({origin, query, stopTreeCache, grid}) { - let output = [] + const output = [] for (let i = 0; i < origin.nMinutes; i++) output.push(new Uint32Array(MAX_TRIP_LENGTH)) propagate({ @@ -35,7 +35,7 @@ export default function getSpectrogramData ({origin, query, stopTreeCache, grid} const val = grid.data[gridy * grid.width + gridx] for (let i = 0; i < travelTimesForDest.length; i++) { - let time = travelTimesForDest[i] + const time = travelTimesForDest[i] if (time === 255) continue // unreachable if (time < MAX_TRIP_LENGTH) { diff --git a/lib/propagation.js b/lib/propagation.js index 3a9ab74..fc5a90a 100644 --- a/lib/propagation.js +++ b/lib/propagation.js @@ -56,12 +56,12 @@ export default function propagate ({ query, stopTreeCache, origin, callback }) { // no need to check that travelTimeToPixel < 255 as travelTimesForDest[minute] is preinitialized to the nontransit time or 255 if (travelTimesForDest[minute] > travelTimeToPixel) { travelTimesForDest[minute] = travelTimeToPixel - let inVehicle = inVehicleTravelTimesForDest[minute] = origin.data[offset + 1] - let wait = waitTimesForDest[minute] = origin.data[offset + 2] + const inVehicle = inVehicleTravelTimesForDest[minute] = origin.data[offset + 1] + const wait = waitTimesForDest[minute] = origin.data[offset + 2] // NB when we're talking about a particular trip, then walk + wait + inVehicle == total // However if you calculate summary statistics for each of these individually, that may // not be true. So we need to calculate walk here and explicitly calculate summary stats about it. - let walkTime = travelTimeToPixel - wait - inVehicle + const walkTime = travelTimeToPixel - wait - inVehicle walkTimesForDest[minute] = walkTime } }