diff --git a/client/src/components/MapView/NavigationMarkers.tsx b/client/src/components/MapView/NavigationMarkers.tsx index 6d043038ae9..2ae62cab8f2 100644 --- a/client/src/components/MapView/NavigationMarkers.tsx +++ b/client/src/components/MapView/NavigationMarkers.tsx @@ -2,6 +2,7 @@ import { TripQueryVariables } from '../../gql/graphql.ts'; import { Marker } from 'react-map-gl'; import markerFlagStart from '../../static/img/marker-flag-start-shadowed.png'; import markerFlagEnd from '../../static/img/marker-flag-end-shadowed.png'; +import { useCoordinateResolver } from './useCoordinateResolver.ts'; export function NavigationMarkers({ setCursor, @@ -14,13 +15,16 @@ export function NavigationMarkers({ setTripQueryVariables: (variables: TripQueryVariables) => void; loading: boolean; }) { + const fromCoordinates = useCoordinateResolver(tripQueryVariables.from); + const toCoordinates = useCoordinateResolver(tripQueryVariables.to); + return ( <> - {tripQueryVariables.from.coordinates && ( + {fromCoordinates && ( setCursor('grabbing')} onDragEnd={(e) => { setCursor('auto'); @@ -36,11 +40,11 @@ export function NavigationMarkers({ )} - {tripQueryVariables.to.coordinates && ( + {toCoordinates && ( setCursor('grabbing')} onDragEnd={(e) => { setCursor('auto'); diff --git a/client/src/components/MapView/useCoordinateResolver.ts b/client/src/components/MapView/useCoordinateResolver.ts new file mode 100644 index 00000000000..e44472df549 --- /dev/null +++ b/client/src/components/MapView/useCoordinateResolver.ts @@ -0,0 +1,30 @@ +import { Location } from '../../gql/graphql.ts'; +import { useQuayCoordinateQuery } from '../../hooks/useQuayCoordinateQuery.ts'; + +interface Coordinates { + latitude: number; + longitude: number; +} + +export function useCoordinateResolver(location: Location): Coordinates | undefined { + const quay = useQuayCoordinateQuery(location); + + if (quay) { + const { latitude, longitude } = quay; + + if (latitude && longitude) { + return { + latitude, + longitude, + }; + } + } + + if (location.coordinates) { + return { + ...location.coordinates, + }; + } + + return undefined; +} diff --git a/client/src/components/SearchBar/LocationInputField.tsx b/client/src/components/SearchBar/LocationInputField.tsx index bfa707776f1..70584e0e6d8 100644 --- a/client/src/components/SearchBar/LocationInputField.tsx +++ b/client/src/components/SearchBar/LocationInputField.tsx @@ -1,8 +1,37 @@ import { Form } from 'react-bootstrap'; -import { COORDINATE_PRECISION } from './constants.ts'; -import { Location } from '../../gql/graphql.ts'; +import { toString, parseLocation } from '../../util/locationConverter.ts'; +import { Location, TripQueryVariables } from '../../gql/graphql.ts'; +import { useCallback, useEffect, useState } from 'react'; + +interface Props { + id: string; + label: string; + tripQueryVariables: TripQueryVariables; + setTripQueryVariables: (tripQueryVariables: TripQueryVariables) => void; + locationFieldKey: 'from' | 'to'; +} + +export function LocationInputField({ id, label, tripQueryVariables, setTripQueryVariables, locationFieldKey }: Props) { + const [value, setValue] = useState(''); + + useEffect(() => { + const initialLocation: Location = tripQueryVariables[locationFieldKey]; + + setValue(toString(initialLocation) || ''); + }, [tripQueryVariables, locationFieldKey]); + + const onLocationChange = useCallback( + (value: string) => { + const newLocation = parseLocation(value) || {}; + + setTripQueryVariables({ + ...tripQueryVariables, + [locationFieldKey]: newLocation, + }); + }, + [tripQueryVariables, setTripQueryVariables, locationFieldKey], + ); -export function LocationInputField({ location, id, label }: { location: Location; id: string; label: string }) { return ( @@ -14,16 +43,13 @@ export function LocationInputField({ location, id, label }: { location: Location size="sm" placeholder="[Click in map]" className="input-medium" - // Intentionally empty for now, but needed because of - // https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable - onChange={() => {}} - value={ - location.coordinates - ? `${location.coordinates?.latitude.toPrecision( - COORDINATE_PRECISION, - )} ${location.coordinates?.longitude.toPrecision(COORDINATE_PRECISION)}` - : '' - } + onChange={(e) => { + setValue(e.target.value); + }} + onBlur={(event) => { + onLocationChange(event.target.value); + }} + value={value} /> ); diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index 73df12fe103..47781532179 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -38,9 +38,21 @@ export function SearchBar({ onRoute, tripQueryVariables, setTripQueryVariables, {showServerInfo && } - + - + diff --git a/client/src/hooks/useQuayCoordinateQuery.ts b/client/src/hooks/useQuayCoordinateQuery.ts new file mode 100644 index 00000000000..6d4655a6838 --- /dev/null +++ b/client/src/hooks/useQuayCoordinateQuery.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react'; +import { request } from 'graphql-request'; +import { Location, QueryType } from '../gql/graphql.ts'; +import { getApiUrl } from '../util/getApiUrl.ts'; +import { graphql } from '../gql'; + +const query = graphql(` + query quayCoordinate($id: String!) { + quay(id: $id) { + latitude + longitude + } + } +`); + +export const useQuayCoordinateQuery = (location: Location) => { + const [data, setData] = useState(null); + + useEffect(() => { + const fetchData = async () => { + if (location.place) { + const variables = { id: location.place }; + try { + setData((await request(getApiUrl(), query, variables)) as QueryType); + } catch (e) { + console.error('Error at useQuayCoordinateQuery', e); + } + } else { + setData(null); + } + }; + fetchData(); + }, [location]); + + return data?.quay; +}; diff --git a/client/src/hooks/useTripQuery.ts b/client/src/hooks/useTripQuery.ts index 515cfae290a..f1f8c859bd0 100644 --- a/client/src/hooks/useTripQuery.ts +++ b/client/src/hooks/useTripQuery.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; import { request } from 'graphql-request'; -import { QueryType, TripQueryVariables } from '../gql/graphql.ts'; +import { Location, QueryType, TripQueryVariables } from '../gql/graphql.ts'; import { getApiUrl } from '../util/getApiUrl.ts'; import { query } from '../static/query/tripQuery.tsx'; @@ -22,10 +22,14 @@ export const useTripQuery: TripQueryHook = (variables) => { } else { if (variables) { setLoading(true); - if (pageCursor) { - setData((await request(getApiUrl(), query, { ...variables, pageCursor })) as QueryType); - } else { - setData((await request(getApiUrl(), query, variables)) as QueryType); + try { + if (pageCursor) { + setData((await request(getApiUrl(), query, { ...variables, pageCursor })) as QueryType); + } else { + setData((await request(getApiUrl(), query, variables)) as QueryType); + } + } catch (e) { + console.error('Error at useTripQuery', e); } setLoading(false); } else { @@ -37,10 +41,14 @@ export const useTripQuery: TripQueryHook = (variables) => { ); useEffect(() => { - if (variables?.from.coordinates && variables?.to.coordinates) { + if (validLocation(variables?.from) && validLocation(variables?.to)) { callback(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [variables?.from, variables?.to]); return [data, loading, callback]; }; + +function validLocation(location: Location | undefined) { + return location && (location.coordinates || location.place); +} diff --git a/client/src/util/locationConverter.ts b/client/src/util/locationConverter.ts new file mode 100644 index 00000000000..952a36a1dc4 --- /dev/null +++ b/client/src/util/locationConverter.ts @@ -0,0 +1,37 @@ +import { COORDINATE_PRECISION } from '../components/SearchBar/constants.ts'; +import { Location } from '../gql/graphql.ts'; + +const DOUBLE_PATTERN = '-{0,1}\\d+(\\.\\d+){0,1}'; + +const LAT_LON_PATTERN = '(' + DOUBLE_PATTERN + ')(\\s+)(' + DOUBLE_PATTERN + ')'; + +export function parseLocation(value: string): Location | null { + const latLonMatch = value.match(LAT_LON_PATTERN); + + if (latLonMatch) { + return { + coordinates: { + latitude: +latLonMatch[1], + longitude: +latLonMatch[4], + }, + }; + } + + return { + place: value, + }; +} + +export function toString(location: Location): string | null { + if (location.coordinates) { + return `${location.coordinates?.latitude.toPrecision( + COORDINATE_PRECISION, + )} ${location.coordinates?.longitude.toPrecision(COORDINATE_PRECISION)}`; + } + + if (location.place) { + return location.place; + } + + return null; +}