Introduction to Vector Tiles and Tippecanoe
Vector tiles are a modern approach to mapping that deliver geographic data in small, efficient packets. Tippecanoe is a powerful tool for creating vector tiles from GeoJSON data. This guide covers integrating Tippecanoe with Java applications for generating, serving, and consuming vector tiles.
Architecture Overview
System Components
public class VectorTileArchitecture {
/*
* Data Flow:
* 1. GeoJSON Sources → 2. Tippecanoe Processing →
* 3. MBTiles Storage → 4. Tile Server → 5. Java Client
*/
}
Tippecanoe Process Integration
Java Process Execution Wrapper
@Component
public class TippecanoeProcessor {
private static final Logger logger = LoggerFactory.getLogger(TippecanoeProcessor.class);
public File generateVectorTiles(File geoJsonFile, TippecanoeOptions options)
throws IOException, InterruptedException {
File outputFile = File.createTempFile("tiles", ".mbtiles");
List<String> command = buildTippecanoeCommand(geoJsonFile, outputFile, options);
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true);
logger.info("Executing: {}", String.join(" ", command));
Process process = processBuilder.start();
// Capture output
String output = new String(process.getInputStream().readAllBytes());
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Tippecanoe failed: " + output);
}
logger.info("Vector tiles generated: {}", outputFile.getAbsolutePath());
return outputFile;
}
private List<String> buildTippecanoeCommand(File input, File output, TippecanoeOptions options) {
List<String> command = new ArrayList<>();
command.add("tippecanoe");
command.add("-o");
command.add(output.getAbsolutePath());
// Add options
if (options.getMaxZoom() != null) {
command.add("-z");
command.add(options.getMaxZoom().toString());
}
if (options.getMinZoom() != null) {
command.add("-Z");
command.add(options.getMinZoom().toString());
}
if (options.getLayerName() != null) {
command.add("-l");
command.add(options.getLayerName());
}
// Force overwrite
command.add("-f");
// Input file
command.add(input.getAbsolutePath());
return command;
}
}
// Configuration options
public class TippecanoeOptions {
private Integer minZoom;
private Integer maxZoom;
private String layerName;
private Boolean includeFullGeometry;
private Integer maximumTileSize;
// Constructors, getters, setters
public TippecanoeOptions() {}
public TippecanoeOptions(Integer minZoom, Integer maxZoom, String layerName) {
this.minZoom = minZoom;
this.maxZoom = maxZoom;
this.layerName = layerName;
}
// Getters and setters...
public Integer getMinZoom() { return minZoom; }
public void setMinZoom(Integer minZoom) { this.minZoom = minZoom; }
public Integer getMaxZoom() { return maxZoom; }
public void setMaxZoom(Integer maxZoom) { this.maxZoom = maxZoom; }
public String getLayerName() { return layerName; }
public void setLayerName(String layerName) { this.layerName = layerName; }
public Boolean getIncludeFullGeometry() { return includeFullGeometry; }
public void setIncludeFullGeometry(Boolean includeFullGeometry) {
this.includeFullGeometry = includeFullGeometry;
}
public Integer getMaximumTileSize() { return maximumTileSize; }
public void setMaximumTileSize(Integer maximumTileSize) {
this.maximumTileSize = maximumTileSize;
}
}
Advanced Tippecanoe Features
Layer-specific Configuration
public class LayerConfiguration {
private String sourceFile;
private String layerName;
private String filterExpression;
private Integer minZoom;
private Integer maxZoom;
private Map<String, String> attributeMapping;
// Constructors, getters, setters
}
@Service
public class AdvancedTippecanoeProcessor {
public File generateMultiLayerTiles(List<LayerConfiguration> layers,
File outputDir) throws Exception {
// Generate layer-specific GeoJSON files
List<File> layerFiles = new ArrayList<>();
for (LayerConfiguration layer : layers) {
File layerFile = preprocessLayerData(layer);
layerFiles.add(layerFile);
}
// Build complex tippecanoe command
List<String> command = new ArrayList<>();
command.add("tippecanoe");
command.add("-o");
command.add(new File(outputDir, "multilayer.mbtiles").getAbsolutePath());
command.add("-z");
command.add("15");
command.add("--coalesce");
command.add("--detect-shared-borders");
// Add layer-specific options
for (int i = 0; i < layers.size(); i++) {
LayerConfiguration layer = layers.get(i);
command.add("-L");
command.add(String.format("%s:%s", layer.getLayerName(),
layerFiles.get(i).getAbsolutePath()));
}
return executeTippecanoe(command);
}
private File preprocessLayerData(LayerConfiguration layer) throws IOException {
// Implement GeoJSON preprocessing with filtering and attribute mapping
ObjectMapper mapper = new ObjectMapper();
JsonNode sourceData = mapper.readTree(new File(layer.getSourceFile()));
// Apply filtering and transformation
JsonNode processedData = processGeoJson(sourceData, layer);
File outputFile = File.createTempFile(layer.getLayerName(), ".geojson");
mapper.writeValue(outputFile, processedData);
return outputFile;
}
}
MBTiles Management
Java MBTiles Reader/Writer
@Component
public class MBTilesManager {
public void createMBTilesDatabase(File mbtilesFile) throws SQLException {
String url = "jdbc:sqlite:" + mbtilesFile.getAbsolutePath();
try (Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement()) {
// Create metadata table
stmt.execute("CREATE TABLE IF NOT EXISTS metadata (" +
"name TEXT PRIMARY KEY, value TEXT)");
// Create tiles table
stmt.execute("CREATE TABLE IF NOT EXISTS tiles (" +
"zoom_level INTEGER, tile_column INTEGER, " +
"tile_row INTEGER, tile_data BLOB, " +
"PRIMARY KEY (zoom_level, tile_column, tile_row))");
// Set metadata
setMetadata(conn, "name", "Generated Vector Tiles");
setMetadata(conn, "format", "pbf");
setMetadata(conn, "version", "2");
}
}
public void addTile(File mbtilesFile, int z, int x, int y, byte[] tileData)
throws SQLException {
String url = "jdbc:sqlite:" + mbtilesFile.getAbsolutePath();
String sql = "INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) " +
"VALUES (?, ?, ?, ?)";
try (Connection conn = DriverManager.getConnection(url);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// Convert to TMS tile row
int tmsY = (1 << z) - 1 - y;
pstmt.setInt(1, z);
pstmt.setInt(2, x);
pstmt.setInt(3, tmsY);
pstmt.setBytes(4, tileData);
pstmt.executeUpdate();
}
}
public byte[] getTile(File mbtilesFile, int z, int x, int y) throws SQLException {
String url = "jdbc:sqlite:" + mbtilesFile.getAbsolutePath();
String sql = "SELECT tile_data FROM tiles WHERE zoom_level = ? " +
"AND tile_column = ? AND tile_row = ?";
try (Connection conn = DriverManager.getConnection(url);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
int tmsY = (1 << z) - 1 - y;
pstmt.setInt(1, z);
pstmt.setInt(2, x);
pstmt.setInt(3, tmsY);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
return rs.getBytes("tile_data");
}
}
return null;
}
private void setMetadata(Connection conn, String name, String value) throws SQLException {
String sql = "INSERT OR REPLACE INTO metadata (name, value) VALUES (?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, name);
pstmt.setString(2, value);
pstmt.executeUpdate();
}
}
}
Vector Tile Server
Spring Boot Tile Server
@RestController
@RequestMapping("/tiles")
public class VectorTileController {
@Autowired
private MBTilesManager mbtilesManager;
@Autowired
private TileCache tileCache;
@GetMapping("/{z}/{x}/{y}.pbf")
public ResponseEntity<byte[]> getVectorTile(
@PathVariable int z,
@PathVariable int x,
@PathVariable int y,
@RequestParam(defaultValue = "default") String tileset) {
// Check cache first
String cacheKey = String.format("%s/%d/%d/%d", tileset, z, x, y);
byte[] tileData = tileCache.get(cacheKey);
if (tileData == null) {
try {
File tilesetFile = getTilesetFile(tileset);
tileData = mbtilesManager.getTile(tilesetFile, z, x, y);
if (tileData != null) {
tileCache.put(cacheKey, tileData);
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
if (tileData == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/vnd.mapbox-vector-tile"))
.header("Content-Encoding", "gzip")
.body(tileData);
}
@GetMapping("/{tileset}/metadata")
public ResponseEntity<Map<String, String>> getTilesetMetadata(
@PathVariable String tileset) {
try {
Map<String, String> metadata = mbtilesManager.getMetadata(
getTilesetFile(tileset));
return ResponseEntity.ok(metadata);
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
private File getTilesetFile(String tileset) {
return new File(String.format("/data/tilesets/%s.mbtiles", tileset));
}
}
@Component
public class TileCache {
private final Cache<String, byte[]> cache;
public TileCache() {
this.cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
}
public byte[] get(String key) {
return cache.getIfPresent(key);
}
public void put(String key, byte[] tileData) {
cache.put(key, tileData);
}
}
Vector Tile Consumption
Java Vector Tile Parser
public class VectorTileParser {
public VectorTile parseTile(byte[] tileData) throws IOException {
// Vector tiles are typically gzipped
byte[] uncompressed = uncompress(tileData);
// Parse using Protobuf (assuming you have vector_tile.proto compiled)
VectorTile.Tile tile = VectorTile.Tile.parseFrom(uncompressed);
return new VectorTile(tile);
}
private byte[] uncompress(byte[] compressed) throws IOException {
try (GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(compressed));
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = gis.read(buffer)) > 0) {
output.write(buffer, 0, len);
}
return output.toByteArray();
}
}
public static class VectorTile {
private final VectorTile.Tile protoTile;
private final Map<String, List<Feature>> layers;
public VectorTile(VectorTile.Tile protoTile) {
this.protoTile = protoTile;
this.layers = parseLayers();
}
private Map<String, List<Feature>> parseLayers() {
Map<String, List<Feature>> result = new HashMap<>();
for (VectorTile.Tile.Layer layer : protoTile.getLayersList()) {
List<Feature> features = new ArrayList<>();
for (VectorTile.Tile.Feature protoFeature : layer.getFeaturesList()) {
Feature feature = parseFeature(protoFeature, layer);
features.add(feature);
}
result.put(layer.getName(), features);
}
return result;
}
private Feature parseFeature(VectorTile.Tile.Feature protoFeature,
VectorTile.Tile.Layer layer) {
// Implement geometry decoding and attribute parsing
Geometry geometry = decodeGeometry(protoFeature, layer);
Map<String, Object> attributes = decodeAttributes(protoFeature, layer);
return new Feature(geometry, attributes);
}
public List<Feature> getFeatures(String layerName) {
return layers.getOrDefault(layerName, Collections.emptyList());
}
}
public static class Feature {
private final Geometry geometry;
private final Map<String, Object> properties;
public Feature(Geometry geometry, Map<String, Object> properties) {
this.geometry = geometry;
this.properties = properties;
}
// Getters
public Geometry getGeometry() { return geometry; }
public Map<String, Object> getProperties() { return properties; }
}
}
Batch Processing Pipeline
@Component
public class VectorTilePipeline {
@Autowired
private TippecanoeProcessor tippecanoeProcessor;
@Autowired
private MBTilesManager mbtilesManager;
@Autowired
private GeoDataService geoDataService;
@Async
public CompletableFuture<File> processDatasetToTiles(String datasetId,
TippecanoeOptions options) {
return CompletableFuture.supplyAsync(() -> {
try {
// Step 1: Extract GeoJSON from source
File geoJsonFile = geoDataService.exportToGeoJson(datasetId);
// Step 2: Generate vector tiles
File mbtilesFile = tippecanoeProcessor
.generateVectorTiles(geoJsonFile, options);
// Step 3: Post-process if needed
postProcessTiles(mbtilesFile);
return mbtilesFile;
} catch (Exception e) {
throw new RuntimeException("Tile generation failed for dataset: " + datasetId, e);
}
});
}
public void processMultipleDatasets(List<String> datasetIds,
TippecanoeOptions options) {
List<CompletableFuture<File>> futures = datasetIds.stream()
.map(id -> processDatasetToTiles(id, options))
.collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenAccept(v -> {
List<File> resultFiles = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
logger.info("Generated {} tilesets", resultFiles.size());
})
.exceptionally(ex -> {
logger.error("Batch processing failed", ex);
return null;
});
}
private void postProcessTiles(File mbtilesFile) {
// Implement any post-processing (metadata updates, optimization, etc.)
}
}
Configuration and Dependencies
Maven Configuration
<dependencies> <!-- SQLite for MBTiles --> <dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> <version>3.36.0.3</version> </dependency> <!-- Protobuf for vector tile parsing --> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.19.4</version> </dependency> <!-- Caching --> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.0.5</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
Application Configuration
@Configuration
@EnableAsync
public class VectorTileConfig {
@Bean
public TippecanoeProcessor tippecanoeProcessor() {
return new TippecanoeProcessor();
}
@Bean
public MBTilesManager mbtilesManager() {
return new MBTilesManager();
}
@Bean
public TaskExecutor tileGenerationExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("tile-gen-");
executor.initialize();
return executor;
}
}
Monitoring and Management
@RestController
@RequestMapping("/admin/tiles")
public class TileManagementController {
@Autowired
private VectorTilePipeline pipeline;
@PostMapping("/generate")
public ResponseEntity<String> generateTiles(@RequestBody TileGenerationRequest request) {
try {
CompletableFuture<File> future = pipeline
.processDatasetToTiles(request.getDatasetId(), request.getOptions());
return ResponseEntity.accepted()
.body("Tile generation started for dataset: " + request.getDatasetId());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Generation failed: " + e.getMessage());
}
}
@GetMapping("/status/{jobId}")
public ResponseEntity<TileGenerationStatus> getStatus(@PathVariable String jobId) {
// Implement status tracking
TileGenerationStatus status = getGenerationStatus(jobId);
return ResponseEntity.ok(status);
}
}
public class TileGenerationRequest {
private String datasetId;
private TippecanoeOptions options;
// Getters and setters
}
Conclusion
This comprehensive Java integration with Tippecanoe enables:
- Programmatic Vector Tile Generation - Automate tile creation from GeoJSON sources
- MBTiles Management - Store and serve vector tiles efficiently
- RESTful Tile Service - Serve vector tiles to web and mobile clients
- Batch Processing - Handle large datasets asynchronously
- Caching and Optimization - Improve performance with intelligent caching
The architecture supports both static tile generation and dynamic tile serving, making it suitable for various mapping applications from simple visualizations to complex GIS systems.