codex/geospatial/haversine_distance.cat
# Distance géodésique : formule de Haversine
# Distance orthodromique entre deux points GPS (latitude, longitude)
# sur la sphère terrestre moyenne (R = 6371 km)
#
# Ref: https://en.wikipedia.org/wiki/Haversine_formula
#
# Exécuter:
#   catnip docs/codex/geospatial/haversine_distance.cat
#

math = import("math")

# Rayon moyen de la Terre (km)
R = 6371.0

# Haversine : distance entre deux points (lat, lon) en degrés
haversine = (p1, p2) => {
    dlat = math.radians(p2[0] - p1[0])
    dlon = math.radians(p2[1] - p1[1])
    a = math.sin(dlat / 2) ** 2 + math.cos(math.radians(p1[0])) * math.cos(math.radians(p2[0])) * math.sin(dlon / 2) ** 2
    2 * R * math.asin(math.sqrt(a))
}

# Coordonnées de villes (lat, lon)

paris     = list(48.8566,    2.3522)
london    = list(51.5074,   -0.1278)
new_york  = list(40.7128,  -74.0060)
tokyo     = list(35.6762,  139.6503)
sydney    = list(-33.8688, 151.2093)
cape_town = list(-33.9249,  18.4241)
rio       = list(-22.9068, -43.1729)
moscow    = list(55.7558,   37.6173)

# Distance directe entre paires

print("⇒ Distances depuis Paris")
print("  → Londres  :", round(haversine(paris, london), 1), "km")
print("  → New York :", round(haversine(paris, new_york), 1), "km")
print("  → Tokyo    :", round(haversine(paris, tokyo), 1), "km")
print("  → Sydney   :", round(haversine(paris, sydney), 1), "km")

# Broadcasting : distances d'un point de référence vers toutes les villes

cities = list(london, new_york, tokyo, sydney, cape_town, rio, moscow)
names  = list("Londres", "New York", "Tokyo", "Sydney", "Le Cap", "Rio", "Moscou")

distances = cities.[(city) => { round(haversine(paris, city), 1) }]

print("\n⇒ Broadcasting : distances depuis Paris")
for i in range(len(names)) {
    print("  ", names[i], ":", distances[i], "km")
}

# Plus proche et plus lointaine

closest  = min(distances)
farthest = max(distances)

print("\n⇒ Extrêmes")
print("  Plus proche :", names[distances.index(closest)], "(", closest, "km )")
print("  Plus loin   :", names[distances.index(farthest)], "(", farthest, "km )")

# Variante curried : haversine comme générateur de fonctions distance

dist_from = (ref) => {
    (point) => { round(haversine(ref, point), 1) }
}

print("\n⇒ Variante curried : distances depuis Tokyo")
from_tokyo = dist_from(tokyo)
tokyo_distances = cities.[(city) => { from_tokyo(city) }]

for i in range(len(names)) {
    print("  ", names[i], ":", tokyo_distances[i], "km")
}

# Matrice de distances

print("\n⇒ Matrice de distances (km)")

sample       = list(paris, london, new_york, tokyo, sydney)
sample_names = list("Paris", "Londres", "New York", "Tokyo", "Sydney")

matrix = sample.[(a) => { sample.[(b) => { round(haversine(a, b)) }] }]

for i in range(len(sample_names)) {
    row = sample_names[i]
    for j in range(len(sample_names)) {
        row = row + "\t" + str(matrix[i][j])
    }
    print(" ", row)
}

# Rayon de recherche : villes à moins de 5000 km de Paris

print("\n⇒ Villes à moins de 5000 km de Paris")
for i in range(len(names)) {
    if distances[i] < 5000 {
        print("  ", names[i], ":", distances[i], "km")
    }
}

# Périmètre d'un trajet

print("\n⇒ Périmètre Paris → Londres → New York → Rio → Le Cap → Tokyo → Paris")
trajet = list(paris, london, new_york, rio, cape_town, tokyo, paris)
etapes = list("Paris→Londres", "Londres→NY", "NY→Rio", "Rio→Le Cap", "Le Cap→Tokyo", "Tokyo→Paris")

total = 0
for i in range(len(etapes)) {
    d = round(haversine(trajet[i], trajet[i + 1]), 1)
    print("  ", etapes[i], ":", d, "km")
    total = total + d
}
print("  Total :", round(total, 1), "km")