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 {