From ea59c51ae80d0cb849398a6c06175101e279fca5 Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Thu, 30 Oct 2025 16:35:19 +0100 Subject: [PATCH 01/29] fix: add notificationid, trainid and stationid attribute --- src/Departure/Departure.tsx | 9 +- src/FeatureDetails/FeatureDetails.tsx | 9 +- .../FeaturesInfosListener.tsx | 22 ++++ src/LayoutState/LayoutState.tsx | 113 ++++++++++++++++-- .../LinesNetworkPlanLayerHighlight.tsx | 36 +----- src/MobilityMap/MobilityMapAttributes.ts | 15 +++ .../NotificationDetails.tsx | 18 +-- src/OverlayDetails/OverlayDetails.tsx | 5 + src/RealtimeLayer/RealtimeLayer.tsx | 38 +----- src/RouteSchedule/RouteSchedule.tsx | 25 ++-- .../RouteScheduleFooter.tsx | 9 +- .../RouteScheduleHeader.tsx | 10 +- src/RouteStop/RouteStop.tsx | 38 ++---- src/RouteStopPlatform/RouteStopPlatform.tsx | 4 +- src/RouteStopProgress/RouteStopProgress.tsx | 10 +- .../SingleClickListener.tsx | 26 +++- src/Station/Station.tsx | 72 ++++------- src/StationHeader/StationHeader.tsx | 6 +- src/utils/hooks/useLnp.tsx | 22 ++++ src/utils/hooks/useRealtimeDepartures.tsx | 45 +++++++ .../hooks/useRealtimeRenderedTrajectory.tsx | 42 +++++++ src/utils/hooks/useRealtimeStation.tsx | 39 ++++++ src/utils/hooks/useRealtimeStopSequences.tsx | 43 +++++++ src/utils/hooks/useRouteStop.tsx | 8 +- src/utils/hooks/useStation.tsx | 22 ---- 25 files changed, 462 insertions(+), 224 deletions(-) create mode 100644 src/utils/hooks/useRealtimeDepartures.tsx create mode 100644 src/utils/hooks/useRealtimeRenderedTrajectory.tsx create mode 100644 src/utils/hooks/useRealtimeStation.tsx create mode 100644 src/utils/hooks/useRealtimeStopSequences.tsx delete mode 100644 src/utils/hooks/useStation.tsx diff --git a/src/Departure/Departure.tsx b/src/Departure/Departure.tsx index a7669b8f..ddbc49d6 100644 --- a/src/Departure/Departure.tsx +++ b/src/Departure/Departure.tsx @@ -14,7 +14,8 @@ export interface DepartureProps { } function Departure({ departure, index, ...props }: DepartureProps) { - const { setStationId, setTrainId } = useMapContext(); + const { realtimeLayer, setFeaturesInfos, setStationId, setTrainId } = + useMapContext(); const departureState = useMemo(() => { return { departure, index }; @@ -23,10 +24,12 @@ function Departure({ departure, index, ...props }: DepartureProps) { return ( + + ); +} + +export default memo(SearchLinesResult); diff --git a/src/SearchLinesResult/index.tsx b/src/SearchLinesResult/index.tsx new file mode 100644 index 00000000..cae069c7 --- /dev/null +++ b/src/SearchLinesResult/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./SearchLinesResult"; +export type { StopsFeature, StopsSearchProps } from "./SearchLinesResult"; diff --git a/src/SearchResults/SearchResults.tsx b/src/SearchResults/SearchResults.tsx new file mode 100644 index 00000000..fd6d24d9 --- /dev/null +++ b/src/SearchResults/SearchResults.tsx @@ -0,0 +1,69 @@ +import { memo } from "preact/compat"; +import { twMerge } from "tailwind-merge"; + +import useI18n from "../utils/hooks/useI18n"; + +import type { StopsResponse } from "mobility-toolbox-js/types"; +import type { HTMLAttributes, PreactDOMAttributes } from "preact"; + +import type { SearchResponse } from "../utils/hooks/useSearchStops"; + +export type StopsFeature = StopsResponse["features"][0]; + +export type SearchStopsResultsProps = { + resultsClassName?: string; + resultsContainerClassName?: string; + searchResponse: SearchResponse; +} & HTMLAttributes & + PreactDOMAttributes; + +/** + * Results list of stops search. + */ +function SearchResults({ + children, + resultsClassName, + resultsContainerClassName, + searchResponse, +}: SearchStopsResultsProps) { + const { t } = useI18n(); + + if (!(searchResponse?.results?.length >= 0)) { + return null; + } + + return ( +
+ {searchResponse.results.length === 0 && ( +
+
+
{t("search_no_results")}
+
+ )} + {searchResponse.results.length > 0 && ( +
    + {children} +
+ )} +
+ ); +} + +export default memo(SearchResults); diff --git a/src/SearchResults/index.tsx b/src/SearchResults/index.tsx new file mode 100644 index 00000000..50a4fa68 --- /dev/null +++ b/src/SearchResults/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./SearchResults"; +export type { StopsFeature, StopsSearchProps } from "./SearchResults"; diff --git a/src/SearchResultsHeader/SearchResultsHeader.tsx b/src/SearchResultsHeader/SearchResultsHeader.tsx new file mode 100644 index 00000000..4d1bb97e --- /dev/null +++ b/src/SearchResultsHeader/SearchResultsHeader.tsx @@ -0,0 +1,33 @@ +import { memo } from "preact/compat"; +import { twMerge } from "tailwind-merge"; + +import type { HTMLAttributes, PreactDOMAttributes } from "preact"; + +export type SearchResultsHeaderProps = { + className?: string; + resultsClassName?: string; +} & HTMLAttributes & + PreactDOMAttributes; + +/** + * Header of list of search results. + */ +function SearchResultsHeader({ + children, + className, + ...props +}: SearchResultsHeaderProps) { + return ( +
+ {children} +
+ ); +} + +export default memo(SearchResultsHeader); diff --git a/src/SearchResultsHeader/index.tsx b/src/SearchResultsHeader/index.tsx new file mode 100644 index 00000000..92b199dc --- /dev/null +++ b/src/SearchResultsHeader/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./SearchResultsHeader"; +export type { StopsFeature, StopsSearchProps } from "./SearchResultsHeader"; diff --git a/src/SearchStopsResult/SearchStopsResult.tsx b/src/SearchStopsResult/SearchStopsResult.tsx new file mode 100644 index 00000000..d45566b6 --- /dev/null +++ b/src/SearchStopsResult/SearchStopsResult.tsx @@ -0,0 +1,48 @@ +import { memo } from "preact/compat"; +import { twMerge } from "tailwind-merge"; + +import centerOnStation from "../utils/centerOnStation"; +import useMapContext from "../utils/hooks/useMapContext"; + +import type { HTMLAttributes, PreactDOMAttributes } from "preact"; + +import type { StopsFeature } from "../StopsSearch"; + +export type SearchStopsResultProps = { + className?: string; + onSelect?: (stop: StopsFeature) => void; + stop: StopsFeature; +} & HTMLAttributes & + PreactDOMAttributes; + +function SearchStopsResult({ + className, + onSelect, + stop, + ...props +}: SearchStopsResultProps) { + const { map } = useMapContext(); + + return ( +
  • + +
  • + ); +} + +export default memo(SearchStopsResult); diff --git a/src/SearchStopsResult/index.tsx b/src/SearchStopsResult/index.tsx new file mode 100644 index 00000000..1b754a98 --- /dev/null +++ b/src/SearchStopsResult/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./SearchStopsResult"; +export type { StopsFeature, StopsSearchProps } from "./SearchStopsResult"; diff --git a/src/ui/InputSearch/InputSearch.tsx b/src/ui/InputSearch/InputSearch.tsx new file mode 100644 index 00000000..7ad30d91 --- /dev/null +++ b/src/ui/InputSearch/InputSearch.tsx @@ -0,0 +1,101 @@ +import { memo } from "preact/compat"; +import { useRef } from "preact/hooks"; +import { twMerge } from "tailwind-merge"; + +import Cancel from "../../icons/Cancel"; +import Search from "../../icons/Search"; +import useI18n from "../../utils/hooks/useI18n"; +import IconButton from "../IconButton"; +import Input from "../Input"; + +import type { PreactDOMAttributes } from "preact"; +import type { HTMLAttributes } from "preact/compat"; + +import type { IconButtonProps } from "../IconButton/IconButton"; +import type { InputProps } from "../Input/Input"; + +export type InputSearchProps = { + cancelButtonClassName?: string; + cancelButtonProps?: IconButtonProps; + className?: string; + inputClassName?: string; + inputContainerClassName?: string; + inputProps?: InputProps; + resultClassName?: string; + resultsClassName?: string; + resultsContainerClassName?: string; + searchIconContainerClassName?: string; + withResultsClassName?: string; +} & HTMLAttributes & + PreactDOMAttributes; + +/** + * Rich search input component. + */ +function InputSearch({ + cancelButtonClassName, + cancelButtonProps, + children, + className, + inputClassName, + inputContainerClassName, + inputProps, + searchIconContainerClassName, + withResultsClassName, +}: InputSearchProps) { + const { t } = useI18n(); + const myRef = useRef(); + + return ( + <> +
    +
    + +
    +
    + + {!!inputProps.value && ( + + + + )} +
    +
    + {children} + + ); +} + +export default memo(InputSearch); diff --git a/src/ui/InputSearch/index.tsx b/src/ui/InputSearch/index.tsx new file mode 100644 index 00000000..2e1a086d --- /dev/null +++ b/src/ui/InputSearch/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./InputSearch"; +export type { StopsFeature, StopsSearchProps } from "./InputSearch"; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 50b15279..886c06c4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -34,6 +34,7 @@ export const LAYER_PROP_IS_EXPORTING = "isExporting"; export const LAYER_PROP_IS_LOADING = "isLoading"; export const MAX_EXTENT = undefined; +export const MAX_EXTENT_4326 = undefined; export const EXPORT_PREFIX = "mwc"; diff --git a/src/utils/hooks/useSearchLines.tsx b/src/utils/hooks/useSearchLines.tsx new file mode 100644 index 00000000..259f75a1 --- /dev/null +++ b/src/utils/hooks/useSearchLines.tsx @@ -0,0 +1,32 @@ +import { useMemo } from "preact/hooks"; + +import { useLnpLinesInfos } from "./useLnp"; + +import type { LineInfo } from "./useLnp"; +import type { SearchResponse } from "./useSearchStops"; + +function useSearchLines(query: string): SearchResponse { + const linesInfos = useLnpLinesInfos(); + + const results = useMemo(() => { + if (!query || !linesInfos) { + return []; + } + return Object.values(linesInfos || {}).filter((line: LineInfo) => { + return ( + line?.short_name?.toLowerCase().includes(query.toLowerCase()) || + line?.long_name?.toLowerCase().includes(query.toLowerCase()) || + line?.id?.toLowerCase().includes(query.toLowerCase()) || + line?.external_id?.toLowerCase().includes(query.toLowerCase()) || + line?.mot?.toLowerCase().includes(query.toLowerCase()) + ); + }); + }, [linesInfos, query]); + + return { + isLoading: false, + results: results || [], + }; +} + +export default useSearchLines; diff --git a/src/utils/hooks/useSearchStops.tsx b/src/utils/hooks/useSearchStops.tsx new file mode 100644 index 00000000..da03dd0d --- /dev/null +++ b/src/utils/hooks/useSearchStops.tsx @@ -0,0 +1,82 @@ +import debounce from "lodash.debounce"; +import { StopsAPI } from "mobility-toolbox-js/ol"; +import { useEffect, useMemo, useState } from "preact/hooks"; + +import { MAX_EXTENT_4326 } from "../constants"; + +import useMapContext from "./useMapContext"; + +import type { StopsParameters, StopsResponse } from "mobility-toolbox-js/types"; + +import type { StopsFeature } from "../../StopsSearch"; + +export interface SearchResponse { + isLoading: boolean; + results: T[] | undefined; +} + +/** + * This hook launch a request to the Stops API. + * + * @param query + */ +function useSearchStops( + query: string, + params?: Partial, +): SearchResponse { + const { apikey, mots, stopsurl } = useMapContext(); + const [results, setResults] = useState(); + const [isLoading, setIsLoading] = useState(false); + + const api: StopsAPI = useMemo(() => { + return new StopsAPI({ apiKey: apikey, url: stopsurl }); + }, [apikey, stopsurl]); + + const debouncedSearch = useMemo(() => { + let abortCtrl: AbortController | undefined; + + return debounce((q) => { + abortCtrl?.abort(); + abortCtrl = new AbortController(); + + const reqParams = { + bbox: MAX_EXTENT_4326?.join(","), + mots, + q, + ...(params ?? {}), + } as StopsParameters; + + setIsLoading(true); + api + .search(reqParams, { signal: abortCtrl.signal }) + .then((res: StopsResponse) => { + setResults(res.features); + setIsLoading(false); + }) + .catch((e) => { + // AbortError is expected + if (e.code !== 20) { + // eslint-disable-next-line no-console + console.error("Failed to fetch stations", e); + return; + } + setIsLoading(false); + }); + }, 150); + }, [api, mots, params]); + + useEffect(() => { + if (!query) { + setResults([]); + return; + } + debouncedSearch(query); + }, [debouncedSearch, query]); + + return { + isLoading, + results: results || [], + }; +} + +export default useSearchStops; From 870eb008cc9230f9d6b6a0a7100796409b900c6e Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Thu, 30 Oct 2025 18:08:32 +0100 Subject: [PATCH 05/29] feat: add trajectories search --- src/Search/Search2.tsx | 45 +++++++++++++++++- .../SearchTrajectoriesResult.tsx | 46 +++++++++++++++++++ src/SearchTrajectoriesResult/index.tsx | 5 ++ src/utils/hooks/useSearchTrajectories.tsx | 34 ++++++++++++++ 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 src/SearchTrajectoriesResult/SearchTrajectoriesResult.tsx create mode 100644 src/SearchTrajectoriesResult/index.tsx create mode 100644 src/utils/hooks/useSearchTrajectories.tsx diff --git a/src/Search/Search2.tsx b/src/Search/Search2.tsx index 619fab25..88c6a971 100644 --- a/src/Search/Search2.tsx +++ b/src/Search/Search2.tsx @@ -5,11 +5,14 @@ import SearchLinesResult from "../SearchLinesResult"; import SearchResults from "../SearchResults"; import SearchResultsHeader from "../SearchResultsHeader"; import SearchStopsResult from "../SearchStopsResult"; +import SearchTrajectoriesResult from "../SearchTrajectoriesResult"; import InputSearch from "../ui/InputSearch"; import useI18n from "../utils/hooks/useI18n"; import useSearchLines from "../utils/hooks/useSearchLines"; import useSearchStops from "../utils/hooks/useSearchStops"; +import useSearchTrajectories from "../utils/hooks/useSearchTrajectories"; +import type { RealtimeTrajectory } from "mobility-toolbox-js/types"; import type { TargetedInputEvent } from "preact"; import type { StopsFeature } from "../SearchResults"; @@ -41,6 +44,7 @@ function Search({ const [open, setOpen] = useState(false); const stops = useSearchStops(query); const lines = useSearchLines(query); + const trajectories = useSearchTrajectories(query); const { t } = useI18n(); const inputPropss: InputProps = useMemo(() => { @@ -80,9 +84,19 @@ function Search({ setOpen(false); }, []); + const onSelectTrajectory = useCallback((trajectory) => { + setSelectedQuery(trajectory.properties.route_identifier); + setOpen(false); + }, []); + const showResults = useMemo(() => { - return open && (!!stops?.results?.length || !!lines?.results?.length); - }, [open, stops, lines]); + return ( + open && + (!!stops?.results?.length || + !!lines?.results?.length || + !!trajectories?.results?.length) + ); + }, [open, stops, lines, trajectories]); const showStopsResults = useMemo(() => { return open && !!stops?.results?.length; @@ -92,6 +106,10 @@ function Search({ return open && !!lines?.results?.length; }, [open, lines]); + const showTrajectoriesResults = useMemo(() => { + return open && !!trajectories?.results?.length; + }, [open, trajectories]); + return ( <> )} + {showTrajectoriesResults && ( + <> + + {t("search_trajectories_results") || "Trajectories"} + + + {trajectories.results.map((trajectory: RealtimeTrajectory) => { + return ( + + ); + })} + + + )} diff --git a/src/SearchTrajectoriesResult/SearchTrajectoriesResult.tsx b/src/SearchTrajectoriesResult/SearchTrajectoriesResult.tsx new file mode 100644 index 00000000..7f996312 --- /dev/null +++ b/src/SearchTrajectoriesResult/SearchTrajectoriesResult.tsx @@ -0,0 +1,46 @@ +import { memo } from "preact/compat"; +import { twMerge } from "tailwind-merge"; + +import RouteIcon from "../RouteIcon"; +import useMapContext from "../utils/hooks/useMapContext"; + +import type { RealtimeTrajectory } from "mobility-toolbox-js/types"; +import type { HTMLAttributes, PreactDOMAttributes } from "preact"; + +export type SearchTrajectoriesResultProps = { + className?: string; + onSelect?: (line: RealtimeTrajectory) => void; + trajectory: RealtimeTrajectory; +} & HTMLAttributes & + PreactDOMAttributes; + +function SearchTrajectoriesResult({ + className, + onSelect, + trajectory, + ...props +}: SearchTrajectoriesResultProps) { + const { setTrainId } = useMapContext(); + return ( +
  • + +
  • + ); +} + +export default memo(SearchTrajectoriesResult); diff --git a/src/SearchTrajectoriesResult/index.tsx b/src/SearchTrajectoriesResult/index.tsx new file mode 100644 index 00000000..6560517d --- /dev/null +++ b/src/SearchTrajectoriesResult/index.tsx @@ -0,0 +1,5 @@ +export { default } from "./SearchTrajectoriesResult"; +export type { + StopsFeature, + StopsSearchProps, +} from "./SearchTrajectoriesResult"; diff --git a/src/utils/hooks/useSearchTrajectories.tsx b/src/utils/hooks/useSearchTrajectories.tsx new file mode 100644 index 00000000..7962be0c --- /dev/null +++ b/src/utils/hooks/useSearchTrajectories.tsx @@ -0,0 +1,34 @@ +import { useMemo } from "preact/hooks"; + +import useMapContext from "./useMapContext"; + +import type { RealtimeTrajectory } from "mobility-toolbox-js/types"; + +import type { SearchResponse } from "./useSearchStops"; + +function useSearchTrajectories( + query: string, +): SearchResponse { + const { realtimeLayer } = useMapContext(); + + const results = useMemo(() => { + if (!query || !realtimeLayer?.trajectories) { + return []; + } + return Object.values(realtimeLayer.trajectories || {}).filter( + (trajectory) => { + console.log("Searching trajectories for query:", trajectory); + return trajectory?.properties.route_identifier + ?.toLowerCase() + .includes(query.toLowerCase()); + }, + ); + }, [realtimeLayer, query]); + + return { + isLoading: false, + results: results || [], + }; +} + +export default useSearchTrajectories; From d4232fb2a068c5bbea53a79f14e22006e2a7e6cc Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Fri, 31 Oct 2025 08:25:01 +0100 Subject: [PATCH 06/29] fix: add translations for search --- .../LinesNetworkPlanDetails.tsx | 4 +- src/MapLayout/MapLayout.tsx | 2 +- src/RouteIcon/RouteIcon.tsx | 9 ++- src/Search/Search2.tsx | 58 ++++++++++++------- src/SearchLinesResult/SearchLinesResult.tsx | 32 +++++----- src/SearchResult/SearchResult.tsx | 25 ++++++++ src/SearchResult/index.tsx | 2 + .../SearchResultsHeader.tsx | 2 +- src/SearchStopsResult/SearchStopsResult.tsx | 26 ++++----- .../SearchTrajectoriesResult.tsx | 26 ++++----- src/utils/getTextForVehicle.ts | 3 + src/utils/hooks/useLnp.tsx | 12 ++-- src/utils/hooks/useSearchLines.tsx | 6 +- src/utils/translations.ts | 17 ++++++ 14 files changed, 140 insertions(+), 84 deletions(-) create mode 100644 src/SearchResult/SearchResult.tsx create mode 100644 src/SearchResult/index.tsx diff --git a/src/LinesNetworkPlanDetails/LinesNetworkPlanDetails.tsx b/src/LinesNetworkPlanDetails/LinesNetworkPlanDetails.tsx index e4a7c755..2cc57c4c 100644 --- a/src/LinesNetworkPlanDetails/LinesNetworkPlanDetails.tsx +++ b/src/LinesNetworkPlanDetails/LinesNetworkPlanDetails.tsx @@ -12,7 +12,7 @@ import type { RealtimeLine } from "mobility-toolbox-js/types"; import type { Feature } from "ol"; import type { PreactDOMAttributes } from "preact"; -import type { LineInfo } from "../utils/hooks/useLnp"; +import type { LnpLineInfo } from "../utils/hooks/useLnp"; const RUNS_PROP = "runs"; @@ -46,7 +46,7 @@ function LinesNetworkPlanDetails({ [layerConfig], ); - const lineInfosByOperator: Record = useMemo(() => { + const lineInfosByOperator: Record = useMemo(() => { const byOperators = {}; [ diff --git a/src/MapLayout/MapLayout.tsx b/src/MapLayout/MapLayout.tsx index eae8ab0f..d85d1784 100644 --- a/src/MapLayout/MapLayout.tsx +++ b/src/MapLayout/MapLayout.tsx @@ -102,7 +102,7 @@ function MapLayout({ {hasSearch && } - {hasSearch && ( + {hasSearch && isSearchOpen && (
    & @@ -31,23 +34,25 @@ function RouteIcon({ departure, displayNoRealtimeIcon = false, line, + lineInfo, stopSequence, trajectory, ...props }: RouteIconProps) { const lineToUse = line || + lineInfo || departure?.line || stopSequence?.line || trajectory?.properties?.line; const type = lineToUse?.type || stopSequence?.type || trajectory?.type; const backgroundColor = getMainColorForVehicle( - line || departure || stopSequence || trajectory, + line || lineInfo || departure || stopSequence || trajectory, ); const color = lineToUse?.text_color || getTextColor(type); let borderColor = lineToUse?.stroke || "black"; const text = getTextForVehicle( - line || departure || stopSequence || trajectory, + line || lineInfo || departure || stopSequence || trajectory, ); const fontSize = fontSizesByNbLetters[text.length] || 12; diff --git a/src/Search/Search2.tsx b/src/Search/Search2.tsx index 88c6a971..ad9d7539 100644 --- a/src/Search/Search2.tsx +++ b/src/Search/Search2.tsx @@ -2,6 +2,7 @@ import { memo } from "preact/compat"; import { useCallback, useMemo, useState } from "preact/hooks"; import SearchLinesResult from "../SearchLinesResult"; +import SearchResult from "../SearchResult"; import SearchResults from "../SearchResults"; import SearchResultsHeader from "../SearchResultsHeader"; import SearchStopsResult from "../SearchStopsResult"; @@ -18,7 +19,7 @@ import type { TargetedInputEvent } from "preact"; import type { StopsFeature } from "../SearchResults"; import type { IconButtonProps } from "../ui/IconButton/IconButton"; import type { InputProps } from "../ui/Input/Input"; -import type { LineInfo } from "../utils/hooks/useLnp"; +import type { LnpLineInfo } from "../utils/hooks/useLnp"; export interface SearchProps { cancelButtonProps?: IconButtonProps; @@ -122,7 +123,7 @@ function Search({ {showStopsResults && ( <> - {t("search_stops_results") || "Stops"} + {t("search_stops_results")} {stops.results.map((stop: StopsFeature) => { return ( - + > + + ); })} @@ -146,7 +147,7 @@ function Search({ {showLinesResults && ( <> - {t("search_lines_results") || "Lines"} + {t("search_lines_results")} - {lines.results.map((line: LineInfo) => { - return ( - - ); - })} + {[...lines.results] + .sort((a, b) => { + if (a.long_name === b.long_name) { + return a.long_name < b.long_name ? 1 : -1; + } + return a.short_name < b.short_name ? 1 : -1; + }) + .map((line: LnpLineInfo) => { + return ( + + + + ); + })} )} {showTrajectoriesResults && ( <> - {t("search_trajectories_results") || "Trajectories"} + {t("search_trajectories_results")} {trajectories.results.map((trajectory: RealtimeTrajectory) => { return ( - + > + + ); })} diff --git a/src/SearchLinesResult/SearchLinesResult.tsx b/src/SearchLinesResult/SearchLinesResult.tsx index 9ff3adf2..b0b6ccc3 100644 --- a/src/SearchLinesResult/SearchLinesResult.tsx +++ b/src/SearchLinesResult/SearchLinesResult.tsx @@ -4,15 +4,15 @@ import { twMerge } from "tailwind-merge"; import RouteIcon from "../RouteIcon"; import useMapContext from "../utils/hooks/useMapContext"; -import type { HTMLAttributes, PreactDOMAttributes } from "preact"; +import type { ButtonHTMLAttributes, PreactDOMAttributes } from "preact"; -import type { LineInfo } from "../utils/hooks/useLnp"; +import type { LnpLineInfo } from "../utils/hooks/useLnp"; export type SearchLinesResultProps = { className?: string; - line: LineInfo; - onSelect?: (line: LineInfo) => void; -} & HTMLAttributes & + line: LnpLineInfo; + onSelect?: (line: LnpLineInfo) => void; +} & ButtonHTMLAttributes & PreactDOMAttributes; function SearchLinesResult({ @@ -23,24 +23,20 @@ function SearchLinesResult({ }: SearchLinesResultProps) { const { setLinesIds } = useMapContext(); return ( -
  • { + onSelect?.(line); + setLinesIds([line.external_id]); + }} > - -
  • + +
    {line.long_name || line.short_name}
    + ); } diff --git a/src/SearchResult/SearchResult.tsx b/src/SearchResult/SearchResult.tsx new file mode 100644 index 00000000..d6d02603 --- /dev/null +++ b/src/SearchResult/SearchResult.tsx @@ -0,0 +1,25 @@ +import { memo } from "preact/compat"; +import { twMerge } from "tailwind-merge"; + +import type { HTMLAttributes, PreactDOMAttributes } from "preact"; + +export type SearchResultProps = { + className?: string; +} & HTMLAttributes & + PreactDOMAttributes; + +function SearchResult({ children, className, ...props }: SearchResultProps) { + return ( +
  • + {children} +
  • + ); +} + +export default memo(SearchResult); diff --git a/src/SearchResult/index.tsx b/src/SearchResult/index.tsx new file mode 100644 index 00000000..fe2e9f35 --- /dev/null +++ b/src/SearchResult/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./SearchResult"; +export type { StopsFeature, StopsSearchProps } from "./SearchResult"; diff --git a/src/SearchResultsHeader/SearchResultsHeader.tsx b/src/SearchResultsHeader/SearchResultsHeader.tsx index 4d1bb97e..28eef485 100644 --- a/src/SearchResultsHeader/SearchResultsHeader.tsx +++ b/src/SearchResultsHeader/SearchResultsHeader.tsx @@ -21,7 +21,7 @@ function SearchResultsHeader({
    diff --git a/src/SearchStopsResult/SearchStopsResult.tsx b/src/SearchStopsResult/SearchStopsResult.tsx index d45566b6..3b8a68ed 100644 --- a/src/SearchStopsResult/SearchStopsResult.tsx +++ b/src/SearchStopsResult/SearchStopsResult.tsx @@ -4,7 +4,7 @@ import { twMerge } from "tailwind-merge"; import centerOnStation from "../utils/centerOnStation"; import useMapContext from "../utils/hooks/useMapContext"; -import type { HTMLAttributes, PreactDOMAttributes } from "preact"; +import type { ButtonHTMLAttributes, PreactDOMAttributes } from "preact"; import type { StopsFeature } from "../StopsSearch"; @@ -12,7 +12,7 @@ export type SearchStopsResultProps = { className?: string; onSelect?: (stop: StopsFeature) => void; stop: StopsFeature; -} & HTMLAttributes & +} & ButtonHTMLAttributes & PreactDOMAttributes; function SearchStopsResult({ @@ -24,24 +24,20 @@ function SearchStopsResult({ const { map } = useMapContext(); return ( -
  • { + centerOnStation(stop, map); + onSelect?.(stop); + }} > - -
  • +
    +
    {stop.properties.name}
    + ); } diff --git a/src/SearchTrajectoriesResult/SearchTrajectoriesResult.tsx b/src/SearchTrajectoriesResult/SearchTrajectoriesResult.tsx index 7f996312..e3e0570a 100644 --- a/src/SearchTrajectoriesResult/SearchTrajectoriesResult.tsx +++ b/src/SearchTrajectoriesResult/SearchTrajectoriesResult.tsx @@ -5,13 +5,13 @@ import RouteIcon from "../RouteIcon"; import useMapContext from "../utils/hooks/useMapContext"; import type { RealtimeTrajectory } from "mobility-toolbox-js/types"; -import type { HTMLAttributes, PreactDOMAttributes } from "preact"; +import type { ButtonHTMLAttributes, PreactDOMAttributes } from "preact"; export type SearchTrajectoriesResultProps = { className?: string; onSelect?: (line: RealtimeTrajectory) => void; trajectory: RealtimeTrajectory; -} & HTMLAttributes & +} & ButtonHTMLAttributes & PreactDOMAttributes; function SearchTrajectoriesResult({ @@ -22,24 +22,20 @@ function SearchTrajectoriesResult({ }: SearchTrajectoriesResultProps) { const { setTrainId } = useMapContext(); return ( -
  • { + onSelect?.(trajectory); + setTrainId(trajectory.properties.train_id); + }} > - -
  • + +
    {trajectory.properties.route_identifier}
    + ); } diff --git a/src/utils/getTextForVehicle.ts b/src/utils/getTextForVehicle.ts index ec6715cb..8e8e7f1c 100644 --- a/src/utils/getTextForVehicle.ts +++ b/src/utils/getTextForVehicle.ts @@ -4,12 +4,15 @@ import type { RealtimeTrajectory, } from "mobility-toolbox-js/types"; +import type { LnpLineInfo } from "./hooks/useLnp"; + const getTextForVehicle = (object: unknown = ""): string => { const name = (object as RealtimeTrajectory)?.properties?.line?.name || // @ts-expect-error bad type definition (object as RealtimeStopSequence)?.line?.name || (object as RealtimeLine)?.name || + (object as LnpLineInfo)?.short_name || (object as string) || ""; diff --git a/src/utils/hooks/useLnp.tsx b/src/utils/hooks/useLnp.tsx index 92b534bd..ab8f8a89 100644 --- a/src/utils/hooks/useLnp.tsx +++ b/src/utils/hooks/useLnp.tsx @@ -6,7 +6,7 @@ import useMapContext from "./useMapContext"; import type { VectorTileSource } from "maplibre-gl"; -export interface LineInfo { +export interface LnpLineInfo { color: string; external_id: string; id: string; @@ -18,7 +18,7 @@ export interface LineInfo { text_color: string; } -export interface StopInfo { +export interface LnpStopInfo { external_id: string; importance: number; long_name: string; @@ -26,8 +26,8 @@ export interface StopInfo { visibility_level: number; } -export type LinesInfos = Record; -export type StopsInfos = Record; +export type LinesInfos = Record; +export type StopsInfos = Record; let cacheLnpSourceInfo: { [LNP_MD_LINES]: LinesInfos; @@ -91,7 +91,7 @@ export function useLnpStopsInfos(): StopsInfos { * This hook search line informations from lnp data. It takes a string in * parameter then it will search if there is a property that exactly match this value. */ -function useLnpLineInfo(text: string): LineInfo { +function useLnpLineInfo(text: string): LnpLineInfo { const linesInfos = useLnpLinesInfos(); if (!linesInfos || !text) { @@ -113,7 +113,7 @@ function useLnpLineInfo(text: string): LineInfo { * This hook search line informations from lnp data. It takes a string in * parameter then it will search if there is a property that exactly match this value. */ -export function useLnpStopInfo(text: string): StopInfo { +export function useLnpStopInfo(text: string): LnpStopInfo { const stationsInfos = useLnpStopsInfos(); if (!stationsInfos || !text) { diff --git a/src/utils/hooks/useSearchLines.tsx b/src/utils/hooks/useSearchLines.tsx index 259f75a1..73dc1de4 100644 --- a/src/utils/hooks/useSearchLines.tsx +++ b/src/utils/hooks/useSearchLines.tsx @@ -2,17 +2,17 @@ import { useMemo } from "preact/hooks"; import { useLnpLinesInfos } from "./useLnp"; -import type { LineInfo } from "./useLnp"; +import type { LnpLineInfo } from "./useLnp"; import type { SearchResponse } from "./useSearchStops"; -function useSearchLines(query: string): SearchResponse { +function useSearchLines(query: string): SearchResponse { const linesInfos = useLnpLinesInfos(); const results = useMemo(() => { if (!query || !linesInfos) { return []; } - return Object.values(linesInfos || {}).filter((line: LineInfo) => { + return Object.values(linesInfos || {}).filter((line: LnpLineInfo) => { return ( line?.short_name?.toLowerCase().includes(query.toLowerCase()) || line?.long_name?.toLowerCase().includes(query.toLowerCase()) || diff --git a/src/utils/translations.ts b/src/utils/translations.ts index 701607ad..43cf97f8 100644 --- a/src/utils/translations.ts +++ b/src/utils/translations.ts @@ -34,7 +34,12 @@ const translations: Translations = { platform_other: "Kan.", platform_rail: "Gl.", print_menu_title: "Drucken", + search_input_cancel: "Eingabe löschen", + search_lines_results: "Linien", search_menu_title: "Suchen", + search_placeholder: "Haltestelle, Linie, Fährte suchen", + search_stops_results: "Haltestellen", + search_trajectories_results: "Fährte", share_email_send: "E-Mail senden", share_image_save: "Bild speichern", share_menu_title: "Teilen", @@ -70,7 +75,11 @@ const translations: Translations = { platform_other: "Std.", // stand platform_rail: "Pl.", print_menu_title: "Print", + search_lines_results: "Lines", search_menu_title: "Search", + search_placeholder: "Search stop, line, route", + search_stops_results: "Stops", + search_trajectories_results: "Routes", share_email_send: "Send email", share_image_save: "Save image", share_menu_title: "Share", @@ -106,7 +115,11 @@ const translations: Translations = { platform_other: "Quai", platform_rail: "Voie", print_menu_title: "Imprimer", + search_lines_results: "Lignes", search_menu_title: "Rechercher", + search_placeholder: "Rechercher un arrêt, une ligne, une route", + search_stops_results: "Arrêts", + search_trajectories_results: "Trajectoires", share_email_send: "Envoyer un email", share_image_save: "Enregistrer l'image", share_menu_title: "Partager", @@ -142,7 +155,11 @@ const translations: Translations = { platform_other: "Cor.", // corsia platform_rail: "Bin.", print_menu_title: "Stampa", + search_lines_results: "Linee", search_menu_title: "Cerca", + search_placeholder: "Cerca fermata, linea, percorso", + search_stops_results: "Fermate", + search_trajectories_results: "Percorsi", share_email_send: "Invia email", share_image_save: "Salva immagine", share_menu_title: "Condividi", From f367f34a378244e1aacb94000e2c32d90dcdeb07 Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Fri, 31 Oct 2025 08:29:58 +0100 Subject: [PATCH 07/29] fix: add translations for search --- src/utils/hooks/useSearchTrajectories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/hooks/useSearchTrajectories.tsx b/src/utils/hooks/useSearchTrajectories.tsx index 7962be0c..612a8d3a 100644 --- a/src/utils/hooks/useSearchTrajectories.tsx +++ b/src/utils/hooks/useSearchTrajectories.tsx @@ -17,7 +17,6 @@ function useSearchTrajectories( } return Object.values(realtimeLayer.trajectories || {}).filter( (trajectory) => { - console.log("Searching trajectories for query:", trajectory); return trajectory?.properties.route_identifier ?.toLowerCase() .includes(query.toLowerCase()); From 787333c18dc709c181d6b678646d315f3c892279 Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Fri, 31 Oct 2025 08:44:28 +0100 Subject: [PATCH 08/29] fix: test if it exists --- src/SearchLinesResult/SearchLinesResult.tsx | 3 ++- src/SearchStopsResult/SearchStopsResult.tsx | 3 ++- src/SearchTrajectoriesResult/SearchTrajectoriesResult.tsx | 3 ++- src/utils/hooks/useSearchLines.tsx | 6 ++++-- src/utils/hooks/useSearchTrajectories.tsx | 6 +++--- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/SearchLinesResult/SearchLinesResult.tsx b/src/SearchLinesResult/SearchLinesResult.tsx index b0b6ccc3..06917ff1 100644 --- a/src/SearchLinesResult/SearchLinesResult.tsx +++ b/src/SearchLinesResult/SearchLinesResult.tsx @@ -21,7 +21,7 @@ function SearchLinesResult({ onSelect, ...props }: SearchLinesResultProps) { - const { setLinesIds } = useMapContext(); + const { linesNetworkPlanLayer, setLinesIds } = useMapContext(); return ( ); } diff --git a/src/SearchLinesResults/SearchLinesResults.tsx b/src/SearchLinesResults/SearchLinesResults.tsx index aa74d076..fa7fa8a7 100644 --- a/src/SearchLinesResults/SearchLinesResults.tsx +++ b/src/SearchLinesResults/SearchLinesResults.tsx @@ -1,15 +1,20 @@ +import { cloneElement, toChildArray } from "preact"; import { memo } from "preact/compat"; import { useCallback, useContext, useMemo } from "preact/hooks"; import { SearchContext } from "../Search/Search2"; -import SearchLinesResult from "../SearchLinesResult/SearchLinesResult"; import SearchResult from "../SearchResult"; import SearchResults from "../SearchResults"; import SearchResultsHeader from "../SearchResultsHeader"; import useI18n from "../utils/hooks/useI18n"; import useSearchLines from "../utils/hooks/useSearchLines"; -import type { SearchResultsProps } from "../Search/Search2"; +import type { ReactElement } from "preact/compat"; + +import type { + SearchResultsChildProps, + SearchResultsProps, +} from "../SearchResults/SearchResults"; import type { LnpLineInfo } from "../utils/hooks/useLnp"; const defaultSort = (a: LnpLineInfo, b: LnpLineInfo) => { @@ -20,25 +25,24 @@ const defaultSort = (a: LnpLineInfo, b: LnpLineInfo) => { }; function SearchLinesResults({ + children, filter, - onSelect, resultClassName, resultsClassName, resultsContainerClassName, sort = defaultSort, -}: SearchResultsProps) { +}: SearchResultsProps) { const { open, query, setOpen, setSelectedQuery } = useContext(SearchContext); const searchResponse = useSearchLines(query); const { t } = useI18n(); - const onSelectLine = useCallback( - (line) => { - setSelectedQuery(line.short_name || line.long_name); + const onSelectResult = useCallback( + (item: LnpLineInfo) => { + setSelectedQuery(item.short_name || item.long_name); setOpen(false); - onSelect?.(line); }, - [onSelect, setOpen, setSelectedQuery], + [setOpen, setSelectedQuery], ); const results = useMemo(() => { @@ -79,8 +83,19 @@ function SearchLinesResults({ > {results.map((item: LnpLineInfo) => { return ( - - + + {toChildArray(children).map( + (child: ReactElement>) => { + const onSelectItem = (itemm: LnpLineInfo, evt: Event) => { + onSelectResult(itemm); + child.props?.onSelectItem?.(itemm, evt); + }; + return cloneElement(child, { + item: item, + onSelectItem, + }); + }, + )} ); })} diff --git a/src/SearchResults/SearchResults.tsx b/src/SearchResults/SearchResults.tsx index 6f50e301..5ad54172 100644 --- a/src/SearchResults/SearchResults.tsx +++ b/src/SearchResults/SearchResults.tsx @@ -7,28 +7,32 @@ import type { HTMLAttributes, PreactDOMAttributes } from "preact"; import type { SearchResponse } from "../utils/hooks/useSearchStops"; -export type SearchResultsProps = { +export type SearchResultsProps = { className?: string; - filter?: (item: unknown) => boolean; - onSelect?: (item: unknown) => void; + filter?: (item: T) => boolean; resultClassName?: string; resultsClassName?: string; resultsContainerClassName?: string; - searchResponse?: SearchResponse; - sort?: (a: unknown, b: unknown) => number; + searchResponse?: SearchResponse; + sort?: (a: T, b: T) => number; } & HTMLAttributes & PreactDOMAttributes; +export interface SearchResultsChildProps { + item: T; + onSelectItem?: (item: T, evt: Event) => void; +} + /** * Results list of search. */ -function SearchResults({ +function SearchResults({ children, className, resultsClassName, searchResponse, ...props -}: SearchResultsProps) { +}: SearchResultsProps) { const { t } = useI18n(); if (!(searchResponse?.results?.length >= 0)) { diff --git a/src/SearchStopsResult/SearchStopsResult.tsx b/src/SearchStopsResult/SearchStopsResult.tsx index b3afc20b..68a6cb20 100644 --- a/src/SearchStopsResult/SearchStopsResult.tsx +++ b/src/SearchStopsResult/SearchStopsResult.tsx @@ -1,29 +1,26 @@ import { memo } from "preact/compat"; import { twMerge } from "tailwind-merge"; -import useFitOnFeatures from "../utils/hooks/useFitOnFeatures"; import useMapContext from "../utils/hooks/useMapContext"; -import type { GeoJSONFeature } from "maplibre-gl"; import type { ButtonHTMLAttributes, PreactDOMAttributes } from "preact"; import type { StopsFeature } from "../utils/hooks/useSearchStops"; export type SearchStopsResultProps = { className?: string; - onSelect?: (stop: StopsFeature) => void; - stop: StopsFeature; + item?: StopsFeature; + onSelectItem?: (stop: StopsFeature, evt: MouseEvent) => void; } & ButtonHTMLAttributes & PreactDOMAttributes; function SearchStopsResult({ className, - onSelect, - stop, + item, + onSelectItem, ...props }: SearchStopsResultProps) { - const { setStationId, stationsLayer } = useMapContext(); - const fitOnFeatures = useFitOnFeatures(); + const { stationsLayer } = useMapContext(); return ( ); } diff --git a/src/SearchStopsResults/SearchStopsResults.tsx b/src/SearchStopsResults/SearchStopsResults.tsx index 3ca4c8e2..e156f067 100644 --- a/src/SearchStopsResults/SearchStopsResults.tsx +++ b/src/SearchStopsResults/SearchStopsResults.tsx @@ -1,3 +1,4 @@ +import { cloneElement, toChildArray } from "preact"; import { memo } from "preact/compat"; import { useCallback, useContext, useMemo } from "preact/hooks"; @@ -5,34 +6,36 @@ import { SearchContext } from "../Search/Search2"; import SearchResult from "../SearchResult"; import SearchResults from "../SearchResults"; import SearchResultsHeader from "../SearchResultsHeader"; -import SearchStopsResult from "../SearchStopsResult"; import useI18n from "../utils/hooks/useI18n"; import useSearchStops from "../utils/hooks/useSearchStops"; -import type { SearchResultsProps } from "../SearchResults/SearchResults"; +import type { ReactElement } from "preact/compat"; + +import type { + SearchResultsChildProps, + SearchResultsProps, +} from "../SearchResults/SearchResults"; import type { StopsFeature } from "../utils/hooks/useSearchStops"; function SearchStopsResults({ + children, filter, - onSelect, resultClassName, resultsClassName, resultsContainerClassName, sort, -}: SearchResultsProps) { +}: SearchResultsProps) { const { open, query, setOpen, setSelectedQuery } = useContext(SearchContext); const searchResponse = useSearchStops(query); const { t } = useI18n(); - const onSelectStop = useCallback( - (stop: StopsFeature) => { - console.log("ici"); - setSelectedQuery(stop.properties.name); + const onSelectResult = useCallback( + (item: StopsFeature) => { + setSelectedQuery(item.properties.name); setOpen(false); - onSelect?.(stop); }, - [onSelect, setOpen, setSelectedQuery], + [setOpen, setSelectedQuery], ); const results = useMemo(() => { @@ -74,7 +77,20 @@ function SearchStopsResults({ {results.map((item: StopsFeature) => { return ( - + {toChildArray(children).map( + ( + child: ReactElement>, + ) => { + const onSelectItem = (itemm: StopsFeature, evt: Event) => { + onSelectResult(itemm); + child.props?.onSelectItem?.(itemm, evt); + }; + return cloneElement(child, { + item: item, + onSelectItem, + }); + }, + )} ); })} From 8c9f37bff520181e70befae582c580b65850efbb Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Tue, 4 Nov 2025 16:51:02 +0100 Subject: [PATCH 21/29] fix: zoom when we load from station id --- src/LayoutState/LayoutState.tsx | 16 +++++++++++++++- src/Station/Station.tsx | 9 +++++++++ src/utils/hooks/useRealtimeStation.tsx | 1 + src/utils/hooks/useSearchStops.tsx | 7 +++++-- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/LayoutState/LayoutState.tsx b/src/LayoutState/LayoutState.tsx index a0f7c92d..300b7aaa 100644 --- a/src/LayoutState/LayoutState.tsx +++ b/src/LayoutState/LayoutState.tsx @@ -1,8 +1,12 @@ import { useCallback, useEffect } from "preact/hooks"; +import useFitOnFeatures from "../utils/hooks/useFitOnFeatures"; import useLnpLineInfo, { useLnpStopInfo } from "../utils/hooks/useLnp"; import useMapContext from "../utils/hooks/useMapContext"; import useRealtimeRenderedTrajectories from "../utils/hooks/useRealtimeRenderedTrajectory"; +import useSearchStops from "../utils/hooks/useSearchStops"; + +import type { GeoJSONFeature } from "ol/format/GeoJSON"; /** * This component is responsible for updating the layout state in the context. @@ -73,6 +77,8 @@ function LayoutState() { const lineInfo = useLnpLineInfo(lineid); const stopInfo = useLnpStopInfo(stationid); const trainInfo = useRealtimeRenderedTrajectories(trainid); + const stopForCoordinate = useSearchStops(stationid); + const fitOnFeatures = useFitOnFeatures(); useEffect(() => { setHasStations(!!tenant); @@ -136,7 +142,15 @@ function LayoutState() { useEffect(() => { setStationId(stopInfo?.external_id); - }, [stopInfo, setStationId]); + + // Center and zoom on th station + const result = (stopForCoordinate?.results || []).find((stop) => { + return stop.properties.uid === stopInfo?.external_id; + }); + if (result) { + fitOnFeatures([result] as GeoJSONFeature[]); + } + }, [stopInfo, setStationId, fitOnFeatures, stopForCoordinate.results]); useEffect(() => { setNotificationId(notificationid); diff --git a/src/Station/Station.tsx b/src/Station/Station.tsx index 2b7991aa..df6dfec3 100644 --- a/src/Station/Station.tsx +++ b/src/Station/Station.tsx @@ -20,6 +20,15 @@ function Station({ className, ...props }: StationProps) { const { stationId } = useMapContext(); const station = useRealtimeStation(stationId); const departures = useRealtimeDepartures(stationId); + // const fit = useFitOnFeatures(); + + // useEffect(() => { + // if (!selectedFeature && station && station?.properties?.uid !== stationId) { + // console.log("station changed, centering on station", station); + // fit([station], map); + // } + // }, [fit, map, selectedFeature, station, stationId]); + if (!station) { return null; } diff --git a/src/utils/hooks/useRealtimeStation.tsx b/src/utils/hooks/useRealtimeStation.tsx index 5ba68449..6066fe66 100644 --- a/src/utils/hooks/useRealtimeStation.tsx +++ b/src/utils/hooks/useRealtimeStation.tsx @@ -22,6 +22,7 @@ function useRealtimeStation(stationId: number | string) { api.subscribe(`station ${stationId}`, ({ content }) => { if (content) { + console.log("Received station update", content); setStation(content as RealtimeStation); } }); diff --git a/src/utils/hooks/useSearchStops.tsx b/src/utils/hooks/useSearchStops.tsx index f1bccc93..c2a77477 100644 --- a/src/utils/hooks/useSearchStops.tsx +++ b/src/utils/hooks/useSearchStops.tsx @@ -29,6 +29,9 @@ function useSearchStops( const [isLoading, setIsLoading] = useState(false); const api: StopsAPI = useMemo(() => { + if (!apikey || !stopsurl) { + return null; + } return new StopsAPI({ apiKey: apikey, url: stopsurl }); }, [apikey, stopsurl]); @@ -66,12 +69,12 @@ function useSearchStops( }, [api, mots, params]); useEffect(() => { - if (!query) { + if (!query || !api) { setResults([]); return; } debouncedSearch(query); - }, [debouncedSearch, query]); + }, [api, debouncedSearch, query]); return { isLoading, From 3874cb843a3770e160acc533406c33afaf6265d1 Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Tue, 4 Nov 2025 17:05:15 +0100 Subject: [PATCH 22/29] fix: load station and zoom --- src/Station/Station.tsx | 8 ----- src/utils/hooks/useFitOnFeatures.tsx | 54 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 src/utils/hooks/useFitOnFeatures.tsx diff --git a/src/Station/Station.tsx b/src/Station/Station.tsx index df6dfec3..f209add5 100644 --- a/src/Station/Station.tsx +++ b/src/Station/Station.tsx @@ -20,14 +20,6 @@ function Station({ className, ...props }: StationProps) { const { stationId } = useMapContext(); const station = useRealtimeStation(stationId); const departures = useRealtimeDepartures(stationId); - // const fit = useFitOnFeatures(); - - // useEffect(() => { - // if (!selectedFeature && station && station?.properties?.uid !== stationId) { - // console.log("station changed, centering on station", station); - // fit([station], map); - // } - // }, [fit, map, selectedFeature, station, stationId]); if (!station) { return null; diff --git a/src/utils/hooks/useFitOnFeatures.tsx b/src/utils/hooks/useFitOnFeatures.tsx new file mode 100644 index 00000000..7d014b59 --- /dev/null +++ b/src/utils/hooks/useFitOnFeatures.tsx @@ -0,0 +1,54 @@ +import { GeoJSON } from "ol/format"; +import { Vector } from "ol/source"; +import { useCallback } from "preact/hooks"; + +import { FIT_ON_FEATURES_MAX_ZOOM_POINT } from "../constants"; + +import useMapContext from "./useMapContext"; + +import type { Feature, Map } from "ol"; +import type { GeoJSONFeature } from "ol/format/GeoJSON"; +const geojson = new GeoJSON(); + +const useFitOnFeatures = () => { + const { isOverlayOpen, map: contextMap } = useMapContext(); + const fitOnFeatures = useCallback( + (features: (Feature | GeoJSONFeature)[], map?: Map) => { + if ((!map && !contextMap) || !features?.length) { + return; + } + let feats = features as Feature[]; + + // Convert to ol features if GeoJSON + const geoJSONFeature = features?.[0] as GeoJSONFeature; + if (geoJSONFeature?.properties && geoJSONFeature?.type === "Feature") { + // Single feature case + feats = geojson.readFeatures( + { + features, + type: "FeatureCollection", + }, + { featureProjection: "EPSG:3857" }, + ); + } + + const mapToUse = map || contextMap; + const extent = new Vector({ features: feats }).getExtent(); + mapToUse.getView().fit(extent, { + duration: 500, + maxZoom: + extent[0] === extent[2] || extent[1] === extent[3] + ? FIT_ON_FEATURES_MAX_ZOOM_POINT + : undefined, + padding: [100, 100, 100, isOverlayOpen ? 400 : 100], + }); + return () => { + mapToUse?.getView().cancelAnimations(); + }; + }, + [contextMap, isOverlayOpen], + ); + return fitOnFeatures; +}; + +export default useFitOnFeatures; From 32493e10b668c6918ec9a361a84635d4c586b6f0 Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Tue, 4 Nov 2025 17:09:56 +0100 Subject: [PATCH 23/29] fix: use ref to avoid rerender --- src/LayoutState/LayoutState.tsx | 1 + src/utils/hooks/useFitOnFeatures.tsx | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/LayoutState/LayoutState.tsx b/src/LayoutState/LayoutState.tsx index 300b7aaa..dc81fbb8 100644 --- a/src/LayoutState/LayoutState.tsx +++ b/src/LayoutState/LayoutState.tsx @@ -141,6 +141,7 @@ function LayoutState() { }, [lineInfo, setLinesIds]); useEffect(() => { + console.log("ici"); setStationId(stopInfo?.external_id); // Center and zoom on th station diff --git a/src/utils/hooks/useFitOnFeatures.tsx b/src/utils/hooks/useFitOnFeatures.tsx index 7d014b59..b9a857c3 100644 --- a/src/utils/hooks/useFitOnFeatures.tsx +++ b/src/utils/hooks/useFitOnFeatures.tsx @@ -1,6 +1,6 @@ import { GeoJSON } from "ol/format"; import { Vector } from "ol/source"; -import { useCallback } from "preact/hooks"; +import { useCallback, useEffect, useRef } from "preact/hooks"; import { FIT_ON_FEATURES_MAX_ZOOM_POINT } from "../constants"; @@ -12,6 +12,13 @@ const geojson = new GeoJSON(); const useFitOnFeatures = () => { const { isOverlayOpen, map: contextMap } = useMapContext(); + + const isOverlayOpenRef = useRef(isOverlayOpen); + + useEffect(() => { + isOverlayOpenRef.current = isOverlayOpen; + }, [isOverlayOpen]); + const fitOnFeatures = useCallback( (features: (Feature | GeoJSONFeature)[], map?: Map) => { if ((!map && !contextMap) || !features?.length) { @@ -40,13 +47,13 @@ const useFitOnFeatures = () => { extent[0] === extent[2] || extent[1] === extent[3] ? FIT_ON_FEATURES_MAX_ZOOM_POINT : undefined, - padding: [100, 100, 100, isOverlayOpen ? 400 : 100], + padding: [100, 100, 100, isOverlayOpenRef.current ? 400 : 100], }); return () => { mapToUse?.getView().cancelAnimations(); }; }, - [contextMap, isOverlayOpen], + [contextMap], ); return fitOnFeatures; }; From c87a14047d653c111d5e66f2d531a362b57376fe Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Tue, 4 Nov 2025 17:18:59 +0100 Subject: [PATCH 24/29] fix: remove log --- src/LayoutState/LayoutState.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/LayoutState/LayoutState.tsx b/src/LayoutState/LayoutState.tsx index dc81fbb8..300b7aaa 100644 --- a/src/LayoutState/LayoutState.tsx +++ b/src/LayoutState/LayoutState.tsx @@ -141,7 +141,6 @@ function LayoutState() { }, [lineInfo, setLinesIds]); useEffect(() => { - console.log("ici"); setStationId(stopInfo?.external_id); // Center and zoom on th station From c6235934d28eb54ffdbb09bb69c3e7e0b318bbf8 Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Tue, 4 Nov 2025 17:19:09 +0100 Subject: [PATCH 25/29] fix: remove log --- src/utils/hooks/useRealtimeStation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/hooks/useRealtimeStation.tsx b/src/utils/hooks/useRealtimeStation.tsx index 6066fe66..5ba68449 100644 --- a/src/utils/hooks/useRealtimeStation.tsx +++ b/src/utils/hooks/useRealtimeStation.tsx @@ -22,7 +22,6 @@ function useRealtimeStation(stationId: number | string) { api.subscribe(`station ${stationId}`, ({ content }) => { if (content) { - console.log("Received station update", content); setStation(content as RealtimeStation); } }); From c31d2439ea083da5229058cabcd22138ed04db4d Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Wed, 5 Nov 2025 09:58:10 +0100 Subject: [PATCH 26/29] fix: use situationId --- src/OverlayDetailsFooter/OverlayDetailsFooter.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/OverlayDetailsFooter/OverlayDetailsFooter.tsx b/src/OverlayDetailsFooter/OverlayDetailsFooter.tsx index 3aa43cd0..449fa0bd 100644 --- a/src/OverlayDetailsFooter/OverlayDetailsFooter.tsx +++ b/src/OverlayDetailsFooter/OverlayDetailsFooter.tsx @@ -29,10 +29,9 @@ function OverlayDetailsFooter({ } let id = feature?.get("id"); - const situation = feature?.get("situation"); - if (situation) { - const situationParsed = JSON.parse(situation); - id = situationParsed?.id || id; + const situationId = feature?.get("situationId"); + if (situationId) { + id = situationId; } return (
    From 2eefd0b59be333b3ee91d9b0782c5916d51d12e9 Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Fri, 7 Nov 2025 12:49:42 +0100 Subject: [PATCH 27/29] fix: up mbt --- package.json | 2 +- .../NotificationDetails.tsx | 25 +++++++++++----- src/RealtimeLayer/RealtimeLayer.tsx | 5 ++-- src/RouteIcon/RouteIcon.tsx | 8 ++--- src/utils/getBgColor.ts | 3 -- src/utils/getDelayColorForVehicle.test.ts | 30 ++++++++++++------- src/utils/getDelayColorForVehicle.ts | 12 ++++---- src/utils/getDelayFontForVehicle.test.ts | 7 ----- src/utils/getDelayFontForVehicle.tsx | 8 ----- src/utils/getDelayTextForVehicle.test.ts | 24 +++++++-------- src/utils/getDelayTextForVehicle.ts | 4 +++ src/utils/getMainColorForVehicle.ts | 13 +++++--- src/utils/getTextFontForVehicle.test.ts | 2 +- src/utils/getTextFontForVehicle.tsx | 14 +++++++-- src/utils/getTextForVehicle.ts | 4 +++ yarn.lock | 8 ++--- 16 files changed, 97 insertions(+), 72 deletions(-) delete mode 100644 src/utils/getBgColor.ts delete mode 100644 src/utils/getDelayFontForVehicle.test.ts delete mode 100644 src/utils/getDelayFontForVehicle.tsx diff --git a/package.json b/package.json index d2980a78..3768a11a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "jspdf": "^3.0.3", "lodash.debounce": "^4.0.8", "maplibre-gl": "^5.10.0", - "mobility-toolbox-js": "3.4.8-beta.0", + "mobility-toolbox-js": "3.4.8-beta.11", "ol": "^10.6.1", "preact": "^10.27.2", "preact-custom-element": "^4.5.1", diff --git a/src/NotificationDetails/NotificationDetails.tsx b/src/NotificationDetails/NotificationDetails.tsx index 84b895d9..415c00dd 100644 --- a/src/NotificationDetails/NotificationDetails.tsx +++ b/src/NotificationDetails/NotificationDetails.tsx @@ -69,6 +69,17 @@ function NotificationDetails({ // Find the current publication(s) at the current date publicationsToDisplay = publicationsArr?.filter(({ publicationWindows }) => { + // In some cases publicationWindows can be undefined here but defined at the + // root of the object so we apply the root publicationWindows to all publications with empty one + if ( + !publicationWindows?.length && + situationParsed?.publicationWindows?.length + ) { + // @ts-expect-error we should not set this value directly + // eslint-disable-next-line no-param-reassign + publicationWindows = situationParsed.publicationWindows; + } + return publicationWindows.find(({ endTime, startTime }) => { const now = new Date(); const startT = new Date(startTime); @@ -197,13 +208,13 @@ function NotificationDetails({ }, )}
    -
    + {textualContent?.description && ( +
    + )} {!!textualContentMultilingual?.images?.length && (
    {textualContentMultilingual.images.map( diff --git a/src/RealtimeLayer/RealtimeLayer.tsx b/src/RealtimeLayer/RealtimeLayer.tsx index 78b458f2..06adf027 100644 --- a/src/RealtimeLayer/RealtimeLayer.tsx +++ b/src/RealtimeLayer/RealtimeLayer.tsx @@ -9,8 +9,8 @@ import { useEffect, useMemo, useState } from "preact/hooks"; import centerOnVehicle from "../utils/centerOnVehicle"; import { LAYER_NAME_REALTIME } from "../utils/constants"; import getDelayColorForVehicle from "../utils/getDelayColorForVehicle"; -import getDelayFontForVehicle from "../utils/getDelayFontForVehicle"; import getDelayTextForVehicle from "../utils/getDelayTextForVehicle"; +import getMainColorForVehicle from "../utils/getMainColorForVehicle"; import getTextFontForVehicle from "../utils/getTextFontForVehicle"; import getTextForVehicle from "../utils/getTextForVehicle"; import useMapContext from "../utils/hooks/useMapContext"; @@ -70,14 +70,15 @@ function RealtimeLayer(props: Partial) { } : undefined, isQueryable: true, + minZoom: 5, // It depends fo the radius mapping in realtimeStyleUtils name: LAYER_NAME_REALTIME, tenant, url: realtimeurl, zIndex: 1, ...props, styleOptions: { + getColor: getMainColorForVehicle, getDelayColor: getDelayColorForVehicle, - getDelayFont: getDelayFontForVehicle, getDelayText: getDelayTextForVehicle, getText: getTextForVehicle, getTextFont: getTextFontForVehicle, diff --git a/src/RouteIcon/RouteIcon.tsx b/src/RouteIcon/RouteIcon.tsx index cb629313..c54f038c 100644 --- a/src/RouteIcon/RouteIcon.tsx +++ b/src/RouteIcon/RouteIcon.tsx @@ -56,12 +56,12 @@ function RouteIcon({ ); const color = lineToUse?.text_color || getTextColor(type); let borderColor = lineToUse?.stroke || "black"; - const text = getTextForVehicle( - line || lineInfo || departure || stopSequence || trajectory, - ); + const objectToUse = + line || lineInfo || departure || stopSequence || trajectory; + const text = getTextForVehicle(objectToUse); const fontSize = fontSizesByNbLetters[text.length] || 12; - const font = getTextFontForVehicle(fontSize, text); + const font = getTextFontForVehicle(objectToUse, null, fontSize, text); // RealtimeIcon only for stopsequence for now const hasRealtime = diff --git a/src/utils/getBgColor.ts b/src/utils/getBgColor.ts deleted file mode 100644 index ee35621c..00000000 --- a/src/utils/getBgColor.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { realtimeConfig } from "mobility-toolbox-js/ol"; - -export default realtimeConfig.getBgColor; diff --git a/src/utils/getDelayColorForVehicle.test.ts b/src/utils/getDelayColorForVehicle.test.ts index b906b83e..94391b03 100644 --- a/src/utils/getDelayColorForVehicle.test.ts +++ b/src/utils/getDelayColorForVehicle.test.ts @@ -2,31 +2,39 @@ import getDelayColorForVehicle from "./getDelayColorForVehicle"; describe("getDelayColorForVehicle", () => { it("returns cancelled color", () => { - expect(getDelayColorForVehicle(0, true, true)).toBe("#dc2626"); - expect(getDelayColorForVehicle(0, true, false)).toBe("#a0a0a0"); + expect(getDelayColorForVehicle(null, null, 0, true, true)).toBe("#dc2626"); + expect(getDelayColorForVehicle(null, null, 0, true, false)).toBe("#a0a0a0"); }); it("returns null delay (no realtime train) color", () => { - expect(getDelayColorForVehicle(null)).toBe("#a0a0a0"); + expect(getDelayColorForVehicle(null, null, null)).toBe("#a0a0a0"); }); it("returns green", () => { - expect(getDelayColorForVehicle(0)).toBe("#16a34a"); - expect(getDelayColorForVehicle(2.49 * 60 * 1000)).toBe("#16a34a"); + expect(getDelayColorForVehicle(null, null, 0)).toBe("#16a34a"); + expect(getDelayColorForVehicle(null, null, 2.49 * 60 * 1000)).toBe( + "#16a34a", + ); }); it("returns yellow", () => { - expect(getDelayColorForVehicle(3 * 60 * 1000)).toBe("#ca8a04"); - expect(getDelayColorForVehicle(4.49 * 60 * 1000 - 1)).toBe("#ca8a04"); + expect(getDelayColorForVehicle(null, null, 3 * 60 * 1000)).toBe("#ca8a04"); + expect(getDelayColorForVehicle(null, null, 4.49 * 60 * 1000 - 1)).toBe( + "#ca8a04", + ); }); it("returns orange", () => { - expect(getDelayColorForVehicle(5 * 60 * 1000)).toBe("#ea580c"); - expect(getDelayColorForVehicle(9.49 * 60 * 1000 - 1)).toBe("#ea580c"); + expect(getDelayColorForVehicle(null, null, 5 * 60 * 1000)).toBe("#ea580c"); + expect(getDelayColorForVehicle(null, null, 9.49 * 60 * 1000 - 1)).toBe( + "#ea580c", + ); }); it("returns red", () => { - expect(getDelayColorForVehicle(10 * 60 * 1000)).toBe("#dc2626"); - expect(getDelayColorForVehicle(180 * 60 * 1000)).toBe("#dc2626"); + expect(getDelayColorForVehicle(null, null, 10 * 60 * 1000)).toBe("#dc2626"); + expect(getDelayColorForVehicle(null, null, 180 * 60 * 1000)).toBe( + "#dc2626", + ); }); }); diff --git a/src/utils/getDelayColorForVehicle.ts b/src/utils/getDelayColorForVehicle.ts index 751a1e5f..a23ff40d 100644 --- a/src/utils/getDelayColorForVehicle.ts +++ b/src/utils/getDelayColorForVehicle.ts @@ -1,13 +1,15 @@ import getDelayColor from "./getDelayColor"; +import type { ViewState } from "mobility-toolbox-js/types"; + /** - * @private - * @param {number} delayInMs Delay in milliseconds. - * @param {boolean} cancelled true if the journey is cancelled. - * @param {boolean} isDelayText true if the color is used for delay text of the symbol. + * Return the delay color depending on an object representing a vehicle or a line. + * This function is used to have the same color on the map and on other components. */ const getDelayColorForVehicle = ( - delayInMs: null | number, + object?: unknown, + viewState?: ViewState, + delayInMs?: number, cancelled?: boolean, isDelayText?: boolean, ): string => { diff --git a/src/utils/getDelayFontForVehicle.test.ts b/src/utils/getDelayFontForVehicle.test.ts deleted file mode 100644 index d51c467b..00000000 --- a/src/utils/getDelayFontForVehicle.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import getDelayFontForVehicle from "./getDelayFontForVehicle"; - -describe("getDelayFontForVehicle", () => { - it("returns font that inherit", () => { - expect(getDelayFontForVehicle(12)).toBe("bold 12px arial"); - }); -}); diff --git a/src/utils/getDelayFontForVehicle.tsx b/src/utils/getDelayFontForVehicle.tsx deleted file mode 100644 index 801fba0a..00000000 --- a/src/utils/getDelayFontForVehicle.tsx +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Return the font for the delay text in the map. - */ -const getDelayFontForVehicle = (fontSize: number) => { - return `bold ${fontSize}px arial`; -}; - -export default getDelayFontForVehicle; diff --git a/src/utils/getDelayTextForVehicle.test.ts b/src/utils/getDelayTextForVehicle.test.ts index 5ebdd86c..2f545505 100644 --- a/src/utils/getDelayTextForVehicle.test.ts +++ b/src/utils/getDelayTextForVehicle.test.ts @@ -2,31 +2,31 @@ import getDelayTextForVehicle from "./getDelayTextForVehicle"; describe("getDelayTextForVehicle", () => { it("returns cancelled character", () => { - expect(getDelayTextForVehicle(7200000, true)).toBe( + expect(getDelayTextForVehicle(null, null, 7200000, true)).toBe( String.fromCodePoint(0x00d7), ); }); it("returns hours (floor)", () => { - expect(getDelayTextForVehicle(7200000)).toBe("+2h"); - expect(getDelayTextForVehicle(7255555)).toBe("+2h1m"); + expect(getDelayTextForVehicle(null, null, 7200000)).toBe("+2h"); + expect(getDelayTextForVehicle(null, null, 7255555)).toBe("+2h1m"); }); it("returns minutes (round)", () => { - expect(getDelayTextForVehicle(120000)).toBe("+2m"); - expect(getDelayTextForVehicle(151000)).toBe("+3m"); + expect(getDelayTextForVehicle(null, null, 120000)).toBe("+2m"); + expect(getDelayTextForVehicle(null, null, 151000)).toBe("+3m"); }); it("doesn't display seconds", () => { - expect(getDelayTextForVehicle(1000)).toBe(""); - expect(getDelayTextForVehicle(30000)).toBe("+1m"); - expect(getDelayTextForVehicle(7255555)).toBe("+2h1m"); + expect(getDelayTextForVehicle(null, null, 1000)).toBe(""); + expect(getDelayTextForVehicle(null, null, 30000)).toBe("+1m"); + expect(getDelayTextForVehicle(null, null, 7255555)).toBe("+2h1m"); }); it("returns empty value", () => { - expect(getDelayTextForVehicle(1000)).toBe(""); - expect(getDelayTextForVehicle(null)).toBe(""); - expect(getDelayTextForVehicle(undefined)).toBe(""); - expect(getDelayTextForVehicle(0)).toBe(""); + expect(getDelayTextForVehicle(null, null, 1000)).toBe(""); + expect(getDelayTextForVehicle(null, null, null)).toBe(""); + expect(getDelayTextForVehicle(null, null, undefined)).toBe(""); + expect(getDelayTextForVehicle(null, null, 0)).toBe(""); }); }); diff --git a/src/utils/getDelayTextForVehicle.ts b/src/utils/getDelayTextForVehicle.ts index cd493425..4b588f5a 100644 --- a/src/utils/getDelayTextForVehicle.ts +++ b/src/utils/getDelayTextForVehicle.ts @@ -1,11 +1,15 @@ import getDelayString from "./getDelayString"; +import type { RealtimeTrajectory, ViewState } from "mobility-toolbox-js/types"; + /** * This function returns the text displays near the vehicle. * We use getDelayString inside it to make sure that RouteSchedule and * the map have the same values. */ const getDelayTextForVehicle = ( + trajectory: RealtimeTrajectory, + viewState: ViewState, delayInMs: number, cancelled = false, ): string => { diff --git a/src/utils/getMainColorForVehicle.ts b/src/utils/getMainColorForVehicle.ts index c842e810..44dea282 100644 --- a/src/utils/getMainColorForVehicle.ts +++ b/src/utils/getMainColorForVehicle.ts @@ -1,4 +1,4 @@ -import getBgColor from "./getBgColor"; +import { realtimeStyleUtils } from "mobility-toolbox-js/ol"; import type { RealtimeDeparture, @@ -8,7 +8,10 @@ import type { RealtimeTrajectory, } from "mobility-toolbox-js/types"; -// This function returns the main color of a line using a line, trajectory, stopsequence or departure object. +/** + * Return the color depending on an object representing a vehicle or a line. + * This function is used to have the same color on the map and on other components. + */ const getMainColorForVehicle = (object: unknown = null): string => { let color = (object as RealtimeTrajectory)?.properties?.line?.color || @@ -34,10 +37,12 @@ const getMainColorForVehicle = (object: unknown = null): string => { type = "rail"; } } - color = getBgColor(type) || getBgColor("rail"); + color = + realtimeStyleUtils.getColorForType(type) || + realtimeStyleUtils.getColorForType("rail"); } - if (color && color[0] !== "#") { + if (color && !color.startsWith("#")) { color = `#${color}`; } diff --git a/src/utils/getTextFontForVehicle.test.ts b/src/utils/getTextFontForVehicle.test.ts index 4926f59e..40898417 100644 --- a/src/utils/getTextFontForVehicle.test.ts +++ b/src/utils/getTextFontForVehicle.test.ts @@ -2,6 +2,6 @@ import getTextFontForVehicle from "./getTextFontForVehicle"; describe("getTextFontForVehicle", () => { it("returns font that inherit", () => { - expect(getTextFontForVehicle(12)).toBe("bold 12px arial"); + expect(getTextFontForVehicle(null, null, 12)).toBe("bold 12px arial"); }); }); diff --git a/src/utils/getTextFontForVehicle.tsx b/src/utils/getTextFontForVehicle.tsx index a1b52824..a9cdefd5 100644 --- a/src/utils/getTextFontForVehicle.tsx +++ b/src/utils/getTextFontForVehicle.tsx @@ -1,8 +1,16 @@ +import type { ViewState } from "mobility-toolbox-js/types"; + /** - * Return the font for the delay text in the map. + * Return the font depending on an object representing a vehicle or a line. + * This function is used to have the same font on the map and on other components. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const getTextFontForVehicle = (fontSize: number, text?: string) => { +const getTextFontForVehicle = ( + object?: unknown, + viewState?: ViewState, + fontSize?: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + text?: string, +) => { return `bold ${fontSize}px arial`; }; diff --git a/src/utils/getTextForVehicle.ts b/src/utils/getTextForVehicle.ts index 8e8e7f1c..381e3c4f 100644 --- a/src/utils/getTextForVehicle.ts +++ b/src/utils/getTextForVehicle.ts @@ -6,6 +6,10 @@ import type { import type { LnpLineInfo } from "./hooks/useLnp"; +/** + * Return the text depending on an object representing a vehicle or a line. + * This function is used to have the same text on the map and on other components. + */ const getTextForVehicle = (object: unknown = ""): string => { const name = (object as RealtimeTrajectory)?.properties?.line?.name || diff --git a/yarn.lock b/yarn.lock index fe9a3b03..b3e86fd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8710,10 +8710,10 @@ mlly@^1.7.4: pkg-types "^1.3.1" ufo "^1.6.1" -mobility-toolbox-js@3.4.8-beta.0: - version "3.4.8-beta.0" - resolved "https://registry.yarnpkg.com/mobility-toolbox-js/-/mobility-toolbox-js-3.4.8-beta.0.tgz#60ae2a432df8ec29264619ea1443c84fa4a41881" - integrity sha512-iVa/uYK0CFJ+a7QJ8bzKVNpdFTLM0rM4L/RxU8ONoWpV3b51Hoo125vH7klGh2xegvWQsu+JDSO3TXRB0h08ew== +mobility-toolbox-js@3.4.8-beta.11: + version "3.4.8-beta.11" + resolved "https://registry.yarnpkg.com/mobility-toolbox-js/-/mobility-toolbox-js-3.4.8-beta.11.tgz#b3e83f770e6bfb17454f1ccbe11695d74d33c7d4" + integrity sha512-w9edNxk0qZpsIhwESOy981PAnxnGueT7ZdgNNbfBkYML5MCGLUz6rLqCS/z7SeSOA1Nfa8q2rJ8JAY18HYtQ8g== dependencies: "@geoblocks/ol-maplibre-layer" "^1.0.3" "@geops/geops-ui" "0.3.6" From 66c0cc288f4a8c9fe0953e6b1547898f5b64f5f2 Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Fri, 7 Nov 2025 15:03:22 +0100 Subject: [PATCH 28/29] fix: make things more logical --- package.json | 2 +- src/RealtimeLayer/RealtimeLayer.tsx | 2 ++ src/RouteIcon/RouteIcon.tsx | 14 +++++--------- src/utils/getTextColor.ts | 7 ------- src/utils/getTextColorForVehicle.ts | 29 +++++++++++++++++++++++++++++ src/utils/getTextForVehicle.ts | 1 - yarn.lock | 8 ++++---- 7 files changed, 41 insertions(+), 22 deletions(-) delete mode 100644 src/utils/getTextColor.ts create mode 100644 src/utils/getTextColorForVehicle.ts diff --git a/package.json b/package.json index 3768a11a..9d71d140 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "jspdf": "^3.0.3", "lodash.debounce": "^4.0.8", "maplibre-gl": "^5.10.0", - "mobility-toolbox-js": "3.4.8-beta.11", + "mobility-toolbox-js": "3.4.8-beta.14", "ol": "^10.6.1", "preact": "^10.27.2", "preact-custom-element": "^4.5.1", diff --git a/src/RealtimeLayer/RealtimeLayer.tsx b/src/RealtimeLayer/RealtimeLayer.tsx index 06adf027..675a1040 100644 --- a/src/RealtimeLayer/RealtimeLayer.tsx +++ b/src/RealtimeLayer/RealtimeLayer.tsx @@ -11,6 +11,7 @@ import { LAYER_NAME_REALTIME } from "../utils/constants"; import getDelayColorForVehicle from "../utils/getDelayColorForVehicle"; import getDelayTextForVehicle from "../utils/getDelayTextForVehicle"; import getMainColorForVehicle from "../utils/getMainColorForVehicle"; +import getTextColorForVehicle from "../utils/getTextColorForVehicle"; import getTextFontForVehicle from "../utils/getTextFontForVehicle"; import getTextForVehicle from "../utils/getTextForVehicle"; import useMapContext from "../utils/hooks/useMapContext"; @@ -81,6 +82,7 @@ function RealtimeLayer(props: Partial) { getDelayColor: getDelayColorForVehicle, getDelayText: getDelayTextForVehicle, getText: getTextForVehicle, + getTextColor: getTextColorForVehicle, getTextFont: getTextFontForVehicle, ...(props?.styleOptions || {}), }, diff --git a/src/RouteIcon/RouteIcon.tsx b/src/RouteIcon/RouteIcon.tsx index c54f038c..b120ab16 100644 --- a/src/RouteIcon/RouteIcon.tsx +++ b/src/RouteIcon/RouteIcon.tsx @@ -2,7 +2,7 @@ import { twMerge } from "tailwind-merge"; import NoRealtime from "../icons/NoRealtime"; import getMainColorForVehicle from "../utils/getMainColorForVehicle"; -import getTextColor from "../utils/getTextColor"; +import getTextColorForVehicle from "../utils/getTextColorForVehicle"; import getTextFontForVehicle from "../utils/getTextFontForVehicle"; import getTextForVehicle from "../utils/getTextForVehicle"; import useMapContext from "../utils/hooks/useMapContext"; @@ -49,17 +49,12 @@ function RouteIcon({ trajectory?.properties?.line; const trainId = stopSequence?.id || departure?.train_id; const trajectoryToUse = trajectory || realtimeLayer?.trajectories[trainId]; - - const type = lineToUse?.type || stopSequence?.type || trajectory?.type; - const backgroundColor = getMainColorForVehicle( - line || lineInfo || departure || stopSequence || trajectory, - ); - const color = lineToUse?.text_color || getTextColor(type); - let borderColor = lineToUse?.stroke || "black"; const objectToUse = line || lineInfo || departure || stopSequence || trajectory; - const text = getTextForVehicle(objectToUse); + const backgroundColor = getMainColorForVehicle(objectToUse); + const color = getTextColorForVehicle(objectToUse); + const text = getTextForVehicle(objectToUse); const fontSize = fontSizesByNbLetters[text.length] || 12; const font = getTextFontForVehicle(objectToUse, null, fontSize, text); @@ -73,6 +68,7 @@ function RouteIcon({ const showNoRealtimeIcon = !!stopSequence || !!departure || !!trajectoryToUse; const isCancelled = stopSequence?.stations[0]?.state === "JOURNEY_CANCELLED"; + let borderColor = lineToUse?.stroke || "black"; if (borderColor === backgroundColor) { borderColor = "black"; } diff --git a/src/utils/getTextColor.ts b/src/utils/getTextColor.ts deleted file mode 100644 index b4fcd0d8..00000000 --- a/src/utils/getTextColor.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { realtimeConfig } from "mobility-toolbox-js/ol"; - -const getTextColor = (type) => { - return realtimeConfig.getTextColor(type); -}; - -export default getTextColor; diff --git a/src/utils/getTextColorForVehicle.ts b/src/utils/getTextColorForVehicle.ts new file mode 100644 index 00000000..eb7c4e84 --- /dev/null +++ b/src/utils/getTextColorForVehicle.ts @@ -0,0 +1,29 @@ +import { realtimeConfig } from "mobility-toolbox-js/ol"; + +import type { + RealtimeDeparture, + RealtimeLine, + RealtimeStopSequence, + RealtimeTrajectory, +} from "mobility-toolbox-js/types"; + +import type { LnpLineInfo } from "./hooks/useLnp"; + +const getTextColorForVehicle = (object: unknown) => { + const textColor = + (object as LnpLineInfo | RealtimeLine).text_color || + (object as RealtimeDeparture | RealtimeStopSequence).line?.text_color || + (object as RealtimeTrajectory).properties?.line?.text_color; + + if (textColor) { + return textColor; + } + + const type = + (object as RealtimeStopSequence).type || + (object as RealtimeTrajectory).properties?.type; + + return realtimeConfig.getTextColorForType(type); +}; + +export default getTextColorForVehicle; diff --git a/src/utils/getTextForVehicle.ts b/src/utils/getTextForVehicle.ts index 381e3c4f..ff427c47 100644 --- a/src/utils/getTextForVehicle.ts +++ b/src/utils/getTextForVehicle.ts @@ -13,7 +13,6 @@ import type { LnpLineInfo } from "./hooks/useLnp"; const getTextForVehicle = (object: unknown = ""): string => { const name = (object as RealtimeTrajectory)?.properties?.line?.name || - // @ts-expect-error bad type definition (object as RealtimeStopSequence)?.line?.name || (object as RealtimeLine)?.name || (object as LnpLineInfo)?.short_name || diff --git a/yarn.lock b/yarn.lock index b3e86fd1..be76090c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8710,10 +8710,10 @@ mlly@^1.7.4: pkg-types "^1.3.1" ufo "^1.6.1" -mobility-toolbox-js@3.4.8-beta.11: - version "3.4.8-beta.11" - resolved "https://registry.yarnpkg.com/mobility-toolbox-js/-/mobility-toolbox-js-3.4.8-beta.11.tgz#b3e83f770e6bfb17454f1ccbe11695d74d33c7d4" - integrity sha512-w9edNxk0qZpsIhwESOy981PAnxnGueT7ZdgNNbfBkYML5MCGLUz6rLqCS/z7SeSOA1Nfa8q2rJ8JAY18HYtQ8g== +mobility-toolbox-js@3.4.8-beta.14: + version "3.4.8-beta.14" + resolved "https://registry.yarnpkg.com/mobility-toolbox-js/-/mobility-toolbox-js-3.4.8-beta.14.tgz#9c8e0ed330465200f3525d0489a90b06dc79b79b" + integrity sha512-CCgrhneYTQPO8ZPzlJvkkGGfOX+UMT39DxOETCcB8WRiYXlz7e7W9shwDNAj6tMZ6pKFO7FhBWx/oO0ogdzaEw== dependencies: "@geoblocks/ol-maplibre-layer" "^1.0.3" "@geops/geops-ui" "0.3.6" From 861566aa45e1823d4b74eaccb72bfcac1e5f0362 Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Fri, 7 Nov 2025 16:49:05 +0100 Subject: [PATCH 29/29] fix: up mbt --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9d71d140..0b9d170b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "jspdf": "^3.0.3", "lodash.debounce": "^4.0.8", "maplibre-gl": "^5.10.0", - "mobility-toolbox-js": "3.4.8-beta.14", + "mobility-toolbox-js": "3.5.0", "ol": "^10.6.1", "preact": "^10.27.2", "preact-custom-element": "^4.5.1", diff --git a/yarn.lock b/yarn.lock index be76090c..16c56375 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8710,10 +8710,10 @@ mlly@^1.7.4: pkg-types "^1.3.1" ufo "^1.6.1" -mobility-toolbox-js@3.4.8-beta.14: - version "3.4.8-beta.14" - resolved "https://registry.yarnpkg.com/mobility-toolbox-js/-/mobility-toolbox-js-3.4.8-beta.14.tgz#9c8e0ed330465200f3525d0489a90b06dc79b79b" - integrity sha512-CCgrhneYTQPO8ZPzlJvkkGGfOX+UMT39DxOETCcB8WRiYXlz7e7W9shwDNAj6tMZ6pKFO7FhBWx/oO0ogdzaEw== +mobility-toolbox-js@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/mobility-toolbox-js/-/mobility-toolbox-js-3.5.0.tgz#12600bfd647b67b690801d20957ada565a59b518" + integrity sha512-D4SH+RfL0DHWHnedrs04ZuXsHYisU3AHb+S1EjXrZmLU2kAnbaeGwpwQQb3GKV159GoO6JUCiQuIyfHsaGFI9w== dependencies: "@geoblocks/ol-maplibre-layer" "^1.0.3" "@geops/geops-ui" "0.3.6"