Offline-first hiking & tourist map for the Polish Tatras — with a real-time 3D terrain engine — built on .NET MAUI.
Real-time 3D terrain: a high-resolution PL + SK orthophoto draped over the Copernicus DEM, with named summits, depth-occluded hiking trails and roads, and per-pixel lighting.
The same engine on Android — Samsung S25 Ultra (Adreno 830, GLES 3.2). Raw OpenGL ES 3.0 draws the terrain mesh, 8 ortho cells (8192×5462 RGBA8, ~1.9 GB VRAM after mipmaps), and depth-tested trail ribbons into a 4× MSAA off-screen FBO; the resolve target is a single-sampled colour-texture FBO whose GL handle is wrapped via SKImage.FromTexture (GRBackendTexture + GRGlTextureInfo) and composed into SkiaSharp's canvas with DrawImage. That texture hand-off sidesteps Android's FBO-0 collision (where Skia's compositor would otherwise repaint its empty surface over our output) and lets the same code path drive Windows ANGLE and Android natively — no platform-specific render branch.
MapaTur is a hiking-trip companion for the Tatra mountains that runs entirely offline. Drop in any raster MBTiles archive, import a Garmin TCX track, download OSM hiking trails ahead of your trip, tap two points on the map, and the app plans an A*-optimal route along marked PTTK trails — then exports it as GPX for any GPS device.
Its standout feature is an interactive 3D terrain view: a from-scratch OpenGL ES 3.0 renderer (ANGLE → Direct3D 11 on Windows) draws a Copernicus ~30 m DEM with a real depth buffer, per-pixel lighting and MSAA, optionally draped with a high-resolution orthophoto (Polish + Slovak imagery composited across the border). Hiking trails, roads and the planned route are draped and depth-occluded by the ridges; named summits and mountain POIs are labelled. No telemetry, no accounts, no ads.
| Feature | Status | Notes |
|---|---|---|
| Offline raster MBTiles rendering | ✅ Verified | Tested with Compass Kraków Tatry Polskie and synthetic demo tiles |
| TCX track import (Garmin v2 schema) | ✅ Verified | Parses Position / AltitudeMeters / HeartRateBpm; skips paused points |
| OSM hiking trail download (Overpass API) | ✅ Verified | Viewport-aware bbox query; persists to local SQLite |
| PTTK color rendering (red/blue/green/yellow/black) | ✅ Verified | Parsed from osmc:symbol tag |
| Tap-to-plan A* routing | ✅ Verified | Distance and Tobler-time cost profiles, pluggable via IEdgeCostFunction |
| Elevation profile aggregation | ✅ Verified | Min/max/ascent/descent from track points |
| GPX 1.1 export | ✅ Verified | Invariant-culture coords, elevation when present |
| Localization (PL/EN) | ✅ Verified | Auto-detects from CultureInfo.CurrentUICulture |
| Accessibility (semantic labels, AA contrast) | ✅ Verified | Screen-reader hints on toolbar; heading level on status |
| Interactive 3D terrain (GPU) | ✅ Verified | OpenGL ES 3.0 / ANGLE renderer, 24-bit depth buffer; orbit / look-around / pan, mouse + keyboard + on-screen pads — see docs/3d-terrain.md |
| High-resolution DEM terrain mesh | ✅ Verified | Copernicus GLO-30 (~30 m), tiled to beat the 16-bit index limit; hypsometric ramp + Lambert hillshade + vertical exaggeration |
| Streaming 1 m detail LOD (GUGiK NMT) | ✅ Verified | Persistent ~30 m base + a 1 m detail patch that follows the gaze (screen-space-error LOD); per-tile roughness keeps ridges/walls sharp while smooth ground coarsens, under a hard vertex budget; crack-free via skirts, planning off the UI thread |
| Depth-occluded 3D trail & route overlays | ✅ Verified | Screen-space ribbon lines, hidden behind ridges, clipped to the DEM edge |
| Named summit overlay | ✅ Verified | DEM peak detection + WGS84 gazetteer (incl. Orla Perć), published elevations, label de-collision |
| Mountain POIs (huts / shelters / chalets / viewpoints) | ✅ Verified | Overpass download; colour-coded markers + labels on 2D map and 3D view (viewpoints as a lookout-tower glyph); per-kind show/hide filter |
| Orthophoto terrain drape | ✅ Verified | Aerial imagery sampled per-pixel over the DEM — GUGiK Geoportal (PL) + ÚGKK ZBGIS (SK) composited cross-border; mipmaps + anisotropic filtering |
| Road overlay (OSM highways) | ✅ Verified | Viewport Overpass download; grey depth-tested ribbons in 3D + 2D layer, independent show/hide |
| Hillshade base layer | ✅ Verified | Multi-layer MBTiles loader + Copernicus hillshade pipeline |
| Time-of-day atmosphere | ✅ Verified | Procedural world-space sky dome, sun disc + Mie halo, aerial-perspective fog; a "Czas" slider drives a deterministic Tatra-latitude solar arc (sunrise → noon → golden hour → night), persisted |
| Procedural clouds + weather | ✅ Verified | Cirrus sky layer + "sea of clouds" inversion (peaks poke through), drifting fBm with morph; cloud-coverage + wind sliders (wind speeds drift & darkens to storm-grey); cloud altitude tracks the sun + random wander; moving cloud shadows on the terrain |
| Night refuge lights | ✅ Verified | Warm window glows switch on in huts / shelters / chalets after sunset, fading in through dusk |
| POI offline cache | ✅ Verified | Downloaded POIs persist to SQLite and re-hydrate within the DEM footprint at startup — refuges + their lights survive a restart with no re-download |
| Camera state persistence | ✅ Verified | Camera framing (target / distance / azimuth / pitch) saved per DEM and restored on reload |
| Cinematic fly-through | ✅ Verified | Scripted camera flight along the Orla Perć ridge (Zawrat → Krzyżne) on a Catmull-Rom spline, slalom over the peaks; the time-of-day sweeps into golden hour mid-flight; on-screen chrome auto-hides for a clean shot |
| GPS dot / live location | ✅ Verified | MAUI Geolocation; blue dot + accuracy halo on 2D & 3D, "Track me" toggle, PL/EN |
| Elevation-aware routing (SRTM) | ⏳ Planned | Currently routes are flat (Overpass geometry lacks ele) |
| Off-trail edges in graph | ⏳ Planned | Cost penalty exists; UI tagging gesture pending |
| Signed store builds (Play / App Store / MSIX) | ⏳ Pending | Requires signing credentials |
The 3D view is a custom real-time renderer, not an off-the-shelf 3D engine:
- OpenGL ES 3.0 on the SkiaSharp
SKGLViewcontext — on Windows ANGLE translates GLES → Direct3D 11; the same path runs natively on Android. - Texture-bridge composition — the renderer draws into an off-screen colour-texture FBO that it owns; the texture handle is wrapped via
SKImage.FromTexture(GRBackendTexture+GRGlTextureInfo) and composed by Skia withDrawImage. Sidesteps Android's FBO-0 collision and unifies the Windows / Android render path (no#ifbranch in the renderer). - 24-bit depth buffer for hardware occlusion — no painter's algorithm, correct from any angle, full DEM resolution.
- Tiled mesh (≤65 536-vertex tiles) built from a Copernicus GLO-30 (~30 m) DEM, with adjustable vertical exaggeration.
- Streaming level-of-detail (Model 1) — over the persistent ~30 m base, a GUGiK NMT 1 m detail patch streams to the look-at point (raycast through the screen centre, not the camera). The window is split into a grid and each tile's resolution is chosen by screen-space error × terrain roughness (local curvature measured at ridge scale): sharp ridges/walls hold full 1 m detail from farther out while smooth valleys step down, all under a hard vertex budget for stable FPS, with skirts hiding the seams between resolutions. The whole plan + mesh build runs on a background thread so flying never stutters, and rich on-device telemetry (per-tile step histogram + timings) drives the tuning.
- Per-pixel lighting (Lambert shading evaluated per fragment from interpolated normals) and 4× MSAA for smooth slopes and ridgelines.
- Orthophoto drape (optional): a high-resolution aerial image sampled per-pixel over the terrain, with mipmaps + anisotropic filtering; falls back to a hypsometric ramp + hillshade when no image is bundled.
- Trails, roads & route as depth-tested screen-space ribbons (occluded by ridges, clipped to the DEM); named summits and mountain POIs with de-cluttered labels (2D overlay drawn by Skia over the GL terrain).
- Procedural atmosphere driven by a single
Atmosphere(timeOfDay, cloudiness, wind)model: a world-space sky dome (gradient + sun disc + Mie halo), aerial-perspective distance fog, coloured sun/shadow lighting on the terrain, cirrus + a "sea of clouds" inversion layer, live weather (drifting/morphing coverage, wind speed + storm-darkening), sun-tracking cloud altitude, moving cloud shadows, and warm night lights in refuges after dusk. Time / cloud / wind sliders, all persisted. - Camera: in-place look-around (tilt) / pan / zoom / altitude via on-screen hold-to-repeat pads that fade out at rest and materialise on hover/press (plus mouse + keyboard on desktop); framing persists per DEM; auto-falls-back to a Skia software renderer on any GL failure, so the view never breaks.
- Cinematic fly-through: a one-tap scripted flight along the Orla Perć ridge — a Catmull-Rom spline through DEM-sampled waypoints, weaving slalom over the summits at constant speed, with the time-of-day sweeping into golden hour and all on-screen chrome auto-hiding for a clean cinematic shot.
Full write-up: docs/3d-terrain.md.
Clean Architecture with five projects + five matching test projects:
src/
├── MapaTur.Domain GeoPoint, Trail, Track, Route, ElevationProfile, DemRaster, MountainPoi, …
├── MapaTur.Application use cases + ports + 3D terrain math (Camera3D, TerrainMesh3D, projections)
├── MapaTur.Infrastructure SQLite, HTTP (Overpass), TCX parser, GPX writer, DEM reader
├── MapaTur.Routing TrailGraph, AStarRouter, Tobler hiking function
└── MapaTur.App MAUI: MapPage + view model, OpenGL ES terrain renderer, DI bootstrap
tests/ 880+ unit + integration tests (xUnit + FluentAssertions + FsCheck)
testdata/ sample-tatry.tcx, overpass-tatry-sample.json, demo MBTiles, DEM generators
docs/
├── adr/ architecture decision records (MADR format)
├── 3d-terrain.md 3D GPU renderer overview
├── ROADMAP.md milestone-tracked feature plan
└── PRIVACY.md what runs locally vs. on network
Dependency direction is inward only: App → Application → Domain, Infrastructure → Application → Domain, Routing → Domain. See docs/adr/0001-clean-architecture.md.
| Concern | Choice | Rationale |
|---|---|---|
| UI framework | .NET MAUI (.NET 10) | One codebase across Android / iOS / Windows / macOS |
| 2D map rendering | Mapsui + BruTile | Cross-platform 2D map, SkiaSharp-backed |
| 3D terrain rendering | Custom OpenGL ES 3.0 renderer (Silk.NET bindings, ANGLE/D3D11) on SKGLView |
GPU depth buffer + shaders; Skia stays for 2D overlays |
| Elevation data | Copernicus DEM GLO-30 (~30 m) → custom .dem binary |
Tiled terrain mesh, generated offline by a Python script |
| Geometry | NetTopologySuite | Industry-standard topology operations |
| Storage | SQLite (Microsoft.Data.Sqlite + BruTile.MbTiles) | Embedded, file-based, no server |
| Routing | Custom A* with pluggable cost functions | Tobler hiking function for hiker-accurate ETA |
| MVVM | CommunityToolkit.Mvvm source generators | [ObservableProperty], [RelayCommand] |
| DI | Microsoft.Extensions.DependencyInjection | Built into MAUI |
| Logging | Serilog | Rolling file sink, exe-relative path |
| Tests | xUnit + FluentAssertions + NSubstitute + FsCheck | Property-based tests for parser/router |
See docs/adr/0002-tech-stack.md for alternatives considered.
- .NET 10 SDK
- MAUI workload:
dotnet workload install maui(ormaui-windows maui-androidfor selective) - A raster MBTiles archive for your region of interest
# Restore + build + test
dotnet build
dotnet test
# Run the Windows desktop variant
dotnet build src/MapaTur.App/MapaTur.App.csproj -f net10.0-windows10.0.19041.0
./src/MapaTur.App/bin/Debug/net10.0-windows10.0.19041.0/win-x64/MapaTur.App.exe- Wczytaj MBTiles (Open MBTiles) → pick a
.mbtilesraster archive. The map zooms to its extent. - Pobierz szlaki (widok) (Download Trails) → fetches OSM hiking relations intersecting the visible bbox via Overpass; renders them in PTTK colors and stores them in
<exe>/data/mapatur-trails.db. - Tap the map twice to set origin and destination — the A* router computes a route over the trail graph; status shows distance / ascent / ETA.
- Eksportuj GPX (Export GPX) → writes a GPX 1.1 file to
<exe>/exports/mapatur-route-YYYYMMDD-HHMMSS.gpx. - Wczytaj TCX (Open TCX) → render a previously recorded Garmin track on the same map.
A synthetic demo MBTiles archive lives at testdata/maps/tatry-demo.mbtiles — generated by generate-tatry-demo.py if you need to regenerate.
- Compass Kraków — paid raster archives for Polish hiking regions (verified compatible)
- MapTiler — global vector + raster downloads (raster only for MapaTur)
- Build your own from Geofabrik PBF + tilemaker — full offline control
Vector MBTiles (PBF tile payloads) are not supported; MapaTur consumes raster PNG/JPG tiles only.
UI strings are sourced from Resources/Localization/AppResources.resx (English, default) and AppResources.pl.resx (Polish). The host OS culture decides which loads at startup. Adding a language: create AppResources.<culture>.resx and add the matching keys.
MapaTur sends no telemetry, has no analytics, no user accounts, no advertising. The only outbound network request is the Overpass trail download you explicitly trigger. Full policy in docs/PRIVACY.md.
dotnet test| Suite | Tests | Focus |
|---|---|---|
MapaTur.Domain.Tests |
134 | Value objects, aggregates (Route), elevation math, DEM (+ crop), POI tags + colours |
MapaTur.Application.Tests |
651 | Overpass queries (trails/POI/roads), 3D terrain math + camera + atmosphere, screen-space LOD + per-tile roughness planner + vertex budget + normal smoothing, route planner + use cases |
MapaTur.Infrastructure.Tests |
86 | TCX/Overpass/POI/road parsers, MBTiles + DEM readers, SQLite (trails/climbing/POI), GPX |
MapaTur.Routing.Tests |
22 | Tobler function, distance/time cost functions, graph snapping, A* correctness |
| Total | 893 | xUnit + FluentAssertions + NSubstitute + FsCheck |
Milestones tracked in docs/ROADMAP.md. Initial milestones (M0–M6), hillshade (M7), climbing POIs (M8), the 3D terrain GPU engine (M9) and the streaming 1 m detail LOD with per-tile roughness are complete and verified live on real Tatra data (Samsung S25 Ultra). Active line of work: pre-bundled offline trail dataset, elevation-aware routing, and signed store builds.
Issues and pull requests are welcome at github.com/Jakub-Syrek/MapaTur. Style and quality requirements:
- English-only code, comments, and commit messages
- Conventional Commits (
feat:,fix:,perf:,refactor:,test:,docs:,chore:) - JSDoc-style XML doc comments on every public member
- SOLID + Clean Architecture dependency direction respected
- Tests for every behaviour change; no
TreatWarningsAsErrors=false - Analyzer noise resolved (NetAnalyzers + Roslynator both enabled at
latest-recommended)
- OpenStreetMap contributors — trail & POI data
- Overpass API — OSM query endpoint
- Copernicus DEM GLO-30 (ESA / AWS Open Data) — elevation model for the 3D terrain
- Mapsui — 2D map rendering library
- SkiaSharp — graphics backend + GL surface host
- Silk.NET — OpenGL ES bindings; ANGLE — GLES→Direct3D translation
- Compass Kraków — Polish Tatry raster MBTiles tested against
- PTTK — Polish Tourist and Sightseeing Society, originators of the red/blue/green/yellow/black trail-marking convention
Copyright (c) Jakub Syrek. All rights reserved.

