google-site-verification: google61fe8ba583a51912.html
Building a TileServer GL in Java: Vector and Raster Tile Server

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

  1. Multiple Tile Formats: PBF (vector), PNG, JPEG, WebP
  2. Caching: In-memory caching with configurable expiration
  3. Database Integration: PostgreSQL with PostGIS support
  4. RESTful API: Standard tile server endpoints
  5. Vector and Raster Support: Both vector and raster tile sources
  6. Styling: Configurable map styles
  7. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper