Skip to content

Commit

Permalink
Directions: Add instructions (#744)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dlurak authored Nov 10, 2024
1 parent 5b925d2 commit c47e154
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 35 deletions.
1 change: 1 addition & 0 deletions src/components/Directions/DirectionsBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const Wrapper = styled(Stack)`
left: 8px;
z-index: 1001; // over the LayerSwitcherButton
width: 340px;
max-height: calc(100vh - 16px);
`;

const getOnrejected = (showToast: ShowToast) => {
Expand Down
71 changes: 71 additions & 0 deletions src/components/Directions/Instructions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import styled from '@emotion/styled';
import { icon, Sign } from './routing/instructions';
import { RoutingResult } from './routing/types';
import { useUserSettingsContext } from '../utils/UserSettingsContext';
import { toHumanDistance } from './helpers';
import { Stack, Typography } from '@mui/material';

type Instruction = RoutingResult['instructions'][number];

const Distance = ({ distance }: { distance: number }) => {
const { userSettings } = useUserSettingsContext();
const { isImperial } = userSettings;

return <>{toHumanDistance(isImperial, distance)}</>;
};

const Icon = ({ sign }: { sign: Sign }) => {
const Component = icon(sign);
return <Component fontSize="large" />;
};

const StyledListItem = styled.li`
display: flex;
flex-direction: column;
gap: 0.25rem;
`;

const Instruction = ({ instruction }: { instruction: Instruction }) => (
<StyledListItem>
<Stack direction="row" alignItems="center">
<Icon sign={instruction.sign} />
{instruction.street_name || instruction.text}
</Stack>
{instruction.distance > 0 && (
<Stack direction="row" alignItems="center" spacing={0.5}>
<Typography
noWrap
color="textSecondary"
variant="body1"
style={{ overflow: 'visible' }}
>
<Distance distance={instruction.distance} />
</Typography>
<hr style={{ width: '100%' }} />
</Stack>
)}
</StyledListItem>
);

const StyledList = styled.ul`
list-style: none;
padding: 0;
max-height: 100%;
display: flex;
flex-direction: column;
gap: 0.25rem;
`;

type Props = {
instructions: Instruction[];
};

export const Instructions = ({ instructions }: Props) => (
<StyledList>
{instructions.map((instruction) => (
<Instruction key={instruction.text} instruction={instruction} />
))}
</StyledList>
);
96 changes: 62 additions & 34 deletions src/components/Directions/Result.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { useMobileMode } from '../helpers';
import { Button, Paper, Typography } from '@mui/material';
import { Button, Paper, Stack, Typography } from '@mui/material';
import React from 'react';
import styled from '@emotion/styled';
import { convertHexToRgba } from '../utils/colorUtils';
import { TooltipButton } from '../utils/TooltipButton';
import { RoutingResult } from './routing/types';
import { t, Translation } from '../../services/intl';
import { CloseButton } from './helpers';
import { CloseButton, toHumanDistance } from './helpers';
import { useUserSettingsContext } from '../utils/UserSettingsContext';
import { Instructions } from './Instructions';

export const StyledPaper = styled(Paper)`
export const StyledPaper = styled(Paper)<{
$height?: string;
$overflow?: string;
}>`
backdrop-filter: blur(10px);
background: ${({ theme }) =>
convertHexToRgba(theme.palette.background.paper, 0.9)};
padding: ${({ theme }) => theme.spacing(2)};
height: ${({ $height }) => $height};
overflow-y: ${({ $overflow }) => $overflow};
`;

export const StyledPaperMobile = styled(Paper)`
Expand All @@ -23,6 +29,8 @@ export const StyledPaperMobile = styled(Paper)`
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)}
${({ theme }) => theme.spacing(0.5)} ${({ theme }) => theme.spacing(2)};
text-align: center;
height: 100%;
overflow-y: auto;
`;

const CloseContainer = styled.div`
Expand All @@ -31,24 +39,6 @@ const CloseContainer = styled.div`
right: 0;
`;

const getHumanMetric = (meters: number) => {
if (meters < 1000) {
return `${Math.round(meters)} m`;
}
return `${(meters / 1000).toFixed(1)} km`;
};

const getHumanImperial = (meters: number) => {
const miles = meters * 0.000621371192;
if (miles < 1) {
return `${Math.round(miles * 5280)} ft`;
}
return `${miles.toFixed(1)} mi`;
};

const toHumanDistance = (isImperial: boolean, meters: number) =>
isImperial ? getHumanImperial(meters) : getHumanMetric(meters);

const toHumanTime = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
Expand All @@ -67,18 +57,18 @@ const PoweredBy = ({ result }: { result: RoutingResult }) => (

type Props = { result: RoutingResult; revealForm: false | (() => void) };

export const Result = ({ result, revealForm }: Props) => {
const isMobileMode = useMobileMode();
const { userSettings } = useUserSettingsContext();
const { isImperial } = userSettings;

const time = toHumanTime(result.time);
const distance = toHumanDistance(isImperial, result.distance);
const ascent = toHumanDistance(isImperial, result.totalAscent);
const MobileResult = ({
result,
revealForm,
time,
distance,
ascent,
}: Props & Record<'time' | 'distance' | 'ascent', string>) => {
const [showInstructions, setShowInstructions] = React.useState(false);

if (isMobileMode) {
return (
<StyledPaperMobile elevation={3}>
return (
<StyledPaperMobile elevation={3}>
<div>
{revealForm && (
<CloseContainer>
<CloseButton />
Expand All @@ -89,24 +79,62 @@ export const Result = ({ result, revealForm }: Props) => {
tooltip={<PoweredBy result={result} />}
color="secondary"
/>
</div>
<Stack>
{result.instructions && (
<Button
size="small"
onClick={() => {
setShowInstructions((x) => !x);
}}
>
{showInstructions ? 'Hide' : 'Show'} instructions
</Button>
)}
{revealForm && (
<Button size="small" onClick={revealForm}>
{t('directions.edit_destinations')}
</Button>
)}
</StyledPaperMobile>
</Stack>
{showInstructions && <Instructions instructions={result.instructions} />}
</StyledPaperMobile>
);
};

export const Result = ({ result, revealForm }: Props) => {
const isMobileMode = useMobileMode();
const { userSettings } = useUserSettingsContext();
const { isImperial } = userSettings;

const time = toHumanTime(result.time);
const distance = toHumanDistance(isImperial, result.distance);
const ascent = toHumanDistance(isImperial, result.totalAscent);

if (isMobileMode) {
return (
<MobileResult
result={result}
revealForm={revealForm}
time={time}
distance={distance}
ascent={ascent}
/>
);
}

return (
<StyledPaper elevation={3}>
<StyledPaper elevation={3} $height="100%" $overflow="auto">
{t('directions.result.time')}: <strong>{time}</strong>
<br />
{t('directions.result.distance')}: <strong>{distance}</strong>
<br />
{t('directions.result.ascent')}: <strong>{ascent}</strong>
<br />
<br />
{result.instructions && (
<Instructions instructions={result.instructions} />
)}
<PoweredBy result={result} />
</StyledPaper>
);
Expand Down
18 changes: 18 additions & 0 deletions src/components/Directions/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,21 @@ export const CloseButton = () => (
<CloseIcon fontSize="small" />
</IconButton>
);

const getHumanMetric = (meters: number) => {
if (meters < 1000) {
return `${Math.round(meters)} m`;
}
return `${(meters / 1000).toFixed(1)} km`;
};

const getHumanImperial = (meters: number) => {
const miles = meters * 0.000621371192;
if (miles < 1) {
return `${Math.round(miles * 5280)} ft`;
}
return `${miles.toFixed(1)} mi`;
};

export const toHumanDistance = (isImperial: boolean, meters: number) =>
isImperial ? getHumanImperial(meters) : getHumanMetric(meters);
4 changes: 3 additions & 1 deletion src/components/Directions/routing/getGraphhopperResults.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Profile, RoutingResult } from './types';
import { LonLat } from '../../../services/types';
import { fetchJson } from '../../../services/fetch';
import { intl } from '../../../services/intl';

const API_KEY = `f189b841-6529-46c6-8a91-51f17477dcda`;

Expand All @@ -17,7 +18,7 @@ export const getGraphhopperResults = async (
const profile = profiles[mode];
const from = points[0].toReversed().join(','); // lon,lat!
const to = points[1].toReversed().join(',');
const url = `https://graphhopper.com/api/1/route?point=${from}&point=${to}&vehicle=${profile}&key=${API_KEY}&type=json&points_encoded=false&instructions=false&snap_prevention=ferry`;
const url = `https://graphhopper.com/api/1/route?point=${from}&point=${to}&vehicle=${profile}&key=${API_KEY}&type=json&points_encoded=false&snap_prevention=ferry&locale=${intl.lang}`;

const data = await fetchJson(url);

Expand All @@ -31,5 +32,6 @@ export const getGraphhopperResults = async (
link: 'https://graphhopper.com/',
bbox: { w, s, e, n },
geojson: data.paths[0].points,
instructions: data.paths[0].instructions,
};
};
73 changes: 73 additions & 0 deletions src/components/Directions/routing/instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
ArrowUpward,
FmdGood,
RampLeft,
RampRight,
RoundaboutLeft,
SportsScore,
TurnLeft,
TurnRight,
TurnSharpRight,
TurnSlightLeft,
TurnSlightRight,
UTurnLeft,
UTurnRight,
} from '@mui/icons-material';
import QuestionMark from '@mui/icons-material/QuestionMark';
import TurnSharpLeft from '@mui/icons-material/TurnSharpLeft';

const GRAPH_HOPPER_INSTRUCTION_CODES = {
[-3]: 'sharp left',
[-2]: 'left',
[-1]: 'slight left',
0: 'straight',
1: 'slight right',
2: 'right',
3: 'sharp right',
4: 'finish reached',
5: 'via reached',
6: 'roundabout',
[-7]: 'keep left',
7: 'keep right',
[-98]: 'unknown direction u-turn',
[-8]: 'left u-turn',
8: 'right u-turn',
};

export type Sign = keyof typeof GRAPH_HOPPER_INSTRUCTION_CODES;

export const icon = (sign: Sign) => {
switch (sign) {
case -3:
return TurnSharpLeft;
case -2:
return TurnLeft;
case -1:
return TurnSlightLeft;
case 0:
return ArrowUpward;
case 1:
return TurnSlightRight;
case 2:
return TurnRight;
case 3:
return TurnSharpRight;
case 4:
return SportsScore;
case 5:
return FmdGood;
case 6:
return RoundaboutLeft;
case -7:
return RampLeft;
case 7:
return RampRight;
case -98:
case -8:
return UTurnLeft;
case 8:
return UTurnRight;
default:
return QuestionMark;
}
};
10 changes: 10 additions & 0 deletions src/components/Directions/routing/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NamedBbox } from '../../../services/getCenter';
import { Sign } from './instructions';

export type Profile = 'car' | 'bike' | 'walk';

Expand All @@ -13,6 +14,15 @@ export type RoutingResult = {
link: string;
bbox: NamedBbox;
geojson: GeoJSON.GeoJSON;
instructions?: {
distance: number;
heading: number;
interval: [number, number];
sign: Sign;
street_name: string;
text: string;
time: string;
}[];
};

export class PointsTooFarError extends Error {
Expand Down

0 comments on commit c47e154

Please sign in to comment.