Skip to content

Commit 6af9b00

Browse files
Merge pull request #68 from conveyal/dev
v0.11.0
2 parents 777a6e5 + 7cc9c87 commit 6af9b00

File tree

8 files changed

+290
-312
lines changed

8 files changed

+290
-312
lines changed

example.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ map.on('click', async function (e) {
136136

137137
console.time('generating surface')
138138
console.time('generating both surfaces')
139-
await bc.generateSurface()
139+
await bc.generateSurface('jobs')
140140
console.timeEnd('generating surface')
141-
await bc2.generateSurface()
141+
await bc2.generateSurface('jobs')
142142
console.timeEnd('generating both surfaces')
143143

144144
if (surfaceLayer) map.removeLayer(surfaceLayer)

lib/get-spectrogram-data.js

Lines changed: 0 additions & 48 deletions
This file was deleted.

lib/get-surface.js

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,37 @@ import dbg from 'debug'
22
import propagate from './propagation'
33
const debug = dbg('browsochrones:get-surface')
44

5+
const MAX_TRIP_LENGTH = 120 // minutes
6+
57
/**
68
* Get a travel time surface and accessibility results for a particular origin.
79
* Pass in references to the query (the JS object stored in query.json), the stopTreeCache, the origin file, the
810
* x and y origin point relative to the query, what parameter you want (BEST_CASE, WORST_CASE or MEDIAN),
9-
* and a cutoff for accessibility calculations. Returns a travel time/accessibility surface which can be used by isochoroneTile and accessibilityForCutoff
11+
* and a cutoff for accessibility calculations. Sets the current surface within the Browsochrones instance,
12+
* and returns data used to draw a spectrogram.
13+
* This is basically just an array of curves like so:
14+
* For each iteration you will have an array of
15+
* [opportunities reachable in 1 minute,
16+
* marginal opportunities reachable in 2 minutes,
17+
* ...
18+
* marginal opportunities reachable in 120 minutes]
19+
*
20+
* This does mean that changing the grid requires recomputing the surface even though that's not technically
21+
* required. It is assumed this is a relatively rare occurence and it's worth the extra work there to
22+
* avoid doing propagation twice, once for getting the surface and once for the spectrogram data.
23+
* It's also better than having duplicate code to avoid computing a surface sometimes. We could add a
24+
* switch to this function to select what is generated but that may even be more complexity than is needed.
1025
*/
11-
export default function getSurface ({origin, query, stopTreeCache, which}) {
12-
debug('generating surface')
26+
export default function getSurfaceAndSpectrogramData ({origin, query, stopTreeCache, grid, which}) {
27+
debug('generating surface and spectrogram data')
1328
const surface = new Uint8Array(query.width * query.height)
1429
const waitTimes = new Uint8Array(query.width * query.height)
1530
const inVehicleTravelTimes = new Uint8Array(query.width * query.height)
1631
const walkTimes = new Uint8Array(query.width * query.height)
1732

33+
const spectrogramData = []
34+
for (let i = 0; i < origin.nMinutes; i++) spectrogramData.push(new Uint32Array(MAX_TRIP_LENGTH))
35+
1836
const transitOffset = getTransitOffset(origin.data[0])
1937

2038
// how many departure minutes are there. skip number of stops
@@ -24,20 +42,38 @@ export default function getSurface ({origin, query, stopTreeCache, which}) {
2442
query,
2543
stopTreeCache,
2644
origin,
27-
callback: ({
45+
callback ({
2846
travelTimesForDest,
2947
walkTimesForDest,
3048
inVehicleTravelTimesForDest,
3149
waitTimesForDest,
3250
x,
3351
y
34-
}) => {
52+
}) {
53+
// handle surface
3554
const pixelIdx = y * query.width + x
3655
// compute and set value for pixel
3756
surface[pixelIdx] = computePixelValue(which, travelTimesForDest)
3857
waitTimes[pixelIdx] = computePixelValue(which, waitTimesForDest)
3958
walkTimes[pixelIdx] = computePixelValue(which, walkTimesForDest)
4059
inVehicleTravelTimes[pixelIdx] = computePixelValue(which, inVehicleTravelTimesForDest)
60+
61+
// handle spectrogram data
62+
const gridx = x + query.west - grid.west
63+
const gridy = y + query.north - grid.north
64+
65+
if (grid.contains(gridx, gridy)) {
66+
const val = grid.data[gridy * grid.width + gridx]
67+
68+
for (let i = 0; i < travelTimesForDest.length; i++) {
69+
const time = travelTimesForDest[i]
70+
if (time !== 255 && time < MAX_TRIP_LENGTH) {
71+
// time - 1 so areas reachable in 1 minute will be included in output[i][0]
72+
// TODO audit all the places we're flooring things
73+
spectrogramData[i][time - 1] += val
74+
}
75+
}
76+
}
4177
}
4278
})
4379

@@ -49,6 +85,7 @@ export default function getSurface ({origin, query, stopTreeCache, which}) {
4985
walkTimes,
5086
inVehicleTravelTimes,
5187
query,
88+
spectrogramData,
5289
nMinutes // TODO already present in query
5390
}
5491
}

lib/get-transitive-data.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ export default function getTransitiveData ({ origin, query, stopTreeCache, netwo
2828

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

3233
output.places.push({
3334
place_id: 'from',
34-
place_name: from.name, // TODO do this with icons, avoid English works (or Portuguese words, for that matter)
35+
place_name: fromName, // todo do this with icons, avoid English works (or Portuguese words, for that matter)
3536
place_lat: fromCoord.lat || pixelToLat(query.north + origin.y, query.zoom),
3637
place_lon: fromCoord.lon || pixelToLon(query.west + origin.x, query.zoom)
3738
})

lib/index.js

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import assert from 'assert'
12
import WebWorkerPromiseInterface from 'web-worker-promise-interface'
23

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

10-
function assert (e, msg) {
11-
if (!e) throw new Error(msg)
12-
}
13-
1411
export default class Browsochrones extends WebWorkerPromiseInterface {
1512
originLoaded = false
1613
query = false
@@ -77,6 +74,7 @@ export default class Browsochrones extends WebWorkerPromiseInterface {
7774
}
7875

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

8280
return this.work({
@@ -87,18 +85,9 @@ export default class Browsochrones extends WebWorkerPromiseInterface {
8785
})
8886
}
8987

90-
async getSpectrogramData (grid) {
91-
assert(this.isLoaded(), 'Accessibility cannot be computed before generating a surface.')
92-
93-
return this.work({
94-
command: 'getSpectrogramData',
95-
message: {
96-
grid
97-
}
98-
})
99-
}
100-
10188
async generateDestinationData ({ from, to }) {
89+
assert(from, '`from` is required to generate destination data.')
90+
assert(to, '`to` is required to generate destination data.')
10291
assert(this.isLoaded(), 'Transitive data cannot be generated if Browsochrones is not fully loaded.')
10392

10493
return this.work({
@@ -128,24 +117,30 @@ export default class Browsochrones extends WebWorkerPromiseInterface {
128117
})
129118
}
130119

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

134124
return this.work({
135125
command: 'generateSurface',
136126
message: {
127+
grid,
137128
which
138129
}
139130
}).then((message) => {
140131
this.surfaceLoaded = true
132+
return message
141133
})
142134
}
143135

144136
latLonToOriginPoint ({lat, lon, lng}) {
137+
assert(lat, 'A latitude is required to generate an origin point.')
138+
assert(lon || lng, 'A longitude is required to generate an origin point.')
145139
assert(this.query, 'Cannot convert lat/lon without query.')
140+
146141
const {north, west, zoom} = this.query
147-
const x = latToPixel(lat, zoom)
148-
const y = lonToPixel(lon || lng, zoom)
142+
const x = lonToPixel(lon || lng, zoom)
143+
const y = latToPixel(lat, zoom)
149144
const ret = {
150145
// TODO should these be rounded instead of floored?
151146
x: (x - west) | 0,

lib/propagation.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* y: Y coordinate of grid for this destination
1414
*/
1515

16-
import fill from 'lodash.fill'
1716
import {getNonTransitTime, ITERATION_WIDTH} from './origin'
1817

1918
export default function propagate ({ query, stopTreeCache, origin, callback }) {
@@ -33,10 +32,10 @@ export default function propagate ({ query, stopTreeCache, origin, callback }) {
3332
const nonTransitTime = getNonTransitTime(origin, {x, y})
3433

3534
// fill with unreachable, or the walk distance
36-
fill(travelTimesForDest, nonTransitTime)
37-
fill(waitTimesForDest, 255)
38-
fill(inVehicleTravelTimesForDest, 255)
39-
fill(walkTimesForDest, nonTransitTime) // when the origin is within walking distance and
35+
travelTimesForDest.fill(nonTransitTime)
36+
waitTimesForDest.fill(255)
37+
inVehicleTravelTimesForDest.fill(255)
38+
walkTimesForDest.fill(nonTransitTime) // when the origin is within walking distance and
4039
// walking is the fastest way to reach the destination, _everything_ is walk time
4140

4241
for (let stopIdx = 0; stopIdx < nStops; stopIdx++) {

lib/worker-handlers.js

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {create as createGrid} from './grid'
99
import * as mercator from './mercator'
1010
import {create as createOrigin} from './origin'
1111
import {create as createStopTreeCache} from './stop-tree-cache'
12-
import getSpectrogramData from './get-spectrogram-data'
1312

1413
module.exports = createHandler({
1514
putGrid (ctx, message) {
@@ -46,13 +45,17 @@ module.exports = createHandler({
4645
ctx.transitiveNetwork = message.network
4746
},
4847
generateSurface (ctx, message) {
49-
ctx.surface = getSurface({
50-
cutoff: message.cutoff,
48+
const { spectrogramData, ...surface } = getSurface({
49+
grid: ctx.grids.get(message.grid),
5150
origin: ctx.origin,
5251
query: ctx.query,
5352
stopTreeCache: ctx.stopTreeCache,
5453
which: message.which
5554
})
55+
56+
ctx.surface = surface
57+
58+
return { spectrogramData }
5659
},
5760
generateDestinationData (ctx, message) {
5861
const { to, from } = message
@@ -120,13 +123,5 @@ module.exports = createHandler({
120123
stopTreeCache: ctx.stopTreeCache,
121124
to: message.point
122125
})
123-
},
124-
getSpectrogramData (ctx, message) {
125-
return getSpectrogramData({
126-
origin: ctx.origin,
127-
query: ctx.query,
128-
stopTreeCache: ctx.stopTreeCache,
129-
grid: ctx.grids.get(message.grid)
130-
})
131126
}
132127
})

0 commit comments

Comments
 (0)