MBTiles is a specification for storing map tiles in a SQLite database, providing an efficient format for packaging and serving map tiles. For Java applications, implementing MBTiles support enables high-performance map tile delivery for web mapping applications, mobile apps, and desktop GIS tools. This guide covers practical patterns for reading, serving, and managing MBTiles databases in Java.
Understanding MBTiles Architecture
Key Components:
- SQLite database with standardized schema
- Tiles table storing zoom, x, y coordinates and tile data
- Metadata table containing map information and bounds
- Grids table (optional) for UTFGrid interaction data
- Compressed tile data in PNG, JPEG, or PBF format
Core Implementation Patterns
1. Project Setup and Dependencies
Configure SQLite and web server dependencies.
Maven Configuration:
<dependencies> <!-- SQLite JDBC --> <dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> <version>3.42.0.0</version> </dependency> <!-- Web Framework --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.1.0</version> </dependency> <!-- Image Processing --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-imaging</artifactId> <version>1.0-alpha3</version> </dependency> <!-- Caching --> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.1.6</version> </dependency> <!-- Compression --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-compress</artifactId> <version>1.23.0</version> </dependency> </dependencies>
2. Domain Models for MBTiles
Create comprehensive Java models for MBTiles data structures.
Core Domain Models:
@Data
public class MBTilesMetadata {
private String name;
private String version = "1.3";
private String description;
private String type = "base_layer";
private String format = "png";
private String bounds; // comma-separated: minLon,minLat,maxLon,maxLat
private String center; // comma-separated: lon,lat,zoom
private Integer minZoom;
private Integer maxZoom;
private String attribution;
private String template;
private String legend;
private String scheme = "xyz";
private List<TileJSON> tilejson = List.of("2.1.0");
private Map<String, String> otherFields;
public BoundingBox getBoundsAsBbox() {
if (bounds == null) return null;
String[] parts = bounds.split(",");
if (parts.length == 4) {
return new BoundingBox(
Double.parseDouble(parts[0]),
Double.parseDouble(parts[1]),
Double.parseDouble(parts[2]),
Double.parseDouble(parts[3])
);
}
return null;
}
public CenterPoint getCenterAsPoint() {
if (center == null) return null;
String[] parts = center.split(",");
if (parts.length == 3) {
return new CenterPoint(
Double.parseDouble(parts[0]),
Double.parseDouble(parts[1]),
Integer.parseInt(parts[2])
);
}
return null;
}
}
@Data
public class TileCoordinate {
private int zoom;
private int x;
private int y;
public TileCoordinate(int zoom, int x, int y) {
this.zoom = zoom;
this.x = x;
this.y = y;
}
public String toString() {
return String.format("%d/%d/%d", zoom, x, y);
}
public boolean isValid(int maxZoom) {
if (zoom < 0 || zoom > maxZoom) return false;
int maxTile = (1 << zoom) - 1;
return x >= 0 && x <= maxTile && y >= 0 && y <= maxTile;
}
public TileCoordinate getParent() {
if (zoom == 0) return null;
return new TileCoordinate(zoom - 1, x / 2, y / 2);
}
}
@Data
public class MapTile {
private TileCoordinate coordinate;
private byte[] data;
private String format;
private Date lastModified;
private Long size;
private Map<String, Object> metadata;
public String getContentType() {
switch (format.toLowerCase()) {
case "png": return "image/png";
case "jpg": case "jpeg": return "image/jpeg";
case "pbf": return "application/x-protobuf";
case "webp": return "image/webp";
default: return "application/octet-stream";
}
}
public boolean isVectorTile() {
return "pbf".equalsIgnoreCase(format);
}
}
@Data
public class BoundingBox {
private double minLon;
private double minLat;
private double maxLon;
private double maxLat;
public BoundingBox(double minLon, double minLat, double maxLon, double maxLat) {
this.minLon = minLon;
this.minLat = minLat;
this.maxLon = maxLon;
this.maxLat = maxLat;
}
public boolean contains(double lon, double lat) {
return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat;
}
public String toCommaSeparated() {
return String.format("%f,%f,%f,%f", minLon, minLat, maxLon, maxLat);
}
}
@Data
public class CenterPoint {
private double longitude;
private double latitude;
private int zoom;
public CenterPoint(double longitude, double latitude, int zoom) {
this.longitude = longitude;
this.latitude = latitude;
this.zoom = zoom;
}
public String toCommaSeparated() {
return String.format("%f,%f,%d", longitude, latitude, zoom);
}
}
@Data
public class GridData {
private TileCoordinate coordinate;
private String gridJson;
private Map<String, String> data;
public JsonNode getGridJson() {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readTree(gridJson);
} catch (Exception e) {
return null;
}
}
}
3. MBTiles Reader and Database Management
Core service for reading MBTiles databases.
MBTiles Reader Service:
@Service
@Slf4j
public class MBTilesReader {
private final DataSource dataSource;
public MBTilesReader(DataSource dataSource) {
this.dataSource = dataSource;
}
public MBTilesMetadata readMetadata(String filePath) throws SQLException, IOException {
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + filePath)) {
MBTilesMetadata metadata = new MBTilesMetadata();
Map<String, String> otherFields = new HashMap<>();
String sql = "SELECT name, value FROM metadata";
try (PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
String name = rs.getString("name");
String value = rs.getString("value");
switch (name.toLowerCase()) {
case "name":
metadata.setName(value);
break;
case "version":
metadata.setVersion(value);
break;
case "description":
metadata.setDescription(value);
break;
case "type":
metadata.setType(value);
break;
case "format":
metadata.setFormat(value);
break;
case "bounds":
metadata.setBounds(value);
break;
case "center":
metadata.setCenter(value);
break;
case "minzoom":
metadata.setMinZoom(Integer.parseInt(value));
break;
case "maxzoom":
metadata.setMaxZoom(Integer.parseInt(value));
break;
case "attribution":
metadata.setAttribution(value);
break;
case "template":
metadata.setTemplate(value);
break;
case "legend":
metadata.setLegend(value);
break;
case "scheme":
metadata.setScheme(value);
break;
default:
otherFields.put(name, value);
}
}
}
metadata.setOtherFields(otherFields);
return metadata;
}
}
public MapTile readTile(String filePath, TileCoordinate coord) throws SQLException {
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + filePath)) {
String sql = "SELECT tile_data, length(tile_data) as size FROM tiles " +
"WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, coord.getZoom());
stmt.setInt(2, coord.getX());
stmt.setInt(3, coord.getY());
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
byte[] tileData = rs.getBytes("tile_data");
long size = rs.getLong("size");
MapTile tile = new MapTile();
tile.setCoordinate(coord);
tile.setData(tileData);
tile.setSize(size);
tile.setLastModified(new Date());
// Detect format from magic bytes
tile.setFormat(detectTileFormat(tileData));
return tile;
}
}
}
}
return null;
}
public MapTile readTile(String filePath, int z, int x, int y) throws SQLException {
// Convert TMS Y to XYZ scheme if needed
MBTilesMetadata metadata = readMetadata(filePath);
if ("tms".equals(metadata.getScheme())) {
y = (1 << z) - 1 - y;
}
return readTile(filePath, new TileCoordinate(z, x, y));
}
public List<TileCoordinate> listTiles(String filePath, Integer zoom) throws SQLException {
List<TileCoordinate> tiles = new ArrayList<>();
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + filePath)) {
String sql = "SELECT zoom_level, tile_column, tile_row FROM tiles";
if (zoom != null) {
sql += " WHERE zoom_level = ?";
}
sql += " ORDER BY zoom_level, tile_column, tile_row";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
if (zoom != null) {
stmt.setInt(1, zoom);
}
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
TileCoordinate coord = new TileCoordinate(
rs.getInt("zoom_level"),
rs.getInt("tile_column"),
rs.getInt("tile_row")
);
tiles.add(coord);
}
}
}
}
return tiles;
}
public GridData readGrid(String filePath, TileCoordinate coord) throws SQLException {
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + filePath)) {
// Check if grids table exists
if (!tableExists(conn, "grids")) {
return null;
}
String sql = "SELECT grid, json FROM grids " +
"WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, coord.getZoom());
stmt.setInt(2, coord.getX());
stmt.setInt(3, coord.getY());
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
GridData grid = new GridData();
grid.setCoordinate(coord);
grid.setGridJson(rs.getString("grid"));
// Parse JSON data if available
String jsonData = rs.getString("json");
if (jsonData != null) {
grid.setData(parseGridData(jsonData));
}
return grid;
}
}
}
}
return null;
}
public MBTilesInfo getDatabaseInfo(String filePath) throws SQLException, IOException {
MBTilesInfo info = new MBTilesInfo();
info.setMetadata(readMetadata(filePath));
info.setFileSize(Files.size(Paths.get(filePath)));
info.setTileCount(countTiles(filePath));
info.setFormats(getTileFormats(filePath));
info.setZoomLevels(getZoomLevels(filePath));
return info;
}
private int countTiles(String filePath) throws SQLException {
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + filePath)) {
String sql = "SELECT COUNT(*) as count FROM tiles";
try (PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
return rs.next() ? rs.getInt("count") : 0;
}
}
}
private Set<String> getTileFormats(String filePath) throws SQLException {
Set<String> formats = new HashSet<>();
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + filePath)) {
// Sample some tiles to detect formats
String sql = "SELECT tile_data FROM tiles LIMIT 10";
try (PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
byte[] data = rs.getBytes("tile_data");
formats.add(detectTileFormat(data));
}
}
}
return formats;
}
private Set<Integer> getZoomLevels(String filePath) throws SQLException {
Set<Integer> zoomLevels = new HashSet<>();
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + filePath)) {
String sql = "SELECT DISTINCT zoom_level FROM tiles ORDER BY zoom_level";
try (PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
zoomLevels.add(rs.getInt("zoom_level"));
}
}
}
return zoomLevels;
}
private boolean tableExists(Connection conn, String tableName) throws SQLException {
String sql = "SELECT name FROM sqlite_master WHERE type='table' AND name=?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, tableName);
try (ResultSet rs = stmt.executeQuery()) {
return rs.next();
}
}
}
private Map<String, String> parseGridData(String jsonData) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(jsonData, new TypeReference<Map<String, String>>() {});
} catch (Exception e) {
log.warn("Failed to parse grid JSON data", e);
return new HashMap<>();
}
}
private String detectTileFormat(byte[] data) {
if (data == null || data.length < 8) return "unknown";
// PNG signature
if (data[0] == (byte) 0x89 && data[1] == 'P' && data[2] == 'N' && data[3] == 'G') {
return "png";
}
// JPEG signature
if (data[0] == (byte) 0xFF && data[1] == (byte) 0xD8) {
return "jpg";
}
// WebP signature
if (data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F') {
return "webp";
}
// PBF (Protocol Buffers) - heuristic
if (data.length > 20 && containsVectorTileMarkers(data)) {
return "pbf";
}
return "unknown";
}
private boolean containsVectorTileMarkers(byte[] data) {
// Simple heuristic for vector tiles
return data.length > 4 && (data[0] == 0x1F || data[0] == 0x78);
}
}
@Data
public class MBTilesInfo {
private MBTilesMetadata metadata;
private long fileSize;
private int tileCount;
private Set<String> formats;
private Set<Integer> zoomLevels;
private Date lastModified;
public String getFormatsString() {
return formats != null ? String.join(", ", formats) : "unknown";
}
public String getZoomRange() {
if (zoomLevels == null || zoomLevels.isEmpty()) return "N/A";
int min = zoomLevels.stream().min(Integer::compare).orElse(0);
int max = zoomLevels.stream().max(Integer::compare).orElse(0);
return min + " - " + max;
}
}
4. MBTiles Serving Service
Handle tile serving with caching and performance optimizations.
Tile Serving Service:
@Service
@Slf4j
public class MBTilesService {
private final MBTilesReader tilesReader;
private final Cache<String, MBTilesMetadata> metadataCache;
private final Cache<String, MapTile> tileCache;
private final Map<String, String> mbtilesRegistry;
public MBTilesService(MBTilesReader tilesReader) {
this.tilesReader = tilesReader;
this.metadataCache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
this.tileCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
this.mbtilesRegistry = new ConcurrentHashMap<>();
}
public void registerMBTiles(String name, String filePath) {
try {
if (!Files.exists(Paths.get(filePath))) {
throw new IllegalArgumentException("MBTiles file not found: " + filePath);
}
MBTilesMetadata metadata = tilesReader.readMetadata(filePath);
if (metadata.getName() == null) {
metadata.setName(name);
}
mbtilesRegistry.put(name, filePath);
metadataCache.put(name, metadata);
log.info("Registered MBTiles: {} -> {} (zoom: {}-{}, tiles: {})",
name, filePath, metadata.getMinZoom(), metadata.getMaxZoom(),
getTileCount(filePath));
} catch (Exception e) {
throw new RuntimeException("Failed to register MBTiles: " + name, e);
}
}
public MapTile serveTile(String mapName, int z, int x, int y) {
String cacheKey = String.format("%s/%d/%d/%d", mapName, z, x, y);
return tileCache.get(cacheKey, key -> {
try {
String filePath = mbtilesRegistry.get(mapName);
if (filePath == null) {
throw new RuntimeException("Map not found: " + mapName);
}
MapTile tile = tilesReader.readTile(filePath, z, x, y);
if (tile == null) {
// Try to find a parent tile for overzooming
tile = findParentTile(mapName, z, x, y);
}
return tile;
} catch (Exception e) {
log.warn("Failed to serve tile {}/{}/{}/{}: {}", mapName, z, x, y, e.getMessage());
return null;
}
});
}
public MapTile serveTile(String mapName, TileCoordinate coord) {
return serveTile(mapName, coord.getZoom(), coord.getX(), coord.getY());
}
public MBTilesMetadata getMetadata(String mapName) {
return metadataCache.get(mapName, key -> {
try {
String filePath = mbtilesRegistry.get(key);
return filePath != null ? tilesReader.readMetadata(filePath) : null;
} catch (Exception e) {
log.warn("Failed to get metadata for: {}", key, e);
return null;
}
});
}
public GridData serveGrid(String mapName, int z, int x, int y) {
try {
String filePath = mbtilesRegistry.get(mapName);
if (filePath == null) return null;
TileCoordinate coord = new TileCoordinate(z, x, y);
return tilesReader.readGrid(filePath, coord);
} catch (Exception e) {
log.warn("Failed to serve grid {}/{}/{}/{}: {}", mapName, z, x, y, e.getMessage());
return null;
}
}
public TileJSON getTileJSON(String mapName) {
MBTilesMetadata metadata = getMetadata(mapName);
if (metadata == null) return null;
TileJSON tileJson = new TileJSON();
tileJson.setTilejson("2.1.0");
tileJson.setName(metadata.getName());
tileJson.setDescription(metadata.getDescription());
tileJson.setVersion(metadata.getVersion());
tileJson.setFormat(metadata.getFormat());
tileJson.setBounds(metadata.getBoundsAsBbox());
tileJson.setCenter(metadata.getCenterAsPoint());
tileJson.setMinZoom(metadata.getMinZoom());
tileJson.setMaxZoom(metadata.getMaxZoom());
tileJson.setAttribution(metadata.getAttribution());
// Build tile URLs
String tileUrl = String.format("/tiles/%s/{z}/{x}/{y}.%s",
mapName, metadata.getFormat());
tileJson.setTiles(List.of(tileUrl));
// Add grids if available
if (hasGrids(mapName)) {
String gridUrl = String.format("/grids/%s/{z}/{x}/{y}.json", mapName);
tileJson.setGrids(List.of(gridUrl));
}
return tileJson;
}
private MapTile findParentTile(String mapName, int z, int x, int y) {
MBTilesMetadata metadata = getMetadata(mapName);
if (metadata == null) return null;
int minZoom = metadata.getMinZoom() != null ? metadata.getMinZoom() : 0;
// Search for parent tiles up the zoom hierarchy
for (int parentZ = z - 1; parentZ >= minZoom; parentZ--) {
int parentX = x / (1 << (z - parentZ));
int parentY = y / (1 << (z - parentZ));
try {
MapTile parentTile = serveTile(mapName, parentZ, parentX, parentY);
if (parentTile != null) {
log.debug("Using parent tile {}/{}/{} for {}/{}/{}",
parentZ, parentX, parentY, z, x, y);
return parentTile;
}
} catch (Exception e) {
// Continue searching
}
}
return null;
}
private boolean hasGrids(String mapName) {
try {
String filePath = mbtilesRegistry.get(mapName);
if (filePath == null) return false;
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + filePath)) {
String sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='grids'";
try (PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
return rs.next();
}
}
} catch (Exception e) {
return false;
}
}
private int getTileCount(String filePath) {
try {
return tilesReader.countTiles(filePath);
} catch (Exception e) {
return -1;
}
}
public Set<String> getRegisteredMaps() {
return mbtilesRegistry.keySet();
}
public void unregisterMap(String mapName) {
mbtilesRegistry.remove(mapName);
metadataCache.invalidate(mapName);
// Note: tileCache entries will expire naturally
log.info("Unregistered MBTiles: {}", mapName);
}
}
@Data
public class TileJSON {
private String tilejson = "2.1.0";
private String name;
private String description;
private String version;
private String format;
private BoundingBox bounds;
private CenterPoint center;
private Integer minZoom;
private Integer maxZoom;
private String attribution;
private List<String> tiles;
private List<String> grids;
private Map<String, Object> otherFields;
}
5. REST API for Tile Serving
Expose MBTiles as web map tile service.
Tile Serving Controller:
@RestController
@RequestMapping("/api/mbtiles")
@Slf4j
public class MBTilesController {
private final MBTilesService tilesService;
public MBTilesController(MBTilesService tilesService) {
this.tilesService = tilesService;
}
@GetMapping("/{mapName}/{z}/{x}/{y}.{format}")
public ResponseEntity<byte[]> serveTile(
@PathVariable String mapName,
@PathVariable int z,
@PathVariable int x,
@PathVariable int y,
@PathVariable String format,
@RequestHeader Map<String, String> headers) {
try {
MapTile tile = tilesService.serveTile(mapName, z, x, y);
if (tile == null) {
return createEmptyTile();
}
// Set cache headers
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.parseMediaType(tile.getContentType()));
responseHeaders.setCacheControl("public, max-age=86400"); // 24 hours
responseHeaders.setLastModified(tile.getLastModified());
responseHeaders.setContentLength(tile.getData().length);
// ETag for caching
String etag = calculateETag(tile.getData());
responseHeaders.setETag("\"" + etag + "\"");
// Check If-None-Match header
String ifNoneMatch = headers.get("if-none-match");
if (ifNoneMatch != null && ifNoneMatch.contains(etag)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.headers(responseHeaders)
.build();
}
return ResponseEntity.ok()
.headers(responseHeaders)
.body(tile.getData());
} catch (Exception e) {
log.warn("Tile serving failed: {}/{}/{}/{} - {}", mapName, z, x, y, e.getMessage());
return createEmptyTile();
}
}
@GetMapping("/{mapName}/grid/{z}/{x}/{y}.json")
public ResponseEntity<GridData> serveGrid(
@PathVariable String mapName,
@PathVariable int z,
@PathVariable int x,
@PathVariable int y) {
try {
GridData grid = tilesService.serveGrid(mapName, z, x, y);
if (grid == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.header("Content-Type", "application/json")
.header("Cache-Control", "public, max-age=3600")
.body(grid);
} catch (Exception e) {
log.warn("Grid serving failed: {}/{}/{}/{} - {}", mapName, z, x, y, e.getMessage());
return ResponseEntity.notFound().build();
}
}
@GetMapping("/{mapName}/tilejson.json")
public ResponseEntity<TileJSON> serveTileJSON(@PathVariable String mapName) {
try {
TileJSON tileJson = tilesService.getTileJSON(mapName);
if (tileJson == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.header("Content-Type", "application/json")
.body(tileJson);
} catch (Exception e) {
log.warn("TileJSON serving failed: {} - {}", mapName, e.getMessage());
return ResponseEntity.notFound().build();
}
}
@GetMapping("/{mapName}/metadata")
public ResponseEntity<MBTilesMetadata> getMetadata(@PathVariable String mapName) {
try {
MBTilesMetadata metadata = tilesService.getMetadata(mapName);
if (metadata == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(metadata);
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
@GetMapping("/maps")
public ResponseEntity<Map<String, String>> getRegisteredMaps() {
Set<String> maps = tilesService.getRegisteredMaps();
Map<String, String> response = new HashMap<>();
maps.forEach(map -> response.put(map, "/api/mbtiles/" + map + "/tilejson.json"));
return ResponseEntity.ok(response);
}
@PostMapping("/register")
public ResponseEntity<String> registerMap(@RequestBody MapRegistrationRequest request) {
try {
tilesService.registerMBTiles(request.getName(), request.getFilePath());
return ResponseEntity.ok("Map registered successfully: " + request.getName());
} catch (Exception e) {
return ResponseEntity.badRequest().body("Registration failed: " + e.getMessage());
}
}
@DeleteMapping("/{mapName}")
public ResponseEntity<String> unregisterMap(@PathVariable String mapName) {
try {
tilesService.unregisterMap(mapName);
return ResponseEntity.ok("Map unregistered: " + mapName);
} catch (Exception e) {
return ResponseEntity.badRequest().body("Unregistration failed: " + e.getMessage());
}
}
private ResponseEntity<byte[]> createEmptyTile() {
// Return a transparent PNG for missing tiles
byte[] transparentPNG = new byte[] {
(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, (byte) 0xC4, (byte) 0x89, 0x00,
0x00, 0x00, 0x0D, 0x49, 0x44, 0x41, 0x54, 0x78, (byte) 0xDA, 0x63, 0x60,
0x00, 0x00, 0x00, 0x05, 0x00, 0x01, 0x5F, (byte) 0x90, (byte) 0x8D, 0x05,
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, (byte) 0xAE, 0x42, 0x60, (byte) 0x82
};
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
headers.setContentLength(transparentPNG.length);
headers.setCacheControl("public, max-age=300"); // 5 minutes for empty tiles
return ResponseEntity.ok()
.headers(headers)
.body(transparentPNG);
}
private String calculateETag(byte[] data) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(data);
return HexFormat.of().formatHex(digest);
} catch (Exception e) {
return Integer.toHexString(Arrays.hashCode(data));
}
}
}
@Data
class MapRegistrationRequest {
private String name;
private String filePath;
}
6. MBTiles Management and Administration
Admin service for managing MBTiles databases.
MBTiles Admin Service:
@Service
@Slf4j
public class MBTilesAdminService {
private final MBTilesReader tilesReader;
public MBTilesAdminService(MBTilesReader tilesReader) {
this.tilesReader = tilesReader;
}
public MBTilesInfo inspectMBTiles(String filePath) {
try {
return tilesReader.getDatabaseInfo(filePath);
} catch (Exception e) {
throw new RuntimeException("Failed to inspect MBTiles: " + filePath, e);
}
}
public ValidationResult validateMBTiles(String filePath) {
ValidationResult result = new ValidationResult();
result.setFilePath(filePath);
result.setValid(true);
try {
// Check file existence and readability
if (!Files.isReadable(Paths.get(filePath))) {
result.setValid(false);
result.getErrors().add("File is not readable");
return result;
}
// Check SQLite database
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + filePath)) {
// Check required tables
if (!tableExists(conn, "metadata")) {
result.setValid(false);
result.getErrors().add("Missing required table: metadata");
}
if (!tableExists(conn, "tiles")) {
result.setValid(false);
result.getErrors().add("Missing required table: tiles");
}
// Check metadata table structure
if (tableExists(conn, "metadata")) {
checkMetadataTable(conn, result);
}
// Check tiles table structure
if (tableExists(conn, "tiles")) {
checkTilesTable(conn, result);
}
// Validate some sample tiles
validateSampleTiles(filePath, result);
}
} catch (Exception e) {
result.setValid(false);
result.getErrors().add("Validation error: " + e.getMessage());
}
return result;
}
public ExportResult exportTiles(String filePath, String outputDir,
Integer minZoom, Integer maxZoom) {
ExportResult result = new ExportResult();
result.setSourceFile(filePath);
result.setOutputDirectory(outputDir);
try {
MBTilesInfo info = tilesReader.getDatabaseInfo(filePath);
List<TileCoordinate> tiles = tilesReader.listTiles(filePath, null);
int actualMinZoom = minZoom != null ? minZoom : info.getMetadata().getMinZoom();
int actualMaxZoom = maxZoom != null ? maxZoom : info.getMetadata().getMaxZoom();
Path outputPath = Paths.get(outputDir);
Files.createDirectories(outputPath);
int exported = 0;
int skipped = 0;
for (TileCoordinate coord : tiles) {
if (coord.getZoom() < actualMinZoom || coord.getZoom() > actualMaxZoom) {
skipped++;
continue;
}
MapTile tile = tilesReader.readTile(filePath, coord);
if (tile != null) {
Path tilePath = outputPath.resolve(
String.format("%d/%d/%d.%s",
coord.getZoom(), coord.getX(), coord.getY(),
tile.getFormat())
);
Files.createDirectories(tilePath.getParent());
Files.write(tilePath, tile.getData());
exported++;
} else {
skipped++;
}
}
result.setExportedCount(exported);
result.setSkippedCount(skipped);
result.setSuccess(true);
} catch (Exception e) {
result.setSuccess(false);
result.setError(e.getMessage());
}
return result;
}
private boolean tableExists(Connection conn, String tableName) throws SQLException {
String sql = "SELECT name FROM sqlite_master WHERE type='table' AND name=?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, tableName);
try (ResultSet rs = stmt.executeQuery()) {
return rs.next();
}
}
}
private void checkMetadataTable(Connection conn, ValidationResult result) throws SQLException {
String sql = "SELECT name, value FROM metadata LIMIT 10";
try (PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
int count = 0;
while (rs.next()) {
count++;
}
if (count == 0) {
result.getWarnings().add("Metadata table is empty");
}
}
}
private void checkTilesTable(Connection conn, ValidationResult result) throws SQLException {
String sql = "SELECT COUNT(*) as count FROM tiles";
try (PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
if (rs.next() && rs.getInt("count") == 0) {
result.getWarnings().add("Tiles table is empty");
}
}
}
private void validateSampleTiles(String filePath, ValidationResult result) throws SQLException {
List<TileCoordinate> sampleTiles = tilesReader.listTiles(filePath, null)
.stream()
.limit(10)
.collect(Collectors.toList());
for (TileCoordinate coord : sampleTiles) {
MapTile tile = tilesReader.readTile(filePath, coord);
if (tile == null || tile.getData() == null || tile.getData().length == 0) {
result.getWarnings().add("Empty or invalid tile at " + coord);
}
}
}
}
@Data
public class ValidationResult {
private String filePath;
private boolean valid;
private List<String> errors = new ArrayList<>();
private List<String> warnings = new ArrayList<>();
private Date validatedAt = new Date();
}
@Data
public class ExportResult {
private String sourceFile;
private String outputDirectory;
private boolean success;
private String error;
private int exportedCount;
private int skippedCount;
private Date exportedAt = new Date();
}
Best Practices for MBTiles Serving
- Caching Strategy: Implement multi-level caching (memory, disk, CDN)
- Error Handling: Gracefully handle missing tiles and database errors
- Performance Optimization: Use connection pooling and prepared statements
- Security: Validate tile coordinates and implement rate limiting
- Monitoring: Track tile serving metrics and database performance
- Compression: Enable GZIP compression for vector tiles and metadata
- Concurrency: Use thread-safe data structures for tile caching
Conclusion: High-Performance Tile Serving
MBTiles serving in Java provides a robust foundation for delivering map tiles efficiently. By leveraging SQLite's performance characteristics, implementing smart caching strategies, and providing comprehensive web APIs, Java applications can serve millions of tiles with minimal resource consumption.
This implementation demonstrates that MBTiles isn't just a storage format—it's a complete tile serving solution that, when combined with Java's enterprise capabilities, enables scalable, high-performance web mapping applications suitable for both small projects and large-scale deployments.
Java Observability, Logging Intelligence & AI-Driven Monitoring (APM, Tracing, Logs & Anomaly Detection)
https://macronepal.com/blog/beyond-metrics-observing-serverless-and-traditional-java-applications-with-thundra-apm/
Explains using Thundra APM to observe both serverless and traditional Java applications by combining tracing, metrics, and logs into a unified observability platform for faster debugging and performance insights.
https://macronepal.com/blog/dynatrace-oneagent-in-java-2/
Explains Dynatrace OneAgent for Java, which automatically instruments JVM applications to capture metrics, traces, and logs, enabling full-stack monitoring and root-cause analysis with minimal configuration.
https://macronepal.com/blog/lightstep-java-sdk-distributed-tracing-and-observability-implementation/
Explains Lightstep Java SDK for distributed tracing, helping developers track requests across microservices and identify latency issues using OpenTelemetry-based observability.
https://macronepal.com/blog/honeycomb-io-beeline-for-java-complete-guide-2/
Explains Honeycomb Beeline for Java, which provides high-cardinality observability and deep query capabilities to understand complex system behavior and debug distributed systems efficiently.
https://macronepal.com/blog/lumigo-for-serverless-in-java-complete-distributed-tracing-guide-2/
Explains Lumigo for Java serverless applications, offering automatic distributed tracing, log correlation, and error tracking to simplify debugging in cloud-native environments. (Lumigo Docs)
https://macronepal.com/blog/from-noise-to-signals-implementing-log-anomaly-detection-in-java-applications/
Explains how to detect anomalies in Java logs using behavioral patterns and machine learning techniques to separate meaningful incidents from noisy log data and improve incident response.
https://macronepal.com/blog/ai-powered-log-analysis-in-java-from-reactive-debugging-to-proactive-insights/
Explains AI-driven log analysis for Java applications, shifting from manual debugging to predictive insights that identify issues early and improve system reliability using intelligent log processing.
https://macronepal.com/blog/titliel-java-logging-best-practices/
Explains best practices for Java logging, focusing on structured logs, proper log levels, performance optimization, and ensuring logs are useful for debugging and observability systems.
https://macronepal.com/blog/seeking-a-loguru-for-java-the-quest-for-elegant-and-simple-logging/
Explains the search for simpler, more elegant logging frameworks in Java, comparing modern logging approaches that aim to reduce complexity while improving readability and developer experience.