Files
Ingress-Dashoboard/plans/geolocation-and-bounds-implementation.md
2026-01-18 12:06:11 +01:00

16 KiB

Piano di Implementazione: Geolocalizzazione e Calcolo Bounds

Panoramica

Questo documento descrive il piano per implementare le seguenti funzionalità nell'Ingress Dashboard:

  1. Geolocalizzazione dell'utente al primo caricamento dell'applicazione
  2. Calcolo del center della viewport della mappa
  3. Calcolo del radius in metri in base allo zoom della mappa per coprire l'intera viewport
  4. 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

  1. Permessi: L'utente deve concedere il permesso di geolocalizzazione. Se rifiuta, usare la posizione di default.
  2. Timeout: Impostare un timeout ragionevole (es. 10 secondi) per evitare attese infinite.
  3. Fallback: Se la geolocalizzazione fallisce, mantenere la posizione di default (Veneto region: [45.57, 12.36]).
  4. Privacy: Non memorizzare la posizione dell'utente in modo persistente.

Calcolo del Radius

  1. Formula: Il raggio viene calcolato come la distanza dal centro al bordo nord-est della viewport usando center.distanceTo(northEast).
  2. Unità: Il raggio è espresso in metri.
  3. Debouncing: Aggiungere un debouncing di 500ms per evitare chiamate API eccessive durante lo spostamento della mappa.

Performance

  1. Debouncing: Usare debouncing per gli eventi moveend e zoomend per ridurre le chiamate API.
  2. Refresh Interval: Mantenere l'intervallo di refresh di 60 secondi per i dati.
  3. Lazy Loading: I dati vengono caricati solo quando necessario (al cambio di filtri o posizione).

API Integration

  1. Parametri: I parametri center e radius vengono aggiunti alla query string dell'API.
  2. Formato: center come [lat, lng] e radius come numero intero in metri.
  3. 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 hooks
  • leaflet: Per le funzioni di calcolo della distanza
  • react: 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

  1. L'applicazione geolocalizza l'utente al primo caricamento
  2. Il center della mappa viene calcolato e passato all'API
  3. Il radius viene calcolato in metri e copre l'intera viewport
  4. I dati vengono aggiornati quando la mappa viene spostata/zoomata
  5. L'API riceve correttamente i parametri center e radius
  6. L'esperienza utente rimane fluida (nessun lag eccessivo)
  7. Il fallback funziona correttamente se la geolocalizzazione fallisce

Ordine di Esecuzione

  1. Aggiornare i tipi in src/types.ts
  2. Aggiornare l'API in src/api.ts
  3. Creare l'hook useMapGeolocation.ts
  4. Creare l'hook useMapBounds.ts
  5. Aggiornare PlayerMap.tsx
  6. Aggiornare App.tsx
  7. Testare l'integrazione completa