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

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

commit bdca2b02c852ee2695e2c133a5b2a5fca0502ffe
Author: Bertil Chapuis <[email protected]>
AuthorDate: Tue Jun 11 11:12:54 2024 +0200

    Port the MARTINI algorithm to java
---
 baremaps-martini/pom.xml                           |  17 ++
 .../java/org/apache/baremaps/martini/Martini.java  | 214 +++++++++++++++++++++
 .../org/apache/baremaps/martini/MartiniTest.java   |  63 ++++++
 .../org/apache/baremaps/martini/MartiniUtils.java  |  51 +++++
 baremaps-martini/src/test/resources/fuji.png       | Bin 0 -> 578765 bytes
 pom.xml                                            |   1 +
 6 files changed, 346 insertions(+)

diff --git a/baremaps-martini/pom.xml b/baremaps-martini/pom.xml
new file mode 100644
index 00000000..5b363fa9
--- /dev/null
+++ b/baremaps-martini/pom.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.baremaps</groupId>
+    <artifactId>baremaps</artifactId>
+    <version>0.7.4-SNAPSHOT</version>
+  </parent>
+  <artifactId>baremaps-martini</artifactId>
+  <dependencies>
+    <dependency>
+      <groupId>com.twelvemonkeys.imageio</groupId>
+      <artifactId>imageio-tiff</artifactId>
+      <version>3.11.0</version>
+    </dependency>
+  </dependencies>
+</project>
diff --git 
a/baremaps-martini/src/main/java/org/apache/baremaps/martini/Martini.java 
b/baremaps-martini/src/main/java/org/apache/baremaps/martini/Martini.java
new file mode 100644
index 00000000..703f5e1f
--- /dev/null
+++ b/baremaps-martini/src/main/java/org/apache/baremaps/martini/Martini.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.martini;
+
+/**
+ * The {@code Martini} class is a port of the MARTINI algorithm for generating 
3D terrain meshes from
+ * height data.
+ * <p>
+ * @see <a href="https://github.com/mapbox/martini";>Martini GitHub
+ */
+public class Martini {
+
+  private final int gridSize;
+  private final int numTriangles;
+  private final int numParentTriangles;
+
+  private final int[] baseCoords;
+
+  public Martini(int gridSize) {
+    this.gridSize = gridSize;
+
+    int tileSize = gridSize - 1;
+    if ((tileSize & (tileSize - 1)) != 0) {
+      throw new IllegalArgumentException("Expected grid size to be 2^n+1, got 
" + gridSize + ".");
+    }
+
+    this.numTriangles = tileSize * tileSize * 2 - 2;
+    this.numParentTriangles = this.numTriangles - tileSize * tileSize;
+
+    this.baseCoords = new int[this.numTriangles * 4];
+    for (int i = 0; i < this.numTriangles; i++) {
+      int id = i + 2;
+      int ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0;
+      if ((id & 1) != 0) {
+        bx = by = cx = tileSize;
+      } else {
+        ax = ay = cy = tileSize;
+      }
+      while ((id >>= 1) > 1) {
+        int mx = (ax + bx) >> 1;
+        int my = (ay + by) >> 1;
+
+        if ((id & 1) != 0) {
+          bx = ax;
+          by = ay;
+          ax = cx;
+          ay = cy;
+        } else {
+          ax = bx;
+          ay = by;
+          bx = cx;
+          by = cy;
+        }
+        cx = mx;
+        cy = my;
+      }
+      int k = i * 4;
+      this.baseCoords[k] = ax;
+      this.baseCoords[k + 1] = ay;
+      this.baseCoords[k + 2] = bx;
+      this.baseCoords[k + 3] = by;
+    }
+  }
+
+  public Tile createTile(float[] terrain) {
+    return new Tile(terrain, gridSize, numTriangles, numParentTriangles, 
baseCoords);
+  }
+
+  public static class Tile {
+
+    private final int gridSize;
+    private final int[] indices;
+    private final float[] errors;
+
+    private int numVertices;
+    private int numTriangles;
+
+    private int[] vertices;
+    private int[] triangles;
+    private int triIndex = 0;
+
+    private Tile(float[] terrain, int gridSize, int numTriangles, int 
numParentTriangles,
+        int[] coords) {
+      if (terrain.length != gridSize * gridSize) {
+        throw new IllegalArgumentException(
+            "Expected terrain data of length " + (gridSize * gridSize) + " (" 
+ gridSize + " x "
+                + gridSize + "), got " + terrain.length + ".");
+      }
+
+      this.gridSize = gridSize;
+      this.indices = new int[this.gridSize * this.gridSize];
+
+      this.errors = new float[terrain.length];
+      for (int i = numTriangles - 1; i >= 0; i--) {
+        int k = i * 4;
+        int ax = coords[k];
+        int ay = coords[k + 1];
+        int bx = coords[k + 2];
+        int by = coords[k + 3];
+        int mx = (ax + bx) >> 1;
+        int my = (ay + by) >> 1;
+        int cx = mx + my - ay;
+        int cy = my + ax - mx;
+
+        float interpolatedHeight = (terrain[ay * gridSize + ax] + terrain[by * 
gridSize + bx]) / 2;
+        int middleIndex = my * gridSize + mx;
+        float middleError = Math.abs(interpolatedHeight - 
terrain[middleIndex]);
+
+        errors[middleIndex] = Math.max(errors[middleIndex], middleError);
+
+        if (i < numParentTriangles) {
+          int leftChildIndex = ((ay + cy) >> 1) * gridSize + ((ax + cx) >> 1);
+          int rightChildIndex = ((by + cy) >> 1) * gridSize + ((bx + cx) >> 1);
+          errors[middleIndex] = Math.max(errors[middleIndex],
+              Math.max(errors[leftChildIndex], errors[rightChildIndex]));
+        }
+      }
+    }
+
+    public Mesh getMesh(float maxError) {
+      int max = gridSize - 1;
+
+      numVertices = 0;
+      numTriangles = 0;
+      countElements(0, 0, max, max, max, 0, maxError);
+      countElements(max, max, 0, 0, 0, max, maxError);
+
+      vertices = new int[numVertices * 2];
+      triangles = new int[numTriangles * 3];
+      triIndex = 0;
+      processTriangle(0, 0, max, max, max, 0, maxError);
+      processTriangle(max, max, 0, 0, 0, max, maxError);
+
+      return new Mesh(vertices, triangles);
+    }
+
+    private void countElements(int ax, int ay, int bx, int by, int cx, int cy, 
float maxError) {
+      int mx = (ax + bx) >> 1;
+      int my = (ay + by) >> 1;
+
+      if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * gridSize + 
mx] > maxError) {
+        countElements(cx, cy, ax, ay, mx, my, maxError);
+        countElements(bx, by, cx, cy, mx, my, maxError);
+      } else {
+        indices[ay * gridSize + ax] =
+            indices[ay * gridSize + ax] != 0 ? indices[ay * gridSize + ax] : 
++numVertices;
+        indices[by * gridSize + bx] =
+            indices[by * gridSize + bx] != 0 ? indices[by * gridSize + bx] : 
++numVertices;
+        indices[cy * gridSize + cx] =
+            indices[cy * gridSize + cx] != 0 ? indices[cy * gridSize + cx] : 
++numVertices;
+        numTriangles++;
+      }
+    }
+
+    private void processTriangle(int ax, int ay, int bx, int by, int cx, int 
cy, float maxError) {
+      int mx = (ax + bx) >> 1;
+      int my = (ay + by) >> 1;
+
+      if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * gridSize + 
mx] > maxError) {
+        processTriangle(cx, cy, ax, ay, mx, my, maxError);
+        processTriangle(bx, by, cx, cy, mx, my, maxError);
+      } else {
+        int a = indices[ay * gridSize + ax] - 1;
+        int b = indices[by * gridSize + bx] - 1;
+        int c = indices[cy * gridSize + cx] - 1;
+
+        vertices[2 * a] = ax;
+        vertices[2 * a + 1] = ay;
+        vertices[2 * b] = bx;
+        vertices[2 * b + 1] = by;
+        vertices[2 * c] = cx;
+        vertices[2 * c + 1] = cy;
+
+        triangles[triIndex++] = a;
+        triangles[triIndex++] = b;
+        triangles[triIndex++] = c;
+      }
+    }
+  }
+
+  public static class Mesh {
+
+    private final int[] vertices;
+    private final int[] triangles;
+
+    public Mesh(int[] vertices, int[] triangles) {
+      this.vertices = vertices;
+      this.triangles = triangles;
+    }
+
+    public int[] getVertices() {
+      return vertices;
+    }
+
+    public int[] getTriangles() {
+      return triangles;
+    }
+  }
+}
diff --git 
a/baremaps-martini/src/test/java/org/apache/baremaps/martini/MartiniTest.java 
b/baremaps-martini/src/test/java/org/apache/baremaps/martini/MartiniTest.java
new file mode 100644
index 00000000..9177bd4a
--- /dev/null
+++ 
b/baremaps-martini/src/test/java/org/apache/baremaps/martini/MartiniTest.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.martini;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.nio.file.Path;
+import javax.imageio.ImageIO;
+import org.junit.jupiter.api.Test;
+
+public class MartiniTest {
+
+  @Test
+  public void generateAMesh() throws IOException {
+    BufferedImage png = ImageIO.read(
+        Path.of("")
+            .toAbsolutePath()
+            .resolveSibling("baremaps-geotiff/src/test/resources/fuji.png")
+            .toAbsolutePath().toFile());
+    float[] terrainGrid = MartiniUtils.mapboxTerrainToGrid(png);
+    var martini = new Martini(png.getWidth() + 1);
+    var tile = martini.createTile(terrainGrid);
+    var mesh = tile.getMesh(500);
+
+    assertArrayEquals(new int[] {
+        320, 64, 256, 128, 320, 128, 384, 128, 256, 0, 288, 160, 256, 192, 
288, 192, 320, 192, 304,
+        176, 256, 256, 288, 224, 352, 160, 320, 160, 512, 0, 384, 0, 128, 128, 
128, 0, 64, 64, 64,
+        0, 0, 0, 32, 32, 192, 192, 384, 384, 512, 256, 384, 256, 320, 320, 
320, 256, 512, 512, 512,
+        128, 448, 192, 384, 192, 128, 384, 256, 512, 256, 384, 0, 512, 128, 
256, 64, 192, 0, 256,
+        64, 128, 32, 96, 0, 128, 32, 64, 16, 48, 0, 64, 0, 32
+    }, mesh.getVertices());
+
+    assertArrayEquals(new int[] {
+        0, 1, 2, 3, 0, 2, 4, 1, 0, 5, 6, 7, 7, 8, 9, 5, 7, 9, 1, 6, 5, 6, 10, 
11, 11, 8, 7, 6, 11,
+        7, 12, 2, 13, 8, 12, 13, 3, 2, 12, 2, 1, 5, 13, 5, 9, 8, 13, 9, 2, 5, 
13, 3, 14, 15, 15, 4,
+        0, 3, 15, 0, 16, 4, 17, 18, 17, 19, 19, 20, 21, 18, 19, 21, 16, 17, 
18, 1, 16, 22, 22, 10,
+        6, 1, 22, 6, 4, 16, 1, 23, 24, 25, 26, 25, 27, 10, 26, 27, 23, 25, 26, 
28, 24, 23, 29, 3,
+        30, 24, 29, 30, 14, 3, 29, 8, 25, 31, 31, 3, 12, 8, 31, 12, 27, 8, 11, 
10, 27, 11, 25, 8,
+        27, 25, 24, 30, 30, 3, 31, 25, 30, 31, 32, 33, 34, 10, 32, 34, 35, 33, 
32, 33, 28, 23, 34,
+        23, 26, 10, 34, 26, 33, 23, 34, 36, 16, 37, 38, 36, 37, 36, 10, 22, 
16, 36, 22, 39, 18, 40,
+        41, 39, 40, 16, 18, 39, 42, 21, 43, 44, 42, 43, 18, 21, 42, 21, 20, 
45, 45, 44, 43, 21, 45,
+        43, 44, 41, 40, 40, 18, 42, 44, 40, 42, 41, 38, 37, 37, 16, 39, 41, 
37, 39, 38, 35, 32, 32,
+        10, 36, 38, 32, 36
+    }, mesh.getTriangles());
+  }
+}
diff --git 
a/baremaps-martini/src/test/java/org/apache/baremaps/martini/MartiniUtils.java 
b/baremaps-martini/src/test/java/org/apache/baremaps/martini/MartiniUtils.java
new file mode 100644
index 00000000..bfae6c7a
--- /dev/null
+++ 
b/baremaps-martini/src/test/java/org/apache/baremaps/martini/MartiniUtils.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.martini;
+
+import java.awt.image.BufferedImage;
+
+public class MartiniUtils {
+
+  public static float[] mapboxTerrainToGrid(BufferedImage png) {
+    int gridSize = png.getWidth() + 1;
+    float[] terrain = new float[gridSize * gridSize];
+
+    int tileSize = png.getWidth();
+
+    // decode terrain values
+    for (int y = 0; y < tileSize; y++) {
+      for (int x = 0; x < tileSize; x++) {
+        int k = (y * tileSize + x) * 4;
+        int r = (png.getRGB(x, y) >> 16) & 0xFF;
+        int g = (png.getRGB(x, y) >> 8) & 0xFF;
+        int b = png.getRGB(x, y) & 0xFF;
+        terrain[y * gridSize + x] = (r * 256 * 256 + g * 256.0f + b) / 10.0f - 
10000.0f;
+      }
+    }
+
+    // backfill right and bottom borders
+    for (int x = 0; x < gridSize - 1; x++) {
+      terrain[gridSize * (gridSize - 1) + x] = terrain[gridSize * (gridSize - 
2) + x];
+    }
+    for (int y = 0; y < gridSize; y++) {
+      terrain[gridSize * y + gridSize - 1] = terrain[gridSize * y + gridSize - 
2];
+    }
+
+    return terrain;
+  }
+}
diff --git a/baremaps-martini/src/test/resources/fuji.png 
b/baremaps-martini/src/test/resources/fuji.png
new file mode 100644
index 00000000..9e4e8b76
Binary files /dev/null and b/baremaps-martini/src/test/resources/fuji.png differ
diff --git a/pom.xml b/pom.xml
index eb1e3bd5..d3c42a91 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,6 +54,7 @@ limitations under the License.
     <module>baremaps-data</module>
     <module>baremaps-geoparquet</module>
     <module>baremaps-maplibre</module>
+    <module>baremaps-martini</module>
     <module>baremaps-openstreetmap</module>
     <module>baremaps-pmtiles</module>
     <module>baremaps-server</module>

Reply via email to