aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json15
-rw-r--r--package.json2
-rw-r--r--src/AppContext.tsx117
-rw-r--r--src/controls/LocateControl.ts70
-rw-r--r--src/pages/Map.tsx34
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='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a>, &copy; <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>
);
}