Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components/directions/maneuvers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';

import type { Leg } from '@/components/types';
import { Clock, MoveHorizontal, DollarSign, Ship } from 'lucide-react';
import { Clock, DollarSign, Ship } from 'lucide-react';
import { getTurnIcon } from '@/utils/get-direction-icon';
import { MetricItem } from '@/components/ui/metric-item';
import { RouteAttributes } from '@/components/ui/route-attributes';
import { formatDuration } from '@/utils/date-time';
Expand Down Expand Up @@ -70,7 +71,7 @@ export const Maneuvers = ({ legs, index }: ManeuversProps) => {
{mnv.type !== 4 && mnv.type !== 5 && mnv.type !== 6 && (
<div className="flex items-center gap-2">
<MetricItem
icon={MoveHorizontal}
icon={getTurnIcon(mnv)}
label="Length"
value={getLength(mnv.length)}
variant="outline"
Expand Down
118 changes: 118 additions & 0 deletions src/utils/get-direction-icon.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, it, expect } from 'vitest';

import { getTurnIcon, type ValhallaStep } from './get-direction-icon';

import {
ArrowUp,
ArrowRight,
ArrowLeft,
ArrowUpRight,
ArrowUpLeft,
CornerDownRight,
CornerDownLeft,
RotateCcw,
GitMerge,
CircleDot,
} from 'lucide-react';

describe('getTurnIcon', () => {
const step = (data: Partial<ValhallaStep>): ValhallaStep => data;

it('returns U-turn icon', () => {
expect(getTurnIcon(step({ instruction: 'Make a u-turn' }))).toBe(RotateCcw);
});

it('returns merge icon', () => {
expect(getTurnIcon(step({ instruction: 'Merge onto highway' }))).toBe(
GitMerge
);
});

it('handles enter + exit roundabout as turn (right)', () => {
expect(
getTurnIcon(
step({
instruction: 'Enter the roundabout and take the 1st exit',
bearing_before: 0,
bearing_after: 90,
})
)
).toBe(ArrowUpRight);
});

it('handles exit roundabout using bearing (left)', () => {
expect(
getTurnIcon(
step({
instruction: 'Exit the roundabout',
type: 15,
bearing_before: 0,
bearing_after: 260,
})
)
).toBe(ArrowLeft);
});

it('returns roundabout icon on enter', () => {
expect(
getTurnIcon(
step({
instruction: 'Enter the roundabout',
type: 15,
})
)
).toBe(CircleDot);
});

it('returns sharp right icon', () => {
expect(getTurnIcon(step({ instruction: 'Make a sharp right' }))).toBe(
CornerDownRight
);
});

it('returns sharp left icon', () => {
expect(getTurnIcon(step({ instruction: 'Make a sharp left' }))).toBe(
CornerDownLeft
);
});

it('returns slight right icon (bear right)', () => {
expect(getTurnIcon(step({ instruction: 'Bear right onto road' }))).toBe(
ArrowUpRight
);
});

it('returns slight left icon (keep left)', () => {
expect(getTurnIcon(step({ instruction: 'Keep left to continue' }))).toBe(
ArrowUpLeft
);
});

it('returns right icon', () => {
expect(getTurnIcon(step({ instruction: 'Turn right onto road' }))).toBe(
ArrowRight
);
});

it('returns left icon (via bearing exit fallback)', () => {
expect(
getTurnIcon(
step({
instruction: 'Exit something',
bearing_before: 0,
bearing_after: 270,
})
)
).toBe(ArrowLeft);
});

it('returns straight icon', () => {
expect(getTurnIcon(step({ instruction: 'Continue straight' }))).toBe(
ArrowUp
);
});

it('returns default icon when no match', () => {
expect(getTurnIcon(step({}))).toBe(ArrowUp);
});
});
127 changes: 127 additions & 0 deletions src/utils/get-direction-icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
ArrowUp,
ArrowRight,
ArrowLeft,
ArrowUpRight,
ArrowUpLeft,
CornerDownRight,
CornerDownLeft,
RotateCcw,
GitMerge,
CircleDot,
} from 'lucide-react';

import type { LucideIcon } from 'lucide-react';

export interface ValhallaStep {
type?: number;
instruction?: string;
bearing_before?: number;
bearing_after?: number;
}

function getDirectionFromBearing(
before: number,
after: number
): 'straight' | 'right' | 'left' | 'slight-right' | 'slight-left' {
const diff = (after - before + 360) % 360;

if (diff <= 10 || diff >= 350) return 'straight';
if (diff <= 90) return 'slight-right';
if (diff <= 180) return 'right';
if (diff <= 270) return 'left';

return 'slight-left';
}

export function getTurnIcon(step: ValhallaStep): LucideIcon {
const text = step.instruction?.toLowerCase() || '';

if (text.includes('u-turn')) return RotateCcw;
if (text.includes('merge')) return GitMerge;

if (text.includes('enter') && text.includes('exit')) {
if (
typeof step.bearing_before === 'number' &&
typeof step.bearing_after === 'number'
) {
const dir = getDirectionFromBearing(
step.bearing_before,
step.bearing_after
);

if (dir === 'right') return ArrowRight;
if (dir === 'left') return ArrowLeft;
if (dir === 'slight-right') return ArrowUpRight;
if (dir === 'slight-left') return ArrowUpLeft;
}

return ArrowRight;
}

if (
text.includes('exit') &&
(text.includes('roundabout') || step.type === 15)
) {
if (
typeof step.bearing_before === 'number' &&
typeof step.bearing_after === 'number'
) {
const dir = getDirectionFromBearing(
step.bearing_before,
step.bearing_after
);

if (dir === 'right') return ArrowRight;
if (dir === 'left') return ArrowLeft;
if (dir === 'slight-right') return ArrowUpRight;
if (dir === 'slight-left') return ArrowUpLeft;
}

return ArrowRight;
}

if (
text.includes('exit') &&
typeof step.bearing_before === 'number' &&
typeof step.bearing_after === 'number'
) {
const dir = getDirectionFromBearing(
step.bearing_before,
step.bearing_after
);

if (dir === 'right') return ArrowRight;
if (dir === 'left') return ArrowLeft;
if (dir === 'slight-right') return ArrowUpRight;
if (dir === 'slight-left') return ArrowUpLeft;
}

if (
text.includes('enter') &&
(text.includes('roundabout') || step.type === 15)
) {
return CircleDot;
}

if (text.includes('sharp right')) return CornerDownRight;
if (text.includes('sharp left')) return CornerDownLeft;
if (
text.includes('slight right') ||
text.includes('bear right') ||
text.includes('keep right')
)
return ArrowUpRight;

if (
text.includes('slight left') ||
text.includes('bear left') ||
text.includes('keep left')
)
return ArrowUpLeft;

if (text.includes('right')) return ArrowRight;
if (text.includes('straight') || text.includes('continue')) return ArrowUp;

return ArrowUp;
}