16 KiB
16 KiB
Piano di Implementazione: Geolocalizzazione e Calcolo Bounds
Panoramica
Questo documento descrive il piano per implementare le seguenti funzionalità nell'Ingress Dashboard:
- Geolocalizzazione dell'utente al primo caricamento dell'applicazione
- Calcolo del center della viewport della mappa
- Calcolo del radius in metri in base allo zoom della mappa per coprire l'intera viewport
- Integrazione dei parametri center e radius nell'API
Architettura Proposta
Componenti Coinvolti
┌─────────────────────────────────────────────────────────────┐
│ App.tsx │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Stato: center, radius, plexts, loading, filters │ │
│ │ │ │
│ │ loadData() → fetchPlexts(center, radius, filters) │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ props: onCenterChange, onRadiusChange
▼
┌─────────────────────────────────────────────────────────────┐
│ PlayerMap.tsx │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ useMapGeolocation() │ │
│ │ - map.locate() al mount │ │
│ │ - onLocationFound → setView(lat, lng, zoom) │ │
│ │ │ │
│ │ useMapBounds() │ │
│ │ - onMoveEnd → calcola center e radius │ │
│ │ - onZoomEnd → calcola center e radius │ │
│ │ - invoca callbacks onCenterChange, onRadiusChange │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ params: center, radius, filters
▼
┌─────────────────────────────────────────────────────────────┐
│ api.ts │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ fetchPlexts(base_url, endpoint, creds, { │ │
│ │ center: [lat, lng], │ │
│ │ radius: meters, │ │
│ │ timestamp_from, timestamp_to, │ │
│ │ player_name, limit │ │
│ │ }) │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Dettagli di Implementazione
1. Aggiornamento dei Tipi (src/types.ts)
Aggiungere le interfacce per i nuovi parametri:
export interface MapCenter {
lat: number;
lng: number;
}
export interface MapBoundsParams {
center: [number, number]; // [lat, lng]
radius: number; // metri
}
2. Aggiornamento dell'API (src/api.ts)
Modificare la funzione fetchPlexts per accettare i nuovi parametri:
export async function fetchPlexts(
base_url: string,
endpoint: string,
creds: string,
params?: {
center?: [number, number]; // [lat, lng]
radius?: number; // metri
timestamp_from?: number;
timestamp_to?: number;
player_name?: string;
limit?: number;
}
): Promise<ApiResponse>
I parametri center e radius verranno aggiunti alla query string.
3. Hook Personalizzato: useMapGeolocation
Creare un nuovo file src/hooks/useMapGeolocation.ts:
import { useEffect } from 'react';
import { useMap } from 'react-leaflet';
interface UseMapGeolocationOptions {
enableHighAccuracy?: boolean;
timeout?: number;
maximumAge?: number;
zoom?: number;
}
export function useMapGeolocation(options: UseMapGeolocationOptions = {}) {
const map = useMap();
useEffect(() => {
map.locate({
setView: true,
maxZoom: options.zoom || 13,
enableHighAccuracy: options.enableHighAccuracy ?? true,
timeout: options.timeout || 10000,
maximumAge: options.maximumAge || 0,
});
const onLocationFound = (e: any) => {
console.log('Location found:', e.latlng);
};
const onLocationError = (e: any) => {
console.error('Location error:', e.message);
// Fallback to default location
map.setView([45.57, 12.36], 12);
};
map.on('locationfound', onLocationFound);
map.on('locationerror', onLocationError);
return () => {
map.off('locationfound', onLocationFound);
map.off('locationerror', onLocationError);
};
}, [map, options]);
}
4. Hook Personalizzato: useMapBounds
Creare un nuovo file src/hooks/useMapBounds.ts:
import { useEffect } from 'react';
import { useMap } from 'react-leaflet';
import L from 'leaflet';
interface UseMapBoundsOptions {
onCenterChange?: (center: [number, number]) => void;
onRadiusChange?: (radius: number) => void;
debounceMs?: number;
}
/**
* Calcola il raggio in metri dai bounds della mappa
* Il raggio è la distanza dal centro al bordo della viewport
*/
function calculateRadiusFromBounds(map: L.Map): number {
const bounds = map.getBounds();
const center = bounds.getCenter();
const northEast = bounds.getNorthEast();
// Calcola la distanza dal centro al bordo nord-est
const radius = center.distanceTo(northEast);
return Math.round(radius);
}
export function useMapBounds(options: UseMapBoundsOptions = {}) {
const map = useMap();
const { onCenterChange, onRadiusChange, debounceMs = 500 } = options;
useEffect(() => {
let timeoutId: NodeJS.Timeout | null = null;
const updateBounds = () => {
const center = map.getCenter();
const radius = calculateRadiusFromBounds(map);
if (onCenterChange) {
onCenterChange([center.lat, center.lng]);
}
if (onRadiusChange) {
onRadiusChange(radius);
}
};
const onMoveEnd = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(updateBounds, debounceMs);
};
const onZoomEnd = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(updateBounds, debounceMs);
};
// Calcola bounds iniziali
updateBounds();
map.on('moveend', onMoveEnd);
map.on('zoomend', onZoomEnd);
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
map.off('moveend', onMoveEnd);
map.off('zoomend', onZoomEnd);
};
}, [map, onCenterChange, onRadiusChange, debounceMs]);
}
5. Aggiornamento di PlayerMap.tsx
Aggiungere le nuove funzionalità:
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import MarkerClusterGroup from "react-leaflet-cluster";
import L from "leaflet";
import { useMapGeolocation } from "../hooks/useMapGeolocation";
import { useMapBounds } from "../hooks/useMapBounds";
interface PlayerMapProps {
plexts: Plext[];
onCenterChange?: (center: [number, number]) => void;
onRadiusChange?: (radius: number) => void;
}
// Componente interno per gli hooks
function MapContent({ onCenterChange, onRadiusChange }: {
onCenterChange?: (center: [number, number]) => void;
onRadiusChange?: (radius: number) => void;
}) {
useMapGeolocation({ zoom: 13 });
useMapBounds({ onCenterChange, onRadiusChange, debounceMs: 500 });
return null;
}
export function PlayerMap({ plexts, onCenterChange, onRadiusChange }: PlayerMapProps) {
// ... codice esistente per i marker ...
return (
<div className="map-container" style={{ height: "600px", width: "100%" }}>
<MapContainer
center={[45.57, 12.36]} // Default fallback
zoom={12}
style={{ height: "100%", width: "100%" }}
scrollWheelZoom={true}
>
<MapContent onCenterChange={onCenterChange} onRadiusChange={onRadiusChange} />
<TileLayer ... />
<MarkerClusterGroup ...>
{/* markers */}
</MarkerClusterGroup>
</MapContainer>
</div>
);
}
6. Aggiornamento di App.tsx
Aggiungere lo stato per center e radius e passarlo all'API:
export function App() {
const [data, setData] = useState<ApiResponse>({ count: 0, plexts: [] });
const [loading, setLoading] = useState(true);
const [timestampFrom, setTimestampFrom] = useState<Date | null>(null);
const [timestampTo, setTimestampTo] = useState<Date | null>(null);
const [playerName, setPlayerName] = useState<string>("all");
const [limit, setLimit] = useState<number>(100);
const [mapCenter, setMapCenter] = useState<[number, number]>([45.57, 12.36]);
const [mapRadius, setMapRadius] = useState<number>(10000); // Default 10km
const loadData = async () => {
try {
const params = {
center: mapCenter,
radius: mapRadius,
timestamp_from: timestampFrom ? timestampFrom.getTime() : undefined,
timestamp_to: timestampTo ? timestampTo.getTime() : undefined,
player_name: playerName !== "all" ? playerName : undefined,
limit,
};
const result = await fetchPlexts(
process.env.PUBLIC_API_BASE_URL || "",
process.env.PUBLIC_API_ENDPOINT || "",
process.env.PUBLIC_API_AUTH_CREDENTIALS || "",
params
);
setData(result);
} catch (error) {
console.error("Error loading data:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
const interval = setInterval(loadData, 60000);
return () => clearInterval(interval);
}, [timestampFrom, timestampTo, playerName, limit, mapCenter, mapRadius]);
// ... resto del codice ...
return (
// ...
<PlayerMap
plexts={data.plexts}
onCenterChange={setMapCenter}
onRadiusChange={setMapRadius}
/>
// ...
);
}
Diagramma di Flusso
flowchart TD
A[App Mount] --> B[PlayerMap Render]
B --> C[MapContainer Initialize]
C --> D[useMapGeolocation Hook]
D --> E{Geolocation Available?}
E -->|Yes| F[map.locate]
E -->|No| G[Use Default Center]
F --> H[locationfound Event]
H --> I[map.setView to User Location]
I --> J[useMapBounds Hook]
J --> K[Calculate Initial Center & Radius]
K --> L[onCenterChange Callback]
L --> M[App State Updated]
M --> N[loadData with center & radius]
N --> O[fetchPlexts API Call]
O --> P[Update Data State]
P --> Q[Re-render Map with New Data]
J --> R[User Moves/Zooms Map]
R --> S[moveend/zoomend Events]
S --> T[Debounced Update]
T --> U[Recalculate Center & Radius]
U --> L
Considerazioni Tecniche
Geolocalizzazione
- Permessi: L'utente deve concedere il permesso di geolocalizzazione. Se rifiuta, usare la posizione di default.
- Timeout: Impostare un timeout ragionevole (es. 10 secondi) per evitare attese infinite.
- Fallback: Se la geolocalizzazione fallisce, mantenere la posizione di default (Veneto region: [45.57, 12.36]).
- Privacy: Non memorizzare la posizione dell'utente in modo persistente.
Calcolo del Radius
- Formula: Il raggio viene calcolato come la distanza dal centro al bordo nord-est della viewport usando
center.distanceTo(northEast). - Unità: Il raggio è espresso in metri.
- Debouncing: Aggiungere un debouncing di 500ms per evitare chiamate API eccessive durante lo spostamento della mappa.
Performance
- Debouncing: Usare debouncing per gli eventi
moveendezoomendper ridurre le chiamate API. - Refresh Interval: Mantenere l'intervallo di refresh di 60 secondi per i dati.
- Lazy Loading: I dati vengono caricati solo quando necessario (al cambio di filtri o posizione).
API Integration
- Parametri: I parametri
centereradiusvengono aggiunti alla query string dell'API. - Formato:
centercome[lat, lng]eradiuscome numero intero in metri. - Compatibilità: Mantenere la compatibilità con i parametri esistenti.
Dipendenze
Nuove Dipendenze (se necessarie)
Non sono richieste nuove dipendenze. React-Leaflet e Leaflet forniscono già tutte le funzionalità necessarie.
Dipendenze Esistenti
react-leaflet: Per la mappa e gli hooksleaflet: Per le funzioni di calcolo della distanzareact: Per gli hooks e la gestione dello stato
Rischi e Mitigazioni
| Rischio | Mitigazione |
|---|---|
| Geolocalizzazione non disponibile | Fallback a posizione di default |
| Troppo chiamate API durante pan/zoom | Debouncing di 500ms |
| Calcolo radius impreciso | Usare distanceTo di Leaflet per precisione |
| Performance con molti marker | Clustering già implementato |
| Permessi geolocalizzazione negati | Gestire errore e usare default |
Criteri di Successo
- ✅ L'applicazione geolocalizza l'utente al primo caricamento
- ✅ Il center della mappa viene calcolato e passato all'API
- ✅ Il radius viene calcolato in metri e copre l'intera viewport
- ✅ I dati vengono aggiornati quando la mappa viene spostata/zoomata
- ✅ L'API riceve correttamente i parametri center e radius
- ✅ L'esperienza utente rimane fluida (nessun lag eccessivo)
- ✅ Il fallback funziona correttamente se la geolocalizzazione fallisce
Ordine di Esecuzione
- Aggiornare i tipi in
src/types.ts - Aggiornare l'API in
src/api.ts - Creare l'hook
useMapGeolocation.ts - Creare l'hook
useMapBounds.ts - Aggiornare
PlayerMap.tsx - Aggiornare
App.tsx - Testare l'integrazione completa