Implémentation Python du moteur géophonique : transformation d'une latitude et d'une longitude en une signature prononçable, puis décodage inverse.
Geofonik transforme les coordonnées géographiques en un entier arbitrairement grand, puis encode cet entier en base 64. Chaque valeur de 0 à 63 est représentée par une syllabe composée d'une consonne et d'une voyelle.
"""
Geofonik
========
Algorithme de conversion géographique.
Une position latitude / longitude est transformée
en une signature composée de syllabes CV.
16 consonnes × 4 voyelles = 64 syllabes.
Chaque syllabe représente une valeur de 0 à 63.
Le moteur utilise un entier arbitrairement grand
(BigInt en JavaScript / int en Python).
La précision n est un paramètre du codage.
Les coordonnées sont tronquées à n décimales.
"""
# ---------------------------------------------------------
# ALPHABET
# ---------------------------------------------------------
CONSONNES = [
"B", "D", "F", "G",
"K", "L", "M", "N",
"P", "R", "S", "T",
"V", "H", "J", "Z"
]
VOYELLES = [
"A", "E", "I", "O"
]
ENC = {}
DEC = {}
_index = 0
for consonne in CONSONNES:
for voyelle in VOYELLES:
syllabe = consonne + voyelle
ENC[_index] = syllabe
DEC[syllabe] = _index
_index += 1
# ---------------------------------------------------------
# NOMBRE DE SYLLABES
# ---------------------------------------------------------
def nb_syllabes(n):
"""
Retourne le nombre de syllabes nécessaires
pour représenter l'espace géographique
à la précision n.
"""
facteur = 10 ** n
espace = (
(180 * facteur)
* (360 * facteur)
)
capacite = 1
syllabes = 0
while capacite < espace:
capacite *= 64
syllabes += 1
return syllabes
# ---------------------------------------------------------
# NORMALISATION
# ---------------------------------------------------------
def normaliser(texte, axe):
"""
Normalise une coordonnée avec éventuellement
une orientation N/S/E/O/W.
"""
texte = texte.strip().upper()
negatif = False
orientation = ""
dernier = texte[-1:] if texte else ""
if dernier in "NSEOW":
orientation = dernier
texte = texte[:-1].strip()
if axe == "lat":
if orientation in ("E", "O", "W"):
raise ValueError(
"Latitude : N ou S uniquement."
)
if orientation == "S":
negatif = True
if axe == "lon":
if orientation in ("N", "S"):
raise ValueError(
"Longitude : E, O ou W uniquement."
)
if orientation in ("O", "W"):
negatif = True
if texte.startswith("-"):
negatif = True
texte = texte[1:]
return ("-" if negatif else "") + texte
# ---------------------------------------------------------
# COORDONNEE → ENTIER
# ---------------------------------------------------------
def to_int(texte, offset, n):
"""
Transforme une coordonnée décimale en entier
dans le système décalé de Geofonik.
Les décimales sont tronquées à n positions.
"""
texte = texte.strip()
negatif = False
if texte.startswith("-"):
negatif = True
texte = texte[1:]
morceaux = texte.split(".")
entier = morceaux[0] or "0"
decimal = morceaux[1] if len(morceaux) > 1 else ""
if len(decimal) > n:
decimal = decimal[:n]
while len(decimal) < n:
decimal += "0"
valeur = int(entier + decimal)
offset_value = offset * (10 ** n)
if negatif:
return offset_value - valeur
return offset_value + valeur
# ---------------------------------------------------------
# ENTIER → COORDONNEE
# ---------------------------------------------------------
def from_int(valeur, offset, n):
"""
Opération inverse de to_int().
"""
offset_value = offset * (10 ** n)
negatif = valeur < offset_value
if negatif:
distance = offset_value - valeur
else:
distance = valeur - offset_value
texte = str(distance)
while len(texte) <= n:
texte = "0" + texte
entier = texte[:-n]
decimal = texte[-n:]
resultat = entier + "." + decimal
if negatif:
return "-" + resultat
return resultat
# ---------------------------------------------------------
# COORDONNEES → SIGNATURE
# ---------------------------------------------------------
def lat_lon_to_code(latitude, longitude, n=4):
"""
Encode une latitude et une longitude.
Paramètres
----------
latitude : float
Latitude comprise entre -90 et +90.
longitude : float
Longitude comprise entre -180 et +180.
n : int
Nombre de décimales conservées.
Retour
------
str
Signature Geofonik.
"""
if not -90 <= latitude <= 90:
raise ValueError(
"Latitude hors limites."
)
if not -180 <= longitude <= 180:
raise ValueError(
"Longitude hors limites."
)
lat_sign = "N" if latitude >= 0 else "S"
lon_sign = "E" if longitude >= 0 else "W"
lat_formatted = (
f"{abs(latitude)}{lat_sign}"
)
lon_formatted = (
f"{abs(longitude)}{lon_sign}"
)
lat_int = to_int(
normaliser(
lat_formatted,
"lat"
),
90,
n
)
lon_int = to_int(
normaliser(
lon_formatted,
"lon"
),
180,
n
)
largeur_longitude = (
360 * (10 ** n)
)
index = (
lat_int * largeur_longitude
+ lon_int
)
nombre = nb_syllabes(n)
valeurs = []
for _ in range(nombre):
valeurs.insert(
0,
index % 64
)
index //= 64
return "".join(
ENC[valeur]
for valeur in valeurs
)
# ---------------------------------------------------------
# SIGNATURE → COORDONNEES
# ---------------------------------------------------------
def code_to_lat_lon(code, n=4):
"""
Décode une signature Geofonik.
Retourne un dictionnaire contenant :
lat
lon
lat_str
lon_str
cell_step
"""
code = (
code.strip()
.upper()
.replace(" ", "")
.replace("-", "")
)
nombre = nb_syllabes(n)
longueur_attendue = nombre * 2
if len(code) != longueur_attendue:
raise ValueError(
f"Code invalide : "
f"{longueur_attendue} caractères "
f"attendus pour n={n}."
)
index = 0
for position in range(0, len(code), 2):
syllabe = code[
position:position + 2
]
if syllabe not in DEC:
raise ValueError(
f"Syllabe inconnue : {syllabe}"
)
valeur = DEC[syllabe]
index = (
index * 64
+ valeur
)
largeur_longitude = (
360 * (10 ** n)
)
lat_int = (
index // largeur_longitude
)
lon_int = (
index % largeur_longitude
)
lat_str = from_int(
lat_int,
90,
n
)
lon_str = from_int(
lon_int,
180,
n
)
lat = float(lat_str)
lon = float(lon_str)
demi_cellule = (
0.5 / (10 ** n)
)
return {
"lat": lat + demi_cellule,
"lon": lon + demi_cellule,
"lat_str": lat_str,
"lon_str": lon_str,
"cell_step": 1 / (10 ** n)
}
# ---------------------------------------------------------
# EXEMPLE
# ---------------------------------------------------------
if __name__ == "__main__":
latitude = 43.730879
longitude = 3.314497
precision = 4
code = lat_lon_to_code(
latitude,
longitude,
precision
)
print("Signature :", code)
position = code_to_lat_lon(
code,
precision
)
print(
"Latitude :",
position["lat"]
)
print(
"Longitude:",
position["lon"]
)
int de Python
permet de travailler avec des entiers de taille
arbitraire, ce qui correspond au rôle du
BigInt dans la version JavaScript.