diff --git a/lib/build-trajectory.js b/lib/build-trajectory.js index e6fbd6c..226d8c4 100644 --- a/lib/build-trajectory.js +++ b/lib/build-trajectory.js @@ -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) [segPtA, distToA] const distToB = distance({type: 'Point', coordinates: segPtB}, pt) + if (distToB === 0) [segPtB, distToB] const perpLength = Math.max(distToA, distToB) const dir = bearing(segPtA, segPtB) @@ -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 @@ -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 @@ -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) { + 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 @@ -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 } } @@ -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: {