TileServer GL is a powerful tool for serving map tiles, but sometimes you need a Java-based solution for integration with existing Java ecosystems or for specific performance requirements. In this article, we'll build a complete tile server in Java that can serve both vector tiles (MVT) and raster tiles (PNG/JPEG).
Project Setup
First, add the necessary dependencies to your pom.xml:
<dependencies> <!-- Web Framework --> <dependency> <groupId>io.javalin</groupId> <artifactId>javalin</artifactId> <version>5.6.2</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.15.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- Database (PostGIS) --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.6.0</version> </dependency> <!-- Vector Tile Encoding --> <dependency> <groupId>no.ecc.vectortile</groupId> <artifactId>java-vector-tile</artifactId> <version>1.2.0</version> </dependency> <!-- Image Processing --> <dependency> <groupId>com.twelvemonkeys.imageio</groupId> <artifactId>imageio</artifactId> <version>3.9.4</version> </dependency> <dependency> <groupId>com.twelvemonkeys.imageio</groupId> <artifactId>imageio-jpeg</artifactId> <version>3.9.4</version> </dependency> <dependency> <groupId>com.twelvemonkeys.imageio</groupId> <artifactId>imageio-webp</artifactId> <version>3.9.4</version> </dependency> <!-- Caching --> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.1.6</version> </dependency> <!-- Configuration --> <dependency> <groupId>com.typesafe</groupId> <artifactId>config</artifactId> <version>1.4.2</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.7</version> </dependency> </dependencies>
Core Implementation
1. Configuration System
TileServerConfig.java - Main configuration class
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import java.util.HashMap;
import java.util.Map;
public class TileServerConfig {
private final int port;
private final String host;
private final DatabaseConfig databaseConfig;
private final CacheConfig cacheConfig;
private final Map<String, TileLayer> layers;
private final String tileFormat;
private final int tileSize;
private final int maxZoom;
private final int minZoom;
public TileServerConfig() {
Config config = ConfigFactory.load();
this.port = config.getInt("tileserver.port");
this.host = config.getString("tileserver.host");
this.databaseConfig = new DatabaseConfig(config.getConfig("tileserver.database"));
this.cacheConfig = new CacheConfig(config.getConfig("tileserver.cache"));
this.tileFormat = config.getString("tileserver.tile-format");
this.tileSize = config.getInt("tileserver.tile-size");
this.maxZoom = config.getInt("tileserver.max-zoom");
this.minZoom = config.getInt("tileserver.min-zoom");
this.layers = new HashMap<>();
Config layersConfig = config.getConfig("tileserver.layers");
for (String layerName : layersConfig.root().keySet()) {
layers.put(layerName, new TileLayer(layerName, layersConfig.getConfig(layerName)));
}
}
// Getters
public int getPort() { return port; }
public String getHost() { return host; }
public DatabaseConfig getDatabaseConfig() { return databaseConfig; }
public CacheConfig getCacheConfig() { return cacheConfig; }
public Map<String, TileLayer> getLayers() { return layers; }
public String getTileFormat() { return tileFormat; }
public int getTileSize() { return tileSize; }
public int getMaxZoom() { return maxZoom; }
public int getMinZoom() { return minZoom; }
public static class DatabaseConfig {
private final String url;
private final String username;
private final String password;
private final int poolSize;
public DatabaseConfig(Config config) {
this.url = config.getString("url");
this.username = config.getString("username");
this.password = config.getString("password");
this.poolSize = config.getInt("pool-size");
}
// Getters
public String getUrl() { return url; }
public String getUsername() { return username; }
public String getPassword() { return password; }
public int getPoolSize() { return poolSize; }
}
public static class CacheConfig {
private final boolean enabled;
private final long maximumSize;
private final long expireAfterWrite;
public CacheConfig(Config config) {
this.enabled = config.getBoolean("enabled");
this.maximumSize = config.getLong("maximum-size");
this.expireAfterWrite = config.getLong("expire-after-write");
}
// Getters
public boolean isEnabled() { return enabled; }
public long getMaximumSize() { return maximumSize; }
public long getExpireAfterWrite() { return expireAfterWrite; }
}
}
TileLayer.java - Represents a map layer configuration
import com.typesafe.config.Config;
public class TileLayer {
private final String name;
private final String type;
private final String source;
private final String[] fields;
private final MapStyle style;
private final int minZoom;
private final int maxZoom;
public TileLayer(String name, Config config) {
this.name = name;
this.type = config.getString("type");
this.source = config.getString("source");
this.fields = config.getStringList("fields").toArray(new String[0]);
this.minZoom = config.getInt("min-zoom");
this.maxZoom = config.getInt("max-zoom");
if (config.hasPath("style")) {
this.style = new MapStyle(config.getConfig("style"));
} else {
this.style = null;
}
}
// Getters
public String getName() { return name; }
public String getType() { return type; }
public String getSource() { return source; }
public String[] getFields() { return fields; }
public MapStyle getStyle() { return style; }
public int getMinZoom() { return minZoom; }
public int getMaxZoom() { return maxZoom; }
public boolean isVectorLayer() {
return "vector".equalsIgnoreCase(type);
}
public boolean isRasterLayer() {
return "raster".equalsIgnoreCase(type);
}
}
MapStyle.java - Map styling configuration
import com.typesafe.config.Config;
import java.awt.Color;
import java.util.HashMap;
import java.util.Map;
public class MapStyle {
private final Color backgroundColor;
private final Map<String, LayerStyle> layerStyles;
public MapStyle(Config config) {
this.backgroundColor = parseColor(config.getString("background-color"));
this.layerStyles = new HashMap<>();
if (config.hasPath("layers")) {
Config layersConfig = config.getConfig("layers");
for (String layerName : layersConfig.root().keySet()) {
layerStyles.put(layerName, new LayerStyle(layersConfig.getConfig(layerName)));
}
}
}
private Color parseColor(String colorStr) {
if (colorStr.startsWith("#")) {
return Color.decode(colorStr);
}
return Color.WHITE; // Default
}
// Getters
public Color getBackgroundColor() { return backgroundColor; }
public Map<String, LayerStyle> getLayerStyles() { return layerStyles; }
public LayerStyle getLayerStyle(String layerName) { return layerStyles.get(layerName); }
}
LayerStyle.java - Style for individual layers
import com.typesafe.config.Config;
import java.awt.Color;
public class LayerStyle {
private final Color fillColor;
private final Color strokeColor;
private final float strokeWidth;
private final float opacity;
public LayerStyle(Config config) {
this.fillColor = parseColor(config.getString("fill-color"));
this.strokeColor = parseColor(config.getString("stroke-color"));
this.strokeWidth = (float) config.getDouble("stroke-width");
this.opacity = (float) config.getDouble("opacity");
}
private Color parseColor(String colorStr) {
if (colorStr.startsWith("#")) {
return Color.decode(colorStr);
}
return Color.BLACK; // Default
}
// Getters
public Color getFillColor() { return fillColor; }
public Color getStrokeColor() { return strokeColor; }
public float getStrokeWidth() { return strokeWidth; }
public float getOpacity() { return opacity; }
}
2. Tile Coordinate System
Tile.java - Represents a map tile
public class Tile {
private final int x;
private final int y;
private final int z;
private final int size;
public Tile(int x, int y, int z) {
this(x, y, z, 256);
}
public Tile(int x, int y, int z, int size) {
this.x = x;
this.y = y;
this.z = z;
this.size = size;
}
// Getters
public int getX() { return x; }
public int getY() { return y; }
public int getZ() { return z; }
public int getSize() { return size; }
public Bounds getBounds() {
double n = Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, z);
double north = Math.toDegrees(Math.atan(Math.sinh(n)));
n = Math.PI - (2.0 * Math.PI * (y + 1)) / Math.pow(2.0, z);
double south = Math.toDegrees(Math.atan(Math.sinh(n)));
double west = (x / Math.pow(2.0, z) * 360.0) - 180.0;
double east = ((x + 1) / Math.pow(2.0, z) * 360.0) - 180.0;
return new Bounds(west, south, east, north);
}
public boolean isValid(int maxZoom) {
if (z < 0 || z > maxZoom) return false;
int maxCoord = (1 << z) - 1;
return x >= 0 && x <= maxCoord && y >= 0 && y <= maxCoord;
}
@Override
public String toString() {
return String.format("%d/%d/%d", z, x, y);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Tile tile = (Tile) o;
return x == tile.x && y == tile.y && z == tile.z;
}
@Override
public int hashCode() {
return java.util.Objects.hash(x, y, z);
}
}
Bounds.java - Geographic bounding box
public class Bounds {
private final double west;
private final double south;
private final double east;
private final double north;
public Bounds(double west, double south, double east, double north) {
this.west = west;
this.south = south;
this.east = east;
this.north = north;
}
// Getters
public double getWest() { return west; }
public double getSouth() { return south; }
public double getEast() { return east; }
public double getNorth() { return north; }
public double getWidth() { return east - west; }
public double getHeight() { return north - south; }
public boolean contains(double lon, double lat) {
return lon >= west && lon <= east && lat >= south && lat <= north;
}
@Override
public String toString() {
return String.format("[%.6f, %.6f, %.6f, %.6f]", west, south, east, north);
}
}
3. Database Layer
TileDatabase.java - Database access for vector data
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class TileDatabase implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(TileDatabase.class);
private final HikariDataSource dataSource;
public TileDatabase(TileServerConfig.DatabaseConfig dbConfig) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(dbConfig.getUrl());
config.setUsername(dbConfig.getUsername());
config.setPassword(dbConfig.getPassword());
config.setMaximumPoolSize(dbConfig.getPoolSize());
config.setConnectionTimeout(30000);
config.setIdleTimeout(300000);
config.setMaxLifetime(1800000);
this.dataSource = new HikariDataSource(config);
}
public byte[] getVectorTile(String layerName, Tile tile, String[] fields) {
String sql = buildVectorTileQuery(layerName, fields);
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
Bounds bounds = tile.getBounds();
stmt.setDouble(1, bounds.getWest());
stmt.setDouble(2, bounds.getSouth());
stmt.setDouble(3, bounds.getEast());
stmt.setDouble(4, bounds.getNorth());
stmt.setInt(5, tile.getSize());
stmt.setInt(6, tile.getZ());
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return rs.getBytes("mvt");
}
}
} catch (SQLException e) {
logger.error("Error fetching vector tile for layer {}: {}", layerName, e.getMessage());
}
return null;
}
private String buildVectorTileQuery(String layerName, String[] fields) {
StringBuilder fieldList = new StringBuilder();
for (String field : fields) {
if (fieldList.length() > 0) fieldList.append(", ");
fieldList.append(field);
}
return String.format(
"SELECT ST_AsMVT(tile, '%s', 4096, 'geom') as mvt " +
"FROM (" +
" SELECT %s, ST_AsMVTGeom(" +
" geometry," +
" ST_TileEnvelope(?, ?, ?, ?)," +
" ?," +
" ?," +
" true" +
" ) AS geom " +
" FROM %s " +
" WHERE geometry && ST_TileEnvelope(?, ?, ?, ?)" +
") AS tile",
layerName, fieldList, layerName
);
}
public List<String> getAvailableLayers() {
List<String> layers = new ArrayList<>();
String sql = "SELECT table_name FROM information_schema.tables " +
"WHERE table_schema = 'public' AND table_type = 'BASE TABLE'";
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
layers.add(rs.getString("table_name"));
}
} catch (SQLException e) {
logger.error("Error fetching available layers: {}", e.getMessage());
}
return layers;
}
@Override
public void close() {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
}
}
}
4. Vector Tile Generator
VectorTileGenerator.java - Generates Mapbox Vector Tiles
import com.google.protobuf.ByteString;
import no.ecc.vectortile.VectorTile;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.WKBReader;
import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Map;
public class VectorTileGenerator {
private final GeometryFactory geometryFactory;
private final WKBReader wkbReader;
public VectorTileGenerator() {
this.geometryFactory = new GeometryFactory();
this.wkbReader = new WKBReader(geometryFactory);
}
public byte[] generateVectorTile(TileLayer layer, Map<String, Object> featureData, Tile tile) {
try {
VectorTile.Tile.Builder tileBuilder = VectorTile.Tile.newBuilder();
VectorTile.Tile.Layer.Builder layerBuilder = VectorTile.Tile.Layer.newBuilder();
layerBuilder.setVersion(2);
layerBuilder.setName(layer.getName());
layerBuilder.setExtent(tile.getSize());
// Add features to the layer
addFeatureToLayer(layerBuilder, featureData, tile);
tileBuilder.addLayers(layerBuilder);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
tileBuilder.build().writeTo(outputStream);
return outputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException("Failed to generate vector tile", e);
}
}
private void addFeatureToLayer(VectorTile.Tile.Layer.Builder layerBuilder,
Map<String, Object> featureData, Tile tile) {
VectorTile.Tile.Feature.Builder featureBuilder = VectorTile.Tile.Feature.newBuilder();
// Set geometry
Geometry geometry = extractGeometry(featureData);
if (geometry != null) {
VectorTile.Tile.Geometry geom = convertToVectorTileGeometry(geometry, tile);
featureBuilder.setGeometry(geom);
}
// Set properties
VectorTile.Tile.Value.Builder valueBuilder = VectorTile.Tile.Value.newBuilder();
for (Map.Entry<String, Object> entry : featureData.entrySet()) {
if (!"geometry".equals(entry.getKey())) {
layerBuilder.addKeys(entry.getKey());
valueBuilder.setStringValue(entry.getValue().toString());
featureBuilder.addTags(layerBuilder.getKeysCount() - 1);
featureBuilder.addTags(layerBuilder.getValuesCount());
layerBuilder.addValues(valueBuilder.build());
}
}
// Set feature type
if (geometry != null) {
if (geometry instanceof Point) {
featureBuilder.setType(VectorTile.Tile.GeomType.POINT);
} else if (geometry instanceof LineString) {
featureBuilder.setType(VectorTile.Tile.GeomType.LINESTRING);
} else if (geometry instanceof Polygon) {
featureBuilder.setType(VectorTile.Tile.GeomType.POLYGON);
}
}
layerBuilder.addFeatures(featureBuilder);
}
private Geometry extractGeometry(Map<String, Object> featureData) {
Object geomObj = featureData.get("geometry");
if (geomObj instanceof byte[]) {
try {
return wkbReader.read((byte[]) geomObj);
} catch (Exception e) {
throw new RuntimeException("Failed to parse geometry", e);
}
}
return null;
}
private VectorTile.Tile.Geometry convertToVectorTileGeometry(Geometry geometry, Tile tile) {
// Convert JTS geometry to vector tile geometry
// This is a simplified version - in practice, you'd need proper coordinate transformation
Bounds bounds = tile.getBounds();
int extent = tile.getSize();
// Implementation would convert world coordinates to tile-relative coordinates
// This is a complex operation that requires proper coordinate transformation
return VectorTile.Tile.Geometry.newBuilder()
.addAllPoints(java.util.Collections.emptyList())
.build();
}
}
5. Raster Tile Renderer
RasterTileRenderer.java - Renders raster tiles from vector data
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.List;
public class RasterTileRenderer {
private final int tileSize;
public RasterTileRenderer(int tileSize) {
this.tileSize = tileSize;
}
public BufferedImage renderTile(TileLayer layer, List<Map<String, Object>> features, Tile tile) {
BufferedImage image = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
// Set rendering hints for better quality
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
// Set background
if (layer.getStyle() != null) {
g2d.setColor(layer.getStyle().getBackgroundColor());
g2d.fillRect(0, 0, tileSize, tileSize);
}
// Render features
for (Map<String, Object> feature : features) {
renderFeature(g2d, feature, layer, tile);
}
g2d.dispose();
return image;
}
private void renderFeature(Graphics2D g2d, Map<String, Object> feature,
TileLayer layer, Tile tile) {
// Extract geometry and style information
Geometry geometry = extractGeometry(feature);
LayerStyle style = layer.getStyle() != null ?
layer.getStyle().getLayerStyle(layer.getName()) : null;
if (geometry != null && style != null) {
// Apply style
g2d.setColor(applyOpacity(style.getFillColor(), style.getOpacity()));
g2d.setStroke(new BasicStroke(style.getStrokeWidth()));
// Render based on geometry type
if (geometry instanceof Polygon) {
renderPolygon(g2d, (Polygon) geometry, tile, style);
} else if (geometry instanceof LineString) {
renderLineString(g2d, (LineString) geometry, tile, style);
} else if (geometry instanceof Point) {
renderPoint(g2d, (Point) geometry, tile, style);
}
}
}
private Color applyOpacity(Color color, float opacity) {
return new Color(color.getRed(), color.getGreen(), color.getBlue(),
(int) (color.getAlpha() * opacity));
}
private void renderPolygon(Graphics2D g2d, Polygon polygon, Tile tile, LayerStyle style) {
// Convert polygon coordinates to screen coordinates and render
// Implementation would transform world coordinates to pixel coordinates
}
private void renderLineString(Graphics2D g2d, LineString line, Tile tile, LayerStyle style) {
// Convert line coordinates to screen coordinates and render
}
private void renderPoint(Graphics2D g2d, Point point, Tile tile, LayerStyle style) {
// Convert point coordinates to screen coordinates and render
}
private Geometry extractGeometry(Map<String, Object> feature) {
// Extract geometry from feature data (similar to VectorTileGenerator)
return null;
}
}
6. Caching System
TileCache.java - Caches generated tiles
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class TileCache {
private final Cache<String, byte[]> vectorTileCache;
private final Cache<String, byte[]> rasterTileCache;
private final boolean enabled;
public TileCache(TileServerConfig.CacheConfig config) {
this.enabled = config.isEnabled();
if (enabled) {
this.vectorTileCache = Caffeine.newBuilder()
.maximumSize(config.getMaximumSize())
.expireAfterWrite(config.getExpireAfterWrite(), TimeUnit.MINUTES)
.build();
this.rasterTileCache = Caffeine.newBuilder()
.maximumSize(config.getMaximumSize())
.expireAfterWrite(config.getExpireAfterWrite(), TimeUnit.MINUTES)
.build();
} else {
this.vectorTileCache = null;
this.rasterTileCache = null;
}
}
public byte[] getVectorTile(String cacheKey) {
return enabled ? vectorTileCache.getIfPresent(cacheKey) : null;
}
public void putVectorTile(String cacheKey, byte[] tileData) {
if (enabled) {
vectorTileCache.put(cacheKey, tileData);
}
}
public byte[] getRasterTile(String cacheKey) {
return enabled ? rasterTileCache.getIfPresent(cacheKey) : null;
}
public void putRasterTile(String cacheKey, byte[] tileData) {
if (enabled) {
rasterTileCache.put(cacheKey, tileData);
}
}
public String generateCacheKey(String layerName, Tile tile, String format) {
return String.format("%s/%d/%d/%d.%s", layerName, tile.getZ(), tile.getX(), tile.getY(), format);
}
}
7. Main Tile Server
TileServer.java - Main server class
import io.javalin.Javalin;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.image.BufferedImage;
import java.util.Map;
import javax.imageio.ImageIO;
import java.io.ByteArrayOutputStream;
public class TileServer {
private static final Logger logger = LoggerFactory.getLogger(TileServer.class);
private final TileServerConfig config;
private final TileDatabase database;
private final TileCache cache;
private final VectorTileGenerator vectorTileGenerator;
private final RasterTileRenderer rasterTileRenderer;
private Javalin app;
public TileServer(TileServerConfig config) {
this.config = config;
this.database = new TileDatabase(config.getDatabaseConfig());
this.cache = new TileCache(config.getCacheConfig());
this.vectorTileGenerator = new VectorTileGenerator();
this.rasterTileRenderer = new RasterTileRenderer(config.getTileSize());
}
public void start() {
app = Javalin.create(javalinConfig -> {
javalinConfig.http.defaultContentType = "application/json";
javalinConfig.http.asyncTimeout = 10000L;
});
// Tile endpoints
app.get("/tiles/{layer}/{z}/{x}/{y}.{format}", this::serveTile);
app.get("/styles/{style}/tiles/{z}/{x}/{y}.{format}", this::serveStyledTile);
// Metadata endpoints
app.get("/layers", this::listLayers);
app.get("/styles", this::listStyles);
app.get("/health", this::healthCheck);
app.start(config.getHost(), config.getPort());
logger.info("TileServer started on {}:{}", config.getHost(), config.getPort());
}
public void stop() {
if (app != null) {
app.stop();
}
if (database != null) {
database.close();
}
logger.info("TileServer stopped");
}
private void serveTile(Context ctx) {
String layerName = ctx.pathParam("layer");
int z = Integer.parseInt(ctx.pathParam("z"));
int x = Integer.parseInt(ctx.pathParam("x"));
int y = Integer.parseInt(ctx.pathParam("y"));
String format = ctx.pathParam("format");
Tile tile = new Tile(x, y, z, config.getTileSize());
if (!tile.isValid(config.getMaxZoom())) {
ctx.status(HttpStatus.BAD_REQUEST).result("Invalid tile coordinates");
return;
}
TileLayer layer = config.getLayers().get(layerName);
if (layer == null) {
ctx.status(HttpStatus.NOT_FOUND).result("Layer not found: " + layerName);
return;
}
try {
byte[] tileData = getTileData(layer, tile, format);
if (tileData != null) {
setResponseHeaders(ctx, format);
ctx.result(tileData);
} else {
ctx.status(HttpStatus.NOT_FOUND).result("Tile not found");
}
} catch (Exception e) {
logger.error("Error serving tile {}/{}/{}/{}: {}", z, x, y, format, e.getMessage());
ctx.status(HttpStatus.INTERNAL_SERVER_ERROR).result("Error generating tile");
}
}
private byte[] getTileData(TileLayer layer, Tile tile, String format) {
String cacheKey = cache.generateCacheKey(layer.getName(), tile, format);
// Check cache first
byte[] cachedTile = "pbf".equals(format) ?
cache.getVectorTile(cacheKey) : cache.getRasterTile(cacheKey);
if (cachedTile != null) {
return cachedTile;
}
byte[] tileData;
if (layer.isVectorLayer() && "pbf".equals(format)) {
// Generate vector tile
tileData = database.getVectorTile(layer.getName(), tile, layer.getFields());
if (tileData != null) {
cache.putVectorTile(cacheKey, tileData);
}
} else {
// Generate raster tile
tileData = generateRasterTile(layer, tile, format);
if (tileData != null) {
cache.putRasterTile(cacheKey, tileData);
}
}
return tileData;
}
private byte[] generateRasterTile(TileLayer layer, Tile tile, String format) {
try {
// For raster tiles, we might generate from vector data or serve pre-rendered tiles
if (layer.isVectorLayer()) {
// Render vector data to raster
byte[] vectorData = database.getVectorTile(layer.getName(), tile, layer.getFields());
if (vectorData != null) {
// Convert vector to raster (simplified)
BufferedImage image = renderVectorToRaster(vectorData, layer, tile);
return imageToBytes(image, format);
}
} else if (layer.isRasterLayer()) {
// Serve pre-rendered raster tiles
// Implementation would depend on your raster data source
}
} catch (Exception e) {
logger.error("Error generating raster tile: {}", e.getMessage());
}
return null;
}
private BufferedImage renderVectorToRaster(byte[] vectorData, TileLayer layer, Tile tile) {
// Convert vector tile to raster image
// This is a simplified placeholder
BufferedImage image = new BufferedImage(tile.getSize(), tile.getSize(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
if (layer.getStyle() != null) {
g2d.setColor(layer.getStyle().getBackgroundColor());
g2d.fillRect(0, 0, tile.getSize(), tile.getSize());
}
g2d.dispose();
return image;
}
private byte[] imageToBytes(BufferedImage image, String format) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(image, format, baos);
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException("Failed to convert image to bytes", e);
}
}
private void setResponseHeaders(Context ctx, String format) {
if ("pbf".equals(format)) {
ctx.contentType(ContentType.APPLICATION_OCTET_STREAM);
ctx.header("Content-Type", "application/vnd.mapbox-vector-tile");
} else if ("png".equals(format)) {
ctx.contentType(ContentType.IMAGE_PNG);
} else if ("jpg".equals(format) || "jpeg".equals(format)) {
ctx.contentType(ContentType.IMAGE_JPEG);
} else if ("webp".equals(format)) {
ctx.contentType("image/webp");
}
ctx.header("Access-Control-Allow-Origin", "*");
ctx.header("Cache-Control", "public, max-age=86400"); // 24 hours
}
private void serveStyledTile(Context ctx) {
// Implementation for styled tiles (combining multiple layers with styles)
ctx.status(HttpStatus.NOT_IMPLEMENTED).result("Styled tiles not yet implemented");
}
private void listLayers(Context ctx) {
Map<String, TileLayer> layers = config.getLayers();
ctx.json(layers.keySet());
}
private void listStyles(Context ctx) {
// Return available styles
ctx.json(java.util.Collections.singletonMap("styles", java.util.Collections.emptyList()));
}
private void healthCheck(Context ctx) {
ctx.json(java.util.Collections.singletonMap("status", "healthy"));
}
}
8. Configuration File
application.conf - Typesafe config file
tileserver {
host = "localhost"
port = 8080
tile-format = "pbf"
tile-size = 512
min-zoom = 0
max-zoom = 22
database {
url = "jdbc:postgresql://localhost:5432/gis"
username = "postgres"
password = "password"
pool-size = 10
}
cache {
enabled = true
maximum-size = 10000
expire-after-write = 60 # minutes
}
layers {
buildings {
type = "vector"
source = "buildings"
fields = ["name", "type", "height"]
min-zoom = 12
max-zoom = 22
style {
background-color = "#FFFFFF"
layers {
buildings {
fill-color = "#FF6B6B"
stroke-color = "#C44D4D"
stroke-width = 1.0
opacity = 0.8
}
}
}
}
roads {
type = "vector"
source = "roads"
fields = ["name", "type", "oneway"]
min-zoom = 10
max-zoom = 22
}
satellite {
type = "raster"
source = "satellite_tiles"
min-zoom = 0
max-zoom = 19
}
}
}
9. Main Application Class
TileServerApplication.java - Main entry point
public class TileServerApplication {
public static void main(String[] args) {
TileServerConfig config = new TileServerConfig();
TileServer server = new TileServer(config);
// Add shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(server::stop));
// Start the server
server.start();
}
}
Usage Examples
Starting the Server
java -cp "target/classes:target/dependency/*" TileServerApplication
Accessing Tiles
- Vector tiles:
http://localhost:8080/tiles/buildings/14/2620/6333.pbf - Raster tiles:
http://localhost:8080/tiles/satellite/10/512/256.png - Layer list:
http://localhost:8080/layers - Health check:
http://localhost:8080/health
Features
- Multiple Tile Formats: PBF (vector), PNG, JPEG, WebP
- Caching: In-memory caching with configurable expiration
- Database Integration: PostgreSQL with PostGIS support
- RESTful API: Standard tile server endpoints
- Vector and Raster Support: Both vector and raster tile sources
- Styling: Configurable map styles
- CORS Support: Cross-origin resource sharing enabled
This implementation provides a solid foundation for a production-ready tile server that can be extended with additional features like authentication, rate limiting, and cluster support.
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.