Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TurnByTurn: Introduce Turn by turn navigation #768

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Binary file added public/directionalArrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 18 additions & 6 deletions src/components/App/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Ref, useEffect, useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import Cookies from 'js-cookie';

import nextCookies from 'next-cookies';
Expand All @@ -25,7 +25,7 @@ import { ClimbingCragDialog } from '../FeaturePanel/Climbing/ClimbingCragDialog'
import { ClimbingContextProvider } from '../FeaturePanel/Climbing/contexts/ClimbingContext';
import { StarsProvider } from '../utils/StarsContext';
import { SnackbarProvider } from '../utils/SnackbarContext';
import { useIsClient, useMobileMode } from '../helpers';
import { useMobileMode } from '../helpers';
import { FeaturePanelInDrawer } from '../FeaturePanel/FeaturePanelInDrawer';
import { UserSettingsProvider } from '../utils/UserSettingsContext';
import { MyTicksPanel } from '../MyTicksPanel/MyTicksPanel';
Expand All @@ -38,7 +38,13 @@ import {
} from '../../services/climbing-areas/getClimbingAreas';
import { DirectionsBox } from '../Directions/DirectionsBox';
import { Scrollbars } from 'react-custom-scrollbars';
import {
TurnByTurnProvider,
useTurnByTurnContext,
} from '../utils/TurnByTurnContext';
import { TurnByTurnNavigation } from '../TurnByTurnNavigation/TurnByTurnNavigation';
import { ClimbingGradesTable } from '../FeaturePanel/Climbing/ClimbingGradesTable';
import { ConfirmationProvider } from '../utils/ConfirmationContext';

const URL_NOT_FOUND_TOAST = {
message: t('url_not_found_toast'),
Expand Down Expand Up @@ -124,8 +130,9 @@ const IndexWithProviders = ({ climbingAreas }: IndexWithProvidersProps) => {
const isMobileMode = useMobileMode();
const { feature, featureShown, homepageShown } = useFeatureContext();
const router = useRouter();
const isMounted = useIsClient();
const scrollRef = useScrollToTopWhenRouteChanged() as any;
const { routingResult } = useTurnByTurnContext();

useUpdateViewFromFeature();
usePersistMapView();
useUpdateViewFromHash();
Expand All @@ -150,8 +157,9 @@ const IndexWithProviders = ({ climbingAreas }: IndexWithProvidersProps) => {
return (
<>
<Loading />
{directions && <DirectionsBox />}
{!directions && <SearchBox withShadow={withShadow} />}
{directions && !routingResult && <DirectionsBox />}
{!(directions || routingResult) && <SearchBox withShadow={withShadow} />}
{routingResult && <TurnByTurnNavigation />}
{featureShown && !isMobileMode && (
<FeaturePanelOnSide scrollRef={scrollRef} />
)}
Expand Down Expand Up @@ -210,7 +218,11 @@ const App: NextPage<Props> = ({
<StarsProvider>
<EditDialogProvider /* TODO supply router.query */>
<QueryClientProvider client={reactQueryClient}>
<IndexWithProviders climbingAreas={climbingAreas} />
<TurnByTurnProvider>
<ConfirmationProvider>
<IndexWithProviders climbingAreas={climbingAreas} />
</ConfirmationProvider>
</TurnByTurnProvider>
</QueryClientProvider>
</EditDialogProvider>
</StarsProvider>
Expand Down
15 changes: 12 additions & 3 deletions src/components/Directions/DirectionsBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import React, { useCallback, useState } from 'react';
import { Stack } from '@mui/material';
import { Result } from './Result';
import { RoutingResult } from './routing/types';
import { Profile, RoutingResult } from './routing/types';
import { useBoolState, useMobileMode } from '../helpers';
import { DirectionsForm } from './DirectionsForm';

Expand All @@ -19,6 +19,7 @@ export const DirectionsBox = () => {
const isMobileMode = useMobileMode();
const [result, setResult] = useState<RoutingResult>(null);
const [revealed, revealForm, hide] = useBoolState(false); // mobile only
const [mode, setMode] = useState<Profile>('car');
const hideForm = isMobileMode && result && !revealed;

const setResultAndHide = useCallback(
Expand All @@ -31,9 +32,17 @@ export const DirectionsBox = () => {

return (
<Wrapper spacing={1}>
<DirectionsForm setResult={setResultAndHide} hideForm={hideForm} />
<DirectionsForm
setResult={setResultAndHide}
hideForm={hideForm}
setMode={setMode}
/>
{result && (
<Result result={result} revealForm={!revealed && revealForm} />
<Result
result={result}
revealForm={!revealed && revealForm}
mode={mode}
/>
)}
</Wrapper>
);
Expand Down
20 changes: 16 additions & 4 deletions src/components/Directions/DirectionsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { getLastFeature } from '../../services/lastFeatureStorage';
import { getCoordsOption } from '../SearchBox/options/coords';
import { getLabel } from '../../helpers/featureLabel';
import { useMapStateContext } from '../utils/MapStateContext';
import { getGlobalMap } from '../../services/mapStorage';
import { Setter } from '../../types';

const getRoutingFailed = (showToast: ShowToast) => {
return (error: unknown) => {
Expand All @@ -34,7 +34,8 @@ const getRoutingFailed = (showToast: ShowToast) => {
showToast(`${t('error')} code ${error.code}`, 'error');
} else {
Sentry.captureException(error);
showToast(`${t('error')} – ${error}`, 'error');
const errorTranslated = t('error');
showToast(`${errorTranslated} – ${error}`, 'error');
throw error;
}
};
Expand Down Expand Up @@ -110,6 +111,7 @@ const useGetOnSubmit = (
type Props = {
setResult: (result: RoutingResult) => void;
hideForm: boolean;
setMode: Setter<Profile>;
};

const useGlobalMapClickOverride = (
Expand All @@ -135,7 +137,11 @@ const useGlobalMapClickOverride = (
};

// generated by https://v0.dev/chat/3MwraSQEqCc
export const DirectionsForm = ({ setResult, hideForm }: Props) => {
export const DirectionsForm = ({
setResult,
hideForm,
setMode: setModeProp,
}: Props) => {
const [loading, setLoading] = useState(false);
const [mode, setMode] = useState<Profile>('car');
const [from, setFrom] = useState<Option>();
Expand All @@ -153,7 +159,13 @@ export const DirectionsForm = ({ setResult, hideForm }: Props) => {
return (
<StyledPaper elevation={3}>
<Stack direction="row" spacing={1} mb={2} alignItems="center">
<ModeToggler value={mode} setMode={setMode} />
<ModeToggler
value={mode}
setMode={(mode) => {
setMode(mode);
setModeProp(mode);
}}
/>
<div style={{ flex: 1 }} />
<div>
<CloseButton />
Expand Down
5 changes: 3 additions & 2 deletions src/components/Directions/ModeToggler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DirectionsBikeIcon from '@mui/icons-material/DirectionsBike';
import DirectionsWalkIcon from '@mui/icons-material/DirectionsWalk';
import React from 'react';
import styled from '@emotion/styled';
import { Profile } from './routing/types';

const StyledToggleButton = styled(ToggleButton)`
padding: 8px;
Expand All @@ -22,11 +23,11 @@ const icons = [

type Props = {
value: string;
setMode: (value: ((prevState: string) => string) | string) => void;
setMode: (value: Profile) => void;
};

export const ModeToggler = ({ value, setMode }: Props) => {
const onChange = (_: unknown, newMode: string | null) => {
const onChange = (_: unknown, newMode: Profile | null) => {
if (newMode !== null) {
setMode(newMode);
}
Expand Down
35 changes: 32 additions & 3 deletions src/components/Directions/Result.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ 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 { Profile, RoutingResult } from './routing/types';
import { t, Translation } from '../../services/intl';
import { CloseButton, toHumanDistance } from './helpers';
import { useUserSettingsContext } from '../utils/UserSettingsContext';
import { Instructions } from './Instructions';
import { useInitTurnByTurnNav } from '../TurnByTurnNavigation/init';

export const StyledPaper = styled(Paper)<{
$height?: string;
Expand Down Expand Up @@ -55,14 +56,39 @@ const PoweredBy = ({ result }: { result: RoutingResult }) => (
</Typography>
);

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

const StartNavigation = ({
result,
mode,
}: {
result: RoutingResult;
mode: Profile;
}) => {
const initTurnByTurn = useInitTurnByTurnNav(result, mode);

return (
<Button
onClick={() => {
initTurnByTurn();
}}
>
Start Navigation
</Button>
);
};

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

Expand Down Expand Up @@ -97,13 +123,14 @@ const MobileResult = ({
{t('directions.edit_destinations')}
</Button>
)}
<StartNavigation result={result} mode={mode} />
</Stack>
{showInstructions && <Instructions instructions={result.instructions} />}
</StyledPaperMobile>
);
};

export const Result = ({ result, revealForm }: Props) => {
export const Result = ({ result, revealForm, mode }: Props) => {
const isMobileMode = useMobileMode();
const { userSettings } = useUserSettingsContext();
const { isImperial } = userSettings;
Expand All @@ -120,6 +147,7 @@ export const Result = ({ result, revealForm }: Props) => {
time={time}
distance={distance}
ascent={ascent}
mode={mode}
/>
);
}
Expand Down Expand Up @@ -158,6 +186,7 @@ export const Result = ({ result, revealForm }: Props) => {
</div>
</Stack>
<Divider sx={{ mt: 2, mb: 3 }} />
<StartNavigation result={result} mode={mode} />
{result.instructions && (
<Instructions instructions={result.instructions} />
)}
Expand Down
11 changes: 7 additions & 4 deletions src/components/Directions/routing/handleRouting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import { LonLat } from '../../../services/types';
import { Profile, RoutingResult } from './types';
import { getGraphhopperResults } from './getGraphhopperResults';
import { LineLayerSpecification } from '@maplibre/maplibre-gl-style-spec';
import { type GeoJSON } from 'geojson';
import { isMobileModeVanilla } from '../../helpers';

// taken from or inspired by cartes.app, LGPL

const slopeColor = (slope) => {
const slopeColor = (slope: number) => {
if (slope < 0) return '#8f53c1'; // give another color for negative slopes ?
if (slope < 3) return '#8f53c1';
// if (slope < 5) return 'yellow';
Expand Down Expand Up @@ -55,7 +54,7 @@ const computeSlopeGradient = (geojson) => {
.flat();

const chunkSize = 30;
const averaged = [];
const averaged: number[] = [];
for (let i = 0; i < littles.length; i += chunkSize) {
const chunk = littles.slice(i, i + chunkSize);
const sum = chunk.reduce((memo, next) => memo + next, 0);
Expand Down Expand Up @@ -149,13 +148,17 @@ const renderOnMap = (

export const destroyRouting = () => {
const map = getGlobalMap();
if (!map) {
return;
}

if (map.getLayer(`${SOURCE}-line`)) {
map.removeLayer(`${SOURCE}-line`);
}
if (map.getLayer(`${SOURCE}-line-casing`)) {
map.removeLayer(`${SOURCE}-line-casing`);
}
if (map?.getSource(SOURCE)) {
if (map.getSource(SOURCE)) {
map.removeSource(SOURCE);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const getPublictransportSource = () => {
id: `${SOURCE_NAME}-lines`,
type: 'line',
source: SOURCE_NAME,
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': ['get', COLOR_ATTRIBUTE],
'line-width': 4,
Expand Down
2 changes: 1 addition & 1 deletion src/components/LayerSwitcher/LayerSwitcherContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { PersonAdd } from '@mui/icons-material';
import { isViewInsideBbox, LayersHeader } from './helpers';
import { LayersHeader, isViewInsideBbox } from './helpers';
import { osmappLayers } from './osmappLayers';
import { Layer, useMapStateContext, View } from '../utils/MapStateContext';
import { Overlays } from './Overlays';
Expand Down
14 changes: 11 additions & 3 deletions src/components/Map/BrowserMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ const BrowserMap = () => {
const { userLayers } = useMapStateContext();
const mobileMode = useMobileMode();
const { setFeature } = useFeatureContext();
const { mapLoaded, setMapLoaded, mapClickOverrideRef } = useMapStateContext();
const { mapLoaded, setMapLoaded, mapClickOverrideRef, mapClickDisabled } =
useMapStateContext();
const { currentTheme } = useUserThemeContext();

const [map, mapRef] = useInitMap();
useAddTopRightControls(map, mobileMode);
useOnMapClicked(map, setFeature, mapClickOverrideRef);
useOnMapClicked(map, setFeature, mapClickOverrideRef, mapClickDisabled);
useOnMapLongPressed(map, setFeature);
useOnMapLoaded(map, setMapLoaded);
useFeatureMarker(map);
Expand All @@ -65,7 +66,14 @@ const BrowserMap = () => {
useUpdateViewOnMove(map, setViewFromMap, setBbox);
useToggleTerrainControl(map);
useUpdateMap(map, viewForMap);
useUpdateStyle(map, activeLayers, userLayers, mapLoaded, currentTheme);
useUpdateStyle(
map,
activeLayers,
userLayers,
mapLoaded,
mapClickDisabled,
currentTheme,
);
usePersistedScaleControl(map);

return <div ref={mapRef} style={{ height: '100%', width: '100%' }} />;
Expand Down
Loading