Inspiration
Navigation apps end at a pin on a road. In Doha that is the wrong place to stop.
Doha is above 40 °C for five months of the year. The part of a journey that decides your day is not the drive — it is the walk from wherever you actually parked to the door you actually need. Every mainstream app treats that walk as a rounding error, and treats where the sun is as no part of the problem at all. If you live here, that is not a minor omission. It is the whole problem.
The second reason is quieter. Navigation is the most intimate telemetry a person emits: everywhere you go, when, and how often. The industry's answer is to send it to an advertising company. I wanted to find out whether a student with one phone could build the other answer — where the server is yours and the app has no idea who you are.
And one more thing kept bothering me. Navigation apps are confidently wrong all the time. They will happily route you down a footpath that does not connect, because admitting "I don't know" is not in the product. I wanted to find out what a navigation app looks like if refusing is a first-class feature.
What it does
Vector is a native Android navigation app with a self-hostable backend. No account, no login, no analytics SDK, no ads. It declares four Android permissions — internet, network state, fine location, coarse location — and no background-location permission; the only two others in the manifest are the Car App Library's navigation-template permissions.
Driving. Turn-by-turn with voice, in Arabic and English street names. Three ranked alternatives. Surveyed traffic signals (899 catalogued nationally) and speed cameras (133) on your route. A 3D navigation camera over a national 3D basemap — 228,872 building instances across 152,029 footprints.
Walking is a real second mode, not a fallback. It has its own pedestrian graph: 1,108,269 nodes, 2,374,052 edges, 14,465 crossing facts and 8,707 barriers — of which 2,247 are locked or private gates that remove 3,874 edges, because a locked gate genuinely splits a pedestrian network.
Shade — the part I care about most. Solar position is computed from a full NOAA closed-form implementation (Julian centuries, equation of centre, nutation and aberration, the five-term equation of time, NOAA's refraction fit). It is deterministic, offline, and validated against published figures — Doha's solar-noon altitude comes out 68.4° ± 0.5. Vector uses it to draw your walking route amber where the model puts it in the sun and teal where it does not, and gives you a slider to scrub the day and watch the route recolour. Both colours are only ever drawn as an estimate: the copy cannot render a shade percentage without the word "estimated", and a test enforces that.
The last mile. A journey is drive → park → walk, composed as one object. Parking comes from a real POI corridor query, and candidates are ranked by the walk, not by distance to your car — a car park 80 m away on the far side of a compound wall is a 600 m walk. Then Vector offers you the shadiest of those walks and tells you plainly what it costs in minutes.
And it refuses. If Vector's footpath data cannot connect two points, it says so and says why: "No continuous walking route" / "The footpaths Vector knows about don't connect these two points". It will not draw a straight line, and it will not quietly fall back to car routing.
How we built it
Client: Kotlin, Jetpack Compose, MapLibre GL. Backend: Python services
behind one docker compose up — routing, geocoder, tile server, edge. Data:
OpenStreetMap and Overture, baked locally into vector tiles and two separate
routing graphs.
Two decisions shaped everything else.
1. The interesting code has no Android in it. Solar position, shade
estimation, route following, walking progress and camera policy all live in
core-geo, a pure-Kotlin module with zero Android dependencies. That is why its
675 tests need no device, no emulator and no GPU, and why the shade model can be
validated against published ephemerides in a plain JVM test. Together with the
app module's 1,115 tests, the project carries 1,790 JVM tests that pass with
zero failures, none of which needs hardware.
2. Routability is decided once, at ingestion. One classifier owns the OSM access model — car, foot, barrier — and the router reads the result rather than re-deriving it. A second classifier is exactly how a map and a router come to disagree about whether a gate is locked.
RevenueCat powers Vector Pro. The entitlement state machine (ProAccess.kt)
is pure — no Android imports, no SDK imports — so every branch is a unit test
rather than a device session: the 7-day offline grace, the rule that a known
expiry always beats the grace, relock, and the paywall-rationing gate. The
cache is seeded synchronously before the first frame, so a paying user never
sees an upsell flicker on cold start. And an unconfigured build resolves to
UNCONFIGURED, which unlocks everything and can never draw a paywall — so a
self-hosted user who never touches RevenueCat gets the whole app, and the
classic "paywall nobody can unlock" bug is structurally unreachable.
Challenges we ran into
The pedestrian graph is broken, and no amount of code fixes it. Qatar has 3,015 mapped pedestrian crossings in the entire country, and arterial roads are the connective tissue. So the walking graph is 2,811 components, the largest holding 32% of all nodes. I spent a long time trying to engineer around this before accepting it is a data problem, not an algorithm problem. What I built instead was honesty: component-aware snapping, and a typed refusal that names its reason.
Shipped is not deployed — and I found out the hard way. Four days of walking-correctness work was committed, tested and green, and not running in production. I only caught it by probing the live API rather than trusting my own notes: a 343 m walk request came back HTTP 200 with a 49 m route, having silently moved the destination 481 m. It answered a question nobody asked. After deploying, the same request returns HTTP 422: "endpoint 25.31200,51.53300 is 481 m from the nearest road (limit 400 m)" — re-checked against production on 2026-09-24. That is the single change I am most glad about.
A green test suite is not evidence a service will start. Deploying that fix
took production down. The cause: Optional[str] used without importing it. It
passed every local test because this laptop runs Python 3.14, where PEP 649
makes annotations lazy — and died instantly in the 3.11 container, where they
are not. The fix was one import. The real fix was a deploy step that
smoke-imports the built image in the target interpreter before it replaces
anything.
Wanting a feature does not make the data exist. I wanted entrance-level
arrival. Qatar has 212 entrance tags in the whole OSM extract. I wanted
traffic-light countdowns; there is no public signal-phase feed. Both were cut,
and the timing model is now a sealed type that cannot express a phase without
a freshness window — so the app is structurally unable to invent one.
Accomplishments that we're proud of
- Shade that is real physics, not a gradient. A full NOAA solar position implementation, validated to fractions of a degree, driving a visual anyone understands in three seconds.
- A navigation app that says "I don't know." The refusal path is typed, tested, and deliberately has no geometry in it at all — it cannot draw a line it does not believe in.
- 1,115 app tests + 675 core-geo tests, none needing a device. Where a test pins a defect, I verified it fails against the pre-fix code before accepting it. A test that cannot fail proves nothing.
- A smaller app, verified on hardware. Enabling R8 shrank the release APK (23.9 MB → 15.8 MB when measured; today's signed arm64 build is 17.6 MB). R8 failures are runtime, not build-time, so I installed it on the phone and drove it through search, routing, the journey card and navigation. Zero crashes, zero ANRs.
- A limitations section I am not embarrassed by. The README states the graph fragmentation, the 0.84% height coverage, and that the 3D extrusion was photographed on the handset in a deterministic simulated GPS replay rather than a real drive — positions synthesised, everything downstream of them the shipping app. Saying so is cheaper than being caught.
What we learned
That the hardest part of a data product is not the algorithm, it is deciding what you are entitled to say. Every feature I cut — signal timing, entrance arrival, shade-aware path selection — was cut for the same reason: the data could not support the claim, and dressing it up would have been the actual failure.
I also learned to distrust my own status board. "Shipped", "tested" and "deployed" are three different things, and I had been treating them as one. Probing the live system instead of reading my own notes found a class of bug I would otherwise have demoed with pride.
What's next
- An automated pixel-level check that a specific footprint's rendered roof lands where it should — today the anchoring is asserted on the data (16/16 anchor checks) and the layer graph, and the result is shown in a recording, but nothing asserts the pixel. Extrusion is photographed on real GPU silicon inside a deterministic simulated GPS replay, not from a moving car.
- Junction-aware camera framing; ramps and bridges as real 3D form.
- Pedestrian graph quality — synthesised crossings and sidewalk-implied edges. The only lever that moves 32% meaningfully.
- Genuinely shade-aware routing: an exposure penalty inside the pedestrian cost model, rather than choosing the shadiest of the walks parking already offers.
- A real store listing and a real product, so Vector Pro stops being demonstrable only through RevenueCat's Test Store.
Limitations
Vector is a real navigation app built on real data, and the data has holes in it. These are the ones that matter, with the numbers behind them. Everything below is a fact about the shipped build, not a roadmap.
The pedestrian graph is fragmented, and no amount of routing code fixes it. Qatar has 3,015 mapped pedestrian crossings in the entire country. Arterial roads are the connective tissue between neighbourhoods, and OSM has no crossing mapped on most of them. The result, measured on the deployed bake:
| the walking graph | value |
|---|---|
| nodes | 1,108,269 |
| edges | 2,374,052 |
| crossing facts | 14,465 |
| barriers | 8,707 |
| connected components | 2,811 |
| largest component's share of all nodes | 32% |
So walking is not "anywhere in Qatar". It is a real router over a genuinely disconnected graph. Vector's response is component-aware snapping (it never snaps a point onto a component that cannot reach the other end) and a typed refusal that says which of three things happened, rather than a straight line across a gap. A refusal here is not a bug being excused; it is the honest answer to a question the data cannot answer.
Shade is an estimate, not geometry. Vector computes the sun's position with a
full NOAA closed-form implementation and scores each walk segment against the
street's orientation and an assumed façade height per road class. It has no
building geometry, no tree data and no ray tracing. A façade height is assumed
because only 1,929 of 228,872 building instances (0.84%) carry an explicit
height_m in the source data — the rest have nothing to cast a shadow with. The
UI therefore cannot render a shade percentage without the word "estimated", and
a test enforces that. Read "62% in shade" as "62% of this walk runs along
streets this model expects to be shadowed at this hour", not as a measurement.
Shade does not choose the path. Vector does not do shade-aware routing — there is no exposure penalty inside the pedestrian cost model. What exists is narrower and is described narrowly: the walk is drawn amber where the model puts the walker in sun and teal where it does not; and where two or more indexed car parks serve the same destination, Vector offers the shadier of those walks, having ranked the car parks by the walk rather than by distance to the car, and tells you what the shadier walk costs in minutes. Choosing a genuinely shadier route is future work.
3D extrusion is confirmed on real hardware — in a simulated drive, not a real
one. The tile bake carries explicit height attributes for the minority of
buildings that have them, the tile bytes production serves for West Bay are
checked into this repo (docs/evidence/tiles/) so the data can be verified
independently, and a screen recording made on a Galaxy S24 Ultra shows extruded
building volumes with roof planes and wall faces at the 60° navigation camera,
drawn from live production tiles
— that recording is the demo video linked on this submission, from 0:42 to 1:12.
What that recording is not is a real-world drive. The GPS positions are a deterministic trace replayed into the device's fused location provider through Google's mock-location API; the navigation, camera, renderer and GPU are the production application, but the movement is synthesised and no one drove the route. Nobody has photographed extrusion from a moving car. This is a 3D basemap, not a height survey.
The demonstration drive is simulated, and is described that way everywhere. When this submission shows Vector navigating, the positions come from that replay. It is built to exercise the real navigation, camera-follow, maneuver and 3D rendering paths without a physical drive, and the wording used in the video and in the written submission says so rather than implying a road test. There is no automated pixel-level proof that a specific building's rendered roof lands on a specific screen pixel: such a test has to model the extrusion displacement as a function of height and pitch, and a test that got that subtly wrong would be worse than no test. What is asserted automatically is the data (every footprint georeferenced, buildings and roads in one coordinate space), the layer graph (buildings and roads share one source, so no independent transform exists) and the camera policy over a whole replay (centre on the vehicle, tilt at the documented cap, no rotation the route did not ask for). What is shown is the recording.
There is no live traffic. The traffic engine has consumed zero probes and
reports edge_count: 0. Vector routes on a static cost model. It does not know
how busy a road is right now, and it does not estimate a delay from conditions
it has not measured.
There is no signal timing and no countdown. Qatar publishes no signal-phase feed. Rather than guess, the timing model is a sealed type that cannot express a phase without a freshness window, so the app is structurally unable to invent a "green in 12 seconds". The signals Vector shows are surveyed locations — 899 catalogued nationally — and speed cameras (133), not states.
Android only. Vector is a native Android app (Kotlin, Jetpack Compose, MapLibre GL). There is no iOS build, no web build, and no watch or CarPlay equivalent. The Car App Library integration exists in the manifest but has not been validated on a real head unit — a sideloaded Car App Library app cannot appear on Android Auto regardless, so it is untested by construction.
No store listing, no users, no revenue. Vector is not on any app store. It has no users, no downloads and no revenue, and no growth metrics are claimed. Purchases are demonstrated through RevenueCat's Test Store on a debug build: those purchases are simulated, no money moves, and nothing bought there is a real subscription. The shipping release build compiles no RevenueCat key at all, which means it has no paid tier — every feature is unlocked — and it also refuses a Test Store key non-waivably, so a simulated purchase can never be mistaken for a shipping one.
Entrance-level arrival is not built. Qatar's OSM extract carries 212
entrance nodes in the whole country, which is not enough to route to a door.
Vector stops at the walk from the parking space to the point nearest the
destination it can justify.
If a claim elsewhere in this submission is not supported by the numbers above, believe the numbers.
Why Next Gen
Submitted to Next Gen as an active student ([email protected]), under the
rule that Next Gen projects are "evaluated using the demonstration video and
code repository" rather than an app-store listing.
Against the criteria:
- A clear, useful idea for its users. Last-mile navigation for a city that is above 40 °C for five months, which estimates how much of the walk from the car to the door is in direct sun. Nobody else ships it.
- Meaningful progress toward a working app. It runs on real hardware against a live backend. 1,115 app tests + 675 core-geo tests, 0 failures. The repository builds and passes them from a clean clone.
- Thoughtful use of RevenueCat. Pro is a genuine product boundary — free gets you there, Pro gets you from the car to the door — with a pure, fully-tested entitlement state machine, and a design where a missing key unlocks everything rather than locking users out.
- Technical choices, product thinking and care. 57 architecture decision
records in
vector-governance/adr/, numbered to ADR-0075; a limitations section that names real numbers; and a refusal path that is a designed feature rather than an error case.
Honest note for the judges: Vector has no app-store listing, so purchases are demonstrated through RevenueCat's Test Store on a debug build — which Next Gen's rules accommodate. Test Store purchases are simulated: no money moves, and nothing bought there is a real subscription. The release build refuses a Test Store key non-waivably, so a simulated purchase can never be mistaken for a shipping one. There are no users, no downloads, no revenue and no growth metrics to report, and I have not claimed any.
Notes for the judges
Next Gen Award entry — no store listing, per the Next Gen rules. Purchases are demonstrated through RevenueCat's Test Store on a debug build. Test Store purchases are simulated: no money moves, and nothing bought there is a real subscription. The shipping release build refuses a Test Store key non-waivably, so a simulated purchase can never be mistaken for a real one.
There are no users, no downloads and no revenue to report, and I have not claimed any.
Three things worth checking in the repository, in order:
vector-android/app/src/main/java/dev/vector/android/pro/ProAccess.kt— the entitlement state machine, pure Kotlin, no SDK and no Android imports: the 7-day offline grace, and the rule that a known expiry always beats the grace.vector-android/core-geo/— solar position and the shade estimate. The shade is a model with an assumed façade height and no building geometry; the UI cannot print a shade percentage without the word "estimated", and a test enforces that.docs/evidence/tiles/westbay-z14-*.mvtandwestbay-z15-*.mvt— the actual bytes production serves for West Bay, so the 3D basemap can be checked against the tiles rather than taken on trust.The Limitations section of the repository README states what Vector cannot do, with numbers.
Built With
- android
- docker
- gradle
- jetpack-compose
- kotlin
- mapbox-vector-tile
- maplibre
- openstreetmap
- osrm
- overture-maps
- postgis
- python
- revenuecat
- robolectric
- vector-tiles
Log in or sign up for Devpost to join the conversation.