Documentation menu
Android API reference
The public Kotlin surface of ai.mapmap:core 0.3.0, the current
release on Maven Central. Install coordinates, credentials and the
root-repository Gradle rule are on
SDKs & installation; this page is the
class-by-class reference.
Requires minSdk 26. The library is a thin, idiomatic Kotlin layer over
the Rust navigation core (libsn_nav_core.so, packaged for arm64-v8a,
armeabi-v7a and x86_64). The UniFFI-generated bindings live in
uniffi.mapmap (and uniffi.ferrostar, served by the same native
library); everything under ai.mapmap is the supported surface. Types
named below without a package (e.g. TerritoryInfo, AdrProfile,
GuidanceUpdate) are generated records/enums in uniffi.mapmap.
| Package | Classes |
|---|---|
ai.mapmap | MapMap |
ai.mapmap.territory | TerritoryStore |
ai.mapmap.routing | OfflineRouting, OfflineRouter, ValhallaMobileRouter, ValhallaConfigWriter |
ai.mapmap.adr | AdrCompliance |
ai.mapmap.nav | NavigationEngine, NavigationState, GuidanceDisplay, LocationProvider, LocationSample, AndroidLocationProvider, ReplayLocationProvider, VoiceGuidance, VoiceProsody, contributingProbe, ProbeUploader, HttpProbeUploader, ProbeUploadOutcome |
ai.mapmap.replay | DriveCorpus, DriveHeader, DriveFix, CorpusFormatException |
What 0.3.0 added over 0.2.0, all additive: offline search and a local
map style on TerritoryStore (openSearch, territoryStyle), a speaking
signal on VoiceGuidance (isSpeaking, onSpeakingChanged), and the
per-trip probe contribution tap in ai.mapmap.nav. Nothing was removed and
no existing signature changed.
ai.mapmap.MapMap
Entry object.
object MapMap {
fun ensureLoaded()
fun defaultGuidanceConfig(): GuidanceConfig
fun defaultAdrProfile(): AdrProfile
}
ensureLoaded(): eagerly load the native core and verify the bindings match it. Optional (the first native call loads lazily), but calling it at app start surfaces packaging problems as an immediateUnsatisfiedLinkErrorinstead of a mid-drive crash.defaultGuidanceConfig(): aGuidanceConfigwith the core's defaults.defaultAdrProfile(): anAdrProfilewith the core's defaults: EU maximum authorised dimensions (Council Directive 96/53/EC), no dangerous goods.
ai.mapmap.territory.TerritoryStore
Coroutine-friendly store of signed offline territory packages, wrapping
the core's TerritoryManager. Every package is ed25519-signature and
BLAKE3-layer-hash verified before promotion; verification is fail-closed
(a package that fails leaves no trace on disk). All calls hop to the
constructor's dispatcher (Dispatchers.IO by default); installs read
whole archives and must never run on the main thread. Implements
AutoCloseable.
class TerritoryStore(
rootDir: File,
verifyingKeyHex: String, // factory public key, 64 hex; pin at build time
dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : AutoCloseable {
val territories: StateFlow<List<TerritoryInfo>>
suspend fun refresh(): List<TerritoryInfo>
suspend fun install(snpkg: File): TerritoryInfo
suspend fun install(snpkg: File, cancel: CancelToken): InstallOutcome
suspend fun installDirectory(packageDir: File): TerritoryInfo
suspend fun installDirectory(packageDir: File, cancel: CancelToken): InstallOutcome
suspend fun setActive(territoryId: String)
suspend fun active(): String?
suspend fun activeTerritory(): TerritoryInfo?
suspend fun layerPath(territoryId: String, kind: MapLayerKind): File
suspend fun territoryStyle(
territoryId: String,
theme: StyleTheme = StyleTheme.LIGHT,
): String
suspend fun openSearch(territoryId: String): TerritorySearch
suspend fun remove(territoryId: String)
suspend fun planUpdate(newManifestJson: String): UpdatePlan
suspend fun checkForUpdate(
territoryId: String,
indexJson: String,
indexSigBase64: String,
fetcher: LayerFetcher,
): AvailableUpdate?
suspend fun applyUpdate(
territoryId: String,
newManifestJson: String,
newManifestSigBase64: String,
fetcher: LayerFetcher,
): UpdateResult
suspend fun applyUpdate(
territoryId: String,
newManifestJson: String,
newManifestSigBase64: String,
fetcher: LayerFetcher,
cancel: CancelToken,
): UpdateOutcome
override fun close()
}
-
territories: hotStateFlowof the installed set; refreshes after every mutating call and onrefresh(). -
install(snpkg): install a packed.snpkgarchive; throwsNavCoreException.Verificationif the signature or any layer hash does not verify.installDirectoryinstalls an unpacked package directory under the same rules. -
layerPath(id, kind): absolute path of one layer, e.g.MapLayerKind.ValhallaTilesfor the on-device router,MapLayerKind.Pmtilesfor MapLibre. -
territoryStyle(id, theme): a complete MapLibre style JSON for an installed territory with local references, mirroring the web SDK'sbuildStyle(). The tile source is apmtiles://<absolute path>URL pointing at the installed layer, so the map renders with the radios off. A package with no baked style layer gets the default MapMap theme compiled on the fly.StyleThemeisLIGHTorDARK. -
openSearch(id): opens the territory's bundled geocode index for fully offline on-device search, returning aTerritorySearch. The handle holds the index open (mmap-backed): create it once per territory and reuse it. Its two calls are synchronous and disk-bound, so keep them off the main thread.kotlinval search = store.openSearch("uk") // Free text: addresses, streets, localities, POIs, UK postcodes. // Prefix-tokenised, so search-as-you-type works. `near` biases ranking // and fills each result's distanceM. val hits: List<Place> = search.search("watford gap", near = null, limit = 10u) // Nearest indexed places to a coordinate, nearest first. val around: List<Place> = search.reverse(52.2, 0.13, limit = 5u)Placecarries aPlaceKindofAddress,Street,Locality,PoiorPostcode. Both calls throwNavCoreExceptionwhen the territory carries no geocode layer. -
remove(id): remove an installed territory (clears the active marker if needed). -
checkForUpdate(...): check the signed update channel. Fail-closed: the channel-index signature is verified before the fetcher is ever invoked.indexJson/indexSigBase64are the exact served bytes ofindex.jsonandindex.json.sig. Returnsnullwhen already on the channel's latest version. -
applyUpdate(...): differential update; unchanged layers hard-linked, changed layers fetched through yourLayerFetcher, staged package fully re-verified, atomic swap. Any failure leaves the install untouched. -
The overloads taking a
CancelTokencan be stopped from any thread withcancel()(and by cancelling the calling coroutine). They returnInstallOutcome/UpdateOutcome:Cancelledis an outcome, not an exception, and leaves nothing partial on disk. See Cancelling an install. -
LayerFetcheris the generated callback interface: one method,fetch(relPath: String, expectedBlake3: String, destPath: String), synchronous by contract (called from a background thread);expectedBlake3is advisory, the core re-verifies everything.
See the worked install/update flows in territories.
ai.mapmap.routing
OfflineRouting
On-device route computation. The core builds the Valhalla request (merging ADR dangerous-goods and dimensional costing options), the injected router executes it against local tiles, and the core parses the response.
object OfflineRouting {
suspend fun route(
router: LocalRouter,
waypoints: List<RoutePoint>, // at least two, in visit order
costing: CostingModel = CostingModel.AUTO,
adr: AdrProfile? = null,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
): RouteResult
fun buildRequest(
waypoints: List<RoutePoint>,
costing: CostingModel = CostingModel.AUTO,
adr: AdrProfile? = null,
): String // raw Valhalla request JSON
fun parseResponse(responseJson: String): RouteResult
fun requireCompatibleCosting(costing: CostingModel, adr: AdrProfile?)
}
- ADR profiles ride on truck costing: supplying
adrwith any costing other thanCostingModel.TRUCKthrowsIllegalArgumentException. routethrowsNavCoreException.Routingwhen the engine returns a Valhalla error body (no route, no tiles for the area).RouteResult.valhallaResponseJsonfeedsNavigationEngine.
OfflineRouter
The on-device engine interface, the core's LocalRouter callback with a
Kotlin name: execute a Valhalla request JSON synchronously, return the raw
response JSON.
interface OfflineRouter : LocalRouter // fun route(requestJson: String): String
ValhallaMobileRouter
OfflineRouter backed by valhalla-mobile's on-device engine.
valhalla-mobile is a compileOnly dependency: apps using this class must
declare io.github.rallista:valhalla-mobile themselves.
class ValhallaMobileRouter(configPath: String) : OfflineRouter {
companion object {
fun fromConfig(configFile: File): ValhallaMobileRouter
fun fromTileDir(tileDir: File, scratchDir: File): ValhallaMobileRouter
}
}
fromTileDir is the usual entry point: point it at the territory's
MapLayerKind.ValhallaTiles layer and a scratch dir (e.g.
context.cacheDir) and it writes the valhalla.json for you.
ValhallaConfigWriter
Writes the minimal valhalla.json the on-device engine needs.
object ValhallaConfigWriter {
const val DEFAULT_MAX_CACHE_BYTES: Long = 268_435_456 // 256 MiB
fun configJson(tileDir: File, maxCacheBytes: Long = DEFAULT_MAX_CACHE_BYTES): String
fun write(tileDir: File, configFile: File,
maxCacheBytes: Long = DEFAULT_MAX_CACHE_BYTES): File
}
ai.mapmap.adr.AdrCompliance
On-device ADR 8.6.4 dangerous-goods enforcement (pre-route via
OfflineRouting.route(adr = …), en-route via checkTunnel).
object AdrCompliance {
fun checkTunnel(profile: AdrProfile, category: AdrTunnelCategory): AdrDecision
fun forbiddenCategories(profile: AdrProfile): List<AdrTunnelCategory>
fun isTunnelRestricted(profile: AdrProfile): Boolean
fun validateProfile(profile: AdrProfile): List<String>
}
checkTunnel: may this vehicle pass a tunnel ofcategory? AAdrDecision.Blockedcarries a human-readable reason citing ADR 8.6.4.forbiddenCategories: all categories the vehicle is barred from, ascending; empty when unrestricted.validateProfile: physical-plausibility check; returns human-readable problems (empty = valid). Pure Kotlin, safe without the native library.
ai.mapmap.nav
NavigationEngine
Turn-by-turn guidance over one computed route, adapting the core's
Ferrostar-based GuidanceSession to coroutines. An engine is bound to a
single route; on off-route, compute a new route and build a new engine.
class NavigationEngine(
routeResponseJson: String,
config: GuidanceConfig = GuidanceConfig(),
dispatcher: CoroutineDispatcher = Dispatchers.Default,
) {
constructor(route: RouteResult,
config: GuidanceConfig = GuidanceConfig(),
dispatcher: CoroutineDispatcher = Dispatchers.Default)
fun navigate(
provider: LocationProvider,
manualAdvance: Flow<Unit> = emptyFlow(),
): Flow<NavigationState>
fun totalSteps(): UInt
}
navigateis a cold flow (each collection starts a fresh session at the first route step) and completes after arrival.manualAdvanceis the tunnel/no-GNSS escape hatch: emitUnitto force-advance a step.- Throws
NavCoreExceptionif the route response cannot start a session.
NavigationState and GuidanceDisplay
data class NavigationState(val fix: LocationSample, val update: GuidanceUpdate) {
val isArrived: Boolean // update is GuidanceUpdate.Arrived
val needsReroute: Boolean // update is GuidanceUpdate.OffRoute
}
object GuidanceDisplay {
fun instruction(update: GuidanceUpdate): String?
fun severity(update: GuidanceUpdate): InstructionSeverity
fun formatDistance(metres: Double): String
fun summarise(update: GuidanceUpdate): String
}
GuidanceUpdate is the core's sealed state: Navigating (carries
currentInstruction, distanceToNextManeuverM, distanceRemainingM,
severity and the road-snapped fix snapped), Arrived, or OffRoute
(carries deviationM). GuidanceDisplay is pure Kotlin (unit-testable on
any JVM).
LocationProvider and LocationSample
data class LocationSample(
val lat: Double,
val lon: Double,
val timestampMs: Long, // ms since the Unix epoch
val speedMps: Double? = null,
val bearingDeg: Double? = null, // [0, 360), clockwise from true north
val horizontalAccuracyM: Double? = null, // 1-sigma; gates step advance
)
interface LocationProvider {
val locations: Flow<LocationSample> // collected once per session
}
Any cold Flow of samples in ascending timestamp order qualifies.
AndroidLocationProvider
Live provider on the platform location stack; no Google Play Services
dependency. Uses LocationManager.FUSED_PROVIDER on API 31+, falling back
to GPS_PROVIDER. Requires ACCESS_FINE_LOCATION granted before the
flow is collected; the SDK manifest declares no permissions.
class AndroidLocationProvider(
context: Context,
minIntervalMs: Long = 1_000L, // 1 Hz default
minDistanceM: Float = 0f,
) : LocationProvider
ReplayLocationProvider
Replays a .drive.jsonl corpus (see
File formats) as a
LocationProvider: the QA/simulator path.
class ReplayLocationProvider(
corpus: DriveCorpus,
timeScale: Double = 1.0, // 1.0 real time, 2.0 double speed, 0.0 = as fast as possible
epochStartMs: Long = 0L, // epoch ms assigned to the first fix
) : LocationProvider
VoiceGuidance
Speaks guidance updates through Android TextToSpeech, with
severity-scaled prosody, earcons and audio focus handled for you. Feed it
from the same flow that drives your UI.
class VoiceGuidance(
context: Context,
language: String? = null, // BCP 47, e.g. "en-GB"; null = device default
baseRate: Float = 1.0f, // base speech rate (1.0 = normal)
) : AutoCloseable {
var muted: Boolean // toggle freely mid-drive
var volume: Float // 0.0–1.0, speech and earcons
val isSpeaking: Boolean // an utterance is being spoken or queued
var onSpeakingChanged: ((Boolean) -> Unit)?
fun handle(update: GuidanceUpdate)
fun handle(state: NavigationState)
override fun close() // release TTS engine, earcon player, audio focus
}
isSpeaking and onSpeakingChanged are the speaking signal a UI ducks
other audio or animates a voice indicator from: true when the first
utterance of a burst is handed to the engine, false once the last
outstanding one ends, including on error, stop, mute and close(). End
transitions arrive on the TTS engine's own callback thread, so hop to the
main thread before touching UI.
VoiceProsody (speech rate/pitch per InstructionSeverity) and the
startup/audio-focus helpers (TtsStartup, AudioFocusPolicy) are public
for testing and custom voice stacks.
Probe contribution
An optional per-trip tap on the guidance flow that accumulates an aggregate
probe batch and uploads it once the trip ends, feeding
data collection. Off unless you turn it on: a
disabled ProbeConfig makes contributingProbe a transparent pass-through
that constructs no collector and uploads nothing, so leaving it off costs
nothing.
public fun Flow<NavigationState>.contributingProbe(
config: ProbeConfig,
uploader: ProbeUploader,
routeRef: String? = null, // opaque served-route token
territory: String? = null,
vehicleClass: String? = null, // a costing class only
now: () -> Long = System::currentTimeMillis,
): Flow<NavigationState>
public interface ProbeUploader {
fun enqueue(body: String) // non-blocking; must never throw
}
public class HttpProbeUploader(
baseUrl: String, // e.g. https://api.mapmap.ai
apiKey: String,
maxAttempts: Int = 4, // total tries, including the first
backoffMillis: (Int) -> Long = { it * 1_000L },
executor: ExecutorService = ..., // default: one daemon thread
onResult: (ProbeUploadOutcome) -> Unit = {},
) : ProbeUploader, AutoCloseable
public enum class ProbeUploadOutcome {
ACCEPTED, REFUSED, NOT_ENABLED, REJECTED, GAVE_UP
}
- Wire it straight onto the guidance stream:
engine.navigate(provider).contributingProbe(ProbeConfig(enabled = consentGranted), uploader).collect { … }. - Consent is the caller's responsibility. Pass
enabled = trueonly for a key whose operator has setprobe_opt_inand, in a consumer app, only with the user's own opt-in. - Only aggregates leave the device: the collector cannot emit a trajectory, and the first and last stretch of every trip is discarded on-device before anything is uploaded.
HttpProbeUploaderPOSTs each body to{baseUrl}/v1/probeon a small daemon executor using only the JDK HTTP client, so the SDK takes on no third-party HTTP dependency. Transient failures (IO or5xx) retry with bounded backoff;400,403and501are permanent and dropped.ProbeUploadOutcomenames which of those happened, for observability and tests. Close it to release the executor.vehicleClassis a costing class such as"truck_40t"and never a registration, VIN or driver id.
ai.mapmap.replay.DriveCorpus
Parser for the .drive.jsonl replay corpus format; schema in
File formats.
data class DriveCorpus(val header: DriveHeader, val fixes: List<DriveFix>) {
companion object {
const val FORMAT_VERSION: Int = 1
fun parse(jsonl: String): DriveCorpus // throws CorpusFormatException
}
}
data class DriveHeader(val formatVersion: Int, val name: String,
val description: String, val routeFixture: String, val seed: Long)
data class DriveFix(val tMs: Long, val lat: Double, val lon: Double,
val speedMps: Double?, val bearingDeg: Double?,
val horizontalAccuracyM: Double?)
The parser enforces the same invariants as the Rust reference
implementation: supported format version, non-empty name, at least one
fix, strictly increasing t_ms, sane coordinate/channel ranges.
Putting it together
// Territory → router → route → guidance:
val tiles = store.layerPath(territoryId, MapLayerKind.ValhallaTiles)
val router = ValhallaMobileRouter.fromTileDir(tiles, context.cacheDir)
val route = OfflineRouting.route(
router = router,
waypoints = listOf(RoutePoint(52.2, 0.13), RoutePoint(52.2045, 0.1388)),
costing = CostingModel.TRUCK,
adr = AdrProfile(heightM = 4.0, grossWeightT = 40.0,
hazmat = true, tunnelCode = AdrTunnelCode.C),
)
NavigationEngine(route).navigate(AndroidLocationProvider(context))
.collect { state ->
ui.instruction = GuidanceDisplay.summarise(state.update)
if (state.needsReroute) recalculate()
}
Android Auto (ai.mapmap:car, ai.mapmap:car-maplibre)
Beta modules that put the same guidance state on the car's display:
NavigationManagerBridge for the Car App Library's NavigationManager,
TurnByTurnNotificationManager, NavigationIntentParser, the
NavigationTemplateBuilder template layer, and CarMapViewHost for
projecting a MapLibre MapView onto the car surface.
Unlike ai.mapmap:core, these two modules are not published to Maven
Central or GitHub Packages: build them from the source checkout until
public artefacts land with sdk-v0.7.0, after head-unit hardware
validation. Install steps, quickstart, entitlement and Play-review gates
on CarPlay & Android Auto.
Next steps
- SDKs & installation: install coordinates, credentials, the verifying-key trust anchor
- Territories: the signed package and update-channel model this SDK consumes
- File formats: channel index, package layout,
.drive.jsonl - CarPlay & Android Auto: the in-car surface