lucidiot's cybrecluster

La version méconnue du texte bien connu, première partie

Lucidiot Informatique 2022-07-11
Le grand retour du terrible standard ISO.


Comme je l'ai expliqué dans mon article sur l'Extended Well-Known Text, c'est en travaillant sur le Well-Known Binary que j'ai appris des choses sur les trois variantes du WKB. J'avais traité le WKT avant le WKB, et donc avant de connaître ces variantes. Le WKT a exactement les mêmes variantes, et je m'efforce donc maintenant de traiter les deux variantes que je n'avais pas encore fait. Dans cet article, nous allons commencer à traiter le Well-Known Text tel qu'il est défini dans les standards ISO.

Je reprends depuis la Wayback Machine le même brouillon de la spécification ISO 13249-3:2016 édité en 2015 que celui que j'avais utilisé pour le WKB ISO, qui n'est donc pas une version définitive, mais je suppose que le standard réel s'en approche plutôt bien puisque c'est un brouillon qui a été soumis à un vote pour approbation avant l'application des derniers correctifs et sa publication.

J'assumerai que vous avez déjà lu mes précédents articles sur le Well-Known Text ainsi que ceux sur le Well-Known Binary, donc je ne rentrerai pas beaucoup dans les détails sur le script m4 sur lequel je me base initialement, ni sur ce qu'est concrètement chaque géométrie que nous allons rencontrer.

Pour rappel, nous partons de mon script initial pour le Well-Known Text standard, avec les quelques corrections appliquées dans l'article sur l'Extended Well-Known Text :

define(ignorecase, `patsubst($1, \w, `[`\&'translit(\&, A-Z, a-z)]')')dnl
define(number, [+-]?(?:\d+(?:[.,]\d*)?|[.,]\d+)(?:ignorecase(E)[+-]?\d+)?)dnl
define(coords_2, number\s+number)dnl
define(coords_3, number\s+number\s+number)dnl
define(coords_4, number\s+number\s+number\s+number)dnl
define(emptyable, `(?:'ignorecase(EMPTY)`|\(\s*$1\s*\))')dnl
define(make_list, emptyable($1(?:\s*,\s*$1)*))dnl
define(make_geometry, `define($1, `ignorecase($2)$3\s*(?:\s+ignorecase(EMPTY)|\(\s*$4\s*\))')$1')dnl
define(make_geometry_list, `make_geometry($1, $2, $3, $4(?:\s*,\s*$4)*)')dnl
define(make_geometries,
`make_geometry(point_$2, POINT, $1, coords_$2)|dnl
make_geometry_list(linestring_$2, LINESTRING, $1, coords_$2)|dnl
make_geometry_list(polygon_$2, (?:POLYGON|TRIANGLE|MULTILINESTRING), $1, make_list(coords_$2))|dnl
make_geometry_list(multipoint_$2, MULTIPOINT, $1, emptyable(coords_$2))|dnl
make_geometry_list(multipolygon_$2, (?:MULTIPOLYGON|POLYHEDRALSURFACE|TIN), $1, make_list(make_list(coords_$2)))|dnl
make_geometry_list(geometrycollection_$2, GEOMETRYCOLLECTION, $1, (?:point_$2|linestring_$2|polygon_$2|multipoint_$2|multipolygon_$2))')dnl
^\s*(?:make_geometries(`', 2)|dnl
make_geometries(\s*(?:ignorecase(Z|M)), 3)|dnl
make_geometries(\s*ignorecase(ZM), 4))\s*$

Préparatifs

Le format WKT ISO redéfinit quelques géométries différemment, et en définit beaucoup d'autres qui dépendent des précédentes. Nous allons « décompresser » un peu notre script pour rendre certaines macros un peu mieux réutilisables :

define(emptyable, `(?:'ignorecase(EMPTY)`|$1)')dnl
define(make_list, \(\s*$1(?:\s*,\s*$1)*\s*\))dnl
define(make_geometry_list, `make_geometry($1, $2, $3, $4(?:\s*,\s*$4)*)')dnl
define(make_geometries,
`define(coords_list_$2, emptyable(make_list(coords_$2)))dnl
define(coords_list_list_$2, emptyable(make_list(coords_list_$2)))dnl
make_geometry(point_$2, POINT, $1, coords_$2)|dnl
make_geometry_list(linestring_$2, LINESTRING, $1, coords_$2)|dnl
make_geometry_list(polygon_$2, POLYGON, $1, coords_list_$2)|dnl
make_geometry_list(triangle_$2, TRIANGLE, $1, coords_list_$2)|dnl
make_geometry_list(multipoint_$2, MULTIPOINT, $1, emptyable(\(\s*coords_$2\s*\)))|dnl
make_geometry_list(multilinestring_$2, MULTILINESTRING, $1, coords_list_$2)|dnl
make_geometry_list(multipolygon_$2, MULTIPOLYGON, $1, coords_list_list_$2)|dnl
make_geometry_list(polyhedralsurface_$2, POLYHEDRALSURFACE, $1, coords_list_list_$2)|dnl
make_geometry_list(tin_$2, TIN, $1, coords_list_list_$2)|dnl
make_geometry_list(geometrycollection_$2, GEOMETRYCOLLECTION, $1, (?:point_$2|linestring_$2|polygon_$2|triangle_$2|multipoint_$2|multilinestring_$2|multipolygon_$2|polyhedralsurface_$2|tin_$2))')dnl

Surface polyédrale

La surface polyédrale change de représentation : lorsqu'elle contient des polygones, on doit désormais y ajouter PATCHES, et chaque polygone est préfixé de son nom de type de géométrie :

POLYHEDRALSURFACE EMPTY
POLYHEDRALSURFACE (
  PATCHES (
    POLYGON (...),
    POLYGON (...)
  )
)

Notons qu'il n'est pas possible d'avoir autre chose qu'un polygone dans une surface polyédrale, et il n'y aucun paramètre autre que PATCHES autorisé dans cette grammaire, donc toutes ces modifications sont superflues.

Notre macro make_geometry_list est conçue pour gérer uniquement des listes assez simples d'éléments ; on ne peut pas ajouter PATCHES à l'intérieur des parenthèses. On va donc utiliser make_geometry à la place.

make_geometry(polyhedralsurface_$2, POLYHEDRALSURFACE, $1, ignorecase(PATCHES)\s*make_list(polygon_$2))|dnl

Notre version précédemment modifiée de make_list renvoie maintenant une liste de ce qu'on donne en argument entourée entre parenthèses ; on y donne donc le polygone entier.

Courbes simples

La grammaire EBNF du standard défini le contenu des lignes brisées, des chaînes d'arcs de cercles, des cercles et des géodésiques l'un après l'autre, dans cet ordre, et ils sont exactement les mêmes. On peut donc copier une macro pour définir les trois types de courbes supplémentaires :

make_geometry_list(circularstring_$2, CIRCULARSTRING, $1, coords_$2)|dnl
make_geometry_list(circle_$2, CIRCLE, $1, coords_$2)|dnl
make_geometry_list(geodesic_$2, GEODESICSTRING, $1, coords_$2)|dnl

On verra plus tard pour potentiellement optimiser cela si ça devient possible ; on va garder un peu de duplication pour l'instant pour réduire les modifications suivantes.

Angle

Le standard introduit un type de géométrie qui ne représente qu'un angle, notamment pour pouvoir gérer les trois unités d'angles connues (degrés, radians et grades). Un angle est représenté par ANGLE, suivi de DEGREES, RADIANS ou GRADIANS selon l'unité choisie, suivi d'un nombre entre parenthèses.

dnl Défini avant make_geometries
define(angle, ignorecase(ANGLE)\s+ignorecase((?:DEGREE|G?RADIAN)S)\s*\(\s*number\s*\))dnl

dnl En fin de script
^\s*(?:angle|dnl
make_geometries(`', 2)|dnl
make_geometries(\s*(?:ignorecase(Z|M)), 3)|dnl
make_geometries(\s*ignorecase(ZM), 4))\s*$

L'angle n'est pas influencé par les dimensions, donc on ne l'intègre pas dans make_geometries. À la place, je le définis assez haut, avant la construction des géométries, car on va référer à l'angle dans des macros suivantes et je veux donc avoir accès à sa définition. Puisqu'un angle compte en tant que du Well-Known Text, il faut quand même l'ajouter à notre expression finale, donc je le fais directement dans notre expression.

Notez que j'ai factorisé les trois termes de degrés, radians et grades ; ils finissent tous par un S, donc le S final est le même pour tout le monde, et la seule différence entre RADIANS et GRADIANS est le G, donc j'ai écrit une seule fois RADIANS et ajouté un G optionnel. Cela me rappelle une calculatrice que j'avais, qui utilisait trois ensembles de lettres pour afficher ces unités : DE, G et RAD. DEG était affiché pour les degrés, RAD pour les radians et GRAD pour les grades.

Direction

La direction est une autre géométrie étrange introduite par le standard pour représenter un angle, mais par rapport à un point cardinal. En SQL, il est possible d'utiliser des arguments à ST_Direction pour indiquer le point cardinal souhaité (une seule lettre parmi N, S, W ou E), mais la représentation en Well-Known Text n'autorise que N, le nord, ce qui rend vraiment cette direction complètement inutile puisqu'il est tout à fait sain d'assumer qu'un angle représentant un azimuth définit son 0 vers le nord.

La représentation d'une direction utilise DIRECTION (N <radians>), et comme l'angle, elle n'est pas influencée par les dimensions. Par contre, on n'utilisera la direction nulle part ailleurs (c'est dire si elle est utile), donc il n'est pas nécessaire d'en faire une macro particulière ou de la placer ailleurs qu'en fin de script.

^\s*(?:angle|dnl
ignorecase(DIRECTION)\s*\(\s*ignorecase(N)\s+number\s*\)|dnl
make_geometries(`', 2)|dnl
make_geometries(\s*(?:ignorecase(Z|M)), 3)|dnl
make_geometries(\s*ignorecase(ZM), 4))\s*$

Vecteur

Le vecteur est un cas particulier de point avec une représentation différente. Un vecteur ne peut pas avoir de mesure, donc il n'est représentable qu'avec XY ou XYZ. Il peut être vide comme le point, mais lorsqu'il n'est pas vide, on ne met pas de parenthèses : VECTOR Z 1 2 3.

Le vecteur introduit un peu de complexité : comment n'utiliser qu'un vecteur autorisant seulement 2 coordonnées quand on est dans les cas XY et XYM, mais autoriser un vecteur en XYZ pour XYZ et XYZM à la fois ? Autrement dit, comment ignorer la dimension M ?

Nous utilisons actuellement make_geometries(\s*(?:ignorecase(Z|M)), 3) pour générer immédiatement toutes les géométries en XYZ et XYM simultanément. Retirer cette optimisation fait que notre expression régulière, actuellement longue de 80931 caractères, atteint 107411 caractères, soit une expression un tiers plus grosse.

Nous pourrions essayer de limiter la casse en subdivisant notre make_geometries: toutes les géométries qui ne sont pas dans le cas particulier où elles ont une partie ne dépendant pas de M restent là où elles sont actuellement, et une nouvelle macro permet de générer les autres géométries en double, une fois pour XYZ et une fois pour XYM. Cela pourrait fonctionner, mais au vu de la complexité de certaines des géométries que nous allons voir et qui utilisent des vecteurs ou ont d'autres cas particuliers liés à la dimensionnalité, ça ne vaut pas vraiment le coup. De plus, vu que la collection de géométries, qui contient toutes les autres géométries et cause de belles duplications, ainsi que d'autres ensembles de géométries ou géométries composées, vont déjà causer une grande quantité de duplications et vont contenir des géométries ayant des cas particuliers face à la dimension M, on peut se contenter de juste dupliquer toutes les géométries, tant pis pour les caractères supplémentaires.

Réécriture de la gestion des dimensions

Nous pouvons donc modifier make_geometries pour qu'elle n'accepte plus qu'un paramètre : les dimensions supplémentaires, qui seront utilisées à la fois pour le nom de macro et pour l'indicateur de dimensionnalité du WKT.

^\s*(?:angle|dnl
ignorecase(DIRECTION)\s*\(\s*ignorecase(N)\s+number\s*\)|dnl
make_geometries(`')|dnl
make_geometries(Z)|dnl
make_geometries(M)|dnl
make_geometries(ZM))\s*$

On doit donc maintenant mettre à jour en conséquence make_geometries, puisque l'argument $2 n'existe plus :

define(make_geometries,
`define(coords_list_$1, emptyable(make_list(coords_$1)))dnl
define(coords_list_list_$1, emptyable(make_list(coords_list_$1)))dnl
make_geometry(point, POINT, $1, coords_$1)|dnl
make_geometry_list(linestring, LINESTRING, $1, coords_$1)|dnl
make_geometry_list(circularstring, CIRCULARSTRING, $1, coords_$1)|dnl
make_geometry_list(circle, CIRCLE, $1, coords_$1)|dnl
make_geometry_list(geodesic, GEODESICSTRING, $1, coords_$1)|dnl
make_geometry_list(polygon, POLYGON, $1, coords_list_$1)|dnl
make_geometry_list(triangle, TRIANGLE, $1, coords_list_$1)|dnl
make_geometry_list(multipoint, MULTIPOINT, $1, emptyable(\(\s*coords_$1\s*\)))|dnl
make_geometry_list(multilinestring, MULTILINESTRING, $1, coords_list_$1)|dnl
make_geometry_list(multipolygon, MULTIPOLYGON, $1, coords_list_list_$1)|dnl
make_geometry(polyhedralsurface, POLYHEDRALSURFACE, $1, ignorecase(PATCHES)\s*make_list(polygon_$1))|dnl
make_geometry_list(tin, TIN, $1, coords_list_list_$1)|dnl
make_geometry_list(geometrycollection, GEOMETRYCOLLECTION, $1, (?:point_$1|linestring_$1|polygon_$1|triangle_$1|multipoint_$1|multilinestring_$1|multipolygon_$1|polyhedralsurface_$1|tin_$1))')dnl

La macro est assez grosse, donc c'est assez difficile à lire, mais j'ai remplacé tous les $2 par $1, et aussi supprimé les _$2 après les noms de macros. On a maintenant déjà ces suffixes dans le troisième paramètre de make_geometry et make_geometry_list ; on peut laisser ces macros-là se débrouiller avec ça. make_geometry_list ne fait qu'appeler make_geometry, donc on peut se contenter de mettre à jour cette dernière :

define(make_geometry, `define($1_$3, `ignorecase($2)\s*ignorecase($3)\s*(?:\s+ignorecase(EMPTY)|\(\s*$4\s*\))')$1_$3')dnl

Cette modification a toutefois un autre impact : lorsque nous appelons make_geometries plusieurs fois, nous allons nous retrouver avec par exemple point_ pour XY, point_Z pour XYZ, point_M pour XYM et point_ZM pour XYZM. C'est aussi le cas pour les coordonnées, une macro de base utilisée par presque toutes les autres ; il va donc nous falloir les redéfinir aussi.

define(coords_, number\s+number)dnl
define(coords_Z, number\s+number\s+number)dnl
define(coords_M, coords_Z)dnl
define(coords_ZM, number\s+number\s+number\s+number)dnl

Je définis coords_M comme un alias de coords_Z, puisqu'elles sont toutes les deux trois nombres. Cela évite d'avoir une logique complexe dans make_geometries pour gérer ce cas. Le nom coords_ avec un underscore final n'est peut-être pas des plus élégants, mais je n'ai pas considéré ça assez important pour m'en occuper vraiment, alors on le laissera comme ça.

Définition du vecteur

Maintenant que nous avons la plomberie nécessaire, nous pouvons définir le vecteur. On va le faire hors du circuit normal pour pouvoir personnaliser notre utilisation des coordonnées selon les dimeensions :

define(vector_, ignorecase(VECTOR)\s+emptyable(coords_))dnl
define(vector_Z, ignorecase(VECTOR)\s+ignorecase(Z)\s+emptyable(coords_Z))dnl
define(vector_M, vector_)dnl
define(vector_ZM, vector_Z)dnl

vector_M et vector_ZM deviennent des alias, pour ignorer complètement la dimension M. Nous contrôlons à nouveau correctement les dimensions dans l'expression régulière ! Il n'y a plus qu'à ajouter les deux formats de vecteurs à notre expression finale :

^\s*(?:angle|dnl
ignorecase(DIRECTION)\s*\(\s*ignorecase(N)\s+number\s*\)|dnl
vector_|vector_Z|dnl
make_geometries(`')|dnl
make_geometries(Z)|dnl
make_geometries(M)|dnl
make_geometries(ZM))\s*$

TIN

Le réseau triangulaire irrégulier, en anglais TIN, est une surface polyédrale dont tous les polygones sont des triangles, et qui dispose d'un grand nombre d'autres éléments que j'ai déjà décrit dans mes articles sur le WKB. En résumé, les TIN peuvent être définis sans aucun triangle par un utilisateur, qui indiquera plutôt des points, des lignes brisées ou des polygones avec une élévation connue, etc., et tous les triangles qui constituent le TIN, et forment une représentation de la topologie du terrain, peuvent être calculés automatiquement. Cela permet de réduire grandement le nombre de mesures à faire sur le terrain quand on n'a pas besoin d'une précision énorme.

Le Well-Known Text standard et l'EWKT, les deux variantes qu'on a déjà vu précédemment, ne permettent que d'indiquer tous les triangles du TIN, sans contenir tous les détails vraiment intéressants. Cette fois-ci, le standard ISO bat les autres sur cette fonctionnalité. Cela dit, la quantité de données différentes à stocker pour le TIN implique que nous allons avoir du pain sur la planche.

Voici un résumé de la syntaxe du TIN, lorsque j'utilise tous les éléments optionnels :

TIN EMPTY
TIN Z (
  PATCHES (
    (1 2 3, 4 5 6, 7 8 9),  # Triangles
    ...
  )
  ELEMENTS (
    POINTS ID 999 MULTIPOINT Z ((1 2 3 4), EMPTY),
    GROUPSPOT TAG "something" MULTIPOINT Z EMPTY,
    BOUNDARY ID 420 TAG "something" POLYGON Z (((1 2 3), (4 5 6))),
    BREAKLINE LINESTRING (1 2, 3 4, 5 6),
    SOFTBREAK TAG "lol" LINESTRING EMPTY,
    CONTROLCONTOUR LINESTRING EMPTY,
    BREAKVOID POLYGON ZM EMPTY,
    DRAPEVOID POLYGON (((1 2), (3 4))),
    VOID POLYGON EMPTY,
    HOLE POLYGON Z EMPTY,
    HOLE POLYGON EMPTY,
    HOLE POLYGON EMPTY,
    STOPLINE LINESTRING Z (1 2 3, 4 5 6)
  )
  MAXSIDELENGTH 42
)

Un TIN, quand il n'est pas vide, se compose au minimum de PATCHES, exactement comme pour la surface polyédrale, mais avec des triangles et sans le nom TRIANGLE devant. Optionnellement, on peut avoir des éléments TIN, dans le groupe ELEMENTS. Chacun de ces éléments a un type, le premier mot qu'on voit sur chaque ligne. Ce type détermine le type de géométrie qu'on voit en fin de ligne. Entre le type d'élément TIN et la géométrie de cet élément, on peut optionnellement ajouter un identifiant unique d'élément TIN, qui est un nombre entier, ou un tag, qui est une chaîne de caractères entre double-apostrophes. Un même type d'élément TIN peut très bien se répéter, et les géométries peuvent être vides. Enfin, on peut également avoir un paramètre MAXSIDELENGTH, la longueur maximale du côté de chaque triangle, qui est un nombre entier.

Nous pouvons commencer par définir la structure générale du TIN, avec PATCHES, ELEMENTS et MAXSIDELENGTH, sans définir les éléments eux-mêmes :

make_geometry(tin, TIN, $1, ignorecase(PATCHES)\s*coords_list_list_$1()(?:\s+ignorecase(ELEMENTS)\s*make_list(elements))?(?:\s+ignorecase(MAXSIDELENGTH)\s+\d+)?)|dnl

Éléments TIN

Nous allons maintenant pouvoir remplir ce elements. Je pourrais en faire une macro, mais ce ne sera pas nécessaire puisque nous n'utiliserons les éléments TIN nulle part ailleurs que dans le TIN, alors on fera le remplacement tout à l'heure. La façon la plus simple de factoriser pour éviter de trop dupliquer notre expression est de regrouper d'abord tous ces types d'éléments par le type de géométrie qu'ils acceptent :

Type de géométrie Types d'éléments TIN
Ligne brisée quelconque STOPLINE
Ligne brisée avec Z BREAKLINE, SOFTBREAK, CONTROLCONTOUR
Polygone quelconque DRAPEVOID, HOLE
Polygone avec Z BOUNDARY, BREAKVOID, VOID
Ensemble de points avec Z POINTS, GROUPSPOT

Ce n'est pas écrit du tout dans la grammaire EBNF, mais le standard requiert que certains des éléments aient une géométrie d'une dimensionnalité particulière.

On n'a plus qu'à définir cinq alternatives dans un groupe non-capturant pour autoriser chacun de ces types, suivi optionnellement d'un identifiant et d'un tag, et toujours suivi du type de géométrie qu'ils souhaitent.

On peut définir les identifiants et tags optionnels dans une macro cette fois. On n'a encore jamais vu les chaînes de caractères dans cette expression régulière, et j'ai dû sortir un autre standard, ISO 9075-2. Ce standard définit les bases du langage SQL, et une partie de la définition de la chaîne de caractères dans ISO 13249-3 fait référence à celui-ci. La chaîne de caractères est définie comme autorisant l'alphabet latin en majuscules et minuscules, les chiffres, les espaces, ainsi que les caractères ()-_.'. Voilà une définition bien bizarre, mais au moins je n'ai pas à gérer de l'échappement de caractères (\").

define(tinelement_label, (?:\s+ignorecase(ID)\s+\d+)?(?:\s+ignorecase(TAG)\s*"[a-zA-Z0-9()_.' -]+")?\s+)dnl

Nous pouvons maintenant définir nos cinq alternatives pour tous les types d'éléments TIN :

(?:ignorecase(STOPLINE)tinelement_label()(?:linestring_|linestring_Z|linestring_M|linestring_ZM)|ignorecase((?:BREAKLINE|SOFTBREAK|CONTROLCONTOUR))tinelement_label()(?:linestring_Z|linestring_ZM)|ignorecase((?:DRAPEVOID|HOLE))tinelement_label()(?:polygon_|polygon_Z|polygon_M|polygon_ZM)|ignorecase((?:BOUNDARY|(?:BREAK)?VOID))(?:polygon_Z|polygon_ZM)|ignorecase((?:POINTS|GROUPSPOT))(?:multipoint_Z|multipoint_ZM))

Expression complète

On notera surtout le () après tinelement_label : j'ai deux appels de macros collés l'un après l'autre, donc les parenthèses vides permettent à m4 de faire la différence entre les deux noms de macros. On peut maintenant tout assembler :

make_geometry(tin, TIN, $1, ignorecase(PATCHES)\s*coords_list_list_$1()(?:\s+ignorecase(ELEMENTS)\s*make_list((?:ignorecase(STOPLINE)tinelement_label()(?:linestring_|linestring_Z|linestring_M|linestring_ZM)|ignorecase((?:BREAKLINE|SOFTBREAK|CONTROLCONTOUR))tinelement_label()(?:linestring_Z|linestring_ZM)|ignorecase((?:DRAPEVOID|HOLE))tinelement_label()(?:polygon_|polygon_Z|polygon_M|polygon_ZM)|ignorecase((?:BOUNDARY|(?:BREAK)?VOID))tinelement_label()(?:polygon_Z|polygon_ZM)|ignorecase((?:POINTS|GROUPSPOT))tinelement_label()(?:multipoint_Z|multipoint_ZM))))?(?:\s+ignorecase(MAXSIDELENGTH)\s+\d+)?)|dnl

On se heurte cependant ici à une erreur de syntaxe : l'apostrophe dans la macro tinelement_label cause des problèmes, parce que les caractères d'échappement de m4 sont ` et '. Utiliser une apostrophe dans cette macro cause une fermeture prématurée de la chaîne de caractères de make_geometries, et cause donc des erreurs de lecture du fichier difficiles à diagnostiquer. On peut cependant utiliser la macro intégrée changequote pour changer les caractères de chaîne de m4 : on va donc changer toutes les chaînes du script juste pour pouvoir accomoder une pauvre apostrophe.

changequote(«, »)dnl
define(ignorecase, «patsubst($1, \w, «[«\&»translit(\&, A-Z, a-z)]»)»)dnl
...
define(emptyable, «(?:»ignorecase(EMPTY)«|$1)»)dnl
...
define(make_geometry, «define($1_$3, «ignorecase($2)\s*ignorecase($3)\s*(?:\s+ignorecase(EMPTY)|\(\s*$4\s*\))»)$1_$3»)dnl
define(make_geometry_list, «make_geometry($1, $2, $3, $4(?:\s*,\s*$4)*)»)dnl
define(make_geometries, «...»)dnl
...
make_geometries(«»)|dnl

J'ai utilisé les guillemets doubles, comme en français, parce que je suis certain de ne jamais tomber dessus dans le WKT, et parce qu'on peut encore les interpréter dans sa tête comme décrivant une chaîne de caractères. Cela dit, si vous connaissez un langage de programmation qui utilise ces caractères non ironiquement, c'est qu'il y a un gros problème.

Génération en deux étapes

Nous arrivons à générer une expression régulière entière, mais le TIN peut encore nous jouer un dernier tour : toutes les géométries ne fonctionnent pas dans les éléments du TIN ! Par exemple, avec le TIN à deux dimensions, seuls les lignes brisées et les polygones à deux dimensions fonctionnent, et toutes les autres géométries, qui demandent d'autres dimensions, ne fonctionnent pas. Avec le TIN Z, les géométries avec et sans la dimension Z fonctionnent, mais toujours pas les géométries avec la dimension M.

En inspectant l'expression régulière complète, on constate que des noms de macros sont encore présents et n'ont pas été interprétés par m4, notamment linestring_ZM ou polygon_M. On peut s'amuser à taper directement ces noms dans du WKT, et voir que l'expression valide effectivement TIN ( PATCHES (...) ELEMENTS ( POINTS multipoint_ZM ) ) au lieu d'une géométrie normale.

La raison pour ces macros mal étendues est une question d'ordre d'exécution. On appelle make_geometries 4 fois : d'abord pour la 2D, ensuite pour la 3D avec Z, ensuite pour la 3D avec M, ensuite pour la 4D avec Z et M. Lorsque le TIN en 2D est défini, les macros des géométries en 3D et 4D ne sont pas encore là, donc ni polygon_Z, ni polygon_M, ni polygon_ZM ne sont définis et ne peuvent être étendus. m4 considère que ces macros ne seront pas disponibles sur ces lignes, même après avoir effectué plusieurs itérations dessus qui auraient ensuite défini ces macros.

Pour résoudre le problème, il faut faire en sorte que toutes les géométries dont dépend le TIN soient entièrement définies, dans toutes leurs dimensions, avant que le TIN ne soit défini. Pour cela, on va devoir découper notre fonction principale make_geometries en deux. J'ai choisi les noms très originaux de make_geometries_1 et make_geometries_2. La deuxième partie contient le TIN et la collection de géométries, et la première partie contient tout le reste. Ainsi, on peut d'abord appeler make_geometries_1 avec les 4 combinaisons de dimensions, avoir toutes les macros correctement définies, et seulement ensuite utiliser make_geometries_2 pour construire le TIN. On place aussi la collection de géométries dans cette dernière macro, puisqu'elle peut contenir un TIN et dépend donc de celui-ci.

define(make_geometries_1, «...»)dnl
define(make_geometries_2,
«make_geometry(tin, TIN, $1, ...)|dnl
make_geometry_list(geometrycollection, GEOMETRYCOLLECTION, $1, ...)»)dnl
^\s*(?:angle|dnl
ignorecase(DIRECTION)\s*\(\s*ignorecase(N)\s+number\s*\)|dnl
vector_|vector_Z|dnl
make_geometries_1(`')|dnl
make_geometries_1(Z)|dnl
make_geometries_1(M)|dnl
make_geometries_1(ZM)|dnl
make_geometries_2(`')|dnl
make_geometries_2(Z)|dnl
make_geometries_2(M)|dnl
make_geometries_2(ZM))\s*$

Cette série d'appels de macros devient répétitive, donc je vais rajouter une autre couche de macros :

define(call_dimensions, «$1(«»)|$1(Z)|$1(M)|$1(ZM)»)dnl
^\s*(?:angle|dnl
ignorecase(DIRECTION)\s*\(\s*ignorecase(N)\s+number\s*\)|dnl
vector_|vector_Z|dnl
call_dimensions(«make_geometries_1»)|dnl
call_dimensions(«make_geometries_2»))\s*$

La nouvelle macro call_dimensions est juste un raccourci pour pouvoir appeler une macro avec les 4 combinaisons de dimensions possibles. Je passe les noms de macros avec des guillemets doubles, sinon chaque macro serait exécutée sans arguments avant d'être passée en paramètre de call_dimensions, ce qui donnerait un résultat absurde.

Avec ce nouveau découpage, le TIN a accès à toutes les macros dont il a besoin, et on peut maintenant enfin avoir une validation correcte pour la syntaxe du TIN.

Script actuel

Script m4 générant une expression régulière pour valider du Well-Known Text ISO, première partie

changequote(«, »)dnl
define(ignorecase, «patsubst($1, \w, «[«\&»translit(\&, A-Z, a-z)]»)»)dnl
define(number, [+-]?(?:\d+(?:[.,]\d*)?|[.,]\d+)(?:ignorecase(E)[+-]?\d+)?)dnl
define(coords_, number\s+number)dnl
define(coords_Z, number\s+number\s+number)dnl
define(coords_M, coords_Z)dnl
define(coords_ZM, number\s+number\s+number\s+number)dnl
define(angle, ignorecase(ANGLE)\s+ignorecase((?:DEGREE|G?RADIAN)S)\s*\(\s*number\s*\))dnl
define(vector_, ignorecase(VECTOR)\s+emptyable(coords_))dnl
define(vector_Z, ignorecase(VECTOR)\s+ignorecase(Z)\s+emptyable(coords_Z))dnl
define(vector_M, vector_)dnl
define(vector_ZM, vector_Z)dnl
define(tinelement_label, (?:\s+ignorecase(ID)\s+\d+)?(?:\s+ignorecase(TAG)\s*"[a-zA-Z0-9()_.' -]+")?\s+)dnl
define(emptyable, «(?:»ignorecase(EMPTY)«|$1)»)dnl
define(make_list, \(\s*$1(?:\s*,\s*$1)*\s*\))dnl
define(make_geometry, «define($1_$3, «ignorecase($2)\s*ignorecase($3)\s*(?:\s+ignorecase(EMPTY)|\(\s*$4\s*\))»)$1_$3»)dnl
define(make_geometry_list, «make_geometry($1, $2, $3, $4(?:\s*,\s*$4)*)»)dnl
define(make_geometries_1,
«define(coords_list_$1, emptyable(make_list(coords_$1)))dnl
define(coords_list_list_$1, emptyable(make_list(coords_list_$1)))dnl
make_geometry(point, POINT, $1, coords_$1)|dnl
make_geometry_list(linestring, LINESTRING, $1, coords_$1)|dnl
make_geometry_list(circularstring, CIRCULARSTRING, $1, coords_$1)|dnl
make_geometry_list(circle, CIRCLE, $1, coords_$1)|dnl
make_geometry_list(geodesic, GEODESICSTRING, $1, coords_$1)|dnl
make_geometry_list(polygon, POLYGON, $1, coords_list_$1)|dnl
make_geometry_list(triangle, TRIANGLE, $1, coords_list_$1)|dnl
make_geometry_list(multipoint, MULTIPOINT, $1, emptyable(\(\s*coords_$1\s*\)))|dnl
make_geometry_list(multilinestring, MULTILINESTRING, $1, coords_list_$1)|dnl
make_geometry_list(multipolygon, MULTIPOLYGON, $1, coords_list_list_$1)|dnl
make_geometry(polyhedralsurface, POLYHEDRALSURFACE, $1, ignorecase(PATCHES)\s*make_list(polygon_$1))»)dnl
define(make_geometries_2,
«make_geometry(tin, TIN, $1, ignorecase(PATCHES)\s*coords_list_list_$1()(?:\s+ignorecase(ELEMENTS)\s*make_list((?:ignorecase(STOPLINE)tinelement_label()(?:linestring_|linestring_Z|linestring_M|linestring_ZM)|ignorecase((?:BREAKLINE|SOFTBREAK|CONTROLCONTOUR))tinelement_label()(?:linestring_Z|linestring_ZM)|ignorecase((?:DRAPEVOID|HOLE))tinelement_label()(?:polygon_|polygon_Z|polygon_M|polygon_ZM)|ignorecase((?:BOUNDARY|(?:BREAK)?VOID))tinelement_label()(?:polygon_Z|polygon_ZM)|ignorecase((?:POINTS|GROUPSPOT))tinelement_label()(?:multipoint_Z|multipoint_ZM))))?(?:\s+ignorecase(MAXSIDELENGTH)\s+\d+)?)|dnl
make_geometry_list(geometrycollection, GEOMETRYCOLLECTION, $1, (?:point_$1|linestring_$1|polygon_$1|triangle_$1|multipoint_$1|multilinestring_$1|multipolygon_$1|polyhedralsurface_$1|tin_$1))»)dnl
define(call_dimensions, «$1(«»)|$1(Z)|$1(M)|$1(ZM)»)dnl
^\s*(?:angle|dnl
ignorecase(DIRECTION)\s*\(\s*ignorecase(N)\s+number\s*\)|dnl
vector_|vector_Z|dnl
call_dimensions(«make_geometries_1»)|dnl
call_dimensions(«make_geometries_2»))\s*$

Conclusion

Nous en sommes maintenant à un point où la majorité de la plomberie dont nous avons besoin pour traiter le WKT ISO est en place. Dans le prochain article, nous verrons tous les autres types de courbes, les ensembles et collections, et terminerons enfin de traiter le Well-Known Text pour de bon.

Si vous pensez qu'une expression régulière de 324 525 caractères est longue, attendez de voir la deuxième partie !


Commentaires

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