diff --git a/README.md b/README.md index 3eda837..39c9071 100644 --- a/README.md +++ b/README.md @@ -21,21 +21,9 @@ dependencies { The library is available on [Maven Central](https://central.sonatype.com/artifact/de.afarber/openmapview). Alternative distribution via [JitPack](https://jitpack.io/#afarber/OpenMapView) is also supported. -## License Compliance +## License -OpenMapView is licensed under the **MIT License** - use freely in commercial apps. - -**Dependency Licenses:** -- All runtime dependencies use Apache 2.0 or MIT licenses (business-friendly) -- View complete [Dependency License Report](https://afarber.github.io/OpenMapView/licenses/) -- No GPL or restrictive copyleft licenses - -**Dependencies (5 total):** -- androidx.core:core-ktx (Apache 2.0) -- androidx.lifecycle:lifecycle-runtime-ktx (Apache 2.0) -- androidx.compose.ui:ui-graphics (Apache 2.0) -- io.ktor:ktor-client-android (Apache 2.0) -- com.jakewharton:disklrucache (Apache 2.0) +OpenMapView is MIT licensed, so you can use it in commercial apps. Its five runtime dependencies (androidx core-ktx, lifecycle-runtime-ktx, Compose ui-graphics, ktor-client-android and DiskLruCache) are all Apache 2.0. See the [Dependency License Report](https://afarber.github.io/OpenMapView/licenses/) for details. ## Features @@ -49,13 +37,12 @@ OpenMapView is licensed under the **MIT License** - use freely in commercial app - External app integration (open in Google Maps, OsmAnd, etc. via geo: URI) - GeoJSON import support - Extensible marker, overlay, and gesture handling -- MIT licensed (use freely in commercial apps) ## API Documentation Full API reference documentation is available at [afarber.github.io/OpenMapView](https://afarber.github.io/OpenMapView/). -The documentation is automatically generated from KDoc comments and updated with every commit to main. +The documentation is generated from KDoc comments and redeployed whenever the library sources change on main. ## Examples @@ -71,7 +58,7 @@ Explore the example applications to see OpenMapView in action: - [Example08Circles](examples/Example08Circles) - Circles with radius, styling, z-index ordering, and click listeners - [Example09Overlays](examples/Example09Overlays) - Tile overlays with transparency control (OpenSeaMap, Railways, Trails, Snow) - [Example10GroundOverlays](examples/Example10GroundOverlays) - Ground overlays with position/bounds modes, rotation, and transparency -- [Example11MapTypes](examples/Example11MapTypes) - Switching between 5 map types (Normal, Terrain, Humanitarian, Cycle, None) +- [Example11MapTypes](examples/Example11MapTypes) - Switching between all 15 map types (OSM, CyclOSM, Thunderforest, CARTO, Stamen and more) - [Example12Toolbar](examples/Example12Toolbar) - Open location in external map apps via geo: URI with OpenStreetMap browser fallback ![Example01Pan Demo](examples/Example01Pan/screenshot.gif) @@ -107,7 +94,7 @@ fun MapViewScreen() { lifecycleOwner.lifecycle.addObserver(this) setCenter(LatLng(51.4661, 7.2491)) - setZoom(14.0) + setZoom(14.0f) // Add markers - Kotlin style (direct instantiation) addMarker(Marker( @@ -138,7 +125,7 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) val mapView = findViewById(R.id.mapView) - mapView.setZoom(14.0) + mapView.setZoom(14.0f) mapView.setCenter(LatLng(51.4661, 7.2491)) } } diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 9c46c02..c65cbd9 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -10,7 +10,7 @@ All contributions must meet the following requirements before being merged: ### 1. Code Formatting -All Kotlin code must follow the project's formatting rules enforced by Spotless with ktlint 1.3.1. +All Kotlin code must follow the project's formatting rules enforced by Spotless with ktlint (versions in `gradle/libs.versions.toml`). **Before committing**, run: diff --git a/docs/GITHUB_WORKFLOWS.md b/docs/GITHUB_WORKFLOWS.md index 5c6c824..456af61 100644 --- a/docs/GITHUB_WORKFLOWS.md +++ b/docs/GITHUB_WORKFLOWS.md @@ -120,9 +120,10 @@ instrumented-test (Phone + Automotive) runs in parallel 7. **build-library** - Builds library AAR (depends on all above, runs in parallel with build-examples) 8. **build-examples** - Builds example APKs (depends on all above, runs in parallel with build-library) 9. **instrumented-test** - Runs instrumentation tests on phone and automotive emulators (PR only) +10. **instrumented-test-status** - Reports one combined "Instrumentation Tests" check for the device matrix, used by branch protection (PR only) **Total Duration**: -- **Maintainer pushes to main**: ~3-4 minutes (no instrumentation tests) +- **Pushes to main** (merged PRs and release commits): ~3-4 minutes (no instrumentation tests) - **Pull Requests**: ~10-15 minutes (includes instrumentation tests on phone + automotive) **Benefits**: @@ -157,7 +158,7 @@ lint ──────┘ │ ├──→ publish - Validates tag format (must be `vMAJOR.MINOR.PATCH`) - Extracts version number (e.g., `v0.2.0` -> `0.2.0`) - Signs artifacts with GPG key -- Uploads to Maven Central OSSRH +- Uploads the bundle to the Maven Central Portal, which publishes it automatically - Generates changelog from git history - Creates GitHub Release with: - Version number and changelog @@ -268,14 +269,14 @@ For the release workflow to function, the project owner must configure these sec **Location**: Settings -> Secrets and variables -> Actions -> Repository secrets ### 1. OSSRH_USERNAME -- **Description**: Sonatype JIRA username -- **Used for**: Authenticating to Maven Central OSSRH -- **How to get**: Created when registering at https://issues.sonatype.org +- **Description**: Username part of a Central Portal user token (the secret keeps its old name) +- **Used for**: Authenticating to the Maven Central Portal +- **How to get**: Generate a user token at https://central.sonatype.com/account ### 2. OSSRH_PASSWORD -- **Description**: Sonatype JIRA password -- **Used for**: Authenticating to Maven Central OSSRH -- **How to get**: Set when registering at https://issues.sonatype.org +- **Description**: Password part of the same Central Portal user token +- **Used for**: Authenticating to the Maven Central Portal +- **How to get**: Shown together with the username when the token is generated ### 3. SIGNING_KEY - **Description**: Base64-encoded GPG private key @@ -349,11 +350,7 @@ git pull # 3. Watch the workflow # Go to: https://github.com/afarber/OpenMapView/actions -# 4. For first release only: manually release staging repository -# Log in to: https://s01.oss.sonatype.org/ -# Find your staging repo, click "Close", then "Release" - -# 5. Verify publication after 2-4 hours +# 4. Verify publication, usually after 10-30 minutes # https://central.sonatype.com/artifact/de.afarber/openmapview ``` @@ -445,7 +442,7 @@ publish job: - Create GitHub Release - PASS | v -Wait 2-4 hours -> Library available on Maven Central +Wait 10-30 minutes -> Library available on Maven Central ``` ## Benefits of This Architecture diff --git a/docs/LIFECYCLE.md b/docs/LIFECYCLE.md index a10459b..427def4 100644 --- a/docs/LIFECYCLE.md +++ b/docs/LIFECYCLE.md @@ -125,7 +125,7 @@ fun MapViewScreen() { lifecycleOwner.lifecycle.addObserver(this) setCenter(LatLng(51.4661, 7.2491)) - setZoom(14.0) + setZoom(14.0f) } } ) diff --git a/docs/MAP_TYPES.md b/docs/MAP_TYPES.md index e115bcc..ec06f51 100644 --- a/docs/MAP_TYPES.md +++ b/docs/MAP_TYPES.md @@ -4,7 +4,7 @@ This guide helps you choose and configure the right map type for your OpenMapVie ## Quick Start -OpenMapView provides **15 map types** from various tile providers. Twelve types work without any configuration, while three premium types require free API keys. +OpenMapView provides **15 map types** from various tile providers. Eleven types work without any configuration (including `NONE`), while four premium types require free API keys. ```kotlin val mapView = OpenMapView(context) @@ -401,7 +401,7 @@ See the **Example11MapTypes** sample app for a complete demonstration of all map val mapView = OpenMapView(context) lifecycle.addObserver(mapView) mapView.setCenter(LatLng(46.8182, 8.2275)) -mapView.setZoom(12.0) +mapView.setZoom(12.0f) mapView.setMapType(MapType.CYCLOSM) // Switch map types dynamically ``` diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 620eda9..644d453 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -23,8 +23,6 @@ Map tiles are decoded using the `RGB_565` bitmap configuration instead of the de val options = BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.RGB_565 inScaled = false - inDither = false - inPreferQualityOverSpeed = false } ``` @@ -322,14 +320,6 @@ Typical memory footprint for a visible viewport at zoom level 10: - **Total active memory:** ~4MB for tiles - **Memory cache capacity:** ~32-64MB depending on device -### Cache Hit Rates - -Expected cache hit rates with typical usage: - -- **Memory cache:** 80-90% for panning within same area -- **Disk cache:** 60-70% on app restart -- **Network downloads:** 10-20% for new areas - ### Network Usage With prefetching enabled: @@ -338,36 +328,20 @@ With prefetching enabled: - **Pan gesture:** 0-4 tile downloads (~0-150KB) from cache - **Zoom change:** 0-20 tile downloads (~0-700KB) depending on zoom delta - ## Benchmarking -To measure performance in your app: +The tile caches are internal, so measure from the outside: -```kotlin -// Memory usage -val memoryInfo = ActivityManager.MemoryInfo() -activityManager.getMemoryInfo(memoryInfo) - -// Cache statistics -val cacheSize = tileCache.size() -val cacheHits = tileCache.hitCount() -val cacheMisses = tileCache.missCount() - -// Rendering performance -Choreographer.getInstance().postFrameCallback { frameTimeNanos -> - // Measure frame time -} -``` +- **Memory:** Android Studio Memory Profiler, or `adb shell dumpsys meminfo ` +- **Network:** Android Studio Network Inspector shows every tile request and whether panning hits the cache +- **Rendering:** Profile GPU Rendering in the developer options, or a `Choreographer.FrameCallback` ## Best Practices For optimal performance: 1. **Lifecycle integration:** Register OpenMapView with lifecycle observer (see [Lifecycle Management](LIFECYCLE.md)) -2. **Memory limits:** Monitor memory usage if displaying multiple maps simultaneously -3. **Cache clearing:** Provide UI to clear cache if storage becomes an issue -4. **Network awareness:** Consider pausing downloads on metered connections -5. **Error handling:** Handle network failures gracefully +2. **Several maps at once:** Each OpenMapView has its own memory cache of up to 1/8 of the heap, so watch memory usage when showing more than one ## References diff --git a/docs/PUBLIC_API.md b/docs/PUBLIC_API.md index 38887cd..4b1a615 100644 --- a/docs/PUBLIC_API.md +++ b/docs/PUBLIC_API.md @@ -179,9 +179,9 @@ These properties apply to: | Method | Return Type | Notes | | ------------------------------ | ----------- | ---------------------------------------------------------------- | | `setCenter(LatLng)` | `void` | Direct method on OpenMapView (not via separate GoogleMap object) | -| `setZoom(double)` | `void` | Direct method on OpenMapView (not via separate GoogleMap object) | -| `getZoom()` | `double` | Direct method on OpenMapView (not via separate GoogleMap object) | -| `setMapType(int)` | `void` | Supports NORMAL, TERRAIN, HUMANITARIAN, CYCLE, NONE | +| `setZoom(float)` | `void` | Direct method on OpenMapView (not via separate GoogleMap object) | +| `getZoom()` | `float` | Direct method on OpenMapView (not via separate GoogleMap object) | +| `setMapType(int)` | `void` | Supports all 15 `MapType` constants, see MAP_TYPES.md | | `getMapType()` | `int` | | | `setMapStyle(MapStyleOptions)` | `boolean` | Not implemented | diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index 4ccb2b6..a35086c 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -168,11 +168,8 @@ Examples: When creating a new release: -- [ ] Ensure all tests pass: `./gradlew test` -- [ ] Run code formatting: `./gradlew spotlessApply` -- [ ] Verify local build: `./gradlew build` -- [ ] Test Maven publication: `./gradlew publishToMavenLocal` -- [ ] Create and push version tag (e.g., `v0.3.0`) +- [ ] Make sure CI on main is green and your local main is up to date +- [ ] Run `./scripts/publish-release.sh` (next patch version) or `./scripts/publish-release.sh vX.Y.Z`. It bumps the version in the docs, commits, creates a signed tag and pushes main and the tag - [ ] Monitor GitHub Actions release workflow - [ ] Verify Maven Central publication (10-30 minutes) - [ ] Verify JitPack build status @@ -360,13 +357,15 @@ git push origin v0.3.0 ## Installation Examples +Replace `` with the latest release shown in the [README](../README.md#installation). + ### Maven Central No repository configuration required: ```kotlin dependencies { - implementation("de.afarber:openmapview:0.1.0") + implementation("de.afarber:openmapview:") } ``` @@ -386,6 +385,6 @@ dependencyResolutionManagement { ```kotlin dependencies { - implementation("com.github.afarber:OpenMapView:0.1.0") + implementation("com.github.afarber:OpenMapView:") } ``` diff --git a/docs/TESTING_INSTRUMENTED.md b/docs/TESTING_INSTRUMENTED.md index b6662e3..30dab60 100644 --- a/docs/TESTING_INSTRUMENTED.md +++ b/docs/TESTING_INSTRUMENTED.md @@ -81,8 +81,10 @@ Test results and reports are uploaded as separate artifacts for each device with ``` openmapview/src/androidTest/kotlin/de/afarber/openmapview/ +├── MapControllerInstrumentationTest.kt # Canvas rendering and markers (7 tests) ├── TileDownloaderInstrumentationTest.kt # Real OSM tile downloads (2 tests) -└── MapControllerInstrumentationTest.kt # Canvas rendering and markers (7 tests) +├── TouchGestureInstrumentationTest.kt # Click vs. drag detection (2 tests) +└── UiSettingsInstrumentationTest.kt # Enabling and disabling gestures (4 tests) ``` ## Dependencies @@ -91,15 +93,17 @@ Instrumented testing dependencies in `openmapview/build.gradle.kts`: ```kotlin dependencies { - androidTestImplementation("androidx.test:core-ktx:1.7.0") - androidTestImplementation("androidx.test:runner:1.7.0") - androidTestImplementation("androidx.test:rules:1.7.0") - androidTestImplementation("androidx.test.ext:junit-ktx:1.3.0") - androidTestImplementation("junit:junit:4.13.2") - androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") + androidTestImplementation(libs.androidx.test.core.ktx) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.junit.ktx) + androidTestImplementation(libs.junit) + androidTestImplementation(libs.kotlinx.coroutines.test) } ``` +The versions are in `gradle/libs.versions.toml`. + ## References - [Android Testing Documentation](https://developer.android.com/training/testing/instrumented-tests) diff --git a/docs/TESTING_UNIT.md b/docs/TESTING_UNIT.md index 46a2d65..94479d0 100644 --- a/docs/TESTING_UNIT.md +++ b/docs/TESTING_UNIT.md @@ -14,14 +14,16 @@ openmapview/src/test/kotlin/de/afarber/openmapview/ ## Test Framework Stack -| Component | Version | Purpose | -|-----------|---------|---------| -| **JUnit** | 4.13.2 | Test runner and assertions | -| **Kotlin Test** | (inherited) | Kotlin-specific test utilities | -| **MockK** | 1.14.11 | Mocking framework for Kotlin | -| **Robolectric** | 4.17 | Android framework shadow implementations | -| **Ktor Client Mock** | 3.6.0 | HTTP client mocking for network tests | -| **Coroutines Test** | 1.11.0 | Testing coroutines and async code | +| Component | Purpose | +|-----------|---------| +| **JUnit 4** | Test runner and assertions | +| **Kotlin Test** | Kotlin-specific test utilities | +| **MockK** | Mocking framework for Kotlin | +| **Robolectric** | Android framework shadow implementations | +| **Ktor Client Mock** | HTTP client mocking for network tests | +| **Coroutines Test** | Testing coroutines and async code | + +The versions are in `gradle/libs.versions.toml` and kept up to date by Dependabot. ## Running Tests @@ -174,7 +176,7 @@ android { } dependencies { - testImplementation("org.robolectric:robolectric:4.14") + testImplementation(libs.robolectric) } ``` diff --git a/examples/Example11MapTypes/README.md b/examples/Example11MapTypes/README.md index 1713ec1..48c2b1f 100644 --- a/examples/Example11MapTypes/README.md +++ b/examples/Example11MapTypes/README.md @@ -2,16 +2,15 @@ [Back to README](../../README.md) -This example demonstrates switching between different map types in OpenMapView, showing various OpenStreetMap-based tile sources with automatic attribution updates. +This example demonstrates switching between different map types in OpenMapView, showing all OpenStreetMap-based tile sources built into the library with automatic attribution updates. ## Features Demonstrated -- Five map types: Normal, Terrain, Humanitarian, Cycle, None -- Dynamic map type switching with buttons +- All 15 map types (`MapType.NONE` to `MapType.STAMEN_WATERCOLOR`), one button each - Automatic attribution text updates -- Tile cache clearing on map type change +- Each map type keeps its own tile cache, so switching back shows cached tiles right away +- Fallback to the standard map with an "API key required" overlay when a key is missing - Material3 UI with button controls -- Error handling for invalid map types ## Screenshot @@ -44,7 +43,7 @@ adb shell am start -n de.afarber.openmapview.example11maptypes/.MainActivity val mapView = OpenMapView(context) // Set map type -mapView.setMapType(MapType.TERRAIN) +mapView.setMapType(MapType.OPENTOPOMAP) // Get current map type val currentType = mapView.getMapType() @@ -52,6 +51,8 @@ val currentType = mapView.getMapType() ### Handle Invalid Types +`setMapType()` throws for values outside the `MapType` constants: + ```kotlin try { mapView.setMapType(99) @@ -60,97 +61,37 @@ try { } ``` -### Key Concepts - -- **MapType.NORMAL**: Standard OpenStreetMap road map (default) -- **MapType.TERRAIN**: OpenTopoMap with elevation contours -- **MapType.HUMANITARIAN**: HOT OSM emphasizing humanitarian features -- **MapType.CYCLE**: CyclOSM showing cycling infrastructure -- **MapType.NONE**: No base map tiles displayed -- **setMapType()**: Change the map's tile source -- **getMapType()**: Query current map type - -## What to Test - -1. **Launch the app** - Default Normal map type loads -2. **Click Terrain** - Switches to OpenTopoMap with contour lines -3. **Click Humanitarian** - Switches to red/orange humanitarian style -4. **Click Cycle** - Switches to CyclOSM with bike lanes -5. **Click None** - Removes all base tiles (overlays still visible) -6. **Click Normal** - Returns to standard OSM tiles -7. **Check attribution** - Bottom-right text updates for each source -8. **Pan and zoom** - New tiles load from current source - ## Map Types Reference -| Type | Constant | Tile Source | Best For | -| ------------ | ---------------------- | ----------------------------- | --------------------------------------------- | -| Normal | `MapType.NORMAL` | OpenStreetMap Standard | General purpose street maps | -| Terrain | `MapType.TERRAIN` | OpenTopoMap | Hiking, elevation, topography | -| Humanitarian | `MapType.HUMANITARIAN` | Humanitarian OSM | Emergency response, infrastructure | -| Cycle | `MapType.CYCLE` | CyclOSM | Cycling routes, bike infrastructure | -| None | `MapType.NONE` | No tiles | Custom overlays only, performance testing | - -## Technical Details - -### What Happens on Map Type Change - -1. Tile source URL template is updated -2. Memory tile cache is cleared -3. Disk tile cache is cleared -4. Attribution text is updated -5. Map view is invalidated and redraws - -### Attribution by Map Type - -- **Normal**: Map data from OpenStreetMap -- **Terrain**: Map data from OpenStreetMap | Map style: OpenTopoMap (CC-BY-SA) -- **Humanitarian**: Humanitarian OpenStreetMap Team -- **Cycle**: CyclOSM | Map data from OpenStreetMap - -### Performance Notes +| Button | Constant | Tile Source | API Key | +| ------ | -------- | ----------- | ------- | +| None | `MapType.NONE` | No tiles | | +| Standard | `MapType.STANDARD` | OpenStreetMap | | +| CyclOSM | `MapType.CYCLOSM` | CyclOSM | | +| Cycle Map | `MapType.CYCLEMAP` | Thunderforest | Thunderforest | +| Transport | `MapType.TRANSPORT` | Thunderforest | Thunderforest | +| Transport Dark | `MapType.TRANSPORT_DARK` | Thunderforest | Thunderforest | +| Tracestrack Topo | `MapType.TRACESTRACK_TOPO` | Tracestrack | Tracestrack | +| Humanitarian | `MapType.HUMANITARIAN` | Humanitarian OSM Team | | +| OPNVKarte | `MapType.OPNVKARTE` | MeMoMaps | | +| OpenTopoMap | `MapType.OPENTOPOMAP` | OpenTopoMap | | +| Carto Light | `MapType.CARTO_LIGHT` | CARTO | | +| Carto Dark | `MapType.CARTO_DARK` | CARTO | | +| Carto Voyager | `MapType.CARTO_VOYAGER` | CARTO | | +| Stamen Toner | `MapType.STAMEN_TONER` | Stadia Maps | | +| Stamen Watercolor | `MapType.STAMEN_WATERCOLOR` | Stadia Maps | | + +See the [Map Types Guide](../../docs/MAP_TYPES.md) for details on each type and on configuring API keys. -- Cache clearing may cause momentary blank tiles -- New tiles download asynchronously -- Each map type has independent cache -- Some tile servers may have rate limits - -## Advanced Usage - -### Programmatic Map Type Selection - -```kotlin -val mapTypes = listOf( - MapType.NORMAL, - MapType.TERRAIN, - MapType.HUMANITARIAN, - MapType.CYCLE -) - -// Cycle through map types -var currentIndex = 0 -button.setOnClickListener { - currentIndex = (currentIndex + 1) % mapTypes.size - mapView.setMapType(mapTypes[currentIndex]) -} -``` - -### Check Current Map Type +## What to Test -```kotlin -when (mapView.getMapType()) { - MapType.NORMAL -> showNormalControls() - MapType.TERRAIN -> showElevationLegend() - MapType.CYCLE -> showCyclingStats() - else -> hideSpecialControls() -} -``` +1. **Launch the app** - The standard OSM map of Switzerland loads +2. **Click through the buttons** - The map and the attribution text change for each source +3. **Click a type that needs a key** - Without a key, the standard map stays and an "API key required" overlay appears +4. **Click None** - Removes all base tiles +5. **Switch back** - Tiles of a type you already viewed come from its cache +6. **Pan and zoom** - New tiles load from the current source ## Map Location -**Default Center:** Bochum, Germany (51.4661°N, 7.2491°E) at zoom 13.0 - -Test different map types at different locations: -- **Terrain**: Best viewed in mountainous areas (Alps, Black Forest) -- **Humanitarian**: Shows infrastructure in any populated area -- **Cycle**: Best in cities with cycling infrastructure +**Default Center:** Switzerland (46.8182N, 8.2275E) at zoom 12, a good place to compare the topographic styles. diff --git a/examples/Example11MapTypes/src/main/kotlin/de/afarber/openmapview/example11maptypes/MainActivity.kt b/examples/Example11MapTypes/src/main/kotlin/de/afarber/openmapview/example11maptypes/MainActivity.kt index a530e26..49a64c9 100644 --- a/examples/Example11MapTypes/src/main/kotlin/de/afarber/openmapview/example11maptypes/MainActivity.kt +++ b/examples/Example11MapTypes/src/main/kotlin/de/afarber/openmapview/example11maptypes/MainActivity.kt @@ -286,7 +286,7 @@ fun MapTypeControls( // API Key Info Text( text = "API key required: Cycle Map, Transport, Transport Dark, " + - "Tracestrack Topo\n\nConfigure in AndroidManifest.xml\nSee docs/API_KEYS.md", + "Tracestrack Topo\n\nConfigure in AndroidManifest.xml\nSee docs/MAP_TYPES.md", style = MaterialTheme.typography.bodySmall, fontSize = 9.sp, lineHeight = 11.sp,