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
STRtreeindex 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.jsonas 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.jsonand in the database. Making the database the only record would remove one step from every rebuild.