Geofonik · source

Python

Implémentation Python du moteur géophonique : transformation d'une latitude et d'une longitude en une signature prononçable, puis décodage inverse.

Principe

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.

01
GPS
02
Entier
03
Base 64
04
CV
05
GPS

Interface Python

lat_lon_to_code() Coordonnées → signature Geofonik
code_to_lat_lon() Signature → coordonnées
nb_syllabes() Nombre de syllabes nécessaire
precision n Nombre de décimales conservées

geofonik.py

geofonik.py
"""
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"]
    )
Le module ne nécessite aucune bibliothèque externe : Python 3 suffit. Le type int de Python permet de travailler avec des entiers de taille arbitraire, ce qui correspond au rôle du BigInt dans la version JavaScript.