lucidiot's cybrecluster

Well-Known Binary ISO avec Kaitai Struct, partie 2

Lucidiot Informatique 2022-02-15
Cet article va être littéralement tordu.


Nous revoilà à traiter la variante incroyablement complexe d'un format de fichier binaire pour des données géospatiales. On va aujourd'hui sortir complètement de tout ce qui a pu être implémenté par la moindre base de données : alors que les types présentés dans la partie 1 existent, parfois seulement partiellement, dans certaines bases de données, certains des types que nous allons voir n'existent tout simplement pas et il n'y a pas d'alternatives disponibles pour eux. On rentre dans une zone où l'ISO a voulu tout standardiser avant que ça n'existe, et avant même que qui que ce soit n'en aie véritablement besoin.

Nous allons définir 9 nouveaux types de géométries qui vont nous permettre de décrire des courbes de diverses manières, de la plus simple à la plus compliquée. Il y a beaucoup de maths impliqués là-dedans, et j'ai fait de mon mieux pour essayer d'expliquer ces courbes alors que moi-même je n'ai pas les compétences ni le courage pour lire les équations et pour y comprendre quelque chose. Décoder les centaines de pages de définitions de l'ISO est déjà bien assez pour mon cerveau.

Chaîne d'arcs de cercles

La chaîne d'arcs de cercles, ou CircularString, est un type de courbe défini nulle part ailleurs que dans cette spécification. Elle est assez proche de la ligne brisée que nous avons maintenant vue assez fréquemment, mais avec une interpolation différente. L'interpolation, c'est la façon dont on va relier les points entre eux pour former la ligne, ou ici les arcs de cercle. La ligne brisée utilise une interpolation linéaire, c'est-à-dire qu'on trace des lignes entre chaque point, mais avec la chaîne d'arcs de cercles, on fait appel à l'interpolation circulaire.

Une chaîne d'arcs de cercles doit avoir un nombre impair de points, et avoir au moins trois points. Si on n'a que trois points, il n'y aura qu'un seul segment d'arc de cercle. S'il y en a cinq, il y aura deux arcs, et avec sept points il y aura trois arcs, et ainsi de suite. Chaque arc de cercle sera défini par trois points : un point de départ, un point central, et un point d'arrivée. L'arc part du point de départ et s'arrête au point d'arrivée, et passe par le point central. Puisque c'est toujours un arc de cercle, c'est tout ce qu'il faut pour pouvoir calculer l'arc complet.

Cette image honteusement volée de la documentation de Microsoft SQL Server donne quelques exemples :

Exemples de chaînes d'arcs de cercles
Exemples de chaînes d'arcs de cercles

Dans le tout premier dessin, on voit d'abord deux lignes droites, formées par des « arcs de cercle » du point 1 au point 3 en passant par le point 2, et du point 3 au point 5 en passant par le point 4. Vu que tous ces points sont sur une même ligne, ou colinéaires, on trace une ligne droite. Mais pour les points 5, 6 et 7, on a un arc de cercle, qui part du point 5, passe par le point 6, et s'arrête au point 7.

Plus concrètement pour nous, la chaîne d'arc de cercles n'est rien d'autre qu'une série de points, exactement comme la ligne brisée. La seule véritable différence est que son identifiant de type n'est plus 2, qui correspond à la ligne brisée, mais 8. Pour des raisons historiques, le numéro 1000001 est également associé à la chaîne d'arcs de cercle, mais son usage est déprécié. On va prendre en charge les deux numéros dans notre énumération :

enums: {
  geometry_type: {
    # ...
    "8": "circular_string",
    "1000001": "circular_string"
  }
}

On peut ensuite réutiliser directement notre type line_string existant, en l'associant au circular_string de l'énumération.

{
  switch-on: "type",
  cases: {
    # ...
    "geometry_type::circular_string": "line_string"
  }
}

Pour la même raison que le triangle dans la partie précédente, on ne pourra pas faire de validations particulières sur le nombre de points pour vérifier qu'il est impair et supérieur à trois.

Cercle

Le cercle est un autre type de courbe. Il est défini par trois points, et fonctionne par interpolation circulaire comme la chaîne d'arcs de cercles. Cependant, au lieu de terminer la course de l'arc de cercle à son point d'arrivée, on continue l'arc de cercle jusqu'à atteindre le point de départ, ce qui donne un cercle complet.

On pourrait utiliser une chaîne d'arcs de cercles à 5 points pour décrire un cercle : le premier et le dernier point seraient les mêmes, et on vérifiera juste que les deux points centraux se trouvent sur le même cercle pour obtenir un cercle complet. Mais avec un cercle explicite, on n'a qu'à définir les trois points sans se soucier du reste.

Le cercle est identifié par le type 18 et utilise la même structure binaire que la ligne brisée et la chaîne d'arcs de cercles, donc on va encore une fois pouvoir réutiliser notre type line_string. On ne va pas pouvoir ici non plus contrôler qu'il y a exactement trois points, donc on se contentera de ça.

enums: {
  geometry_type: {
    # ...
    "18": "circle"
  }
}
# ...
types: {
  geometry: {
    seq: ({
      switch-on: "type",
      cases: {
        # ...
        "geometry_type::circle": "line_string"
      }
    } | wkb_seq)
  }
}

Chaîne de géodésiques

Une géodésique est une courbe plutôt étrange à décrire. Techniquement, c'est une ligne droite… sauf que la surface sur laquelle la ligne droite se trouve n'est pas plate. Dans le contexte des systèmes d'information géographiques, la différence entre la ligne droite et la géodésique est que la ligne droite est toujours strictement droite, donc qu'elle pourrait passer à travers la planète Terre par exemple, alors que la géodésique va suivre la courbure de la Terre.

Pour être exact, elle va utiliser une interpolation géodésique, et va donc suivre la forme de la planète décrite par le système de référence spatial. Si vous utilisez la projection que tout le monde connaît, WGS84 (sous l'identifiant 4326), alors vous suivrez une forme ellipsoïde. Si vous utilisez Web Mercator (identifiant 3857), la projection des cartes Google Maps ou OpenStreetMap, alors ce sera une sphère. Si vous n'utilisez aucune projection (identifiant 0), alors… il ne se passera rien, et vous aurez une ligne droite tout à fait classique.

Pour rajouter encore plus de confusion, il est intéressant de savoir que dans les implémentations concrètes des GIS, la ligne droite est déjà utilisable comme une géodésique ; elle ne traverse par la Terre par défaut. Si vous définissez sur une ligne brisée un système de référence spatial, des fonctions de calcul de longueur comme ST_Length vont en tenir compte : l'unité de mesure peut changer, passant par exemple aux pieds si vous utilisez l'identifiant 2249 (Massachusetts State Plane Feet), et la courbure éventuelle définie par le système de référence influencera la distance affichée. Il faut revenir à l'identifiant 0 (pas de système de référence) pour avoir des calculs géométriques classiques avec des lignes droites.

Cela explique pourquoi on ne rencontre tout simplement pas la géodésique dans les véritables bases de données ; elle est déjà là, et c'est beaucoup plus logique comme ça. Mais nous allons quand même prendre en charge la chaîne de géodésiques définie par ISO, qui est encore une autre ligne brisée mais avec une interpolation explicitement géodésique. Il n'y a pas la moindre contrainte supplémentaire, donc on peut sans remords utiliser encore une fois le line_string :

enums: {
  geometry_type: {
    # ...
    "19": "geodesic_string"
  }
}
# ...
types: {
  geometry: {
    seq: ({
      switch-on: "type",
      cases: {
        # ...
        "geometry_type::geodesic_string": "line_string"
      }
    } | wkb_seq)
  }
}

Vecteur

Pour pouvoir continuer dans l'aventure des courbes, il va nous falloir introduire deux types qualifiés de « types de support » : ils sont là pour permettre la prise en charge d'autres types de géométries, mais ne sont pas vraiment utiles tout seuls. Cela n'empêche certainement pas le standard ISO de s'amuser à permettre leur sérialisation de manière complètement isolée pour rajouter encore plus de complexité.

Le vecteur est quelque chose d'assez simple : c'est exactement comme un point, avec une simple différence sémantique. Au lieu d'être une position absolue, il représente une différence de position. Il a donc comme le point deux coordonnées, et cela veut donc dire qu'on peut encore une fois réutiliser un type existant.

enums: {
  geometry_type: {
    # ...
    "101": "vector"
  }
}
# ...
types: {
  geometry: {
    seq: ({
      switch-on: "type",
      cases: {
        # ...
        "geometry_type::vector": "point"
      }
    } | wkb_seq)
  }
}

Placement affine

Le vecteur seul n'est pas vraiment très utile ; c'est un peu comme un type de support d'un type de support. Le type véritablement intéressant est appelé le placement affine. Un placement affine définit en fait un nouveau repère géométrique : un point classique, appelé l'emplacement, définit le point d'origine, puis des vecteurs, appelés des directions de référence, définissent une unité dans chaque dimension du repère.

01 66 00 00 00 Point WKB 02 00 00 00 Vecteur WKB 1 Vecteur WKB 2 Boutisme Type Emplacement Vecteurs Nombre de vecteurs

Si on travaille dans un repère à deux dimensions, il doit y avoir deux vecteurs à deux dimensions, et dans un repère à trois dimensions, trois vecteurs à trois dimensions. Le premier vecteur définira comment traduire un X=1 en des coordonnées dans le véritable système de référence spatial, le second un Y=1, et l'éventuel troisième un Z=1. Cette définition étrange de repères facilite la construction de courbes qu'on verra par la suite ; en travaillant dans un repère mathématique plus classique, dans de la géométrie pure, on ne se soucie plus des problèmes liés aux projections géographiques.

Grâce à notre travail précédent, on peut construire immédiatement ce nouveau type :

enums: {
  geometry_type: {
    # ...
    "102": "affine_placement"
  }
}
# ...
types: {
  geometry: {
    seq: ({
      switch-on: "type",
      cases: {
        # ...
        "geometry_type::affine_placement": "affine_placement"
      }
    } | wkb_seq),
    types: {
      affine_placement: {
        seq: (
          [
            {
              id: "location",
              type: "point"
            }
          ] + ({ id: "reference_directions", type: "point" } | counted)
        )
      }
    }
  }
}

Courbe elliptique

On commence à s'approcher des courbes pures et dures, celles qu'on rencontre un peu plus souvent en dehors du contexte géospatial. La courbe elliptique pourrait aussi être appelée « arc d'ellipse » : on va définir une ellipse, puis définir deux angles à partir desquels on commence et termine l'arc d'ellipse.

Une ellipse se définit avec trois attributs : un emplacement de référence — autre nom pour le placement affine — et la longueur des deux axes qui la composent. Dans une ellipse, l'axe majeur est le segment qui part du centre de l'ellipse et se dirige vers le point le plus éloigné du centre (l'apogée, si on parlait d'une orbite), et l'axe mineur est perpendiculaire, dirigé vers le point le plus proche (le périgée). Dans le contexte du standard ISO, on se fiche de quel axe est le plus grand ; on peut les mettre dans l'ordre qu'on veut, les deux axes sont juste appelés U et V. Il ne manque ensuite plus que les deux angles pour découper la courbe.

01 14 00 00 00 Placement affine WKB Boutisme Type Emplacement de 00 00 00 00 00 00 F0 3F 00 00 00 00 00 00 F0 3F Longueurs des axes 00 00 00 00 00 00 F0 3F 00 00 00 00 00 00 F0 3F Angles rence

Notons que le placement affine utilisé ici sera à nouveau précédé par un boutisme et un type. Nous allons donc définir un nouveau type pour ce placement affine avec son en-tête :

types: {
  wkb_affine_placement: ({ seq: ("affine_placement" | wkb_seq) } | endianize)
}

On réutilise wkb_seq qui génère déjà une séquence contenant un type de géométrie, puis endianize pour ajouter le boutisme. On peut maintenant utiliser ça dans un type relativement simple pour la courbe elliptique :

enums: {
  geometry_type: {
    # ...
    "20": "elliptical_curve"
  }
}
# ...
types: {
  geometry: {
    seq: ({
      switch-on: "type",
      cases: {
        # ...
        "geometry_type::elliptical_curve": "elliptical_curve"
      }
    } | wkb_seq),
    types: {
      elliptical_curve: {
        seq: [
          {
            id: "reference_location",
            type: "wkb_affine_placement"
          },
          {
            id: "u_axis_length",
            type: "f8"
          },
          {
            id: "v_axis_length",
            type: "f8"
          },
          {
            id: "start_angle",
            type: "f8"
          },
          {
            id: "end_angle",
            type: "f8"
          }
        ]
      }
    }
  }
}

Courbe NURBS

Les B-splines rationnelles non-uniformes, abrégées en anglais NURBS, sont une généralisation des courbes de Bézier et des courbes B-splines. Pour comprendre un peu mieux ce qu'elles sont, le plus simple est un petit peu d'histoire.

Bien avant l'arrivée du dessin assisté par ordinateur (CAD), il était possible de dessiner des cercles, ellipses, etc. à l'aide de compas, de rapporteurs, ou de règles. Mais pour le dessin de courbes complexes, il n'y avait pas grand chose de plus que la main levée, ce qui n'était pas vraiment acceptable pour certaines constructions telles que la proue d'un bateau, qui doit avoir une forme aérodynamique et hydrodynamique. Les constructeurs de navires avaient donc recours à des équivalents à échelle réelle de chaque courbe, créés à l'aide d'une longue bande de bois souple. On place des points, appelés points de contrôle, et on fait passer le bois par tous ces points. Il va se tordre pour atteindre une forme qui l'évitera de se plier tout en suivant tous les points, donc on obtient une courbe arbitraire. Ces morceaux de bois étaient appelés des splines.

Il existe plusieurs modélisations mathématiques des courbes générées ainsi par des splines, et NURBS est une généralisation de ces modélisations avec la particularité qu'on peut aussi assigner un vecteur à chaque point, appelé vecteur nodal ou en anglais knot vector. Un vecteur nodal change la façon dont un point de contrôle particulier va influencer la courbe. Si vous avez déjà joué avec des courbes dans des outils de dessin vectoriel ou de CAD, avec des « poignées » à tirer qui influencent sur la courbe à chacun de ses points, alors vous avez déjà joué avec des vecteurs nodaux. On peut aussi assigner un poids à chaque point, qui modifiera à quel point le point influence la forme de la courbe.

Dans le standard ISO, une courbe NURBS se définit par un degré, qui indique le degré des polynômes utilisés dans la courbe, puis par une liste de points de contrôle, et une liste de nœuds.

01 15 00 00 00 01 02 00 00 00 Point NURBS 1 Point NURBS 2 Boutisme Type Degré Points de Nombre de points de 04 00 00 00 1 2 3 4 Nombre de contrô le contrô le ud ud ud ud uds uds

Points de contrôle

La définition des points de contrôle NURBS est assez étrange dans le WKB, puisqu'il y a un bit complètement inutile inséré entre le point et le poids associé au point. Ce n'est même pas un octet (byte), c'est vraiment un seul bit. J'ai cherché assez longtemps dans les centaines de pages du standard et je n'ai pas pu trouver une seule mention de ce bit, ou d'un booléen quelconque qui aurait une signification pour le point NURBS.

01 Point WKB 1 00 00 00 00 00 00 F0 3F Boutisme Point Un bit Poids

Dans Kaitai Struct, il est techniquement possible de traiter des données plus petites qu'un octet en utilisant des types comme b1 pour un seul bit. Cependant, la plupart des langages de programmation ne travaillent qu'au niveau de l'octet, donc si on veut lire un seul bit, on doit forcément lire un octet entier. Kaitai n'est pas capable de gérer tout le décalage qu'un seul bit pourrait causer sur tous les autres éléments d'un fichier, donc il n'est pas capable de prendre en charge un bit seul. On peut découper un octet en plusieurs bits, par exemple s'il y a plusieurs champs dans un seul octet, mais on ne peut pas lire rien qu'un seul bit et passer à la suite sans perdre 7 autres bits.

Je vais donc ignorer complètement ce bit embêtant. Il n'y a de toute façon pas d'implémentation existante sur laquelle je pourrais me baser pour voir concrètement ce à quoi ressemblerait un point de contrôle…

types: {
  nurbs_point: ({
    seq: [
      {
        id: "point",
        type: "wkb_point"
      },
      {
        id: "weight",
        type: "f8"
      }
    ]
  } | endianize)
}

Nœuds

Je parlais tout à l'heure de vecteurs nodaux, mais la notion de vecteur ici est plutôt étrange et c'est en fait un nombre entre 0 et 1. Le nombre de nœuds est toujours égal à nombre de points de contrôle + degré + 1, donc dans mon schéma, avec un degré 1 et 2 points de contrôle, on a 4 nœuds. Cependant, il est aussi possible d'optimiser un peu les nœuds et d'utiliser la notion de multiplicité d'un nœud pour répéter plusieurs fois le même vecteur nodal : si la multiplicité est de 2 par exemple, un seul nœud s'appliquera à deux points de contrôle en même temps. On a donc en plus de la valeur du vecteur nodal une multiplicité sous la forme d'un seul octet non signé.

01 00 00 00 00 00 00 F0 3F 01 Boutisme Valeur Multiplicité

On peut traduire ça assez facilement avec u1 comme on l'a fait pour le boutisme :

types: {
  knot: ({
    seq: [
      {
        id: "value",
        type: "f8"
      },
      {
        id: "multiplicity",
        type: "u1"
      }
    ]
  } | endianize)
}

Définition de la courbe NURBS

Maintenant qu'on a défini séparément les deux sous-parties de la courbe NURBS, on peut en ajouter la définition ; on va utiliser, comme pour le TIN qu'on a vu dans la partie précédente, une somme de tableaux pour concaténer le degré avec les deux listes.

enums: {
  geometry_type: {
    # ...
    "21": "nurbs_curve"
  }
}
# ...
types: {
  geometry: {
    seq: ({
      switch-on: "type",
      cases: {
        # ...
        "geometry_type::nurbs_curve": "nurbs_curve"
      }
    } | wkb_seq),
    types: {
      nurbs_curve: {
        seq: (
          [
            {
              id: "degree",
              type: "u1"
            }
          ]
          + ({ id: "control_points", type: "nurbs_point" } | counted("control_point_"))
          + ({ id: "knots", type: "knot" } | counted("knot_"))
        )
      }
    }
  }
}

Clothoïde

La clothoïde est un autre type de courbe qui a d'autres noms dus aux plusieurs mathématiciens qui l'ont étudié à plusieurs reprises sans savoir qu'ils redécouvraient une courbe déjà établie ; on l'appelle donc la spirale d'Euler, la spirale de Fresnel, la spirale de Cornu, ou encore la spirale de Talbot. La clothoïde se constitue de deux spirales (une dans le sens négatif, une dans le sens positif), et peut se concevoir concrètement comme un virage qui devient de plus en plus serré, de façon linéaire, sans jamais s'arrêter de se serrer.

On peut vraiment utiliser l'analogie d'un virage car la clothoïde est utilisée notamment dans le contexte de raccordements routiers ou ferroviaires, puisqu'elle permet de faire la transition entre une droite et un arc de cercle en minimisant la force centrifuge. On l'utilise aussi en typographie, car elle peut être définie de façon plus légère que les courbes de Bézier avec moins de points de contrôle et présente d'autres propriétés utiles en cas de redimensionnement.

Puisque plusieurs mathématiciens se sont penchés dessus sur une période de 200 ans, il y a plusieurs définitions de la courbe qu'on peut utiliser. Le standard ISO utilise la définition à l'aide de l'intégrale de Fresnel, définie dans le domaine des nombres complexes.

01 16 00 00 00 Placement affine WKB 00 00 00 00 00 00 F0 3F Boutisme Type Emplacement de Facteur 00 00 00 00 00 00 F0 3F 00 00 00 00 00 00 F0 3F Distance de Distance de fin rence d'é chelle but

On retrouve à nouveau le placement affine, pour éviter que la courbe ne soit déformée trop tôt par la projection spatiale utilisée. Le facteur d'échelle ne semble pas être clairement défini, mais je suppose que c'est utilisé pour agrandir ou réduire la courbe, mais c'est déjà possible sans ce facteur en modifiant les vecteurs dans le placement affine. La distance de début et de fin définissent l'intervalle t utilisé dans la fonction p(t) de l'intégrale de Fresnel ; c'est ce qui définit quelle partie de la spirale complète on veut utiliser pour définir notre courbe. On peut ainsi utiliser juste la section de la spirale qui permet de faire la transition entre un rail droit et un rail en arc de cercle par exemple.

On peut représenter ce type assez facilement sans autant d'appels de fonctions que précédemment :

enums: {
  geometry_type: {
    # ...
    "22": "clothoid"
  }
}
# ...
types: {
  geometry: {
    seq: ({
      switch-on: "type",
      cases: {
        # ...
        "geometry_type::clothoid": "clothoid"
      }
    } | wkb_seq),
    types: {
      clothoid: {
        seq: [
          {
            id: "reference_location",
            type: "wkb_affine_placement"
          },
          {
            id: "scale_factor",
            type: "f8"
          },
          {
            id: "start_distance",
            type: "f8"
          },
          {
            id: "end_distance",
            type: "f8"
          }
        ]
      }
    }
  }
}

Courbe en spirale

La courbe en spirale est une version plus générale de la clothoïde ; elle permet de définir des courbes à partir plusieurs types de spirales, dont la clothoïde, et rend donc le type qu'on vient à peine de voir complètement inutile. Le seul argument auquel je peux penser qui puisse justifier ce genre de duplication serait une raison historique, une rétrocompatibilité avec d'anciennes versions du standard. Je dirais bien une compatibilité avec des implémentations existantes qui auraient fourni la clothoïde comme extension non-standard, mais comme je ne trouve aucune implémentation qui prenne en charge la clothoïde et qu'on a déjà pu observer que l'ISO se fiche pas mal des implémentations et part tout seul dans ses délires avec ce standard, ça n'est certainement pas le cas.

Nous perdons cette fois le facteur d'échelle, et à la place on a un nouvel attribut de longueur de courbe, et une chaîne de caractères pour le type de spirale à utiliser. Le type de spirale se limite à clothoid, bloss, biquadratic, sine et cosine, mais la spécification n'indique pas aux implémentations d'appliquer une véritable restriction sur ce type, donc on s'en fiche.

01 17 00 00 00 Placement affine WKB 00 00 00 00 00 00 F0 3F Boutisme Type Emplacement de Longueur de courbe 00 00 00 00 00 00 F0 3F 00 00 00 00 00 00 F0 3F VARCHAR 64 Distance de Distance de fin Type de spirale rence but

La chaîne de caractères du type de spirale se structure comme celles qu'on avait vu dans l'élément TIN, avec un octet pour la longueur suivi du texte dans un encodage non spécifié. On peut donc réutiliser le type wkb_string qu'on avait défini là bas :

enums: {
  geometry_type: {
    # ...
    "23": "spiral_curve"
  }
}
# ...
types: {
  geometry: {
    seq: ({
      switch-on: "type",
      cases: {
        # ...
        "geometry_type::spiral_curve": "spiral_curve"
      }
    } | wkb_seq),
    types: {
      spiral_curve: {
        seq: [
          {
            id: "reference_location",
            type: "wkb_affine_placement"
          },
          {
            id: "length",
            type: "f8"
          },
          {
            id: "start_distance",
            type: "f8"
          },
          {
            id: "end_distance",
            type: "f8"
          },
          {
            id: "spiral_type",
            type: "wkb_string"
          }
        ]
      }
    }
  }
}

Script actuel

Script jq générant un schéma Kaitai Struct pour le WKB ISO, partie 2

#!/usr/bin/jq
# Generates a Kaitai Struct-compatible JSON structure to parse
# the ISO Well-Known Binary.
# Usage: oq -n -o simpleyaml -f wkb_iso.jq  wkb_iso.ksy

def endianize: {
  seq: [
    {
      id: "endianness",
      type: "u1"
    },
    {
      id: "body",
      type: "body"
    }
  ],
  types: {
    "body": (. + {
      meta: {
        endian: {
          "switch-on": "_parent.endianness",
          cases: {
            "0": "be",
            "1": "le"
          }
        }
      }
    })
  }
};

def counted(prefix): [
  {
    id: "\(prefix)count",
    type: "u4"
  },
  . + {
    repeat: "expr",
    "repeat-expr": "\(prefix)count"
  }
];

def counted: counted("");

def wkb_seq: [
  {
    id: "type",
    type: "u4",
    enum: "geometry_type"
  },
  {
    id: "data",
    type: .
  }
];

{
  meta: {
    id: "wkb_iso",
    title: "ISO Well-Known Binary",
    license: "AGPL-3.0-or-later",
    xref: {
      iso: "19125-3:2015"
    }
  },
  "doc-ref": [
    "https://www.ogc.org/standards/sfa",
    "http://libgeos.org/specifications/wkb/"
  ],
  seq: [
    {
      id: "geometry",
      type: "geometry"
    }
  ],
  enums: {
    geometry_type: {
      "0": "geometry",
      "1": "point",
      "2": "line_string",
      "3": "polygon",
      "4": "multi_point",
      "5": "multi_line_string",
      "6": "multi_polygon",
      "7": "geometry_collection",
      "8": "circular_string",
      "15": "polyhedral_surface",
      "16": "tin",
      "17": "triangle",
      "18": "circle",
      "19": "geodesic_string",
      "20": "elliptical_curve",
      "21": "nurbs_curve",
      "22": "clothoid",
      "23": "spiral_curve",
      "101": "vector",
      "102": "affine_placement",
      "1000001": "circular_string"
    }
  },
  types: {
    geometry: ({
      seq: ({
        "switch-on": "type",
        cases: {
          "geometry_type::point": "point",
          "geometry_type::line_string": "line_string",
          "geometry_type::polygon": "polygon",
          "geometry_type::multi_point": "multi_point",
          "geometry_type::multi_line_string": "multi_line_string",
          "geometry_type::multi_polygon": "multi_polygon",
          "geometry_type::geometry_collection": "geometry_collection",
          "geometry_type::circular_string": "line_string",
          "geometry_type::polyhedral_surface": "multi_polygon",
          "geometry_type::tin": "tin",
          "geometry_type::triangle": "polygon",
          "geometry_type::circle": "line_string",
          "geometry_type::geodesic_string": "line_string",
          "geometry_type::elliptical_curve": "elliptical_curve",
          "geometry_type::nurbs_curve": "nurbs_curve",
          "geometry_type::clothoid": "clothoid",
          "geometry_type::spiral_curve": "spiral_curve",
          "geometry_type::vector": "point",
          "geometry_type::affine_placement": "affine_placement"
        }
      } | wkb_seq),
      types: {
        point: {
          seq: [
            {
              id: "x",
              type: "f8"
            },
            {
              id: "y",
              type: "f8"
            }
          ]
        },
        line_string: {
          seq: ({ id: "points", type: "point" } | counted)
        },
        polygon: {
          seq: ({ id: "rings", type: "line_string" } | counted)
        },
        wkb_point: ({ seq: ("point" | wkb_seq) } | endianize),
        wkb_line_string: ({ seq: ("line_string" | wkb_seq) } | endianize),
        wkb_polygon: ({ seq: ("polygon" | wkb_seq) } | endianize),
        multi_point: {
          seq: ({ id: "points", type: "wkb_point" } | counted)
        },
        multi_line_string: {
          seq: ({ id: "line_strings", type: "wkb_line_string" } | counted)
        },
        multi_polygon: {
          seq: ({ id: "polygons", type: "wkb_polygon" } | counted)
        },
        geometry_collection: {
          seq: ({ id: "geometries", type: "geometry" } | counted)
        },
        wkb_string: {
          seq: [
            {
              id: "length",
              type: "u1"
            },
            {
              id: "value",
              type: "str",
              size: "length",
              encoding: "UTF-8"
            }
          ]
        },
        tin_element: ({
          seq: [
            {
              id: "type",
              type: "wkb_string"
            },
            {
              id: "id",
              type: "s4"
            },
            {
              id: "tag",
              type: "wkb_string"
            },
            {
              id: "geometry",
              type: "geometry"
            }
          ]
        } | endianize),
        tin: {
          seq: (
            ({ id: "triangles", type: "wkb_polygon" } | counted("triangle_"))
            + ({ id: "elements", type: "tin_element" } | counted("element_"))
            + [
              {
                id: "max_side_length",
                type: "f8"
              }
            ]
          )
        },
        affine_placement: {
          seq: (
            [
              {
                id: "location",
                type: "point"
              }
            ] + ({ id: "reference_directions", type: "point" } | counted)
          )
        },
        wkb_affine_placement: ({ seq: ("affine_placement" | wkb_seq) } | endianize),
        elliptical_curve: {
          seq: [
            {
              id: "reference_location",
              type: "wkb_affine_placement"
            },
            {
              id: "u_axis_length",
              type: "f8"
            },
            {
              id: "v_axis_length",
              type: "f8"
            },
            {
              id: "start_angle",
              type: "f8"
            },
            {
              id: "end_angle",
              type: "f8"
            }
          ]
        },
        nurbs_point: ({
          seq: [
            {
              id: "point",
              type: "wkb_point"
            },
            # One bit is ignored here due to unclear specification
            {
              id: "weight",
              type: "f8"
            }
          ]
        } | endianize),
        knot: ({
          seq: [
            {
              id: "value",
              type: "f8"
            },
            {
              id: "multiplicity",
              type: "u1"
            }
          ]
        } | endianize),
        nurbs_curve: {
          seq: (
            [
              {
                id: "degree",
                type: "u1"
              }
            ]
            + ({ id: "control_points", type: "nurbs_point" } | counted("control_point_"))
            + ({ id: "knots", type: "knot" } | counted("knot_"))
          )
        },
        clothoid: {
          seq: [
            {
              id: "reference_location",
              type: "wkb_affine_placement"
            },
            {
              id: "scale_factor",
              type: "f8"
            },
            {
              id: "start_distance",
              type: "f8"
            },
            {
              id: "end_distance",
              type: "f8"
            }
          ]
        },
        spiral_curve: {
          seq: [
            {
              id: "reference_location",
              type: "wkb_affine_placement"
            },
            {
              id: "length",
              type: "f8"
            },
            {
              id: "start_distance",
              type: "f8"
            },
            {
              id: "end_distance",
              type: "f8"
            },
            {
              id: "spiral_type",
              type: "wkb_string"
            }
          ]
        }
      }
    } | endianize)
  }
}

Conclusion

Nous venons de traiter les géométries les plus compliquées de cette spécification ; il nous en reste encore quelques unes, mais elles seront plutôt répétitives. Nous ne sommes cependant pas au bout de nos peines, car il n'y a pas que des nouvelles géométries à traiter. Le script jq a encore l'air d'être assez lisible, ou en tous cas on peut encore distinguer correctement la structure générale d'un schéma Kaitai Struct, mais cela pourrait vite changer.

J'ai dit plusieurs fois que les courbes n'étaient que peu ou pas implémentées, ou qu'elles n'étaient pas utiles ; ce n'est bien sûr pas complètement vrai, mais la réalité est que personne ne s'embête à utiliser des courbes. C'est tout simplement trop coûteux en termes de calculs à effectuer par rapport à des dessins basés sur des lignes : tous les algorithmes de traitement de géométries deviennent bien plus complexes et bien plus difficiles à optimiser. La quasi-totalité des données géographiques utilisent des approximations de courbes, par exemple avec des lignes brisées ayant beaucoup de points pour essayer de suivre la courbe, et tout le monde s'en contente très bien ; pourquoi s'embêter à aller gérer des courbes que personne ne demandent ? Le jeu n'en vaut pas la chandelle, et c'est probablement la raison principale pour laquelle ce standard ISO semble plutôt ignoré.


Commentaires

Il n'y a pour l'instant aucun commentaire. Soyez le premier !