Skip to content

fix: support raster MBTiles import and derive center from bounds#474

Merged
torlando-tech merged 5 commits intomainfrom
claude/fix-issue-296-pJCIv
Feb 16, 2026
Merged

fix: support raster MBTiles import and derive center from bounds#474
torlando-tech merged 5 commits intomainfrom
claude/fix-issue-296-pJCIv

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

  • Fix MBTiles imports without center metadata defaulting to 0,0 (Gulf of Guinea) — now derives center from bounds metadata
  • Add raster tile rendering support for imported PNG/JPEG MBTiles files (e.g., from MeshChatX)
  • Auto-detect tile format from MBTiles metadata and generate appropriate style (raster vs vector)
  • Cache generated style JSON on import so offline maps actually render

Test plan

  • Import raster MBTiles file from MeshChatX — renders correctly at correct location
  • Existing unit tests pass (OfflineMapStyleBuilderTest, OfflineMapsViewModelTest)
  • Import vector (PBF) MBTiles file — verify vector style still generated
  • Verify orphaned file recovery also generates cached style

Fixes #296

🤖 Generated with Claude Code

@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Feb 16, 2026

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 16, 2026

Greptile Summary

This PR fixes two issues with MBTiles imports: (1) maps without center metadata no longer default to 0,0 (Gulf of Guinea) by deriving the center from bounds, and (2) raster tile MBTiles files (PNG/JPEG) are now rendered correctly via auto-detected style generation.

  • Center fallback from bounds: OfflineMapRegionRepository.extractMbtilesMetadata now parses bounds before center, using the bounds midpoint as a fallback when center metadata is absent. The radius calculation is also simplified with nullable types.
  • Raster style generation: New buildRasterOfflineStyle and buildAutoOfflineStyle methods in OfflineMapStyleBuilder detect tile format via the MBTiles metadata table and generate the appropriate MapLibre style JSON.
  • Style caching on import: OfflineMapsViewModel now generates and caches the style JSON file during both user-initiated imports and orphaned file recovery, stored under offline_styles/{regionId}.json and persisted via updateLocalStylePath.
  • The getTileFormat function duplicates the SQLite database access already performed by extractMbtilesMetadata during the same import flow. Consider consolidating to avoid opening the database twice.
  • No new unit tests are added for the three new public methods (buildRasterOfflineStyle, buildAutoOfflineStyle, getTileFormat), and the test plan shows vector MBTiles verification is still unchecked.

Confidence Score: 4/5

  • This PR is safe to merge with low risk — changes are well-structured with proper error handling and fallbacks.
  • The core logic changes (center derivation from bounds, raster style generation) are straightforward and correct. Error handling is defensive with non-fatal catches. The only concerns are minor: duplicate database access during import, and lack of new unit tests for the three added public methods. No runtime-breaking issues found.
  • OfflineMapStyleBuilder.kt has new public methods without test coverage and a duplicate database access pattern.

Important Files Changed

Filename Overview
app/src/main/java/com/lxmf/messenger/map/OfflineMapStyleBuilder.kt Adds buildRasterOfflineStyle, buildAutoOfflineStyle, and getTileFormat for raster MBTiles support with auto-detection. getTileFormat duplicates the database access pattern from OfflineMapRegionRepository.extractMbtilesMetadata. Minor: no new unit tests for the added methods.
app/src/main/java/com/lxmf/messenger/viewmodel/OfflineMapsViewModel.kt Adds cacheStyleForRegion to generate and persist style JSON on import and orphan recovery. Clean error handling with non-fatal catch. Orphan recovery now captures region IDs to also cache styles.
data/src/main/java/com/lxmf/messenger/data/repository/OfflineMapRegionRepository.kt Fixes center derivation to fall back to bounds midpoint when center metadata is missing, resolving the Gulf of Guinea (0,0) default. Clean refactor of bounds parsing with nullable types. Radius calculation correctly simplified.

Sequence Diagram

sequenceDiagram
    participant User
    participant VM as OfflineMapsViewModel
    participant Repo as OfflineMapRegionRepository
    participant Builder as OfflineMapStyleBuilder
    participant DB as SQLiteDatabase
    participant FS as FileSystem

    User->>VM: importMbtilesFile(uri)
    VM->>FS: Copy URI to offline_maps/
    VM->>Repo: importOrphanedFile(file)
    Repo->>DB: Open MBTiles SQLite (extractMbtilesMetadata)
    DB-->>Repo: metadata (name, bounds, center, zoom)
    Note over Repo: Derive center from bounds<br/>if center metadata missing
    Repo->>Repo: Insert OfflineMapRegionEntity
    Repo-->>VM: regionId

    VM->>VM: cacheStyleForRegion(regionId, file, name)
    VM->>Builder: buildAutoOfflineStyle(path, name)
    Builder->>DB: Open MBTiles SQLite (getTileFormat)
    DB-->>Builder: format (pbf/png/jpg)
    alt format == "pbf"
        Builder->>Builder: buildOfflineStyle (vector)
    else format != "pbf"
        Builder->>Builder: buildRasterOfflineStyle (raster)
    end
    Builder-->>VM: styleJson

    VM->>FS: Write offline_styles/{regionId}.json
    VM->>Repo: updateLocalStylePath(regionId, path)
Loading

Last reviewed commit: 5a9b6c3

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +275 to +299
fun getTileFormat(mbtilesPath: String): String {
var db: android.database.sqlite.SQLiteDatabase? = null
return try {
db =
android.database.sqlite.SQLiteDatabase.openDatabase(
mbtilesPath,
null,
android.database.sqlite.SQLiteDatabase.OPEN_READONLY,
)
db
.rawQuery(
"SELECT value FROM metadata WHERE name = 'format'",
null,
).use { cursor ->
if (cursor.moveToFirst()) cursor.getString(0) else "png"
}
} catch (
@Suppress("SwallowedException") e: Exception,
) {
Log.w("OfflineMapStyleBuilder", "Failed to read tile format from $mbtilesPath", e)
"png"
} finally {
db?.close()
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate SQLite access pattern

getTileFormat opens the MBTiles SQLite database and reads from the metadata table, which is the same pattern already used by OfflineMapRegionRepository.extractMbtilesMetadata. Both callers are invoked during the same import flow — extractMbtilesMetadata when creating the region entity, and getTileFormat when generating the style.

Consider extracting the format field during extractMbtilesMetadata (which already reads SELECT name, value FROM metadata) and passing it through, rather than opening the database a second time. This would avoid the extra file I/O and be more maintainable.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/map/OfflineMapStyleBuilder.kt
Line: 275:299

Comment:
**Duplicate SQLite access pattern**

`getTileFormat` opens the MBTiles SQLite database and reads from the `metadata` table, which is the same pattern already used by `OfflineMapRegionRepository.extractMbtilesMetadata`. Both callers are invoked during the same import flow — `extractMbtilesMetadata` when creating the region entity, and `getTileFormat` when generating the style.

Consider extracting the `format` field during `extractMbtilesMetadata` (which already reads `SELECT name, value FROM metadata`) and passing it through, rather than opening the database a second time. This would avoid the extra file I/O and be more maintainable.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +256 to +267
fun buildAutoOfflineStyle(
mbtilesPath: String,
name: String = "Offline Map",
): String {
val format = getTileFormat(mbtilesPath)
Log.d("OfflineMapStyleBuilder", "Detected tile format '$format' for $mbtilesPath")
return if (format == "pbf") {
buildOfflineStyle(mbtilesPath, name)
} else {
buildRasterOfflineStyle(mbtilesPath, name)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing unit tests for new public methods

buildAutoOfflineStyle, buildRasterOfflineStyle, and getTileFormat are all new public methods but there are no corresponding tests in OfflineMapStyleBuilderTest. The PR description notes existing tests pass, but the test plan shows vector MBTiles verification is unchecked. At minimum, buildRasterOfflineStyle can be tested (like the existing buildOfflineStyle tests) to verify the generated JSON structure contains a raster source and raster layer.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/map/OfflineMapStyleBuilder.kt
Line: 256:267

Comment:
**Missing unit tests for new public methods**

`buildAutoOfflineStyle`, `buildRasterOfflineStyle`, and `getTileFormat` are all new public methods but there are no corresponding tests in `OfflineMapStyleBuilderTest`. The PR description notes existing tests pass, but the test plan shows vector MBTiles verification is unchecked. At minimum, `buildRasterOfflineStyle` can be tested (like the existing `buildOfflineStyle` tests) to verify the generated JSON structure contains a raster source and raster layer.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

torlando-tech and others added 4 commits February 16, 2026 16:59
MBTiles files without a "center" metadata field (common in exports from
MeshChatX and other tools) defaulted to 0,0 — showing the Gulf of Guinea
instead of the actual region. Now falls back to calculating center from
bounds metadata.

Also adds raster tile rendering support: imported PNG/JPEG MBTiles files
now generate a raster style (instead of the hardcoded vector style) so
they actually render on the map. The format is auto-detected from the
MBTiles metadata table during import.

Fixes #296

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Build a combined MapLibre style from all completed MBTiles regions,
auto-detecting raster vs vector format for each. Raster layers render
below vector layers so both types coexist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refresh the default region on resume so changing the favorite
immediately animates the camera to the new region center.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous approach (minZoom or radius-based) was too zoomed out for
tiles to render. Use the region's maxZoom (clamped to 14) which is the
most detailed level the tiles support and guarantees visible rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech torlando-tech force-pushed the claude/fix-issue-296-pJCIv branch from 5a9b6c3 to 4ba5b09 Compare February 16, 2026 22:15
…read

- Update MapTileSourceManagerTest to pass Context as first constructor arg
- Update OfflineMapStyleBuilderTest for new List<String> signature
- Stub getCompletedRegionsWithMbtiles in test setup
- Move combined style build + file write to Dispatchers.IO
- Use maxZoom for favorite region initial zoom level

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +178 to +181
},
)
if (!hasVector) {
vectorSourceId = sourceId
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: When combining multiple vector MBTiles files, only the first file's map data is rendered because all layers are incorrectly pointed to the first vector source.
Severity: HIGH

Suggested Fix

Modify the layer generation logic. Instead of replacing the source on a single BASE_LAYERS template with the first vectorSourceId, iterate through each detected vector source. For each vector source, create a distinct set of layers that correctly reference that source's unique ID. This ensures that layers are generated for every vector file, allowing all of them to be rendered on the map.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: app/src/main/java/com/lxmf/messenger/map/OfflineMapStyleBuilder.kt#L178-L181

Potential issue: The `buildCombinedOfflineStyle` function is designed to combine
multiple MBTiles files. When multiple vector (pbf) files are provided, it correctly
creates a source for each (e.g., `vector-0`, `vector-1`). However, it only stores the ID
of the first source in the `vectorSourceId` variable. Subsequently, all base vector
layers are configured to use only this first source ID. This results in a map where only
the tiles from the first imported vector file are rendered, while all other vector files
are invisible, despite being processed.

@torlando-tech torlando-tech merged commit 47dd707 into main Feb 16, 2026
9 of 10 checks passed
@torlando-tech torlando-tech deleted the claude/fix-issue-296-pJCIv branch February 16, 2026 23:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow external import of map files

1 participant