Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
3825501
docs(maps): plugin README, repo registration, OSM/MapLibre tutorial
fryanpan Jun 5, 2026
9c2f4fd
feat(maps): plugin scaffold, build config (JaCoCo + test deps), entry…
fryanpan Jun 5, 2026
d762e23
feat(maps): domain + util — bbox/zoom math, region-id rules, atomic f…
fryanpan Jun 5, 2026
4dce5dc
feat(maps): slicer — PMTiles v3 range-slicing of a bbox region (+ uni…
fryanpan Jun 5, 2026
bed67ac
feat(maps): data layer — region cache, downloader (injectable IO seam…
fryanpan Jun 5, 2026
1f10ddd
feat(maps): UI — Maps tab, region manager, bbox picker, download wizard
fryanpan Jun 5, 2026
e239546
feat(maps): project template — emits an offline MapLibre region-map a…
fryanpan Jun 5, 2026
3fa84c1
feat(maps): offline-build support — vendored project-local MapLibre M…
fryanpan Jun 5, 2026
d45ed3b
feat(maps): bundled assets — Natural Earth basemap, Noto fonts, icons…
fryanpan Jun 5, 2026
b28d8fd
fix(maps): DoD review fixes — hilbert-curve attribution + prefs context
fryanpan Jun 5, 2026
862e98b
review(maps): address @jatezzz — Dispatchers.IO sweep + SRP refactor …
fryanpan Jul 13, 2026
e56a8b5
chore(maps): move to plugins/Maps and adopt the addon layout (ADFA-2436)
fryanpan Oct 14, 2026
99c3541
build(maps): align with the repo toolchain (AGP 9.3.1, Kotlin 2.3.21,…
fryanpan Oct 14, 2026
058652d
fix(maps): tooltip category is plugin_<pluginId> (ADFA-2436)
fryanpan Oct 14, 2026
cf34db0
fix(maps): register a tooltip entry for the template card's tag (ADFA…
fryanpan Oct 14, 2026
835f29b
fix(maps): read the vector archive name from IIAB's catalog (ADFA-2436)
fryanpan Oct 14, 2026
aa56693
fix(maps): drop Play Services location; use LocationManager directly …
fryanpan Oct 14, 2026
a71053a
fix(maps): template tile server skips fully before sending a 206 (ADF…
fryanpan Oct 14, 2026
aef19c5
fix(maps): template tile server honours suffix ranges (ADFA-2436)
fryanpan Oct 14, 2026
f03401a
test(maps): declare the template tree as a unit-test input (ADFA-2436)
fryanpan Oct 14, 2026
f905870
fix(maps): template tile server times out idle clients and caps its t…
fryanpan Oct 14, 2026
8d48204
fix(maps): check a 206 body's size before buffering it (ADFA-2436)
fryanpan Oct 14, 2026
a21675e
fix(maps): Bbox is a data class; heightKm doc matches the code (ADFA-…
fryanpan Oct 14, 2026
3e2a518
fix(maps): range cache hands out copies and promotes on a repeat put …
fryanpan Oct 14, 2026
83ebb5f
fix(maps): check the maps dir is inside the project before creating i…
fryanpan Oct 14, 2026
d7226b1
fix(maps): delete a region by its folder, not its meta.json id (ADFA-…
fryanpan Oct 14, 2026
13743bb
fix(maps): move the location-off dialog and delete messages to string…
fryanpan Oct 14, 2026
8fd2fd6
build(maps): package only the ABIs Code on the Go ships (ADFA-2436)
fryanpan Oct 14, 2026
5ab1fa2
docs(maps): arm64-only note; correct the apply and tutorial pointers …
fryanpan Oct 14, 2026
538f996
fix(maps): download progress checks the fragment's view, not the para…
fryanpan Oct 14, 2026
e3dd95f
docs(maps): THIRD_PARTY_LICENSES lists the kotlin-stdlib version the …
fryanpan Oct 14, 2026
ccbf047
docs(maps): template server's read timeout KDoc says what it bounds (…
fryanpan Oct 14, 2026
12e21d1
fix(maps): bound the IIAB catalog fetch with a 5 s call timeout (ADFA…
fryanpan Oct 14, 2026
498674a
test(maps): cover template build, plugin lifecycle, font extraction (…
fryanpan Oct 14, 2026
8b55ee6
fix(maps): dispose closes the shared HTTP clients' idle connections (…
fryanpan Oct 14, 2026
77bf2b4
chore(maps): declare plugin.editor_tabs in the manifest (ADFA-2436)
fryanpan Oct 14, 2026
73757d3
test(maps): a range the stream cannot reach gets a 500 before any hea…
fryanpan Oct 14, 2026
4b56d2f
test(maps): cover slicer, range-fetch and installer edge cases (ADFA-…
fryanpan Oct 14, 2026
fad1b39
test(maps): drive RegionDownloader.download end to end through a cach…
fryanpan Oct 26, 2026
7791994
chore(maps): declare an IDE version range and build provenance in the…
fryanpan Oct 26, 2026
216ef29
chore(maps): add the per-plugin .gitignore from the Random-XKCD templ…
fryanpan Oct 26, 2026
c6e7cbe
fix(maps): a cancelled coroutine is never read as an ordinary failure…
fryanpan Oct 26, 2026
5f732ff
refactor(maps): log through the plugin logger and drop trace-only log…
fryanpan Oct 26, 2026
52a8d5d
fix(maps): move the remaining snackbar, size and header text to strin…
fryanpan Oct 26, 2026
8b952c8
fix(maps): Hilbert.tileIdToZxy decodes zoom 26, the deepest zoom it e…
fryanpan Oct 26, 2026
c3ffd0f
fix(maps): the emitter reports a maps dir it could not create (ADFA-2…
fryanpan Oct 26, 2026
9f6cc8f
refactor(maps): one traffic-stats tag constant instead of three copie…
fryanpan Oct 26, 2026
5786499
feat(maps): ship the generated app for armeabi-v7a as well as arm64-v…
fryanpan Oct 27, 2026
f841489
fix(maps): the Offline OSM Map template builds on AGP 9 with no hand …
fryanpan Oct 27, 2026
ef8402a
test(maps): check every .module digest and the 32-bit doc claim posit…
fryanpan Oct 27, 2026
78ed1dd
fix(maps): point the IIAB fallback archive at the 2026-10-01 build (A…
fryanpan Oct 27, 2026
e42c443
fix(maps): LAN downloads find the archive an older IIAB box holds (AD…
fryanpan Oct 27, 2026
464ef34
test(maps): the dispose test no longer depends on test order (ADFA-2436)
fryanpan Oct 27, 2026
fcbc910
test(maps): check the slicer against go-pmtiles extract; fix a racy f…
fryanpan Oct 27, 2026
cd85de1
chore(maps): drop kotlin-stdlib versions no generated project resolve…
fryanpan Oct 27, 2026
1c028e8
test(maps): cover the LAN archive probe's 200 and no-answer branches …
fryanpan Oct 27, 2026
301ae37
chore(maps): drop vendored binaries no generated project resolves (AD…
fryanpan Oct 27, 2026
3f813fd
feat(maps): long-press help on the region list and the four wizard sc…
fryanpan Oct 28, 2026
e5e53b8
docs(maps): ship the tutorial as docs/index.html and explain the Maps…
fryanpan Oct 28, 2026
8c8bded
build(maps): drop the jitpack repository (ADFA-2436)
fryanpan Oct 28, 2026
802e03b
build(maps): drop the Jetifier and R8 full-mode flags (ADFA-2436)
fryanpan Oct 28, 2026
1f1d44d
feat(maps): find a LAN box's archive from its /maps/data/ listing (AD…
fryanpan Oct 28, 2026
30c5c5a
docs(maps): name the Offline OSM Map template and MapLibre's retained…
fryanpan Oct 28, 2026
9d3b6b5
fix(maps): clear the two Kotlin compile warnings (ADFA-2436)
fryanpan Oct 28, 2026
1d245b7
fix(maps): cancel in-flight HTTP calls on dispose (ADFA-2436)
fryanpan Oct 28, 2026
57b9be5
fix(maps): device-walk UI fixes on the region list, save and download…
fryanpan Oct 28, 2026
d3290e7
refactor(maps): split the bbox picker and region manager fragments (A…
fryanpan Oct 28, 2026
fca0a2c
fix(maps): escape the quotes aapt2 was stripping from two strings (AD…
fryanpan Oct 28, 2026
3c059ea
fix(maps): keep save-summary keys and download snackbars readable at …
fryanpan Oct 28, 2026
298216a
style(maps): 4-space indent in the files split out of the two fragmen…
fryanpan Oct 28, 2026
d4eaf48
fix(maps): turn Next off when the chosen box holds no map tiles (ADFA…
fryanpan Oct 28, 2026
936ec67
fix(maps): name regions in download snackbars, and say when Apply hit…
fryanpan Oct 28, 2026
dc54073
fix(maps): frame a saved region's whole box when Refresh opens the pi…
fryanpan Oct 28, 2026
1b6df80
fix(maps): update the project's map when the active region is downloa…
fryanpan Oct 28, 2026
7f5f63b
fix(maps): declare PACKAGE_NAME in the template metadata (ADFA-2436)
fryanpan Oct 28, 2026
6aa3a1e
fix(maps): template tile server stops queueing connections behind idl…
fryanpan Oct 31, 2026
e8ea0ff
fix(maps): a glyph range the app does not bundle no longer blanks map…
fryanpan Oct 31, 2026
f57fb95
fix(maps): shrink the picker's bbox 250 ms after the map settles, not…
fryanpan Oct 31, 2026
1a2baf1
fix(maps): keep region-list snackbars inside the Maps panel (ADFA-2436)
fryanpan Oct 31, 2026
33e6b4f
fix(maps): a pinch that starts on the bbox zooms the map (ADFA-2436)
fryanpan Oct 31, 2026
3e515c1
fix(maps): the bbox auto-fit never makes the selection larger (ADFA-2…
fryanpan Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego
| [`plugins/Jetpack-Compose-Preview/`](plugins/Jetpack-Compose-Preview/) | Renders Compose preview functions on the device. |
| [`plugins/Keystore-Generator/`](plugins/Keystore-Generator/) | Creates and manages app signing keystores on the device. |
| [`plugins/Layout-Editor/`](plugins/Layout-Editor/) | Edits Android XML layouts by dragging views. |
| [`plugins/Maps/`](plugins/Maps/) | Builds offline map apps from OpenStreetMap regions. |
| [`plugins/Markdown-Previewer/`](plugins/Markdown-Previewer/) | Shows a live preview of Markdown and HTML files. |
| [`plugins/NDK-Installer/`](plugins/NDK-Installer/) | Installs the NDK and CMake, and adds a native project template. |
| [`plugins/Project-to-Template/`](plugins/Project-to-Template/) | Turns the open project into a reusable template. |
Expand Down
29 changes: 29 additions & 0 deletions plugins/Maps/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Gradle
.gradle/
build/
gradle-app.setting
!gradle-wrapper.jar
.gradletasknamecache

# IDE
.idea/
*.iml
*.ipr
*.iws
.project
.classpath
.settings/
.kotlin/

# Local configuration
local.properties

# OS
.DS_Store
Thumbs.db

# Logs
*.log

# Test outputs
test-results/
96 changes: 96 additions & 0 deletions plugins/Maps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Maps: Offline OpenStreetMap for Code on the Go

`Maps` scaffolds an OpenStreetMap app in minutes. It downloads a map region and wires it into a
MapLibre renderer, so the generated app shows a real, pannable map with no network connection —
built for offline-first, low-bandwidth use.

You start by creating a project from the **Offline OSM Map** template in the New Project wizard
— a basic MapLibre map app. The plugin then adds a **Maps** tab to the editor bottom sheet,
where you download a map region and bundle it into that app.

## Features

- **Start from a template**: create an **Offline OSM Map** project (Kotlin or Java) — a basic
MapLibre map app, ready to drop a region into.
- **Download a region**: from the **Maps** tab, draw a bounding box on a world map and the
plugin slices out just that region's OpenStreetMap vector tiles (PMTiles) from an
Internet-in-a-Box source — with a live size estimate and an automatic zoom cap to keep
downloads small.
- **Bundle it into your app**: apply a downloaded region to your **Offline OSM Map** project and
its tiles are copied into the app's assets, ready to render offline.
- **Builds and runs offline**: the generated app renders tiles — place and street-name labels
included — through MapLibre and an in-process loopback HTTP server, with no network calls. It
even builds on-device without internet, since MapLibre is vendored.

## Architecture

```
plugins/Maps/
├── addon.json # Gallery metadata
├── build.gradle.kts # Build configuration (plugin API via ../../libs)
├── settings.gradle.kts # Buildscript classpath (shared ../../libs)
├── maps.html # Full plugin documentation
├── src/main/
│ ├── AndroidManifest.xml # Plugin metadata, permissions, icons
│ ├── kotlin/org/appdevforall/maps/
│ │ ├── MapsPlugin.kt # Main plugin class (registers the tab + template)
│ │ ├── domain/ # Pure types: bbox, zoom cap, region models
│ │ ├── data/ # Region cache, downloader, installer, stores
│ │ ├── slicer/ # PMTiles region slicer (Hilbert range decomposition)
│ │ ├── templates/ # Project template + MapLibre app emitter
│ │ ├── ui/ # Maps tab, bbox picker, download wizard
│ │ └── util/ # Atomic file I/O, byte-size formatting
│ └── assets/
│ ├── templates/region-map/ # The emitted MapLibre app (+ offline Maven repo)
│ ├── docs/index.html # In-IDE OSM + MapLibre tutorial
│ ├── maps/natural-earth-*.pmtiles # Bundled world basemap
│ ├── fonts/ # Noto Sans label glyphs (OFL)
│ └── icon_day.png / icon_night.png
└── README.md
```

### Plugin metadata (AndroidManifest.xml)
- **plugin.id**: `org.appdevforall.maps`
- **plugin.name**: `Maps`
- **plugin.author**: App Dev For All
- **plugin.main_class**: `org.appdevforall.maps.MapsPlugin`
- **plugin.permissions**: `filesystem.read`, `filesystem.write`, `network.access`, `native.code`

## Building the plugin

```bash
cd plugins/Maps
../../gradlew assemblePlugin # release -> build/plugin/maps.cgp
../../gradlew assemblePluginDebug # debug
```

### Installation
1. Import the `.cgp` through Code on the Go's Plugin Manager.
2. Open the **Maps** tab in the editor bottom sheet, or create a new project from the
**Offline OSM Map** template.

## Usage

1. **Create a new project** from the **Offline OSM Map** template (New Project wizard) — this
scaffolds a basic MapLibre map app (Kotlin or Java).
2. Open the **Maps** tab in the editor bottom sheet and tap **Download new region**: draw a
bounding box, name it, and **Save**.
3. **Apply** the downloaded region — its tiles are bundled into your app's assets.
4. Build and run. The app renders your region offline.

## Dependencies
- `plugin-api`: Code on the Go Plugin API (`compileOnly`, shared `../../libs/plugin-api.jar`)
- `org.maplibre.gl:android-sdk-opengl`: offline vector map rendering
- `com.github.davidmoten:hilbert-curve`: PMTiles tile-range decomposition
- `okhttp`, `androidx.*`, `material`, `kotlinx-coroutines`: networking and UI

### Requirements
- Android API 28+ (Android 9)
- Minimum IDE version: 26.38 (the first release that bundles AGP 9, which the generated project needs)
- Network once, to download a region's tiles (the generated app then runs and builds offline)

## License

Provided as-is for educational and development purposes. OpenStreetMap data is © OpenStreetMap
contributors (ODbL); bundled fonts are Noto Sans (OFL). See
`src/main/assets/THIRD_PARTY_LICENSES.txt` for full attribution.
16 changes: 16 additions & 0 deletions plugins/Maps/addon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"summary": "Builds offline map apps from OpenStreetMap regions.",
"description": "Download an OpenStreetMap region from an Internet-in-a-Box server and bundle it into an app made from the Offline OSM Map template. The app shows the map with no internet connection.",
"tags": [
"maps",
"openstreetmap",
"offline",
"templates"
],
"origin": "appdevforall",
"license": "AGPL-3.0-or-later",
"author": {
"name": "App Dev For All",
"url": "https://www.appdevforall.org"
}
}
175 changes: 175 additions & 0 deletions plugins/Maps/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("com.android.application")
id("com.itsaky.androidide.plugins.build")
// Unit-test coverage. CoGo's root build uses JaCoCo 0.8.11 + Sonar; this
// plugin keeps a single-module report (`./gradlew jacocoTestReport`) so the
// covered-logic claim in the PR is reproducible. The UI layer is intentionally
// 0% here — fragments + MapLibre GL are device-tested via android-qa, not JVM.
jacoco
}

jacoco {
toolVersion = "0.8.11"
}

pluginBuilder {
pluginName = "maps"
}

android {
namespace = "org.appdevforall.maps"
compileSdk = 36

defaultConfig {
applicationId = "org.appdevforall.maps"
minSdk = 28
targetSdk = 36
versionCode = 1
versionName = "1.0.0"

// Code on the Go ships for these two ABIs only; MapLibre's x86 and x86_64 libraries
// would add about 21 MB that no host can load.
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
}
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

packaging {
resources {
excludes += setOf(
"META-INF/versions/9/OSGI-INF/MANIFEST.MF",
"META-INF/DEPENDENCIES",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/NOTICE",
"META-INF/NOTICE.txt"
)
}
jniLibs.useLegacyPackaging = true
}

androidResources {
noCompress.add("pmtiles")
}

testOptions {
unitTests {
// Production code logs via android.util.Log; in plain JVM unit tests
// android.util.Log is the stubbed android.jar and throws "not mocked".
// Returning default values makes Log calls no-ops so the pure parsing
// logic stays unit-testable.
isReturnDefaultValues = true
}
}
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}

dependencies {
// plugin-api jar is the canonical plugin contract; IDE provides at runtime.
compileOnly(files("../../libs/plugin-api.jar"))

implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.fragment:fragment-ktx:1.8.8")
implementation("androidx.fragment:fragment:1.8.8")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.3.21")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")

// MapLibre OpenGL ES — Vulkan unreliable on API 26-29 target audience.
// 13.x fixes PMTiles file-source range-header handling vs 11.x.
implementation("org.maplibre.gl:android-sdk-opengl:13.1.0")

implementation("com.squareup.okhttp3:okhttp:4.12.0")

// Hilbert-curve range decomposition for bbox→tile-id slicing.
implementation("com.github.davidmoten:hilbert-curve:0.2.3")

testImplementation("junit:junit:4.13.2")
testImplementation("org.json:json:20210307")
// Local HTTP stub for unit-testing the range-fetch + download paths
// (HttpRangeFetcher, RegionDownloader) without hitting a real server.
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
// plugin-api is compileOnly for the main set (host-provided at runtime). Unit
// tests run on a plain JVM with no host, so they need it on the test classpath
// to compile + run the host-facing classes (MapsPlugin) and the test fakes.
testImplementation(files("../../libs/plugin-api.jar"))
}

tasks.wrapper {
gradleVersion = "9.6.1"
distributionType = Wrapper.DistributionType.BIN
}

// Disable AAR metadata checks that fail under the plugin-builder
// pipeline (Beepy and Forms use the same workaround).
tasks.matching {
it.name.contains("checkDebugAarMetadata") ||
it.name.contains("checkReleaseAarMetadata")
}.configureEach {
enabled = false
}

tasks.withType<Test>().configureEach {
// Template tests read these files by path, so declare them or a template edit stays UP-TO-DATE.
inputs.dir("src/main/assets/templates").withPropertyName("templates")
System.getProperty("runOnlineSlicerTests")?.let {
systemProperty("runOnlineSlicerTests", it)
}
testLogging {
events("standardOut")
showStandardStreams = true
}
// Match CoGo's JaCoCo agent config so coverage data is written.
extensions.configure<JacocoTaskExtension> {
isIncludeNoLocationClasses = true
excludes = listOf("jdk.internal.*")
}
}

// Single-module unit-test coverage report. Run: `./gradlew jacocoTestReport`
// (HTML at build/reports/jacoco/jacocoTestReport/html/index.html). UI classes
// register 0% — they're device-tested via android-qa, not JVM-unit-testable.
tasks.register<JacocoReport>("jacocoTestReport") {
dependsOn("testDebugUnitTest")
val fileFilter = listOf(
"**/R.class", "**/R\$*.class", "**/BuildConfig.*",
"**/Manifest*.*", "**/*Test*.*", "**/databinding/**",
)
// AGP 9 compiles Kotlin itself and writes the classes here, not to tmp/kotlin-classes.
val classes = fileTree(
layout.buildDirectory.dir("intermediates/built_in_kotlinc/debug/compileDebugKotlin/classes")
) { exclude(fileFilter) }
classDirectories.setFrom(classes)
// A wrong class path yields an empty report that still passes, so fail instead.
doFirst {
check(!classes.isEmpty) { "jacocoTestReport: no main classes under ${classes.dir}" }
}
sourceDirectories.setFrom(files("src/main/kotlin"))
// Scope execution data to the exact exec file (not a build-dir-wide scan) so
// Gradle doesn't see this task as implicitly consuming other tasks' outputs.
executionData.setFrom(layout.buildDirectory.file("jacoco/testDebugUnitTest.exec"))
reports {
xml.required.set(true)
html.required.set(true)
}
}
10 changes: 10 additions & 0 deletions plugins/Maps/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Android properties
android.useAndroidX=true

# Kotlin
kotlin.code.style=official

# Memory settings for builds
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.caching=true
Loading
Loading