diff --git a/scripts/test.mjs b/scripts/test.mjs index 2c5d122..0014778 100644 --- a/scripts/test.mjs +++ b/scripts/test.mjs @@ -180,6 +180,31 @@ async function run() { check(namedTurns.some((t) => t.instruction.toLowerCase().includes('onto 21st st')), 'turn step includes destination street ("Turn right onto 21st St")'); check(turns[turns.length - 1].instruction === 'Arrive at destination', 'last step is "Arrive at destination"'); + // Same-street merge: a road that bends (>30\u00b0 heading change) but keeps + // ONE name must collapse into a single step rather than two. + { + // S-shape: north, then a 45\u00b0 bend, then north again — all "Oak Ave". + const bendPath = []; + for (let i = 0; i <= 10; i++) bendPath.push([36.1 + i * 0.000017, -95.9]); // north + for (let i = 1; i <= 10; i++) bendPath.push([36.10017 + i * 0.000017, -95.9 + i * 0.000012]); // NE bend + for (let i = 1; i <= 10; i++) bendPath.push([36.10034 + i * 0.000012, -95.89988 + i * 0.000017]); // east-ish + const bent = buildTurnInstructions(bendPath, () => 'Oak Ave'); + const oakSteps = bent.filter((t) => t.instruction.toLowerCase().includes('oak ave')); + check(oakSteps.length === 1, `same-street merge: a bent "Oak Ave" collapses to 1 step (got ${oakSteps.length})`); + // Sum of all non-arrival step distances should still cover the route. + const namedCount = bent.filter((t) => t.instruction !== 'Arrive at destination').length; + check(namedCount >= 1, 'merged path still has instruction steps'); + } + + // Merge must NOT merge two distinct streets. + { + const mpath = []; + for (let i = 0; i <= 10; i++) mpath.push([36.1 + i * 0.000017, -95.9]); // north on A St + for (let i = 1; i <= 20; i++) mpath.push([36.10017, -95.9 + i * 0.000017]); // east on B St + const mt = buildTurnInstructions(mpath, (i) => (i < 10 ? 'A St' : 'B St')); + check(mt.some((t) => t.instruction.toLowerCase().includes('b st')), 'distinct streets are NOT merged (B St step present)'); + } + const { route } = await import(resolve(__dirname, '..', 'src', 'lib', 'routing.ts')); // Disconnected destination -> partial path. const g2 = { diff --git a/src/lib/routing.ts b/src/lib/routing.ts index c993f58..f96f98c 100644 --- a/src/lib/routing.ts +++ b/src/lib/routing.ts @@ -182,7 +182,10 @@ export function buildTurnInstructions( merged.push({ ...leg }); } } - const steps: TurnStep[] = []; + // Build one entry per leg, carrying its street name so later steps on the + // SAME street can be combined. + type LegStep = { distanceM: number; instruction: string; bearing: number; name?: string }; + const raw: LegStep[] = []; function legEnd(li: number): number { return li + 1 < merged.length ? merged[li + 1].segStart - 1 : segBearing.length - 1; } @@ -199,10 +202,28 @@ export function buildTurnInstructions( const angle = turnAngle(prevBearing, bearing); instruction = name ? `${maneuverLabel(angle)} onto ${name}` : maneuverLabel(angle); } - steps.push({ distanceM: Math.round(leg.dist), instruction, bearing }); + raw.push({ distanceM: Math.round(leg.dist), instruction, bearing, name }); } - // Canonical final arrival step. - steps.push({ distanceM: 0, instruction: 'Arrive at destination', bearing: segBearing[legs[legs.length - 1].segStart] }); + + // Merge consecutive steps that share the same street name into a single + // step, summing their distance. This collapses redundant "Continue on Pine + // St" / "Turn onto Pine St" entries when a street merely bends or dips. + const steps: TurnStep[] = []; + let lastStreet: string | undefined; + for (const step of raw) { + const last = steps[steps.length - 1]; + if (last && step.name !== undefined && lastStreet === step.name) { + // Same street as the previous step — absorb distance into it. + last.distanceM += step.distanceM; + continue; + } + steps.push({ distanceM: step.distanceM, instruction: step.instruction, bearing: step.bearing }); + lastStreet = step.name !== undefined ? step.name : lastStreet; + } + + // Canonical final arrival step. Use the final step's segment bearing. + const lastBearing = steps.length ? steps[steps.length - 1].bearing : (segBearing.length ? segBearing[segBearing.length - 1] : 0); + steps.push({ distanceM: 0, instruction: 'Arrive at destination', bearing: lastBearing }); return steps; }