Skip to content

Commit

Permalink
lib/build-trajectory: fix bugs & edge cases 🐛
Browse files Browse the repository at this point in the history
The algorithm is still not perfect.

Once ad-freiburg/pfaedle#7 is implemented,
we should rely on {shapes,stop_times}.shape_dist_traveled.
  • Loading branch information
derhuerst committed May 27, 2021
1 parent 732b193 commit ca608bb
Show file tree
Hide file tree
Showing 2 changed files with 1,544 additions and 19 deletions.
103 changes: 84 additions & 19 deletions lib/build-trajectory.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ const {default: linesIntersection} = require('@turf/line-intersect')
// returns [distToSegment, nearestPoint]
const nearestOnSegment = (pt, segPtA, segPtB) => {
const distToA = distance({type: 'Point', coordinates: segPtA}, pt)
if (distToA === 0) return [segPtA, distToA]
const distToB = distance({type: 'Point', coordinates: segPtB}, pt)
if (distToB === 0) return [segPtB, distToB]

const perpLength = Math.max(distToA, distToB)
const dir = bearing(segPtA, segPtB)
Expand Down Expand Up @@ -60,6 +62,7 @@ const addInterpolatedTimes = (shapePt, fromI, toI) => {
}
}

// todo: use shape_dist_traveled if available
const buildTrajectory = async (shapeId, shapePoints, schedule, allStopLocs) => {
// We violate the GeoJSON spec by a certain extent here:
// > A position is an array of numbers. There MUST be two or more
Expand Down Expand Up @@ -92,6 +95,51 @@ const buildTrajectory = async (shapeId, shapePoints, schedule, allStopLocs) => {
const arrivals = Array.from(schedule.arrivals)
const departures = Array.from(schedule.departures)
const stopLocs = await Promise.all(schedule.stops.map(id => allStopLocs.get(id)))
const insertedShapePts = new Set()

// handle 1st stop if before 1st segment
const [firstNearest, firstDistance] = nearestOnSegment(stopLocs[0], shapePts[0], shapePts[1])
if (firstNearest === shapePts[0] && firstDistance > 0) {
// 1st stop is neither closer to 2nd shape pt nor to perpendicular into 1st segment
stopIds.shift()
const arrival = arrivals.shift()
const departure = departures.shift()
const stopLoc = stopLocs.shift()
const newShapePt = [
stopLoc[0], // longitude
stopLoc[1], // latitude
null, // altitude
arrival,
departure,
]
debug('adding helper shape segment', newShapePt, 'at 0')
shapePts.unshift(newShapePt)
insertedShapePts.add(newShapePt)
}

// handle last stop if after last segment
const [lastNearest, lastDistance] = nearestOnSegment(
stopLocs[stopLocs.length - 1],
shapePts[shapePts.length - 2],
shapePts[shapePts.length - 1],
)
if (lastNearest === shapePts[shapePts.length - 1] && lastDistance > 0) {
// last stop is neither closer to 2nd-last shape pt nor to perpendicular into last segment
stopIds.pop()
const arrival = arrivals.pop()
const departure = departures.pop()
const stopLoc = stopLocs.pop()
const newShapePt = [
stopLoc[0], // longitude
stopLoc[1], // latitude
null, // altitude
arrival,
departure,
]
debug('adding helper shape segment', newShapePt, 'at', shapePts.length)
shapePts.push(newShapePt)
insertedShapePts.add(newShapePt)
}

// iterate over shape segments, identify nearby stops, add arrival/departure times
let stopsI = 0
Expand All @@ -104,16 +152,32 @@ const buildTrajectory = async (shapeId, shapePoints, schedule, allStopLocs) => {
const [
nearest, distance,
] = nearestOnSegment(stopLoc, fromShapePt, toShapePt)
const iNearest = nearest === fromShapePt
? i - 1
: (nearest === toShapePt ? i : NaN)
debug({
stopsI, stopId: stopIds[stopsI], stopLoc,
i, fromShapePt: shapePts[i - 1], toShapePt: shapePts[i],
nearest: nearest === fromShapePt ? 'fromShapePt' : (nearest === toShapePt ? 'toShapePt' : 'intersection'),
stopsI, maxStopsI: stopIds.length - 1,
stopId: stopIds[stopsI], stopLoc,
i, maxI: shapePts.length,
fromShapePt: shapePts[i - 1], toShapePt: shapePts[i],
iNearest, nearest,
distance, prevDistance, prevPrevDistance,
iPrevNearest, prevNearest,
})

// Has the shape just approached the stop?
if (
if (distance === 0 && !Number.isNaN(iNearest)) {
const shapePt = shapePts[iNearest]
debug('adding times to shape pt', shapePt, 'at', iNearest)
shapePt[3] = arrivals[stopsI]
shapePt[4] = departures[stopsI]

stopsI++
i-- // nearest was 1 iter. ago, repeat loop with *current* segment
prevPrevDistance = prevDistance = Infinity
prevNearest = null
iPrevNearest = NaN
} else if (
// Has the shape come closest to the stop during the last segment?
prevDistance < prevPrevDistance
&& distance >= prevDistance
&& prevDistance < .3 // 300m
Expand All @@ -132,32 +196,26 @@ const buildTrajectory = async (shapeId, shapePoints, schedule, allStopLocs) => {
]
debug('adding perpendicular as shape pt', newShapePt, 'at', iPrevNearest)
shapePts.splice(iPrevNearest, 0, newShapePt)
insertedShapePts.add(newShapePt)
i++
} else {
// previous nearest was a shape point
debug('adding times to shape pt', shapePts[iPrevNearest], 'at', iPrevNearest)
shapePts[iPrevNearest][3] = arrivals[stopsI]
shapePts[iPrevNearest][4] = departures[stopsI]
const shapePt = shapePts[iPrevNearest]
debug('adding times to shape pt', shapePt, 'at', iPrevNearest)
shapePt[3] = arrivals[stopsI]
shapePt[4] = departures[stopsI]
}

stopsI++
i-- // nearest was 1 iter. ago, repeat loop with *current* segment
prevPrevDistance = prevDistance = Infinity
prevNearest = null
iPrevNearest = NaN
stopsI++
} else {
prevPrevDistance = prevDistance
prevDistance = distance
if (nearest === fromShapePt) {
prevNearest = fromShapePt
iPrevNearest = i - 1
} else if (nearest === toShapePt) {
prevNearest = toShapePt
iPrevNearest = i
} else { // nearest is perpendicular into segment
prevNearest = nearest
iPrevNearest = NaN
}
iPrevNearest = iNearest
prevNearest = nearest
}
}

Expand All @@ -178,6 +236,13 @@ const buildTrajectory = async (shapeId, shapePoints, schedule, allStopLocs) => {
}
}

for (let i = 0, l = shapePts.length; i < l; i++) {
if (insertedShapePts.has(shapePts[i])) {
shapePts.splice(i, 1) // remove it
i--
}
}

return {
type: 'Feature',
properties: {
Expand Down
Loading

0 comments on commit ca608bb

Please sign in to comment.