import { getGraphByZoom, RealtimeLayer as MtbRealtimeLayer, } from "mobility-toolbox-js/ol"; import GeoJSON from "ol/format/GeoJSON"; import { unByKey } from "ol/Observable"; import { Vector } from "ol/source"; import { memo } from "preact/compat"; 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 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"; import type { RealtimeLayerOptions } from "mobility-toolbox-js/ol/layers/RealtimeLayer"; import type { RealtimeMot, RealtimeTrainId, StyleMetadataGraphs, } from "mobility-toolbox-js/types"; const TRACKING_ZOOM = 16; const useGraphs = window.location?.href?.includes("graphs=true"); const geojson = new GeoJSON(); function RealtimeLayer(props: Partial) { const { apikey, baseLayer, isFollowing, isTracking, linesNetworkPlanLayer, map, mots, realtimebboxparameters, realtimeurl, setIsFollowing, setIsTracking, setRealtimeLayer, tenant, trainId, } = useMapContext(); const [graphByZoom, setGraphByZoom] = useState<(null | string)[]>([]); const [isLnpVisible, setIsLnpVisible] = useState(false); const layer = useMemo(() => { if (!apikey || !realtimeurl) { return null; } const lay = new MtbRealtimeLayer({ apiKey: apikey, bboxParameters: realtimebboxparameters ?.split(" ") .reduce((acc, string) => { if (!string) { return acc; } const [key, value] = string.split("="); acc[key] = value; return acc; }, {}), getMotsByZoom: mots ? () => { return mots.split(",") as RealtimeMot[]; } : 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, getDelayText: getDelayTextForVehicle, getText: getTextForVehicle, getTextColor: getTextColorForVehicle, getTextFont: getTextFontForVehicle, ...(props?.styleOptions || {}), }, }); return lay; }, [apikey, realtimeurl, realtimebboxparameters, mots, tenant, props]); useEffect(() => { let key; if (!map || !layer) { return; } if (map.getView()?.getCenter()) { map.addLayer(layer); } else { key = map.once("moveend", () => { map.addLayer(layer); }); } setRealtimeLayer(layer); return () => { if (key) { unByKey(key); } map.removeLayer(layer); setRealtimeLayer(null); }; }, [map, setRealtimeLayer, layer]); // Behavior when vehicle is selected or not. useEffect(() => { if (!trainId) { setIsFollowing(false); } }, [trainId, setIsFollowing]); // Behavior when user tracking is activated or not. useEffect(() => { const olKeys = []; if (isTracking) { setIsFollowing(false); } return () => { unByKey(olKeys); }; }, [isTracking, setIsFollowing]); // Deactive auto zooming when the user pans the map useEffect(() => { if (!map) { return; } let onMovestartKey = null; onMovestartKey = map.getView().on("change:center", (evt) => { if (evt.target.getInteracting()) { setIsFollowing(false); setIsTracking(false); } }); return () => { unByKey(onMovestartKey); }; }, [map, setIsFollowing, setIsTracking]); useEffect(() => { let interval = null; if (layer) { layer.engine.useThrottle = !isFollowing; // We deactivate the bbox on moveend when following to have smoother experience. // The correct bbox will be set later by getting the full trajectory. layer.engine.isUpdateBboxOnMoveEnd = !isFollowing; // layer.useRequestAnimationFrame = isFollowing; layer.allowRenderWhenAnimating = !!isFollowing; } if (!isFollowing || !trainId || !map || !layer) { return; } setIsTracking(false); const followVehicle = async (id: RealtimeTrainId) => { let vehicle = id && layer?.trajectories?.[id]; if (!vehicle) { const message = await layer.api.getTrajectory(trainId, layer.mode); vehicle = message?.content; // We get the fulltrajectory to set the api bbox correctly. const fullMessage = await layer.api.getFullTrajectory( trainId, layer.mode, undefined, ); if (fullMessage?.content) { try { const features = geojson.readFeatures(fullMessage.content); const extent = new Vector({ features }).getExtent(); layer.api.bbox = [...extent, ...layer.api.bbox.slice(4)]; } catch (err) { // eslint-disable-next-line no-console console.warn( "Error parsing full trajectory feature:", err, fullMessage?.content, ); } } } const success = await centerOnVehicle(vehicle, map, TRACKING_ZOOM); // Once the map is zoomed on the vehicle we follow him, only recenter , no zoom changes. if (success === true) { interval = setInterval(() => { void centerOnVehicle( layer?.trajectories?.[trainId], map, TRACKING_ZOOM, ); }, 1000); } }; void followVehicle(trainId); return () => { clearInterval(interval); }; }, [isFollowing, map, layer, trainId, setIsTracking]); useEffect(() => { if (trainId) { // No animation, it's nicer for the user. const center = layer?.trajectories?.[trainId]?.properties?.coordinate; if (center) { map.getView().setCenter(center); } } }, [map, trainId, layer]); // Subscribe to the full trajectory of the selected vehicle. useEffect(() => { if (!trainId || !layer?.api) { return; } layer.selectedVehicleId = trainId; layer.highlightTrajectory(trainId).catch((err) => { // eslint-disable-next-line no-console console.error("Error highlighting trajectory:", err); }); return () => { if (layer?.selectedVehicleId) { layer.api?.unsubscribeFullTrajectory(layer.selectedVehicleId); layer.selectedVehicleId = null; layer.vectorLayer.getSource().clear(); } }; }, [trainId, layer, layer?.api]); // Get graphs value useEffect(() => { if (!map || !baseLayer) { return; } const key = map.once("rendercomplete", () => { const metadata = baseLayer.mapLibreMap?.getStyle()?.metadata as { graphs: StyleMetadataGraphs; }; const tmpGraphByZoom = []; for (let i = 0; i < 26; i++) { tmpGraphByZoom.push(getGraphByZoom(i, metadata?.graphs)); } setGraphByZoom(tmpGraphByZoom); }); return () => { unByKey(key); }; }, [map, baseLayer, layer]); // Watch lnp visibility useEffect(() => { const key = linesNetworkPlanLayer?.on("change:visible", () => { const visible = linesNetworkPlanLayer.getVisible(); setIsLnpVisible(visible); }); setIsLnpVisible(linesNetworkPlanLayer?.getVisible() || false); return () => { unByKey(key); }; }, [linesNetworkPlanLayer]); // Apply graphByZoom only when lnp layer is there and visible useEffect(() => { if (!layer || !graphByZoom?.length) { return; } if (useGraphs && isLnpVisible) { layer.engine.graphByZoom = graphByZoom; } else { layer.engine.graphByZoom = []; } layer.engine.setBbox(); }, [isLnpVisible, layer, graphByZoom, linesNetworkPlanLayer]); return null; } export default memo(RealtimeLayer);