← Projects

GTEZ Atlas

I built a map of land requests and zone layers for the staff of the Golden Triangle Economic Zone authority, from files scattered across a shared Drive.

Year
2026
Context
For the Golden Triangle Economic Zone authority
Built with
React, MapLibre, Python, PostGIS

Overview

The Golden Triangle Economic Zone authority receives requests for land inside the zone. Each request lived in its own folder on a shared Google Drive, with its boundary drawn in Google Earth as a KMZ file and its letters scanned as PDFs. The authority also kept a working Google Earth file with its own layers, such as roads, utilities and planned land use. Nobody could see all of this on one map, or tell quickly which requests overlapped or fell outside the zone.

I built GTEZ Atlas to fix that. It is an internal tool for the authority's staff, and it has two parts.

  • A Python pipeline that reads the Drive and the Google Earth file, cleans them, and builds one dataset.
  • A React app that shows that dataset on a map, with search, a dashboard, a request table, printable board reports and an editor.

The interface works in Arabic and English, since the source files and most of the staff work in Arabic. I wrote the first version as a single HTML page with Leaflet, then moved the map to MapLibre and rebuilt the interface in React.

How it works

One script, tools/refresh.sh, rebuilds everything in order.

  • The crawler lists the Drive folders through Google's public folder view, 16 at a time, and writes a manifest of every file with its path and date. It then downloads only the KMZ and KML files it does not already have.
  • A converter reads each KMZ, finds every placemark, and writes points, lines and polygons to one GeoJSON file.
  • The register script groups those shapes into requests using the folder structure, and reads the request type and date from the Arabic folder names.
  • Shapely then computes how much of each request lies inside the zone boundary, and the exact area where two requests overlap. An STRtree index keeps that from comparing every pair.
  • Every PDF is read on the Mac with Apple's Vision framework, in Arabic and English, so no document goes to a cloud service. Rules then pull decisions and dates out of the authority's standard letters.
  • The result is loaded into PostgreSQL with PostGIS. A small Python server is the only way to reach it. It checks logins, gives each person a role, and records every request in an access log.

The React app holds no data itself. It logs in to that server and loads the datasets from it.

Design decisions

  • Corrections go in a log, not in the source. Staff fix names, categories and statuses in the editor, and each change is added to edits.json as one numbered entry with a date and a name. Nothing is overwritten, so the file is its own history. Later entries win over earlier ones, and switching an entry off undoes it. The pipeline applies the log on every rebuild, so a fresh crawl of the Drive never loses a correction.
  • Stable ids from content. Shapes get ids from a hash of their folder name and outline, so an edit still points at the right shape after the Drive changes.
  • One rebuild for many edits. The server waits 4 seconds after the last change before it rebuilds, so a burst of edits costs one rebuild.
  • The boundary is a recorded decision. Which outline counts as the official zone boundary is itself an entry in the log. When it changes, the pipeline archives the old one and writes a log of every request whose position changed, with a note on how to reverse it.
  • The app ships without data. The hosted build is only code. The data stays in the database on the authority's machine and is sent after login, with a reduced view for demonstration accounts.
  • Everything runs locally. OCR, parsing and the database all run on one Mac.

Problems I ran into

Most of the work was cleaning data that people had filed by hand over several years.

  • The Drive had no fixed structure. A request could sit two or three folders deep, be split across two folders, or include a copy of another request's boundary for reference. The register script merges folders that share a request number and drops copied boundaries by comparing a hash of their rounded coordinates.
  • The Google Earth file held copies of itself, CAD hatching drawn as hundreds of small lines, and Google's own sample tour. The cleaning script drops duplicates, merges the hatching into outlines, keeps only shapes inside a box around Egypt, and simplifies the rest so a browser can draw it.
  • Arabic text was inconsistent. The same word appeared with different forms of alef, with or without short vowels, and with Arabic or Western digits. Search normalizes all of these before it compares, and the coordinate parser in the editor reads Arabic digits and Arabic commas as well as UTM, degrees and degrees, minutes and seconds.
  • OCR misread handwritten years, so the letter parser rejects any date outside a plausible range.
  • Leaflet stalled after each zoom once many layers were on. MapLibre draws on the GPU, but its own lookup returned the wrong pin while pins animated, so I wrote my own hit test.

Results

The whole dataset now rebuilds from the Drive with one command, and staff corrections survive every rebuild.

  • Staff can see every request and the authority's own layers on one map, search across names and the text of the scanned letters, and open a request to see where it sits against the zone and its neighbours.
  • Overlaps between requests are computed as real shapes, so the map can draw the shared area instead of only flagging a pair. Edges that only touch, or slivers under 100 m², are ignored.
  • Board reports print with map figures drawn the way survey maps are, with a coordinate frame, a scale bar, a north arrow, a locator inset and numbered boundary points that match the coordinate table. One hidden map is reused for every figure, because a page of live maps would run out of WebGL contexts.
  • In my notes, the Leaflet version stalled 150 to 270 ms after a zoom. The MapLibre version does not stall.
  • Access is controlled by personal accounts with four roles, and every request to the data is logged.

What I'd do next

  • The land use layer is traced from images of a plan, so its edges are approximate. I would replace it with the real GIS layer as soon as the authority's GIS staff can export it.
  • The letter parser uses fixed rules that match the authority's own letter template. Letters from other bodies are skipped for now, and they would need a different approach.
  • The pipeline has no automated tests. A small set of sample folders with known results would catch a broken rule before it reaches the map.
  • The edit log lives both in edits.json and in the database. Making the database the only record would remove one step from every rebuild.

← Projets

GTEZ Atlas

J'ai construit une carte des demandes de terrain et des couches de la zone pour le personnel de l'autorité de la Zone économique du Triangle d'or, à partir de fichiers dispersés dans un Drive partagé.

Année
2026
Contexte
Pour l'autorité de la Zone économique du Triangle d'or
Outils
React, MapLibre, Python, PostGIS

Aperçu

L'autorité de la Zone économique du Triangle d'or reçoit des demandes de terrain à l'intérieur de la zone. Chaque demande se trouvait dans son propre dossier sur un Google Drive partagé, avec sa limite dessinée dans Google Earth sous forme de fichier KMZ et ses lettres numérisées en PDF. L'autorité tenait aussi un fichier Google Earth de travail avec ses propres couches, comme les routes, les réseaux et l'usage prévu des terrains. Personne ne pouvait voir tout cela sur une seule carte, ni savoir rapidement quelles demandes se chevauchaient ou sortaient de la zone.

J'ai construit GTEZ Atlas pour régler ce problème. C'est un outil interne pour le personnel de l'autorité, et il a deux parties.

  • Une chaîne de traitement en Python qui lit le Drive et le fichier Google Earth, les nettoie et construit un seul jeu de données.
  • Une application React qui affiche ce jeu de données sur une carte, avec une recherche, un tableau de bord, un tableau des demandes, des rapports imprimables pour le conseil et un éditeur.

L'interface fonctionne en arabe et en anglais, puisque les fichiers d'origine et la plupart du personnel sont en arabe. J'ai écrit la première version sous forme d'une seule page HTML avec Leaflet, puis j'ai passé la carte à MapLibre et refait l'interface en React.

Fonctionnement

Un seul script, tools/refresh.sh, reconstruit tout dans l'ordre.

  • Le robot liste les dossiers du Drive par la vue publique de Google, 16 à la fois, et écrit un manifeste de chaque fichier avec son chemin et sa date. Il télécharge ensuite seulement les fichiers KMZ et KML qu'il n'a pas encore.
  • Un convertisseur lit chaque KMZ, trouve chaque repère et écrit les points, les lignes et les polygones dans un seul fichier GeoJSON.
  • Le script du registre regroupe ces formes en demandes d'après la structure des dossiers, et lit le type et la date de la demande dans les noms de dossiers en arabe.
  • Shapely calcule ensuite la part de chaque demande qui se trouve dans la limite de la zone, et la surface exacte où deux demandes se chevauchent. Un index STRtree évite de comparer toutes les paires.
  • Chaque PDF est lu sur le Mac avec le framework Vision d'Apple, en arabe et en anglais, donc aucun document ne part vers un service en ligne. Des règles extraient ensuite les décisions et les dates des lettres types de l'autorité.
  • Le résultat est chargé dans PostgreSQL avec PostGIS. Un petit serveur Python est le seul moyen d'y accéder. Il vérifie les connexions, donne un rôle à chaque personne et enregistre chaque requête dans un journal d'accès.

L'application React ne contient elle-même aucune donnée. Elle se connecte à ce serveur et y charge les jeux de données.

Choix de conception

  • Les corrections vont dans un journal, pas dans la source. Le personnel corrige les noms, les catégories et les statuts dans l'éditeur, et chaque changement est ajouté à edits.json comme une entrée numérotée, avec une date et un nom. Rien n'est écrasé, donc le fichier est son propre historique. Les entrées récentes l'emportent sur les anciennes, et désactiver une entrée l'annule. La chaîne applique le journal à chaque reconstruction, donc un nouveau passage sur le Drive ne perd jamais une correction.
  • Des identifiants stables tirés du contenu. Les formes reçoivent un identifiant calculé à partir du nom de leur dossier et de leur contour, donc une correction vise toujours la bonne forme après un changement sur le Drive.
  • Une reconstruction pour plusieurs corrections. Le serveur attend 4 secondes après le dernier changement avant de reconstruire, donc une série de corrections ne coûte qu'une reconstruction.
  • La limite est une décision enregistrée. Le contour qui compte comme limite officielle de la zone est lui-même une entrée du journal. Quand il change, la chaîne archive l'ancien et écrit la liste de toutes les demandes dont la position a changé, avec la façon d'annuler.
  • L'application est publiée sans données. La version hébergée ne contient que le code. Les données restent dans la base sur la machine de l'autorité et ne sont envoyées qu'après connexion, avec une vue réduite pour les comptes de démonstration.
  • Tout tourne en local. L'OCR, l'analyse et la base de données tournent sur un seul Mac.

Problèmes rencontrés

La plus grande partie du travail a été de nettoyer des données classées à la main pendant plusieurs années.

  • Le Drive n'avait pas de structure fixe. Une demande pouvait se trouver à deux ou trois niveaux de dossiers, être répartie sur deux dossiers, ou contenir une copie de la limite d'une autre demande pour référence. Le script du registre fusionne les dossiers qui partagent un numéro de demande et retire les limites copiées en comparant un hachage de leurs coordonnées arrondies.
  • Le fichier Google Earth contenait des copies de lui-même, des hachures de CAO dessinées en centaines de petites lignes, et la visite d'exemple de Google. Le script de nettoyage retire les doublons, fusionne les hachures en contours, garde seulement les formes situées dans un rectangle autour de l'Égypte, et simplifie le reste pour qu'un navigateur puisse le dessiner.
  • Le texte arabe n'était pas uniforme. Le même mot apparaissait avec différentes formes d'alif, avec ou sans voyelles courtes, et avec des chiffres arabes ou occidentaux. La recherche normalise tout cela avant de comparer, et l'outil de saisie de coordonnées de l'éditeur lit les chiffres et les virgules arabes, ainsi que l'UTM, les degrés décimaux et les degrés, minutes et secondes.
  • L'OCR lisait mal les années écrites à la main, donc l'analyse des lettres rejette toute date hors d'une plage plausible.
  • Leaflet se figeait après chaque zoom quand beaucoup de couches étaient affichées. MapLibre dessine sur le GPU, mais sa propre recherche d'objet renvoyait le mauvais repère pendant l'animation des repères, donc j'ai écrit mon propre test de clic.

Résultats

Tout le jeu de données se reconstruit maintenant depuis le Drive en une seule commande, et les corrections du personnel survivent à chaque reconstruction.

  • Le personnel voit toutes les demandes et les couches de l'autorité sur une seule carte, peut chercher dans les noms et dans le texte des lettres numérisées, et peut ouvrir une demande pour voir sa position par rapport à la zone et à ses voisines.
  • Les chevauchements entre demandes sont calculés comme de vraies formes, donc la carte peut dessiner la surface commune au lieu de seulement signaler une paire. Les bords qui se touchent et les éclats de moins de 100 m² sont ignorés.
  • Les rapports du conseil s'impriment avec des cartes dessinées comme des plans d'arpentage, avec un cadre de coordonnées, une échelle, une flèche du nord, un encart de situation et des points de limite numérotés qui correspondent au tableau de coordonnées. Une seule carte cachée sert pour toutes les figures, parce qu'une page de cartes actives épuiserait les contextes WebGL.
  • D'après mes notes, la version Leaflet se figeait de 150 à 270 ms après un zoom. La version MapLibre ne se fige pas.
  • L'accès passe par des comptes personnels avec quatre rôles, et chaque requête vers les données est journalisée.

Pour la suite

  • La couche d'usage des terrains est tracée à partir d'images d'un plan, donc ses bords sont approximatifs. Je la remplacerais par la vraie couche SIG dès que le service SIG de l'autorité pourra l'exporter.
  • L'analyse des lettres utilise des règles fixes qui correspondent au modèle de lettre de l'autorité. Les lettres d'autres organismes sont ignorées pour l'instant, et il faudrait une autre approche pour elles.
  • La chaîne de traitement n'a pas de tests automatisés. Un petit ensemble de dossiers d'exemple avec des résultats connus repérerait une règle cassée avant qu'elle n'atteigne la carte.
  • Le journal des corrections existe à la fois dans edits.json et dans la base de données. Faire de la base le seul registre retirerait une étape à chaque reconstruction.