diff --git a/src/components/Directions/DirectionsBox.tsx b/src/components/Directions/DirectionsBox.tsx
index 364d5d635..d33a2e3d0 100644
--- a/src/components/Directions/DirectionsBox.tsx
+++ b/src/components/Directions/DirectionsBox.tsx
@@ -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) => {
diff --git a/src/components/Directions/Instructions.tsx b/src/components/Directions/Instructions.tsx
new file mode 100644
index 000000000..17f123287
--- /dev/null
+++ b/src/components/Directions/Instructions.tsx
@@ -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 ;
+};
+
+const StyledListItem = styled.li`
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+`;
+
+const Instruction = ({ instruction }: { instruction: Instruction }) => (
+
+
+
+ {instruction.street_name || instruction.text}
+
+ {instruction.distance > 0 && (
+
+
+
+
+
+
+ )}
+
+);
+
+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) => (
+
+ {instructions.map((instruction) => (
+
+ ))}
+
+);
diff --git a/src/components/Directions/Result.tsx b/src/components/Directions/Result.tsx
index e2ae15ef3..1c4381a9a 100644
--- a/src/components/Directions/Result.tsx
+++ b/src/components/Directions/Result.tsx
@@ -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)`
@@ -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`
@@ -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);
@@ -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 (
-
+ return (
+
+
{revealForm && (
@@ -89,17 +79,52 @@ export const Result = ({ result, revealForm }: Props) => {
tooltip={}
color="secondary"
/>
+
+
+ {result.instructions && (
+
+ )}
{revealForm && (
)}
-
+
+ {showInstructions && }
+
+ );
+};
+
+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 (
+
);
}
return (
-
+
{t('directions.result.time')}: {time}
{t('directions.result.distance')}: {distance}
@@ -107,6 +132,9 @@ export const Result = ({ result, revealForm }: Props) => {
{t('directions.result.ascent')}: {ascent}
+ {result.instructions && (
+
+ )}
);
diff --git a/src/components/Directions/helpers.tsx b/src/components/Directions/helpers.tsx
index a977e1f70..18824ec2a 100644
--- a/src/components/Directions/helpers.tsx
+++ b/src/components/Directions/helpers.tsx
@@ -47,3 +47,21 @@ export const CloseButton = () => (
);
+
+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);
diff --git a/src/components/Directions/routing/getGraphhopperResults.ts b/src/components/Directions/routing/getGraphhopperResults.ts
index 3cce79174..2de42cce2 100644
--- a/src/components/Directions/routing/getGraphhopperResults.ts
+++ b/src/components/Directions/routing/getGraphhopperResults.ts
@@ -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`;
@@ -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);
@@ -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,
};
};
diff --git a/src/components/Directions/routing/instructions.ts b/src/components/Directions/routing/instructions.ts
new file mode 100644
index 000000000..c1e5990d6
--- /dev/null
+++ b/src/components/Directions/routing/instructions.ts
@@ -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;
+ }
+};
diff --git a/src/components/Directions/routing/types.ts b/src/components/Directions/routing/types.ts
index 701e94a28..eabe53fa7 100644
--- a/src/components/Directions/routing/types.ts
+++ b/src/components/Directions/routing/types.ts
@@ -1,4 +1,5 @@
import { NamedBbox } from '../../../services/getCenter';
+import { Sign } from './instructions';
export type Profile = 'car' | 'bike' | 'walk';
@@ -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 {