mini-macau
# Mini Map Macau ππβοΈπ₯οΈ > **[mini-map-macau.app](https://mini-map-macau.app/)** [](https://mini-map-macau.app/) [](https://github.com/asdfghj1237890/mini-macau/tags) [](https://github.com/asdfghj1237890/mini-macau/actions/workflows/deploy.yml) [](https://github.com/asdfghj1237890/mini-macau/actions/workflows/update-flights.yml) [](https://github.com/asdfghj1237890/mini-macau/actions/workflows/update-ferry-schedules.yml) [](./LICENSE) [](https://react.dev/) [](https://www.typescriptlang.org/) [](https://vitejs.dev/) [](https://maplibre.org/) 3D visualization of Macau's public transit, ferry, and aviation system, inspired by [Mini Tokyo 3D](https://minitokyo3d.com) and [Mini Taiwan](https://mini-taiwan-learning-project.itsmigu.com/). Visualizes the **Macau Light Rapid Transit (LRT)**, **bus network**, **HKβMacau ferry routes**, and **MFM airport flights** on an interactive 3D map. Vehicles move along actual geometry in a **timetable-driven simulation**, with an **opt-in RT mode** that replaces simulated bus positions with live DSAT realtime data. > **How "live" is this?** See [Data freshness & update strategy](#data-freshness--update-strategy) for a per-layer breakdown β LRT / buses / flights / ferries each sit at a different point on the simulated-to-live spectrum.  [](https://github.com/asdfghj1237890/mini-macau/releases/download/readme-assets-v1/demo-01.mp4) [](https://github.com/asdfghj1237890/mini-macau/releases/download/readme-assets-v1/demo-02.mp4) ## Contents - [Features](#features) - [Architecture](#architecture) - [Tech Stack](#tech-stack) - [Getting Started](#getting-started) - [Data Pipeline](#data-pipeline) - [Data Sources](#data-sources) - [Data freshness & update strategy](#data-freshness--update-strategy) - [Project Structure](#project-structure) - [Performance Notes](#performance-notes) - [Acknowledgements](#acknowledgements) - [License](#license) ## Features - **3D LRT vehicles** β 3 lines, 15 stations, real track geometry and elevated viaducts - **3D Bus fleet** β 92 routes, road-snapped via OSRM, with accurate cross-harbour bridge geometry - **3D Aircraft** β 176 real MFM flights (87 dep + 89 arr) with detailed airplane models, apron stands, and taxi paths - **3D Ferries** β 6 HK/Shenzhen β Macau sea routes (TurboJET + CotaiJet) with jetfoil-shaped hull, red belly belt, and multi-deck cabin - **Timetable-driven simulation** β Schedule-synced playback with ETAs, service status, and trilingual labels (EN / ηΉδΈ / PT) - **RT mode (opt-in)** β Toggle replaces simulated bus positions with the DSAT live feed; simulation stays active for LRT / flights / ferries - **Time controls** β Play/pause, 1Γβ60Γ speed, jump-to-now, free date/time picker - **Vehicle tracking** β Click-to-follow with smooth camera and free zoom/pan <details> <summary><strong>Full feature list</strong></summary> - **3D LRT vehicles** β All 3 lines (Taipa, Seac Pai Van, Hengqin) with 15 stations, rendered as 3D models with real track geometry and elevated viaducts - **3D Bus fleet** β 92 routes with road-snapped paths via OSRM, including accurate bridge geometry (MacauβTaipa bridges) - **3D Aircraft** β 176 real MFM airport flights (87 departures + 89 arrivals) with detailed airplane models (fuselage, swept wings, vertical tail in airline colors, engine nacelles, window rows, cockpit windshield); aircraft park at 12 apron stands before departure and taxi along waypoint paths before takeoff - **Landing & holding patterns** β Aircraft approach from North or South with multi-waypoint landing routes; when the runway is occupied, arriving flights enter a realistic circular holding pattern above the airport and smoothly transition back to the landing route when clear - **3D Ferries** β 6 sea routes (Hong Kong Outer Harbour / Taipa / Sheung Wan, HKIA, Shenzhen Airport, Shekou) served by TurboJET and CotaiJet, rendered as jetfoil models (pontoon hull, red belt, white TurboJET band, cabin, windows, wheelhouse, roof) following great-circle paths with wake-aware headings - **Real-time simulation** β Vehicles move along routes based on timetables, service frequencies, and schedule types (MonβThu / Friday / SatβSun) - **ETA & vehicle info** β Click any vehicle or station to see live ETAs, next arrivals, route details, and service status - **Flight info** β Click any aircraft to see flight number, airline, destination/origin (with localized names), scheduled time, aircraft type, and live/sim status - **Ferry info** β Click any ferry to see operator, route, origin/destination port (localized), scheduled departure, crossing time, and live progress - **Automated ferry data** β GitHub Actions workflow scrapes TurboJET and CotaiJet timetables monthly and commits updated schedules if changed - **Time controls** β Play, pause (spacebar), speed up (1Γβ60Γ), jump to current time, or pick any date/time with the DateTimePicker; Esc toggles the sidebar menu - **Vehicle tracking** β Click a vehicle to follow it with smooth camera animation; freely zoom/pan while tracking - **Route visibility** β Toggle individual bus routes by group (Peninsula, Cross-Harbour, Taipa/Cotai, Night, Special); auto-mode shows only routes currently in service - **3D/2D toggle** β Switch between perspective and top-down views - **Dark/Light mode** β Two map styles (CARTO Dark Matter / Positron) - **Trilingual UI** β English / ηΉι«δΈζ / PortuguΓͺs β flight destinations, station names, and all labels switch with the language - **Cyberpunk-styled menu** β Hamburger menu with Orbitron-font title and gradient branding - **Responsive mobile UI** β Hamburger menu for map controls, compact legend buttons (LRT/Bus), optimized touch layout with safe-area support - **Lazy loading** β Code-split panels (VehicleInfoPanel, StationInfoPanel, FlightInfoPanel) for fast initial load - **Automated flight data** β GitHub Actions workflow syncs MFM flight schedules from the [AviationStack](https://aviationstack.com/) API daily </details> ## Architecture Three clean stages: upstream sources get normalized by Python into versioned static JSON, which the browser runtime replays on a simulated clock. RT mode adds a parallel live-feed path for buses only. ```mermaid flowchart LR subgraph Sources["External sources"] OSM[OpenStreetMap] MLM[MLM LRT timetables] DSATFreq[DSAT bus frequencies] AS[AviationStack API] TJ[TurboJET Β· CotaiJet] DSATrt[DSAT realtime feed] end subgraph Pipeline["Python pipeline Β· GitHub Actions"] Scripts["data/scripts/*.py<br/>(manual regen)"] CronF["update-flights.yml<br/>(daily)"] CronFerry["update-ferry-schedules.yml<br/>(monthly)"] end subgraph Static["Bundled static JSON (public/data/)"] J1[lrt-lines Β· stations Β· trips] J2[bus-routes Β· bus-stops] J3[flights.json] J4[ferry-schedules.json] end subgraph Runtime["Browser runtime"] Sim["Simulation engine<br/>timetable-driven playback"] RT["RT client<br/>/api/dsat/batch Β· 8s cache"] UI[MapLibre layers + React UI] end OSM --> Scripts MLM --> Scripts DSATFreq --> Scripts AS --> CronF TJ --> CronFerry Scripts --> J1 Scripts --> J2 CronF --> J3 CronFerry --> J4 J1 --> Sim J2 --> Sim J3 --> Sim J4 --> Sim DSATrt -.->|opt-in RT toggle| RT Sim --> UI RT --> UI ``` ## Tech Stack | Layer | Technology | |-------|-----------| | Frontend | React 19, TypeScript 6, Vite 8 | | 3D Map | MapLibre GL JS, custom WebGL fill-extrusion layers | | Geo utilities | Turf.js (nearest-point-on-line) + custom precomputed-polyline cache | | Styling | Tailwind CSS v4 | | Fonts | Orbitron, JetBrains Mono, Noto Sans HK (Google Fonts) | | Data pipeline | Python 3.13+, uv, OpenStreetMap Overpass API, OSRM | | Flight data | [AviationStack API](https://aviationstack.com/) (daily sync) | | Ferry data | [TurboJET](https://www2.turbojet.com.hk/) + [CotaiJet](https://www.cotaiwaterjet.com/) timetables (monthly web scraper) | | Deployment | Cloudflare Pages (via GitHub Actions) | | Analytics | Google Analytics (gtag.js) | ## Getting Started ### Prerequisites - [Node.js](https://nodejs.org/) 20+ - npm - [uv](https://docs.astral.sh/uv/) (for data pipeline only) ### Install & Run ```bash npm install npm run dev ``` The app will be available at `http://localhost:5173`. ### Build for Production ```bash npm run build npm run preview ``` ## Data Pipeline Transit data is pre-generated and included in `public/data/`. <details> <summary><strong>Regenerate transit data</strong></summary> ```bash cd data # Set up Python environment uv sync # Run all data extraction scripts uv run python main.py ``` This will: 1. Extract LRT track geometry from OpenStreetMap (`railway=light_rail` ways) 2. Extract bus routes and stops from OpenStreetMap + [motransportinfo.com](https://www.motransportinfo.com) reference data 3. Fetch bridge approach geometry for accurate cross-harbour routing 4. Snap bus routes to roads via OSRM with custom bridge geometry patching 5. Generate timetables based on published service frequencies 6. Output JSON files to `data/output/` Then copy the output to `public/data/`. </details> <details> <summary><strong>Flight data sync</strong></summary> Flight schedules are fetched from the [AviationStack](https://aviationstack.com/) API and stored as a static JSON file: ```bash cd data # Fetch today's MFM flights (requires API key) AVIATIONSTACK_API_KEY=your_key uv run python scripts/fetch_flights.py # Fetch a specific date AVIATIONSTACK_API_KEY=your_key uv run python scripts/fetch_flights.py 2026-04-19 ``` The sync: - Pulls arrivals and departures for MFM (IATA: `MFM`) from the AviationStack flights endpoint - Filters by the target date's active schedule - Validates aircraft type codes (ICAO format like A320, B738) - Outputs `public/data/flights.json` with times in Macau local (UTC+8) This is also automated via GitHub Actions (`.github/workflows/update-flights.yml`), which runs daily at 04:00 Macau time (UTC+8) and commits updated flight data if changed. </details> <details> <summary><strong>Ferry schedule scraper</strong></summary> Ferry timetables are scraped from the operator sites and stored as a single static JSON file with 6 routes across two operators (TurboJET and CotaiJet): ```bash cd data # Scrape the current month's schedules for all routes uv run python scripts/fetch_ferry_schedules.py ``` The scraper: - Pulls TurboJET schedules for Hong Kong (Outer Harbour), Hong Kong (Taipa), HKIA, Shenzhen Airport, and Shekou - Pulls CotaiJet schedule for Hong Kong (Sheung Wan) β Macau Taipa - Records `fetchedAtUtc` and `effectiveAs` metadata so stale data is easy to spot - Outputs `public/data/ferry-schedules.json` Automated via GitHub Actions (`.github/workflows/update-ferry-schedules.yml`), which runs on the 1st of each month at 00:00 UTC (08:00 Macau) and commits updates if changed. </details> ## Data Sources - **LRT tracks & stations** β [OpenStreetMap](https://www.openstreetmap.org/) (railway=light_rail relations) - **LRT timetables** β [MLM ζΎ³ιθΌθ»θ‘δ»½ζιε ¬εΈ](https://www.mlm.com.mo/) official per-station timetable publications (Taipa / Seac Pai Van / Hengqin lines) - **Bus routes & stops** β OpenStreetMap + [motransportinfo.com](https://www.motransportinfo.com) curated stop data - **Road-snapped routes** β [OSRM](http://project-osrm.org/) with custom bridge approach geometry - **Bus timetables** β Based on published DSAT service frequencies - **Flight schedules** β [AviationStack API](https://aviationstack.com/) (MFM arrivals + departures) - **Ferry schedules** β [TurboJET](https://www2.turbojet.com.hk/zh-tw/%E6%B5%B7-%E8%88%B9/) + [CotaiJet](https://m.cotaiwaterjet.com/hk/ferry-schedule/hongkong-macau-taipa.html) official monthly timetables ## Data freshness & update strategy Not every layer is equally "live." The default view is **fully simulated**; RT mode is the only path that touches an actual realtime feed, and even then only for buses. | Layer | Mode | Source | Refresh cadence | Staleness indicator | |-------|------|--------|-----------------|---------------------| | **LRT** | Simulated | OSM geometry + MLM published per-station timetable | Manual regen (`uv run python data/main.py`) | None β static JSON | | **Bus (default)** | Simulated | OSM geometry + DSAT published service frequencies | Manual regen | None β static JSON | | **Bus (RT toggle)** | **Live** | DSAT realtime feed via nginx `/api/dsat/batch` proxy | Client polls every 15 s Β· server edge-caches 8 s | Per-bus `lastAt`; stale beyond 60 s window | | **Flights** | Static daily sync | [AviationStack API](https://aviationstack.com/) | Daily at 04:00 Macau time β `update-flights.yml` | `fetchedAtUtc` embedded in `flights.json` | | **Ferries** | Static monthly sync | TurboJET + CotaiJet timetable pages (scraped) | 1st of month Β· `update-ferry-schedules.yml` | `fetchedAtUtc` + `effectiveAs` in `ferry-schedules.json` | **What each mode means** - **Simulated** β Vehicles are placed on pre-generated polylines and moved by the client clock using the published timetable. They don't reflect any single bus or train's actual position at that moment; they show "what the schedule says should be moving through this segment right now." - **Live (RT mode)** β The client polls DSAT's realtime endpoint (a batch fan-out proxied through nginx with an 8 s shared cache). DSAT itself only publishes current-stop, direction, and speed per plate β not continuous GPS β so the client interpolates between consecutive stop reports. RT mode is opt-in via the control-panel toggle; when off, buses fall back to the simulated timetable. - **Static sync** β A scheduled GitHub Actions job fetches upstream data and commits a new `public/data/*.json` if it changed. The app reads whatever was in the last build; there is no per-page-load fetch for flights or ferries. ## Project Structure <details> <summary><strong>File tree</strong></summary> ``` mini-macau/ βββ src/ β βββ components/ β β βββ MapView.tsx # Main map + hamburger menu β β βββ ControlPanel.tsx # Playback speed controls β β βββ TimeDisplay.tsx # Clock + DateTimePicker trigger β β βββ DateTimePicker.tsx # Date/time selection overlay β β βββ LineLegend.tsx # LRT/Bus/Flight legend (desktop + mobile) β β βββ VehicleInfoPanel.tsx # Vehicle detail + ETA β β βββ StationInfoPanel.tsx # Station detail + next arrivals β β βββ FlightInfoPanel.tsx # Flight detail panel β β βββ FerryInfoPanel.tsx # Ferry detail panel β βββ engines/ β β βββ simulationEngine.ts # Timetable-driven vehicle + flight position computation β βββ hooks/ β β βββ useSimulationClock.ts # RAF-based clock with speed/pause β β βββ useTransitData.ts # JSON data loader β βββ layers/ β β βββ Bus3DLayer.ts # 3D bus model (fill-extrusion) β β βββ LRT3DLayer.ts # 3D LRT model (fill-extrusion) β β βββ Flight3DLayer.ts # 3D airplane model (fill-extrusion) β β βββ Ferry3DLayer.ts # 3D jetfoil model (fill-extrusion, 8 layers) β β βββ VehicleLayer.ts # 2D vehicle circles + labels β βββ App.tsx # Root layout + state management β βββ main.tsx # React entry point with I18nProvider β βββ routeGroups.ts # Bus route grouping logic β βββ i18n.tsx # Internationalization (EN / ηΉδΈ / PT) β βββ types.ts # TypeScript interfaces β βββ index.css # Tailwind + MapLibre control overrides βββ public/ β βββ data/ β β βββ lrt-lines.json β β βββ stations.json β β βββ trips.json β β βββ bus-routes.json β β βββ bus-stops.json β β βββ flights.json # MFM flight schedules (with localized names) β β βββ ferry-schedules.json # TurboJET + CotaiJet monthly timetables β βββ favicon.svg β βββ icons.svg β βββ og-image.png β βββ sitemap.xml β βββ robots.txt βββ data/ β βββ scripts/ β β βββ extract_lrt_osm.py β β βββ extract_bus_data.py β β βββ fetch_bus_data.py β β βββ fetch_bridge_geometry.py β β βββ fetch_flights.py # AviationStack flight data sync (MFM) β β βββ fetch_ferry_schedules.py # TurboJET + CotaiJet monthly scraper β β βββ osrm_route.py β β βββ patch_bus_bridges.py β β βββ generate_timetable.py β βββ bus_reference/ β βββ output/ β βββ main.py βββ .github/workflows/ β βββ deploy.yml # Cloudflare Pages CI/CD β βββ docker-release.yml # Docker image release on new tag β βββ service-status.yml # Upstream service availability check β βββ update-flights.yml # Daily flight data update β βββ update-ferry-schedules.yml # Monthly ferry data update βββ index.html ``` </details> ## Performance Notes Simulating 300β400 moving vehicles at 20 Hz while MapLibre re-draws 3D extrusions every frame puts real pressure on the main thread. A few optimizations worth calling out: <details> <summary><strong>Polyline progress lookup β <code>cumKm</code> + binary search</strong></summary> The simulation asks the same question once per vehicle per tick: *given a route and a progress β [0, 1], where on the polyline is the vehicle, and which way is it facing?* The original implementation used Turf's [`along`](https://turfjs.org/docs/api/along) twice per vehicle (once for position, once for a 1-metre-ahead lookahead to derive bearing). `along` walks the coordinate array from index 0 and sums haversine distances until it reaches the target km β **O(n) haversines per call**. At ~400 vehicles Γ 2 calls Γ 20 Hz Γ 100-point routes, that worked out to roughly **12 000 full-route scans per second**, all on the main thread. Key observation: each route's geometry is immutable, so the per-segment work only needs to happen once. On first touch we cache: - `cumKm[i]` β cumulative kilometres from `coords[0]` to `coords[i]` (`Float64Array`) - `segBearing[i]` β heading of segment `coords[i] β coords[i+1]` (`Float64Array`) Per-call cost then collapses to a binary search on `cumKm` (β 8 comparisons for a 150-point route), a linear interpolation between two lat/lng pairs, and a table lookup for bearing. No trig in the hot loop, and no second `along` call since the segment index already tells us the heading. We deliberately don't cache a per-line "last index" hint: multiple vehicles share the same polyline at different progress values, so a shared hint would thrash. `O(log n)` is cheap enough that per-vehicle state isn't worth it. See [`simulationEngine.ts`](src/engines/simulationEngine.ts) (`getLineCache` / `interpolateOnLine`). </details> <details> <summary><strong>One bus-routes source instead of 92</strong></summary> MapLibre GeoJSON sources are **tiled in a web worker**: the worker clips each source's features to tile boundaries, tessellates lines into triangle strips, and ships vertex buffers back to the main thread. Originally each of the 92 bus routes was its own `addSource` + `addLayer`, meaning every zoom level change forced 92 separate `postMessage` round-trips and 92 independent tile-index rebuilds. Consolidating into a single `bus-routes` source (one tile index, one round-trip per reindex) drastically cut worker chatter during zoom. Per-route dimming β previously `setPaintProperty('bus-route-${id}', 'line-opacity', β¦)` against 92 layers β became `setFeatureState({ source: 'bus-routes', id }, { inService })` on one layer, with opacity driven by a `['case', ['==', ['feature-state', 'inService'], false], DIM, FULL]` paint expression. `setFeatureState` doesn't recompile paint; `setPaintProperty` does. </details> <details> <summary><strong>Two-tier animation throttle</strong></summary> Moving 300+ buses as 3D fill-extrusion polygons is heavy (each bus is 8 quads Γ lat/lng math). Moving them as 2D circles is almost free (just a `setData` on a Point FeatureCollection). The animate loop splits them: simulation + 2D circle updates run every 50 ms unconditionally, while 3D polygon rebuilds throttle to 160 ms whenever the map is actively moving (`movestart` / `moveend` set a `mapBusy` flag). During zoom gestures the 2D layer keeps vehicles visibly moving at full cadence while the expensive 3D rebuild backs off, leaving MapLibre's own render pipeline more time to finish zoom frames. </details> <details> <summary><strong>Decouple zoom display from React re-renders</strong></summary> The zoom indicator in the HUD used to be a `useState`, so every `map.on('zoom', β¦)` event caused `<MapView>` to re-render β which is a *huge* component with map refs, ETA panels, and layer toggles. Now zoom lives in an external store read via [`useSyncExternalStore`](https://react.dev/reference/react/useSyncExternalStore), and only a tiny `<ZoomText>` leaf subscribes. The rest of `<MapView>` stays stable during pinch/scroll zoom. </details> ## Acknowledgements <details> <summary><strong>Inspiration</strong></summary> - [Mini Tokyo 3D](https://github.com/nagix/mini-tokyo-3d) β Original inspiration for the concept - [Mini Taiwan](https://mini-taiwan-learning-project.itsmigu.com/) β Sister project inspiration </details> <details> <summary><strong>Data sources</strong></summary> - [OpenStreetMap](https://www.openstreetmap.org/) β LRT track geometry, bus routes, and stop locations - [MLM ζΎ³ιθΌθ»θ‘δ»½ζιε ¬εΈ](https://www.mlm.com.mo/) β Official per-station LRT timetables (used to hand-transcribe `data/scripts/generate_timetable.py` for the Taipa / Seac Pai Van / Hengqin lines) - [MoTransport Info](https://motransportinfo.com/zh/search) β Curated Macau bus stop reference data - [DSAT 巴士θ³θ¨](https://bis.dsat.gov.mo/macauweb/index.html?language=zh-tw&fromDzzp=false) β Official Macau bus realtime feed (live bus positions in RT mode) - [AviationStack](https://aviationstack.com/) β MFM flight schedule data (arrivals + departures) - [TurboJET](https://www2.turbojet.com.hk/) β Ferry timetable (Hong Kong, HKIA, Shenzhen Airport, Shekou routes) - [CotaiJet](https://www.cotaiwaterjet.com/) β Ferry timetable (Hong Kong β Macau Taipa route) </details> <details> <summary><strong>Libraries, tiles, and fonts</strong></summary> - [MapLibre GL JS](https://maplibre.org/) β Open-source map rendering - [CARTO](https://carto.com/) β Basemap tiles (Dark Matter / Positron) - [OpenFreeMap](https://openfreemap.org/) β 3D building tiles - [OSRM](http://project-osrm.org/) β Road routing engine - [Turf.js](https://turfjs.org/) β Geospatial analysis - [Google Fonts](https://fonts.google.com/specimen/Orbitron) β Orbitron, JetBrains Mono, Noto Sans HK </details> ## License [MIT](./LICENSE)