Skip to content

Commit

Permalink
Merge pull request #47 from conveyal/dev
Browse files Browse the repository at this point in the history
Add additional code to produce spectrograms
  • Loading branch information
mattwigway authored Oct 27, 2016
2 parents 7a5df0b + 3456a5d commit f373050
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 58 deletions.
50 changes: 50 additions & 0 deletions lib/get-spectrogram-data.js
Original file line number Diff line number Diff line change
@@ -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}) {
const 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++) {
const 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
}
68 changes: 15 additions & 53 deletions lib/get-surface.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -21,63 +19,27 @@ 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,
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')

Expand Down
9 changes: 8 additions & 1 deletion lib/grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -16,6 +21,8 @@ export function create (data) {
north: header[2],
width: header[3],
height: header[4],
data: array
data: array,
min,
max
}
}
21 changes: 19 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -73,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',
Expand Down Expand Up @@ -255,12 +269,15 @@ 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
}
})
}
}

export const createGrid = createGridFunc
81 changes: 81 additions & 0 deletions lib/propagation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* 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, 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
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
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.
const walkTime = travelTimeToPixel - wait - inVehicle
walkTimesForDest[minute] = walkTime
}
}
}
}

callback({
travelTimesForDest,
walkTimesForDest,
inVehicleTravelTimesForDest,
waitTimesForDest,
x,
y
})
}
}
}
10 changes: 10 additions & 0 deletions lib/worker-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -108,6 +109,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)]
Expand All @@ -127,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)
})
}
})
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,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"
Expand Down

0 comments on commit f373050

Please sign in to comment.