Skip to content

Commit

Permalink
Merge pull request #66 from conveyal/single-propagation
Browse files Browse the repository at this point in the history
feat(propagation): Generate spectrogram data and surfaces simultaneou…
  • Loading branch information
trevorgerhardt authored Dec 7, 2016
2 parents 1e49515 + 71cf011 commit 7cc9c87
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 312 deletions.
4 changes: 2 additions & 2 deletions example.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ map.on('click', async function (e) {

console.time('generating surface')
console.time('generating both surfaces')
await bc.generateSurface()
await bc.generateSurface('jobs')
console.timeEnd('generating surface')
await bc2.generateSurface()
await bc2.generateSurface('jobs')
console.timeEnd('generating both surfaces')

if (surfaceLayer) map.removeLayer(surfaceLayer)
Expand Down
48 changes: 0 additions & 48 deletions lib/get-spectrogram-data.js

This file was deleted.

47 changes: 42 additions & 5 deletions lib/get-surface.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,37 @@ import dbg from 'debug'
import propagate from './propagation'
const debug = dbg('browsochrones:get-surface')

const MAX_TRIP_LENGTH = 120 // minutes

/**
* 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
* x and y origin point relative to the query, what parameter you want (BEST_CASE, WORST_CASE or MEDIAN),
* and a cutoff for accessibility calculations. Returns a travel time/accessibility surface which can be used by isochoroneTile and accessibilityForCutoff
* and a cutoff for accessibility calculations. Sets the current surface within the Browsochrones instance,
* and returns 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]
*
* This does mean that changing the grid requires recomputing the surface even though that's not technically
* required. It is assumed this is a relatively rare occurence and it's worth the extra work there to
* avoid doing propagation twice, once for getting the surface and once for the spectrogram data.
* It's also better than having duplicate code to avoid computing a surface sometimes. We could add a
* switch to this function to select what is generated but that may even be more complexity than is needed.
*/
export default function getSurface ({origin, query, stopTreeCache, which}) {
debug('generating surface')
export default function getSurfaceAndSpectrogramData ({origin, query, stopTreeCache, grid, which}) {
debug('generating surface and spectrogram data')
const surface = new Uint8Array(query.width * query.height)
const waitTimes = new Uint8Array(query.width * query.height)
const inVehicleTravelTimes = new Uint8Array(query.width * query.height)
const walkTimes = new Uint8Array(query.width * query.height)

const spectrogramData = []
for (let i = 0; i < origin.nMinutes; i++) spectrogramData.push(new Uint32Array(MAX_TRIP_LENGTH))

const transitOffset = getTransitOffset(origin.data[0])

// how many departure minutes are there. skip number of stops
Expand All @@ -24,20 +42,38 @@ export default function getSurface ({origin, query, stopTreeCache, which}) {
query,
stopTreeCache,
origin,
callback: ({
callback ({
travelTimesForDest,
walkTimesForDest,
inVehicleTravelTimesForDest,
waitTimesForDest,
x,
y
}) => {
}) {
// handle surface
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)

// handle spectrogram data
const gridx = x + query.west - grid.west
const gridy = y + query.north - grid.north

if (grid.contains(gridx, gridy)) {
const val = grid.data[gridy * grid.width + gridx]

for (let i = 0; i < travelTimesForDest.length; i++) {
const time = travelTimesForDest[i]
if (time !== 255 && 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
spectrogramData[i][time - 1] += val
}
}
}
}
})

Expand All @@ -49,6 +85,7 @@ export default function getSurface ({origin, query, stopTreeCache, which}) {
walkTimes,
inVehicleTravelTimes,
query,
spectrogramData,
nMinutes // TODO already present in query
}
}
Expand Down
3 changes: 2 additions & 1 deletion lib/get-transitive-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ export default function getTransitiveData ({ origin, query, stopTreeCache, netwo

// from can be left null and it will be inferred from the destination
const fromCoord = from != null ? ll(from) : {}
const fromName = from != null ? from.name : null

output.places.push({
place_id: 'from',
place_name: from.name, // TODO do this with icons, avoid English works (or Portuguese words, for that matter)
place_name: fromName, // todo do this with icons, avoid English works (or Portuguese words, for that matter)
place_lat: fromCoord.lat || pixelToLat(query.north + origin.y, query.zoom),
place_lon: fromCoord.lon || pixelToLon(query.west + origin.x, query.zoom)
})
Expand Down
31 changes: 13 additions & 18 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from 'assert'
import WebWorkerPromiseInterface from 'web-worker-promise-interface'

import {create as createGridFunc} from './grid'
Expand All @@ -7,10 +8,6 @@ import workerHandlers from './worker-handlers'
const imageHeight = 256
const imageWidth = 256

function assert (e, msg) {
if (!e) throw new Error(msg)
}

export default class Browsochrones extends WebWorkerPromiseInterface {
originLoaded = false
query = false
Expand Down Expand Up @@ -77,6 +74,7 @@ export default class Browsochrones extends WebWorkerPromiseInterface {
}

async getAccessibilityForGrid (grid, cutoff = 60) {
assert(grid, 'A grid is required to get accessibility.')
assert(this.isLoaded(), 'Accessibility cannot be computed before generating a surface.')

return this.work({
Expand All @@ -87,18 +85,9 @@ export default class Browsochrones extends WebWorkerPromiseInterface {
})
}

async getSpectrogramData (grid) {
assert(this.isLoaded(), 'Accessibility cannot be computed before generating a surface.')

return this.work({
command: 'getSpectrogramData',
message: {
grid
}
})
}

async generateDestinationData ({ from, to }) {
assert(from, '`from` is required to generate destination data.')
assert(to, '`to` is required to generate destination data.')
assert(this.isLoaded(), 'Transitive data cannot be generated if Browsochrones is not fully loaded.')

return this.work({
Expand Down Expand Up @@ -128,24 +117,30 @@ export default class Browsochrones extends WebWorkerPromiseInterface {
})
}

async generateSurface (which = 'MEDIAN') {
async generateSurface (grid, which = 'MEDIAN') {
assert(grid, 'A grid is required to generate a surface.')
assert(this.isLoaded(), 'Surface cannot be generated if Browsochrones is not fully loaded.')

return this.work({
command: 'generateSurface',
message: {
grid,
which
}
}).then((message) => {
this.surfaceLoaded = true
return message
})
}

latLonToOriginPoint ({lat, lon, lng}) {
assert(lat, 'A latitude is required to generate an origin point.')
assert(lon || lng, 'A longitude is required to generate an origin point.')
assert(this.query, 'Cannot convert lat/lon without query.')

const {north, west, zoom} = this.query
const x = latToPixel(lat, zoom)
const y = lonToPixel(lon || lng, zoom)
const x = lonToPixel(lon || lng, zoom)
const y = latToPixel(lat, zoom)
const ret = {
// TODO should these be rounded instead of floored?
x: (x - west) | 0,
Expand Down
9 changes: 4 additions & 5 deletions lib/propagation.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
* 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 }) {
Expand All @@ -33,10 +32,10 @@ export default function propagate ({ query, stopTreeCache, origin, callback }) {
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
travelTimesForDest.fill(nonTransitTime)
waitTimesForDest.fill(255)
inVehicleTravelTimesForDest.fill(255)
walkTimesForDest.fill(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++) {
Expand Down
17 changes: 6 additions & 11 deletions lib/worker-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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 @@ -46,13 +45,17 @@ module.exports = createHandler({
ctx.transitiveNetwork = message.network
},
generateSurface (ctx, message) {
ctx.surface = getSurface({
cutoff: message.cutoff,
const { spectrogramData, ...surface } = getSurface({
grid: ctx.grids.get(message.grid),
origin: ctx.origin,
query: ctx.query,
stopTreeCache: ctx.stopTreeCache,
which: message.which
})

ctx.surface = surface

return { spectrogramData }
},
generateDestinationData (ctx, message) {
const { to, from } = message
Expand Down Expand Up @@ -120,13 +123,5 @@ 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)
})
}
})
Loading

0 comments on commit 7cc9c87

Please sign in to comment.