import StopDataProvider, { type Stop } from "../data/StopDataProvider"; import "./map.css"; import { DEFAULT_STYLE, loadStyle } from "app/maps/styleloader"; import type { Feature as GeoJsonFeature, Point } from "geojson"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import Map, { GeolocateControl, Layer, NavigationControl, Source, type MapLayerMouseEvent, type MapRef, type StyleSpecification, } from "react-map-gl/maplibre"; import { useNavigate } from "react-router"; import { PlannerOverlay } from "~/components/PlannerOverlay"; import { StopSheet } from "~/components/StopSummarySheet"; import { REGION_DATA } from "~/config/RegionConfig"; import { usePageTitle } from "~/contexts/PageTitleContext"; import { usePlanner } from "~/hooks/usePlanner"; import { useApp } from "../AppContext"; import "../tailwind-full.css"; // Componente principal del mapa export default function StopMap() { const { t } = useTranslation(); const navigate = useNavigate(); usePageTitle(t("navbar.map", "Mapa")); const [stops, setStops] = useState< GeoJsonFeature< Point, { stopId: string; name: string; lines: string[]; cancelled?: boolean; prefix: string; } >[] >([]); const [selectedStop, setSelectedStop] = useState(null); const [isSheetOpen, setIsSheetOpen] = useState(false); const { mapState, updateMapState, theme } = useApp(); const mapRef = useRef(null); const { searchRoute, origin, setOrigin } = usePlanner(); // Style state for Map component const [mapStyle, setMapStyle] = useState(DEFAULT_STYLE); // Set default origin to current location on first load (map page) useEffect(() => { // On the map page, always default to current location on load, // overriding any previously used address. The user can change it after. if (!navigator.geolocation) return; navigator.geolocation.getCurrentPosition( async (pos) => { try { // Keep display as "Current location" until a search is performed setOrigin({ name: t("planner.current_location"), label: "GPS", lat: pos.coords.latitude, lon: pos.coords.longitude, layer: "current-location", }); } catch (_) { // ignore } }, () => { // ignore geolocation errors; user can set origin manually }, { enableHighAccuracy: true, timeout: 10000 } ); }, [setOrigin, t]); // Handle click events on clusters and individual stops const onMapClick = (e: MapLayerMouseEvent) => { const features = e.features; if (!features || features.length === 0) { console.debug( "No features found on map click. Position:", e.lngLat, "Point:", e.point ); return; } const feature = features[0]; console.debug("Map click feature:", feature); const props: any = feature.properties; handlePointClick(feature); }; useEffect(() => { StopDataProvider.getStops().then((data) => { const features: GeoJsonFeature< Point, { stopId: string; name: string; lines: string[]; cancelled?: boolean; prefix: string; } >[] = data.map((s) => ({ type: "Feature", geometry: { type: "Point", coordinates: [s.longitude as number, s.latitude as number], }, properties: { stopId: s.stopId, name: s.name.original, lines: s.lines, cancelled: s.cancelled ?? false, prefix: s.stopId.startsWith("renfe:") ? "stop-renfe" : s.cancelled ? "stop-vitrasa-cancelled" : "stop-vitrasa", }, })); setStops(features); }); }, []); useEffect(() => { //const styleName = "carto"; const styleName = "openfreemap"; loadStyle(styleName, theme) .then((style) => setMapStyle(style)) .catch((error) => console.error("Failed to load map style:", error)); }, [theme]); useEffect(() => { const handleMapChange = () => { if (!mapRef.current) return; const map = mapRef.current.getMap(); if (!map) return; const center = map.getCenter(); const zoom = map.getZoom(); updateMapState([center.lat, center.lng], zoom); }; const handleStyleImageMissing = (e: any) => { // Suppress warnings for missing sprite images from base style // This prevents console noise from OpenFreeMap's missing icons if (!mapRef.current) return; const map = mapRef.current.getMap(); if (!map || map.hasImage(e.id)) return; // Log warning for our own icons if they are missing if (e.id.startsWith("stop-")) { console.warn(`Missing icon image: ${e.id}`); } // Add a transparent 1x1 placeholder to prevent repeated warnings map.addImage(e.id, { width: 1, height: 1, data: new Uint8Array(4), }); }; if (mapRef.current) { const map = mapRef.current.getMap(); if (map) { map.on("moveend", handleMapChange); map.on("styleimagemissing", handleStyleImageMissing); } } return () => { if (mapRef.current) { const map = mapRef.current.getMap(); if (map) { map.off("moveend", handleMapChange); map.off("styleimagemissing", handleStyleImageMissing); } } }; }, [mapRef.current]); const getLatitude = (center: any) => Array.isArray(center) ? center[0] : center.lat; const getLongitude = (center: any) => Array.isArray(center) ? center[1] : center.lng; const handlePointClick = (feature: any) => { const props: any = feature.properties; if (!props || !props.stopId) { console.warn("Invalid feature properties:", props); return; } const stopId = props.stopId; // fetch full stop to get lines array StopDataProvider.getStopById(stopId) .then((stop) => { if (!stop) { console.warn("Stop not found:", stopId); return; } setSelectedStop(stop); setIsSheetOpen(true); }) .catch((err) => { console.error("Error fetching stop details:", err); }); }; return (
searchRoute(o, d, time, arriveBy)} onNavigateToPlanner={() => navigate("/planner")} clearPickerOnOpen={true} showLastDestinationWhenCollapsed={false} /> {selectedStop && ( setIsSheetOpen(false)} stop={selectedStop} /> )}
); }