Maps, when your GPS is ass
  • Kotlin 98.8%
  • Nix 1.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Aleksandr 013211c103 karti: honest GPS feedback and double-tap drag zoom
The GPS button reported "жду GPS-фикс" in a two-second toast and then went
silent for as long as the platform kept searching. Progress now lives in the
status line, names the provider actually in use, times out after 45 s with a
clear message, and ignores repeat taps while a request is in flight. A fix
recenters the map instantly instead of animating across it.

Zoom also works one-handed: double-tap, hold, drag up to zoom in and down to
zoom out, anchored at the tapped point; a plain double tap still zooms a level.

Co-Authored-By: Eva
2026-08-10 16:46:06 +03:00
app karti: honest GPS feedback and double-tap drag zoom 2026-08-10 16:46:06 +03:00
.gitignore untrack root build/ directory 2026-07-28 17:16:09 +03:00
AGENTS.md karti: Yandex and Google tile providers 2026-08-10 15:10:28 +03:00
build.gradle.kts kart: step-based dead-reckoning walking navigator 2026-07-25 11:58:09 +03:00
flake.lock kart: step-based dead-reckoning walking navigator 2026-07-25 11:58:09 +03:00
flake.nix karti: rename from kart, selectable tile provider, docs 2026-07-28 17:15:41 +03:00
gradle.properties kart: step-based dead-reckoning walking navigator 2026-07-25 11:58:09 +03:00
README.md karti: honest GPS feedback and double-tap drag zoom 2026-08-10 16:46:06 +03:00
settings.gradle.kts karti: rename from kart, selectable tile provider, docs 2026-07-28 17:15:41 +03:00

karti

A pedestrian navigator for Android that tracks your position without GPS, by dead reckoning: every step moves the marker a fixed stride length along the current compass heading. The network is only needed for map tiles and address search — tracking itself works in airplane mode.

Quick start

Build and install:

git clone ssh://forgejo@git.desu.church:61488/nero/karti.git && cd karti
nix develop
gradle assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk

Use:

  1. Where am I — tap ✎ then tap the map, or tap the GPS button: a one-shot fix, progress in the status line, the map jumps straight to the point once it lands. Gives up after 45 s if nothing is caught.
  2. Where to — type an address into search, or tap ⌖ then tap the map.
  3. Check your stride length (the "Шаг" button, 0.7 m by default): enter your height and the stride is estimated as 0.414 × height, or use GPS-калибровка — walk ≥50 m steadily with GPS on, press finish, and the stride is measured for real (steps counted by the sensor, distance summed from GPS with jitter filtering; needs ≥30 steps and ≥30 m to apply).
  4. Hit start walking and go: the marker advances step by step, drawing a trail and a line to the destination.
  5. Position drifted? Re-anchor it (✎ or GPS) — accumulated drift resets.

Zoom is pinch, or one-handed: double-tap and keep the finger down on the second tap, then drag up to zoom in, down to zoom out (120 dp per level, anchored at the tapped point). A double tap without dragging zooms in a level as usual.

Map layer is switched with the ▦ button: OSM Mapnik, OpenTopoMap, Яндекс Карты/Спутник, Google Карты/Спутник/Гибрид, USGS Topo/Satellite, or your own tile server — an XYZ URL with {z}/{x}/{y} placeholders.

Paths can be recorded or drawn, then edited and exported:

  • Record: press ⏺ Записать, walk, then 💾 Сохранить — the recording becomes an editable path and a compact ASCII string of it lands in the clipboard, safe to paste into any chat (a 500 m city walk is ~38 characters; ⏺ again discards the recording).
  • Draw: the pencil-route button toggles draw mode — sketch the road with a finger; the stroke is recognized into lines and arcs, so you see clean geometry, not hand wobble. While a path exists, a stroke that starts and ends on it redraws that section; otherwise the stroke replaces the path.
  • Edit: path vertices are draggable (long-press a vertex marker, then move). The ✂ button keeps only a section: tap its first and last vertex. Long-press 💾 Сохранить to delete the path.

Offline maps — the layers dialog (▦) has two entries that remove the tile-server dependency entirely:

  • Скачать область в офлайн-архив — downloads the visible area (current zoom +4 levels, capped at z16) from the active online source into a standalone sqlite archive in app storage, then switches the map to it.
  • Скачать город заранее — type a city or district name; its bounding box comes from Nominatim, and a dialog offers z14/z15/z16 depth with a tile-count and size estimate before downloading the whole city into an archive.
  • Офлайн-архивы — pick a saved archive or import your own MBTiles/sqlite/zip/gemf file. In archive mode the app makes no network requests at all; tiles come only from the local file (zooming past the archive's levels shows blank tiles).

Requires Android 8.0+ (minSdk 26). A hardware step detector and compass are recommended; without a step detector, steps are counted from the accelerometer.

How it works

  • Steps: TYPE_STEP_DETECTOR, falling back to an accelerometer peak detector (EMA baseline, hysteresis, ≥300 ms between steps).

  • Heading: TYPE_ROTATION_VECTOR, EMA-smoothed on unit-vector components (no jump across 0°/360°). Holding the phone upright remaps the axes. Magnetic declination comes from GeomagneticField at the current position.

  • Position: DeadReckoning.kt — equirectangular approximation; its projection error is negligible at walking distances. The real error sources are the compass and stride length, hence re-anchoring.

  • Map/search: osmdroid and Nominatim. The tile provider is selectable (built-ins plus an arbitrary XYZ server), with a separate tile cache per source.

  • Yandex tiles: Yandex renders in EPSG:3395 (elliptical Mercator) while the XYZ grid is EPSG:3857 (spherical) — the same tile index is ~20 km off in latitude at 55°N. TileSources.kt therefore fetches the one or two Yandex tiles a requested tile overlaps and resamples them row by row (longitude matches, only latitude shifts); the result is cached and archived like any other tile.

  • Path format: PathCodec.kt, three layers. Geometry: points quantized to 1e-5° (~1.1 m), simplified with Douglas-Peucker, consecutive segments greedily merged into circular arcs (per-segment bulge = tan θ/4, 0 = straight). Binary: the first byte picks whichever representation is smaller — cartesian varint deltas (raw or deflated) or a polar bitstream (absolute start in 51 bits, then per segment: turn as signed Elias gamma on a 9-bit heading grid, length in meters as gamma, bulge; quantization is closed-loop, so error never accumulates). Text: Z85 base85 — printable ASCII with no space/quotes/backslash, so it pastes anywhere. Lossy within ~2 m, below dead-reckoning noise; 800 steps encode to 15 chars (straight), 38 (city walk), 90 (constant wobble); the floor is ~13 chars, almost all of it the absolute start point.

Last position, stride length, and the chosen map layer persist in SharedPreferences.

Code

File Contents
app/src/main/java/dev/nerosama/karti/DeadReckoning.kt pure dead-reckoning math
app/src/main/java/dev/nerosama/karti/PathCodec.kt path fitting (lines/arcs) + encoding
app/src/main/java/dev/nerosama/karti/TileSources.kt Google/Yandex tile sources + Yandex reprojection
app/src/main/java/dev/nerosama/karti/MainActivity.kt UI, sensors, geocoding, GPS fix, path editor
flake.nix dev environment: JDK 17, gradle, Android SDK 34