File size: 2,649 Bytes
639f871
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
"""
core/distance.py — Matrice delle distanze e tempi di percorrenza.
Supporta Haversine (offline) e profilo turista per la velocità.
"""
from __future__ import annotations
import math
from typing import Union, Optional, TYPE_CHECKING
from .models import PoI
from config import ROUTE_DETOUR_FACTOR

if TYPE_CHECKING:
    from .profile import TouristProfile


def haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
    """Distanza geodetica tra due coordinate in chilometri."""
    R = 6371.0
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    dphi       = math.radians(lat2 - lat1)
    dlambda    = math.radians(lon2 - lon1)
    a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2
    return R * 2 * math.asin(math.sqrt(a))


class DistanceMatrix:
    """
    Precalcola tutte le distanze tra i PoI (km).
    I TEMPI vengono calcolati on-the-fly tramite il TouristProfile,
    così un cambio di modalità non richiede di ricostruire la matrice.
    """

    def __init__(self, pois: list[PoI], profile: Optional["TouristProfile"] = None):
        self.pois    = pois
        self.profile = profile
        self.idx     = {poi.id: i for i, poi in enumerate(pois)}
        n            = len(pois)
        self._dist   = [[0.0] * n for _ in range(n)]  # km (invariante)

    def build(self):
        """Popola la matrice delle distanze. Chiama una volta sola."""
        for i, a in enumerate(self.pois):
            for j, b in enumerate(self.pois):
                if i == j:
                    continue
                km = haversine_km(a.lat, a.lon, b.lat, b.lon) * ROUTE_DETOUR_FACTOR
                self._dist[i][j] = km

    def dist(self, a: Union[PoI, str], b: Union[PoI, str]) -> float:
        """Distanza in km tra due PoI."""
        ia = self.idx[a.id if isinstance(a, PoI) else a]
        ib = self.idx[b.id if isinstance(b, PoI) else b]
        return self._dist[ia][ib]

    def time(self, a: Union[PoI, str], b: Union[PoI, str]) -> int:
        """Tempo di percorrenza in minuti, rispettando la modalità del profilo."""
        km = self.dist(a, b)
        return self._km_to_min(km)

    def time_from_coord(self, lat: float, lon: float, poi: PoI) -> int:
        """Tempo in minuti da coordinate arbitrarie (es. hotel) a un PoI."""
        km = haversine_km(lat, lon, poi.lat, poi.lon) * ROUTE_DETOUR_FACTOR
        return self._km_to_min(km)

    def _km_to_min(self, km: float) -> int:
        if self.profile is not None:
            return self.profile.travel_time_min(km)
        # Fallback sicuro: a piedi 4.5 km/h
        return max(1, int((km / 4.5) * 60))