diff options
| -rw-r--r-- | package-lock.json | 15 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | src/AppContext.tsx | 117 | ||||
| -rw-r--r-- | src/controls/LocateControl.ts | 70 | ||||
| -rw-r--r-- | src/pages/Map.tsx | 34 |
5 files changed, 227 insertions, 11 deletions
diff --git a/package-lock.json b/package-lock.json index ed1d99b..8e41673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3028,6 +3028,21 @@ "node": ">=0.10.0" } }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index af8de10..ce118ae 100644 --- a/package.json +++ b/package.json @@ -44,4 +44,4 @@ "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "*" } -}
\ No newline at end of file +} diff --git a/src/AppContext.tsx b/src/AppContext.tsx index 74fb00c..4ff391b 100644 --- a/src/AppContext.tsx +++ b/src/AppContext.tsx @@ -1,9 +1,17 @@ /* eslint-disable react-refresh/only-export-components */ import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; +import { LatLngTuple } from 'leaflet'; type Theme = 'light' | 'dark'; type TableStyle = 'regular'|'grouped'; +interface MapState { + center: LatLngTuple; + zoom: number; + userLocation: LatLngTuple | null; + hasLocationPermission: boolean; +} + interface AppContextProps { theme: Theme; setTheme: React.Dispatch<React.SetStateAction<Theme>>; @@ -12,8 +20,19 @@ interface AppContextProps { tableStyle: TableStyle; setTableStyle: React.Dispatch<React.SetStateAction<TableStyle>>; toggleTableStyle: () => void; + + mapState: MapState; + setMapCenter: (center: LatLngTuple) => void; + setMapZoom: (zoom: number) => void; + setUserLocation: (location: LatLngTuple | null) => void; + setLocationPermission: (hasPermission: boolean) => void; + updateMapState: (center: LatLngTuple, zoom: number) => void; } +// Coordenadas por defecto centradas en Vigo +const DEFAULT_CENTER: LatLngTuple = [42.229188855975046, -8.72246955783102]; +const DEFAULT_ZOOM = 14; + const AppContext = createContext<AppContextProps | undefined>(undefined); export const AppProvider = ({ children }: { children: ReactNode }) => { @@ -55,8 +74,104 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { }, [tableStyle]); //#endregion + //#region Map State + const [mapState, setMapState] = useState<MapState>(() => { + const savedMapState = localStorage.getItem('mapState'); + if (savedMapState) { + try { + const parsed = JSON.parse(savedMapState); + return { + center: parsed.center || DEFAULT_CENTER, + zoom: parsed.zoom || DEFAULT_ZOOM, + userLocation: parsed.userLocation || null, + hasLocationPermission: parsed.hasLocationPermission || false + }; + } catch (e) { + console.error('Error parsing saved map state', e); + } + } + return { + center: DEFAULT_CENTER, + zoom: DEFAULT_ZOOM, + userLocation: null, + hasLocationPermission: false + }; + }); + + const setMapCenter = (center: LatLngTuple) => { + setMapState(prev => { + const newState = { ...prev, center }; + localStorage.setItem('mapState', JSON.stringify(newState)); + return newState; + }); + }; + + const setMapZoom = (zoom: number) => { + setMapState(prev => { + const newState = { ...prev, zoom }; + localStorage.setItem('mapState', JSON.stringify(newState)); + return newState; + }); + }; + + const setUserLocation = (userLocation: LatLngTuple | null) => { + setMapState(prev => { + const newState = { ...prev, userLocation }; + localStorage.setItem('mapState', JSON.stringify(newState)); + return newState; + }); + }; + + const setLocationPermission = (hasLocationPermission: boolean) => { + setMapState(prev => { + const newState = { ...prev, hasLocationPermission }; + localStorage.setItem('mapState', JSON.stringify(newState)); + return newState; + }); + }; + + const updateMapState = (center: LatLngTuple, zoom: number) => { + setMapState(prev => { + const newState = { ...prev, center, zoom }; + localStorage.setItem('mapState', JSON.stringify(newState)); + return newState; + }); + }; + //#endregion + + // Tratar de obtener la ubicación del usuario cuando se carga la aplicación si ya se había concedido permiso antes + useEffect(() => { + if (mapState.hasLocationPermission && !mapState.userLocation) { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + const { latitude, longitude } = position.coords; + setUserLocation([latitude, longitude]); + }, + (error) => { + console.error('Error getting location:', error); + setLocationPermission(false); + } + ); + } + } + }, []); + return ( - <AppContext.Provider value={{ theme, setTheme, toggleTheme, tableStyle, setTableStyle, toggleTableStyle }}> + <AppContext.Provider value={{ + theme, + setTheme, + toggleTheme, + tableStyle, + setTableStyle, + toggleTableStyle, + mapState, + setMapCenter, + setMapZoom, + setUserLocation, + setLocationPermission, + updateMapState + }}> {children} </AppContext.Provider> ); diff --git a/src/controls/LocateControl.ts b/src/controls/LocateControl.ts index 4ef8243..8153cf1 100644 --- a/src/controls/LocateControl.ts +++ b/src/controls/LocateControl.ts @@ -1,7 +1,71 @@ -import {createControlComponent} from '@react-leaflet/core'; -import {LocateControl as LeafletLocateControl} from 'leaflet.locatecontrol'; +import { createControlComponent } from '@react-leaflet/core'; +import { LocateControl as LeafletLocateControl, LocateOptions } from 'leaflet.locatecontrol'; import "leaflet.locatecontrol/dist/L.Control.Locate.min.css"; +import { useEffect } from 'react'; +import { useMap } from 'react-leaflet'; +import { useApp } from '../AppContext'; +interface EnhancedLocateControlProps { + options?: LocateOptions; +} + +// Componente que usa el contexto para manejar la localización +export const EnhancedLocateControl = (props: EnhancedLocateControlProps) => { + const map = useMap(); + const { mapState, setUserLocation, setLocationPermission } = useApp(); + + useEffect(() => { + // Configuración por defecto del control de localización + const defaultOptions: LocateOptions = { + position: 'topright', + strings: { + title: 'Mostrar mi ubicación', + }, + flyTo: true, + onLocationError: (err) => { + console.error('Error en la localización:', err); + setLocationPermission(false); + }, + onLocationFound: (e) => { + setUserLocation([e.latitude, e.longitude]); + setLocationPermission(true); + }, + returnToPrevBounds: true, + showPopup: false, + }; + + // Combinamos las opciones por defecto con las personalizadas + const options = { ...defaultOptions, ...props.options }; + + // Creamos la instancia del control + const locateControl = new LeafletLocateControl(options); + + // Añadimos el control al mapa + locateControl.addTo(map); + + // Si tenemos permiso de ubicación y ya conocemos la ubicación del usuario, + // podemos activarla automáticamente + if (mapState.hasLocationPermission && mapState.userLocation) { + // Esperamos a que el mapa esté listo + setTimeout(() => { + try { + locateControl.start(); + } catch (e) { + console.error('Error al iniciar la localización automática', e); + } + }, 1000); + } + + return () => { + // Limpieza al desmontar el componente + locateControl.remove(); + }; + }, [map, mapState.hasLocationPermission, mapState.userLocation, props.options, setLocationPermission, setUserLocation]); + + return null; +}; + +// Exportamos también el control base por compatibilidad export const LocateControl = createControlComponent( - (props) => new LeafletLocateControl(props) + (props) => new LeafletLocateControl(props) );
\ No newline at end of file diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx index e7a7b2f..9abb7a3 100644 --- a/src/pages/Map.tsx +++ b/src/pages/Map.tsx @@ -6,10 +6,11 @@ import 'react-leaflet-markercluster/styles' import { useEffect, useState } from 'react'; import LineIcon from '../components/LineIcon'; import { Link } from 'react-router'; -import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet"; +import { MapContainer, TileLayer, Marker, Popup, useMapEvents } from "react-leaflet"; import MarkerClusterGroup from "react-leaflet-markercluster"; import { Icon, LatLngTuple } from "leaflet"; -import { LocateControl } from "../controls/LocateControl"; +import { EnhancedLocateControl } from "../controls/LocateControl"; +import { useApp } from "../AppContext"; const icon = new Icon({ iconUrl: '/map-pin-icon.png', @@ -21,21 +22,43 @@ const icon = new Icon({ const sdp = new StopDataProvider(); +// Componente auxiliar para detectar cambios en el mapa +const MapEventHandler = () => { + const { updateMapState } = useApp(); + + const map = useMapEvents({ + moveend: () => { + const center = map.getCenter(); + const zoom = map.getZoom(); + updateMapState([center.lat, center.lng], zoom); + } + }); + + return null; +}; + +// Componente principal del mapa export function StopMap() { const [stops, setStops] = useState<Stop[]>([]); - const position: LatLngTuple = [42.229188855975046, -8.72246955783102] + const { mapState } = useApp(); useEffect(() => { sdp.getStops().then((stops) => { setStops(stops); }); }, []); return ( - <MapContainer center={position} zoom={14} scrollWheelZoom={true} style={{ height: '100%' }}> + <MapContainer + center={mapState.center} + zoom={mapState.zoom} + scrollWheelZoom={true} + style={{ height: '100%' }} + > <TileLayer attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a>, © <a href="https://carto.com/attributions">CARTO</a>' url="https://d.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png" /> - <LocateControl /> + <EnhancedLocateControl /> + <MapEventHandler /> <MarkerClusterGroup> {stops.map((stop) => ( <Marker key={stop.stopId} position={[stop.latitude, stop.longitude] as LatLngTuple} icon={icon}> @@ -49,7 +72,6 @@ export function StopMap() { </Marker> ))} </MarkerClusterGroup> - </MapContainer> ); } |
