This is an automated email from the ASF dual-hosted git repository.

bchapuis pushed a commit to branch raster-processing
in repository https://gitbox.apache.org/repos/asf/incubator-baremaps.git


The following commit(s) were added to refs/heads/raster-processing by this push:
     new f94eb342 Add Chaikin smoother and improve hillshade and contour server
f94eb342 is described below

commit f94eb342781b176b6f9be74888372023639b116a
Author: Bertil Chapuis <[email protected]>
AuthorDate: Wed Jul 31 11:31:17 2024 +0200

    Add Chaikin smoother and improve hillshade and contour server
---
 .../cli/raster/{HillShade.java => Hillshade.java}  | 208 +++++++++++++++------
 .../org/apache/baremaps/cli/raster/Raster.java     |   2 +-
 .../org/apache/baremaps/tilestore/TileStore.java   |   3 +-
 .../openstreetmap/utils/GeometryUtils.java         |  22 +++
 .../baremaps/raster/elevation/ChaikinSmoother.java | 130 +++++++------
 .../baremaps/raster/elevation/ElevationUtils.java  |   9 +-
 .../baremaps/raster/elevation/RasterUtils.java     |  25 ++-
 .../raster/elevation/ChaikinSmootherTest.java      |  30 +--
 .../baremaps/raster/elevation/ContourRenderer.java |  18 +-
 .../src/main/resources/raster/hillshade.html       |  34 +++-
 10 files changed, 316 insertions(+), 165 deletions(-)

diff --git 
a/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/HillShade.java 
b/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Hillshade.java
similarity index 53%
rename from 
baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/HillShade.java
rename to 
baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Hillshade.java
index d9fcd0ae..0ccc7209 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/HillShade.java
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Hillshade.java
@@ -31,28 +31,35 @@ import 
com.linecorp.armeria.server.annotation.JacksonResponseConverterFunction;
 import com.linecorp.armeria.server.annotation.Param;
 import com.linecorp.armeria.server.cors.CorsService;
 import com.linecorp.armeria.server.docs.DocService;
+import com.linecorp.armeria.server.file.HttpFile;
 import java.awt.*;
 import java.awt.image.BufferedImage;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.net.URL;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.function.Function;
 import java.util.function.Supplier;
+import java.util.zip.GZIPOutputStream;
 import javax.imageio.ImageIO;
-import org.apache.baremaps.raster.elevation.ElevationUtils;
-import org.apache.baremaps.raster.elevation.HillshadeCalculator;
+import org.apache.baremaps.maplibre.vectortile.*;
+import org.apache.baremaps.raster.elevation.*;
+import org.apache.baremaps.server.TileResource;
 import org.apache.baremaps.tilestore.TileCoord;
 import org.apache.baremaps.tilestore.TileStore;
 import org.apache.baremaps.tilestore.TileStoreException;
+import org.locationtech.jts.geom.util.AffineTransformation;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Option;
 
 @Command(name = "hillshade", description = "Start a tile server that computes 
hillshades.")
-public class HillShade implements Callable<Integer> {
+public class Hillshade implements Callable<Integer> {
 
   @Option(names = {"--host"}, paramLabel = "HOST", description = "The host of 
the server.")
   private String host = "localhost";
@@ -68,11 +75,22 @@ public class HillShade implements Callable<Integer> {
 
     var objectMapper = objectMapper();
     var jsonResponseConverter = new 
JacksonResponseConverterFunction(objectMapper);
-    var tileStore = new HillShadeTileStore();
 
-    serverBuilder.annotatedService(new HillShadeTileResource(() -> tileStore),
+    LoadingCache<TileCoord, BufferedImage> cache = Caffeine.newBuilder()
+        .maximumSize(1000)
+        .build(this::getImage);
+
+    var rasterHillshadeTileStore = new RasterHillshadeTileStore(cache);
+    serverBuilder.annotatedService(new HillShadeTileResource(() -> 
rasterHillshadeTileStore),
+        jsonResponseConverter);
+
+    var contourTileStore = new ContourTileStore(cache);
+    serverBuilder.annotatedService(new TileResource(() -> contourTileStore),
         jsonResponseConverter);
 
+    var index = HttpFile.of(ClassLoader.getSystemClassLoader(), 
"/raster/hillshade.html");
+    serverBuilder.service("/", index.asService());
+
     serverBuilder.decorator(CorsService.builderForAnyOrigin()
         .allowAllRequestHeaders(true)
         .allowRequestMethods(
@@ -102,63 +120,59 @@ public class HillShade implements Callable<Integer> {
     return 0;
   }
 
-  public static class HillShadeTileStore implements TileStore {
+  private String url = 
"https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";;
 
-    // private String url = 
"https://s3.amazonaws.com/elevation-tiles-prod/geotiff/{z}/{x}/{y}.tif";;
-    // private String url = 
"https://demotiles.maplibre.org/terrain-tiles/{z}/{x}/{y}.png";;
-    private String url = 
"https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";;
-
-    private final LoadingCache<TileCoord, BufferedImage> cache = 
Caffeine.newBuilder()
-        .maximumSize(1000)
-        .build(this::getImage);
+  public BufferedImage getImage(TileCoord tileCoord) throws IOException {
+    var tileUrl = new URL(this.url
+        .replace("{z}", String.valueOf(tileCoord.z()))
+        .replace("{x}", String.valueOf(tileCoord.x()))
+        .replace("{y}", String.valueOf(tileCoord.y())));
+    return ImageIO.read(tileUrl);
+  }
 
-    public HillShadeTileStore() {
-      // Default constructor
+  public static BufferedImage getKernel(TileCoord tileCoord,
+      Function<TileCoord, BufferedImage> provider) {
+    BufferedImage z1 =
+        provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() - 1, 
tileCoord.z()));
+    BufferedImage z2 =
+        provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() - 1, 
tileCoord.z()));
+    BufferedImage z3 =
+        provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() - 1, 
tileCoord.z()));
+    BufferedImage z4 =
+        provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y(), 
tileCoord.z()));
+    BufferedImage z5 = provider.apply(tileCoord);
+    BufferedImage z6 =
+        provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y(), 
tileCoord.z()));
+    BufferedImage z7 =
+        provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() + 1, 
tileCoord.z()));
+    BufferedImage z8 =
+        provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() + 1, 
tileCoord.z()));
+    BufferedImage z9 =
+        provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() + 1, 
tileCoord.z()));
+    int kernelSize = z5.getWidth() * 3;
+    BufferedImage kernel = new BufferedImage(kernelSize, kernelSize, 
z5.getType());
+    for (int y = 0; y < z5.getHeight(); y++) {
+      for (int x = 0; x < z5.getWidth(); x++) {
+        kernel.setRGB(x, y, z1.getRGB(x, y));
+        kernel.setRGB(x + z5.getWidth(), y, z2.getRGB(x, y));
+        kernel.setRGB(x + 2 * z5.getWidth(), y, z3.getRGB(x, y));
+        kernel.setRGB(x, y + z5.getHeight(), z4.getRGB(x, y));
+        kernel.setRGB(x + z5.getWidth(), y + z5.getHeight(), z5.getRGB(x, y));
+        kernel.setRGB(x + 2 * z5.getWidth(), y + z5.getHeight(), z6.getRGB(x, 
y));
+        kernel.setRGB(x, y + 2 * z5.getHeight(), z7.getRGB(x, y));
+        kernel.setRGB(x + z5.getWidth(), y + 2 * z5.getHeight(), z8.getRGB(x, 
y));
+        kernel.setRGB(x + 2 * z5.getWidth(), y + 2 * z5.getHeight(), 
z9.getRGB(x, y));
+      }
     }
+    return kernel;
+  }
 
-    public BufferedImage getImage(TileCoord tileCoord) throws IOException {
-      var tileUrl = new URL(this.url
-          .replace("{z}", String.valueOf(tileCoord.z()))
-          .replace("{x}", String.valueOf(tileCoord.x()))
-          .replace("{y}", String.valueOf(tileCoord.y())));
-      return ImageIO.read(tileUrl);
-    }
+  public static class RasterHillshadeTileStore implements TileStore {
 
-    public BufferedImage getKernel(TileCoord tileCoord, Function<TileCoord, 
BufferedImage> provider)
-        throws IOException {
-      BufferedImage z1 =
-          provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() - 1, 
tileCoord.z()));
-      BufferedImage z2 =
-          provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() - 1, 
tileCoord.z()));
-      BufferedImage z3 =
-          provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() - 1, 
tileCoord.z()));
-      BufferedImage z4 =
-          provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y(), 
tileCoord.z()));
-      BufferedImage z5 = provider.apply(tileCoord);
-      BufferedImage z6 =
-          provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y(), 
tileCoord.z()));
-      BufferedImage z7 =
-          provider.apply(new TileCoord(tileCoord.x() - 1, tileCoord.y() + 1, 
tileCoord.z()));
-      BufferedImage z8 =
-          provider.apply(new TileCoord(tileCoord.x(), tileCoord.y() + 1, 
tileCoord.z()));
-      BufferedImage z9 =
-          provider.apply(new TileCoord(tileCoord.x() + 1, tileCoord.y() + 1, 
tileCoord.z()));
-      int kernelSize = z5.getWidth() * 3;
-      BufferedImage kernel = new BufferedImage(kernelSize, kernelSize, 
z5.getType());
-      for (int y = 0; y < z5.getHeight(); y++) {
-        for (int x = 0; x < z5.getWidth(); x++) {
-          kernel.setRGB(x, y, z1.getRGB(x, y));
-          kernel.setRGB(x + z5.getWidth(), y, z2.getRGB(x, y));
-          kernel.setRGB(x + 2 * z5.getWidth(), y, z3.getRGB(x, y));
-          kernel.setRGB(x, y + z5.getHeight(), z4.getRGB(x, y));
-          kernel.setRGB(x + z5.getWidth(), y + z5.getHeight(), z5.getRGB(x, 
y));
-          kernel.setRGB(x + 2 * z5.getWidth(), y + z5.getHeight(), 
z6.getRGB(x, y));
-          kernel.setRGB(x, y + 2 * z5.getHeight(), z7.getRGB(x, y));
-          kernel.setRGB(x + z5.getWidth(), y + 2 * z5.getHeight(), 
z8.getRGB(x, y));
-          kernel.setRGB(x + 2 * z5.getWidth(), y + 2 * z5.getHeight(), 
z9.getRGB(x, y));
-        }
-      }
-      return kernel;
+    private final LoadingCache<TileCoord, BufferedImage> cache;
+
+    public RasterHillshadeTileStore(LoadingCache<TileCoord, BufferedImage> 
cache) {
+      this.cache = cache;
     }
 
     @Override
@@ -174,8 +188,8 @@ public class HillShade implements Callable<Integer> {
             image.getHeight() + 2);
 
         var grid = ElevationUtils.imageToGrid(buffer);
-        var hillshade =
-            new HillshadeCalculator(grid, buffer.getWidth(), 
buffer.getHeight(), 1, true)
+        var hillshadeGrid =
+            new HillshadeCalculator(grid, buffer.getWidth(), 
buffer.getHeight(), 1, false)
                 .calculate(45, 315);
 
         // Create an output image
@@ -183,7 +197,7 @@ public class HillShade implements Callable<Integer> {
             new BufferedImage(image.getWidth(), image.getHeight(), 
BufferedImage.TYPE_BYTE_GRAY);
         for (int y = 0; y < image.getHeight(); y++) {
           for (int x = 0; x < image.getWidth(); x++) {
-            int value = (int) hillshade[(y + 1) * buffer.getHeight() + x + 1];
+            int value = (int) hillshadeGrid[(y + 1) * buffer.getHeight() + x + 
1];
             hillshadeImage.setRGB(x, y, new Color(value, value, 
value).getRGB());
           }
         }
@@ -229,7 +243,7 @@ public class HillShade implements Callable<Integer> {
       this.tileStoreSupplier = tileStoreSupplier;
     }
 
-    @Get("regex:^/tiles/(?<z>[0-9]+)/(?<x>[0-9]+)/(?<y>[0-9]+).png")
+    @Get("regex:^/raster/(?<z>[0-9]+)/(?<x>[0-9]+)/(?<y>[0-9]+).png")
     @Blocking
     public HttpResponse tile(@Param("z") int z, @Param("x") int x, @Param("y") 
int y) {
       TileCoord tileCoord = new TileCoord(x, y, z);
@@ -255,4 +269,76 @@ public class HillShade implements Callable<Integer> {
       }
     }
   }
+
+  public static class ContourTileStore implements TileStore {
+
+    private final LoadingCache<TileCoord, BufferedImage> cache;
+
+    public ContourTileStore(LoadingCache<TileCoord, BufferedImage> cache) {
+      this.cache = cache;
+    }
+
+    @Override
+    public ByteBuffer read(TileCoord tileCoord) throws TileStoreException {
+      var image = cache.get(tileCoord);
+
+      var kernel = getKernel(tileCoord, cache::get);
+
+      image = kernel.getSubimage(
+          image.getWidth() - 4,
+          image.getHeight() - 4,
+          image.getWidth() + 8,
+          image.getHeight() + 8);
+
+      var grid = ElevationUtils.imageToGrid(image);
+
+      var features = new ArrayList<Feature>();
+
+      for (int level = 0; level < 9000; level += 100) {
+        var contours = new ContourTracer(grid, image.getWidth(), 
image.getHeight(), false, false)
+            .traceContours(level);
+
+        for (var contour : contours) {
+
+          contour = AffineTransformation
+              .translationInstance(-4, -4)
+              .scaleInstance(16, 16)
+              .transform(contour);
+
+          // contour = new ChaikinSmoother(2, 0.25).transform(contour);
+
+          features.add(new Feature(level, Map.of("level", 
String.valueOf(level)), contour));
+        }
+      }
+
+      var layer = new Layer("elevation", 4096, features);
+      var tile = new Tile(List.of(layer));
+      var vectorTile = new VectorTileEncoder().encodeTile(tile);
+      try (var baos = new ByteArrayOutputStream()) {
+        var gzip = new GZIPOutputStream(baos);
+        vectorTile.writeTo(gzip);
+        gzip.close();
+        return ByteBuffer.wrap(baos.toByteArray());
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+
+    }
+
+    @Override
+    public void write(TileCoord tileCoord, ByteBuffer blob) throws 
TileStoreException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void delete(TileCoord tileCoord) throws TileStoreException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void close() throws Exception {
+      // Do nothing
+    }
+  }
+
 }
diff --git 
a/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Raster.java 
b/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Raster.java
index f0dfa288..a4ee5a4a 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Raster.java
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Raster.java
@@ -23,7 +23,7 @@ import picocli.CommandLine;
 import picocli.CommandLine.Command;
 
 @Command(name = "raster", description = "Raster processing commands.",
-    subcommands = {HillShade.class},
+    subcommands = {Hillshade.class},
     sortOptions = false)
 public class Raster implements Runnable {
 
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/tilestore/TileStore.java 
b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/TileStore.java
index e7f34b11..bd2b13a7 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/tilestore/TileStore.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/TileStore.java
@@ -19,6 +19,7 @@ package org.apache.baremaps.tilestore;
 
 
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.List;
@@ -42,7 +43,7 @@ public interface TileStore extends AutoCloseable {
    * @return the content of the tiles
    * @throws TileStoreException
    */
-  default List<ByteBuffer> read(List<TileCoord> tileCoords) throws 
TileStoreException {
+  default List<ByteBuffer> read(List<TileCoord> tileCoords) throws 
TileStoreException, IOException {
     var blobs = new ArrayList<ByteBuffer>();
     for (var tileCoord : tileCoords) {
       blobs.add(read(tileCoord));
diff --git 
a/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/utils/GeometryUtils.java
 
b/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/utils/GeometryUtils.java
index e0d6813e..8199306a 100644
--- 
a/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/utils/GeometryUtils.java
+++ 
b/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/utils/GeometryUtils.java
@@ -21,6 +21,7 @@ import static org.locationtech.jts.io.WKBConstants.wkbNDR;
 
 import org.apache.baremaps.openstreetmap.function.ProjectionTransformer;
 import org.locationtech.jts.geom.*;
+import org.locationtech.jts.geom.util.AffineTransformation;
 import org.locationtech.jts.io.ParseException;
 import org.locationtech.jts.io.WKBReader;
 import org.locationtech.jts.io.WKBWriter;
@@ -93,4 +94,25 @@ public class GeometryUtils {
     return new ProjectionTransformer(inputSRID, outputSRID);
   }
 
+  /**
+   * Scales a geometry by a factor.
+   *
+   * @param geometry The geometry to scale
+   * @param factor The factor to scale by
+   * @return The scaled geometry
+   */
+  public static Geometry scale(Geometry geometry, double factor) {
+    AffineTransformation transform = 
AffineTransformation.scaleInstance(factor, factor);
+    return transform.transform(geometry);
+  }
+
+  public static Geometry createEnvelope(int xMin, int yMin, int xMax, int 
yMax) {
+    return new GeometryFactory().createPolygon(new Coordinate[] {
+        new Coordinate(xMin, yMin),
+        new Coordinate(xMin, yMax),
+        new Coordinate(xMax, yMax),
+        new Coordinate(xMax, yMin),
+        new Coordinate(xMin, yMin)
+    });
+  }
 }
diff --git 
a/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/ChaikinSmoother.java
 
b/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/ChaikinSmoother.java
index d585c9ae..6376c36a 100644
--- 
a/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/ChaikinSmoother.java
+++ 
b/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/ChaikinSmoother.java
@@ -17,71 +17,87 @@
 
 package org.apache.baremaps.raster.elevation;
 
-import java.util.Arrays;
-import org.locationtech.jts.geom.Coordinate;
-
-public class ChaikinSmoother {
-
-  private final Coordinate[] coordinates;
-  private final double minX;
-  private final double minY;
-  private final double maxX;
-  private final double maxY;
-  private final boolean isOpen;
-
-  public ChaikinSmoother(Coordinate[] coordinates, double minX, double minY, 
double maxX,
-      double maxY) {
-    this.coordinates = Arrays.copyOf(coordinates, coordinates.length);
-    this.minX = minX;
-    this.minY = minY;
-    this.maxX = maxX;
-    this.maxY = maxY;
-    this.isOpen = !coordinates[0].equals(coordinates[coordinates.length - 1]);
+import org.locationtech.jts.geom.*;
+import org.locationtech.jts.geom.impl.CoordinateArraySequence;
+import org.locationtech.jts.geom.util.GeometryTransformer;
+
+public class ChaikinSmoother extends GeometryTransformer {
+
+  private final int iterations;
+
+  private final double factor;
+
+  public ChaikinSmoother(int iterations, double factor) {
+    this.iterations = iterations;
+    this.factor = factor;
   }
 
-  public Coordinate[] smooth(int iterations, double factor) {
-    Coordinate[] result = isOpen
-        ? Arrays.copyOf(coordinates, coordinates.length - 1)
-        : coordinates;
-
-    double f1 = 1 - factor;
-    double f2 = factor;
-
-    // Apply the algorithm repeatedly
-    for (int n = 0; n < iterations; n++) {
-      Coordinate[] temp = new Coordinate[isOpen ? 2 * result.length - 2 : 2 * 
result.length];
-
-      for (int i = 0; i < result.length; i++) {
-        if (isOnBoundary(result[i]) || isOnBoundary(result[(i + 1) % 
result.length])) {
-          temp[2 * i] = result[i];
-          temp[2 * i + 1] = result[(i + 1) % result.length];
-        } else {
-          temp[2 * i] = new Coordinate(
-              f1 * result[i].x + f2 * result[(i + 1) % result.length].x,
-              f1 * result[i].y + f2 * result[(i + 1) % result.length].y);
-          temp[2 * i + 1] = new Coordinate(
-              f2 * result[i].x + f1 * result[(i + 1) % result.length].x,
-              f2 * result[i].y + f1 * result[(i + 1) % result.length].y);
-        }
-      }
+  @Override
+  protected CoordinateSequence transformCoordinates(
+      CoordinateSequence coordinateSequence,
+      Geometry parent) {
+    return smooth(coordinateSequence, iterations, factor);
+  }
 
-      if (isOpen) {
-        temp[0] = result[0];
-        temp[temp.length - 1] = result[result.length - 1];
-      }
+  public static LinearRing smooth(LinearRing linearRing, int iterations, 
double factor) {
+    CoordinateSequence coordinateSequence =
+        smooth(linearRing.getCoordinateSequence(), iterations, factor);
+    return linearRing.getFactory().createLinearRing(coordinateSequence);
+  }
+
+  public static LineString smooth(LineString lineString, int iterations, 
double factor) {
+    CoordinateSequence coordinateSequence =
+        smooth(lineString.getCoordinateSequence(), iterations, factor);
+    return lineString.getFactory().createLineString(coordinateSequence);
+  }
 
-      result = temp;
+  public static Coordinate[] smooth(Coordinate[] coordinates, int iterations, 
double factor) {
+    return smooth(new CoordinateArraySequence(coordinates), iterations, 
factor).toCoordinateArray();
+  }
+
+  public static CoordinateSequence smooth(CoordinateSequence 
coordinateSequence, int iterations,
+      double factor) {
+    if (CoordinateSequences.isRing(coordinateSequence)) {
+      return new 
CoordinateArraySequence(chaikin(coordinateSequence.toCoordinateArray(), 2, 
0.25));
+    } else {
+      Coordinate[] original = coordinateSequence.toCoordinateArray();
+      Coordinate[] smoothed = chaikin(original, iterations, factor);
+      int sumOfSquares = (iterations * (iterations + 1) * (2 * iterations + 
1)) / 6;
+      int trimmedLength = smoothed.length - sumOfSquares;
+      Coordinate[] result = new Coordinate[trimmedLength + 2];
+      result[0] = original[0];
+      System.arraycopy(smoothed, 0, result, 1, trimmedLength);
+      result[trimmedLength + 1] = original[original.length - 1];
+      return new CoordinateArraySequence(result);
     }
+  }
 
-    if (!isOpen) {
-      result = Arrays.copyOf(result, result.length + 1);
-      result[result.length - 1] = result[0];
+  private static Coordinate[] chaikin(Coordinate[] coordinates, int 
iterations, double factor) {
+    if (iterations <= 0) {
+      return coordinates;
     }
 
-    return result;
-  }
+    for (int i = 0; i < iterations; i++) {
+      int l = coordinates.length;
+      double f1 = 1 - factor;
+      double f2 = factor;
 
-  private boolean isOnBoundary(Coordinate coord) {
-    return coord.x == minX || coord.x == maxX || coord.y == minY || coord.y == 
maxY;
+      Coordinate[] smoothed = new Coordinate[l * 2];
+      for (int j = 0; j < l; j++) {
+        Coordinate c1 = coordinates[j];
+        Coordinate c2 = coordinates[(j + 1) % l];
+        smoothed[j * 2] = new Coordinate(
+            f1 * c1.getX() + f2 * c2.getX(),
+            f1 * c1.getY() + f2 * c2.getY());
+        smoothed[j * 2 + 1] = new Coordinate(
+            f2 * c1.getX() + f1 * c2.getX(),
+            f2 * c1.getY() + f1 * c2.getY());
+      }
+
+      coordinates = smoothed;
+    }
+
+    return coordinates;
   }
+
 }
diff --git 
a/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/ElevationUtils.java
 
b/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/ElevationUtils.java
index e605fe38..671e2689 100644
--- 
a/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/ElevationUtils.java
+++ 
b/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/ElevationUtils.java
@@ -45,7 +45,7 @@ public class ElevationUtils {
 
     for (int y = 0; y < height; y++) {
       for (int x = 0; x < width; x++) {
-        grid[y * width + x] = pixelToElevation(image.getRGB(x, y));
+        grid[y * width + x] = pixelToElevationTerrarium(image.getRGB(x, y));
       }
     }
 
@@ -80,6 +80,13 @@ public class ElevationUtils {
     return (r * ELEVATION_SCALE + g * 256.0 + b) / 10.0 - ELEVATION_OFFSET;
   }
 
+  private static double pixelToElevationTerrarium(int rgb) {
+    int r = (rgb >> 16) & 0xFF;
+    int g = (rgb >> 8) & 0xFF;
+    int b = rgb & 0xFF;
+    return (r * 256 + g + b / 256) - 32768;
+  }
+
   private static int elevationToPixel(double elevation) {
     int value = (int) ((elevation + ELEVATION_OFFSET) * 10.0);
     int r = (value >> 16) & 0xFF;
diff --git 
a/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Raster.java 
b/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/RasterUtils.java
similarity index 58%
copy from baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Raster.java
copy to 
baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/RasterUtils.java
index f0dfa288..fbe06eba 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Raster.java
+++ 
b/baremaps-raster/src/main/java/org/apache/baremaps/raster/elevation/RasterUtils.java
@@ -15,20 +15,19 @@
  * limitations under the License.
  */
 
-package org.apache.baremaps.cli.raster;
+package org.apache.baremaps.raster.elevation;
 
+import java.awt.*;
+import java.awt.image.BufferedImage;
 
-
-import picocli.CommandLine;
-import picocli.CommandLine.Command;
-
-@Command(name = "raster", description = "Raster processing commands.",
-    subcommands = {HillShade.class},
-    sortOptions = false)
-public class Raster implements Runnable {
-
-  @Override
-  public void run() {
-    CommandLine.usage(this, System.out);
+public class RasterUtils {
+  public static BufferedImage resizeImage(
+      BufferedImage originalImage, int targetWidth, int targetHeight) {
+    Image resultingImage =
+        originalImage.getScaledInstance(targetWidth, targetHeight, 
Image.SCALE_DEFAULT);
+    BufferedImage outputImage =
+        new BufferedImage(targetWidth, targetHeight, 
BufferedImage.TYPE_INT_RGB);
+    outputImage.getGraphics().drawImage(resultingImage, 0, 0, null);
+    return outputImage;
   }
 }
diff --git 
a/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Raster.java 
b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/ChaikinSmootherTest.java
similarity index 54%
copy from baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Raster.java
copy to 
baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/ChaikinSmootherTest.java
index f0dfa288..44581357 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/raster/Raster.java
+++ 
b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/ChaikinSmootherTest.java
@@ -15,20 +15,28 @@
  * limitations under the License.
  */
 
-package org.apache.baremaps.cli.raster;
+package org.apache.baremaps.raster.elevation;
 
+import static org.junit.jupiter.api.Assertions.*;
 
+import org.junit.jupiter.api.Test;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LineString;
 
-import picocli.CommandLine;
-import picocli.CommandLine.Command;
+class ChaikinSmootherTest {
 
-@Command(name = "raster", description = "Raster processing commands.",
-    subcommands = {HillShade.class},
-    sortOptions = false)
-public class Raster implements Runnable {
-
-  @Override
-  public void run() {
-    CommandLine.usage(this, System.out);
+  @Test
+  void smoothLineString() {
+    LineString lineString = new GeometryFactory().createLineString(new 
Coordinate[] {
+        new Coordinate(0, 0),
+        new Coordinate(1, 1),
+    });
+    Geometry smoothedLineString = new ChaikinSmoother(2, 
0.25).transform(lineString);
+    System.out.println(smoothedLineString);
   }
+
+
+
 }
diff --git 
a/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/ContourRenderer.java
 
b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/ContourRenderer.java
index f373d904..c578f151 100644
--- 
a/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/ContourRenderer.java
+++ 
b/baremaps-raster/src/test/java/org/apache/baremaps/raster/elevation/ContourRenderer.java
@@ -18,7 +18,6 @@
 package org.apache.baremaps.raster.elevation;
 
 import java.awt.*;
-import java.awt.image.BufferedImage;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.List;
@@ -40,7 +39,7 @@ public class ContourRenderer {
     var image = ImageIO.read(path);
 
     // Downscale the image by a factor of 16
-    image = resizeImage(image, 32, 32);
+    image = RasterUtils.resizeImage(image, 32, 32);
 
     // Convert the image to a grid
     double[] grid = ElevationUtils.imageToGrid(image);
@@ -50,7 +49,7 @@ public class ContourRenderer {
             .traceContours(0, 9000, 100);
 
     // Scale the image back to its original size
-    image = resizeImage(image, image.getWidth() * 16, image.getHeight() * 16);
+    image = RasterUtils.resizeImage(image, image.getWidth() * 16, 
image.getHeight() * 16);
 
     // Scale the contour back to its original size
     contour = contour.stream()
@@ -65,8 +64,7 @@ public class ContourRenderer {
     // Smooth the contour with the Chaikin algorithm
     contour = contour.stream()
         .map(polygon -> {
-          var coordinates =
-              new ChaikinSmoother(polygon.getCoordinates(), 0, 0, 512, 
512).smooth(2, 0.25);
+          var coordinates = ChaikinSmoother.smooth(polygon.getCoordinates(), 
2, 0.25);
           return (Geometry) new GeometryFactory().createPolygon(coordinates);
         })
         .toList();
@@ -79,16 +77,6 @@ public class ContourRenderer {
     frame.setVisible(true);
   }
 
-  private static BufferedImage resizeImage(BufferedImage originalImage, int 
targetWidth,
-      int targetHeight) {
-    Image resultingImage =
-        originalImage.getScaledInstance(targetWidth, targetHeight, 
Image.SCALE_DEFAULT);
-    BufferedImage outputImage =
-        new BufferedImage(targetWidth, targetHeight, 
BufferedImage.TYPE_INT_RGB);
-    outputImage.getGraphics().drawImage(resultingImage, 0, 0, null);
-    return outputImage;
-  }
-
   // Custom Canvas to draw the contours
   static class ContourCanvas extends Canvas {
 
diff --git a/baremaps-server/src/main/resources/raster/hillshade.html 
b/baremaps-server/src/main/resources/raster/hillshade.html
index 3aedd2ac..2638580e 100644
--- a/baremaps-server/src/main/resources/raster/hillshade.html
+++ b/baremaps-server/src/main/resources/raster/hillshade.html
@@ -24,6 +24,7 @@
 <div id="map"></div>
 <script>
     const map = (window.map = new maplibregl.Map({
+        antialias: true,
         container: 'map',
         zoom: 11,
         center: [11.5519, 47.2719],
@@ -34,17 +35,38 @@
                 rasterSource: {
                     type: 'raster',
                     'tiles': [
-                        'http://localhost:9000/tiles/{z}/{x}/{y}.png'
+                        'http://localhost:9000/raster/{z}/{x}/{y}.png'
                     ],
                     tileSize: 256
                 },
+                contourSource: {
+                    type: 'vector',
+                    tiles: [
+                        'http://localhost:9000/tiles/{z}/{x}/{y}.mvt'
+                    ],
+                },
             },
             layers: [
+                // {
+                //     'id': 'raster',
+                //     'type': 'raster',
+                //     'source': 'rasterSource',
+                //     'paint': {}
+                // },
                 {
-                    'id': 'raster',
-                    'type': 'raster',
-                    'source': 'rasterSource',
-                    'paint': {}
+                    'id': 'contour',
+                    'type': 'line',
+                    'source': 'contourSource',
+                    'source-layer': 'elevation',
+                    'layout': {},
+                    'paint': {
+                        'line-color': '#f00',
+                        'line-width': 1,
+                    },
+                    layout: {
+                        'line-join': 'round',
+                        'line-cap': 'round',
+                    },
                 },
             ],
         },
@@ -52,6 +74,8 @@
         maxPitch: 85
     }));
 
+    // map.showTileBoundaries = true;
+
     map.addControl(
         new maplibregl.NavigationControl({
             visualizePitch: false,

Reply via email to