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

adrabble pushed a commit to branch 568-experiment-with-3d-tiles
in repository https://gitbox.apache.org/repos/asf/incubator-baremaps.git


The following commit(s) were added to refs/heads/568-experiment-with-3d-tiles 
by this push:
     new 4a9ac0b5 Make a first experiment with 3d tiles
4a9ac0b5 is described below

commit 4a9ac0b52c80a8765b331efa75b388362045bc6d
Author: Antoine Drabble <[email protected]>
AuthorDate: Wed Jan 4 16:59:00 2023 +0100

    Make a first experiment with 3d tiles
---
 .run/tdtiles-create.run.xml                        |  11 +
 .run/tdtiles-serve.run.xml                         |  11 +
 .../java/org/apache/baremaps/cli/Baremaps.java     |   3 +-
 .../org/apache/baremaps/cli/tdtiles/Serve.java     |  71 +++++
 .../org/apache/baremaps/cli/tdtiles/TdTiles.java   |  28 ++
 baremaps-core/pom.xml                              |   4 +
 .../org/apache/baremaps/tdtiles/GltfBuilder.java   | 325 +++++++++++++++++++++
 .../org/apache/baremaps/tdtiles/TdTilesStore.java  |  81 +++++
 .../apache/baremaps/tdtiles/building/Building.java |   7 +
 .../baremaps/tdtiles/subtree/Availability.java     |   4 +
 .../apache/baremaps/tdtiles/subtree/Subtree.java   |   7 +
 .../apache/baremaps/server/TdTilesResources.java   | 104 +++++++
 .../src/main/resources/tdtiles/favicon.ico         | Bin 0 -> 15086 bytes
 .../src/main/resources/tdtiles/index.html          |  43 +++
 .../src/main/resources/tdtiles/tileset.json        |  24 ++
 examples/tdtiles/README.md                         |   5 +
 examples/tdtiles/indexes.sql                       |   5 +
 examples/tdtiles/workflow.json                     |  42 +++
 pom.xml                                            |   6 +
 19 files changed, 780 insertions(+), 1 deletion(-)

diff --git a/.run/tdtiles-create.run.xml b/.run/tdtiles-create.run.xml
new file mode 100644
index 00000000..e795524c
--- /dev/null
+++ b/.run/tdtiles-create.run.xml
@@ -0,0 +1,11 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="tdtiles-create" type="Application" 
factoryName="Application">
+    <option name="MAIN_CLASS_NAME" value="org.apache.baremaps.cli.Baremaps" />
+    <module name="baremaps-cli" />
+    <option name="PROGRAM_PARAMETERS" value="workflow execute --file 
workflow.json" />
+    <option name="WORKING_DIRECTORY" value="examples/tdtiles" />
+    <method v="2">
+      <option name="Make" enabled="true" />
+    </method>
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/.run/tdtiles-serve.run.xml b/.run/tdtiles-serve.run.xml
new file mode 100644
index 00000000..67337bd7
--- /dev/null
+++ b/.run/tdtiles-serve.run.xml
@@ -0,0 +1,11 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="tdtiles-serve" type="Application" 
factoryName="Application">
+    <option name="MAIN_CLASS_NAME" value="org.apache.baremaps.cli.Baremaps" />
+    <module name="baremaps-cli" />
+    <option name="PROGRAM_PARAMETERS" value="tdtiles serve --database 
jdbc:postgresql://localhost:5432/baremaps?user=baremaps&amp;password=baremaps" 
/>
+    <option name="WORKING_DIRECTORY" value="examples/tdtiles" />
+    <method v="2">
+      <option name="Make" enabled="true" />
+    </method>
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/baremaps-cli/src/main/java/org/apache/baremaps/cli/Baremaps.java 
b/baremaps-cli/src/main/java/org/apache/baremaps/cli/Baremaps.java
index b792f068..1b763cf7 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/Baremaps.java
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/Baremaps.java
@@ -24,6 +24,7 @@ import org.apache.baremaps.cli.geocoder.Geocoder;
 import org.apache.baremaps.cli.iploc.IpLoc;
 import org.apache.baremaps.cli.map.Map;
 import org.apache.baremaps.cli.ogcapi.OgcApi;
+import org.apache.baremaps.cli.tdtiles.TdTiles;
 import org.apache.baremaps.cli.workflow.Workflow;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.LogManager;
@@ -38,7 +39,7 @@ import picocli.CommandLine.Option;
 
 @Command(name = "baremaps", description = "A toolkit for producing vector 
tiles.",
     versionProvider = VersionProvider.class, subcommands = {Workflow.class, 
Database.class,
-        Map.class, Geocoder.class, IpLoc.class, OgcApi.class},
+        Map.class, TdTiles.class, Geocoder.class, IpLoc.class, OgcApi.class},
     sortOptions = false)
 public class Baremaps implements Callable<Integer> {
 
diff --git 
a/baremaps-cli/src/main/java/org/apache/baremaps/cli/tdtiles/Serve.java 
b/baremaps-cli/src/main/java/org/apache/baremaps/cli/tdtiles/Serve.java
new file mode 100644
index 00000000..3ba44913
--- /dev/null
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/tdtiles/Serve.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed 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.cli.tdtiles;
+
+import io.servicetalk.http.netty.HttpServers;
+import io.servicetalk.http.router.jersey.HttpJerseyRouterBuilder;
+import java.util.concurrent.Callable;
+import org.apache.baremaps.cli.Options;
+import org.apache.baremaps.database.PostgresUtils;
+import org.apache.baremaps.server.CorsFilter;
+import org.apache.baremaps.server.TdTilesResources;
+import org.glassfish.hk2.utilities.binding.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+import picocli.CommandLine.Option;
+
+import javax.sql.DataSource;
+
+@Command(name = "serve", description = "Start a 3d tile server.")
+public class Serve implements Callable<Integer> {
+
+  private static final Logger logger = LoggerFactory.getLogger(Serve.class);
+
+  @Mixin
+  private Options options;
+
+  @Option(names = {"--database"}, paramLabel = "DATABASE",
+      description = "The JDBC url of Postgres.", required = true)
+  private String database;
+
+  @Option(names = {"--host"}, paramLabel = "HOST", description = "The host of 
the server.")
+  private String host = "localhost";
+
+  @Option(names = {"--port"}, paramLabel = "PORT", description = "The port of 
the server.")
+  private int port = 9000;
+
+  @Override
+  public Integer call() throws Exception {
+    var datasource = PostgresUtils.dataSource(database);
+
+    // Configure the application
+    var application =
+        new 
ResourceConfig().register(CorsFilter.class).register(TdTilesResources.class).register(new
 AbstractBinder() {
+              @Override
+              protected void configure() {
+                bind(datasource).to(DataSource.class);
+              }
+            });
+
+    var httpService = new 
HttpJerseyRouterBuilder().buildBlockingStreaming(application);
+    var serverContext = 
HttpServers.forPort(port).listenBlockingStreamingAndAwait(httpService);
+
+    logger.info("Listening on {}", serverContext.listenAddress());
+
+    serverContext.awaitShutdown();
+    return 0;
+  }
+}
diff --git 
a/baremaps-cli/src/main/java/org/apache/baremaps/cli/tdtiles/TdTiles.java 
b/baremaps-cli/src/main/java/org/apache/baremaps/cli/tdtiles/TdTiles.java
new file mode 100644
index 00000000..5ffcf186
--- /dev/null
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/tdtiles/TdTiles.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed 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.cli.tdtiles;
+
+
+
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "tdtiles", description = "3d Tiles commands.", subcommands = 
{Serve.class},
+    sortOptions = false)
+public class TdTiles implements Runnable {
+
+  @Override
+  public void run() {
+    CommandLine.usage(this, System.out);
+  }
+}
diff --git a/baremaps-core/pom.xml b/baremaps-core/pom.xml
index c5cc7fb5..90ce69a5 100644
--- a/baremaps-core/pom.xml
+++ b/baremaps-core/pom.xml
@@ -58,6 +58,10 @@
       <groupId>de.bytefish</groupId>
       <artifactId>pgbulkinsert</artifactId>
     </dependency>
+    <dependency>
+      <groupId>de.javagl</groupId>
+      <artifactId>jgltf-model-builder</artifactId>
+    </dependency>
     <dependency>
       <groupId>it.unimi.dsi</groupId>
       <artifactId>fastutil</artifactId>
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/GltfBuilder.java 
b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/GltfBuilder.java
new file mode 100644
index 00000000..b32bd6cf
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/GltfBuilder.java
@@ -0,0 +1,325 @@
+/*
+ * Licensed 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.tdtiles;
+
+import de.javagl.jgltf.model.NodeModel;
+import de.javagl.jgltf.model.creation.GltfModelBuilder;
+import de.javagl.jgltf.model.creation.MaterialBuilder;
+import de.javagl.jgltf.model.creation.MeshPrimitiveBuilder;
+import de.javagl.jgltf.model.impl.*;
+import de.javagl.jgltf.model.io.v2.GltfAssetV2;
+import de.javagl.jgltf.model.io.v2.GltfAssetWriterV2;
+import de.javagl.jgltf.model.io.v2.GltfAssetsV2;
+import de.javagl.jgltf.model.v2.MaterialModelV2;
+import org.apache.baremaps.tdtiles.building.Building;
+import org.apache.commons.lang3.ArrayUtils;
+import org.locationtech.jts.geom.*;
+import org.locationtech.jts.geomgraph.Edge;
+import org.locationtech.jts.math.Vector3D;
+import org.locationtech.jts.triangulate.DelaunayTriangulationBuilder;
+
+import java.io.*;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.*;
+
+public class GltfBuilder {
+
+    /**
+     * Create a node from a building.
+     *
+     * @param building
+     * @return
+     */
+    public static NodeModel createNode(Building building, float tolerance) {
+        DelaunayTriangulationBuilder delaunayTriangulationBuilder = new 
DelaunayTriangulationBuilder();
+        delaunayTriangulationBuilder.setSites(building.geometry());
+        delaunayTriangulationBuilder.setTolerance(tolerance);
+
+        float[] translation = cartesian3Degrees((float) 
building.geometry().getCoordinates()[0].y, (float) 
building.geometry().getCoordinates()[0].x, 0);
+
+        Geometry triangulation = delaunayTriangulationBuilder.getTriangles(new 
GeometryFactory()); // initial triangulation
+        if(triangulation.getNumGeometries() == 0){
+            return new DefaultNodeModel();
+        }
+
+        List<Float> vertices = new ArrayList<>();
+        List<Integer> indices = new ArrayList<>();
+        List<Float> normals = new ArrayList<>();
+
+        createRoof(building, translation, triangulation, vertices, indices);
+        HashSet<Edge> edges = getExteriorEdges(triangulation);
+        createWalls(building, translation, vertices, indices, edges);
+        createNormals(vertices, normals);
+
+        MeshPrimitiveBuilder meshPrimitiveBuilder =
+                MeshPrimitiveBuilder.create();
+        
meshPrimitiveBuilder.setIntIndicesAsShort(IntBuffer.wrap(ArrayUtils.toPrimitive(indices.toArray(new
 Integer[0]), 0)));
+        
meshPrimitiveBuilder.addPositions3D(FloatBuffer.wrap(ArrayUtils.toPrimitive(vertices.toArray(new
 Float[0]), 0.0F)));
+        
meshPrimitiveBuilder.addNormals3D(FloatBuffer.wrap(ArrayUtils.toPrimitive(normals.toArray(new
 Float[0]), 0.0F)));
+
+        DefaultMeshPrimitiveModel meshPrimitiveModel =
+                meshPrimitiveBuilder.build();
+
+        // Create a material, and assign it to the mesh primitive
+        MaterialBuilder materialBuilder = MaterialBuilder.create();
+        materialBuilder.setBaseColorFactor(1f, 1f, 1f, 1.0f);
+        materialBuilder.setDoubleSided(false);
+        MaterialModelV2 materialModel = materialBuilder.build();
+        materialModel.setMetallicFactor(0.0f);
+        materialModel.setOcclusionStrength(0.0f);
+        materialModel.setRoughnessFactor(1.0f);
+        meshPrimitiveModel.setMaterialModel(materialModel);
+
+        // Create a mesh with the mesh primitive
+        DefaultMeshModel meshModel = new DefaultMeshModel();
+        meshModel.addMeshPrimitiveModel(meshPrimitiveModel);
+
+        // Create a node with the mesh
+        DefaultNodeModel nodeModel = new DefaultNodeModel();
+        nodeModel.addMeshModel(meshModel);
+        //nodeModel.setScale(new float[]{5f,5f,5f});
+        nodeModel.setTranslation(translation);
+        return nodeModel;
+    }
+
+    /**
+     * Compute the normal for each vertex in the vertices array.
+     * Vertices are grouped by 3, so we can compute the normal for each 
triangle.
+     * The normal is the normalized cross product of the two vectors of the 
triangle.
+     *
+     * @param vertices
+     * @param normals
+     */
+    private static void createNormals(List<Float> vertices, List<Float> 
normals) {
+        for (int i = 0; i < vertices.size(); i += 9) {
+            float[] v1 = new float[]{vertices.get(i), vertices.get(i + 1), 
vertices.get(i + 2)};
+            float[] v2 = new float[]{vertices.get(i + 3), vertices.get(i + 4), 
vertices.get(i + 5)};
+            float[] v3 = new float[]{vertices.get(i + 6), vertices.get(i + 7), 
vertices.get(i + 8)};
+            float[] u = new float[]{v2[0] - v1[0], v2[1] - v1[1], v2[2] - 
v1[2]};
+            float[] v = new float[]{v3[0] - v1[0], v3[1] - v1[1], v3[2] - 
v1[2]};
+            float[] normal = new float[]{u[1] * v[2] - u[2] * v[1], u[2] * 
v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]};
+            float length = (float) Math.sqrt(normal[0] * normal[0] + normal[1] 
* normal[1] + normal[2] * normal[2]);
+            normal[0] /= length;
+            normal[1] /= length;
+            normal[2] /= length;
+            normals.add(normal[0]);
+            normals.add(normal[1]);
+            normals.add(normal[2]);
+            normals.add(normal[0]);
+            normals.add(normal[1]);
+            normals.add(normal[2]);
+            normals.add(normal[0]);
+            normals.add(normal[1]);
+            normals.add(normal[2]);
+        }
+    }
+
+    /**
+     * Iterate over the edges and extrude them to create the walls based on 
the build height.
+     *
+     * @param building
+     * @param translation
+     * @param vertices
+     * @param indices
+     * @param edges the exterior edges
+     */
+    private static void createWalls(Building building, float[] translation, 
List<Float> vertices, List<Integer> indices, HashSet<Edge> edges) {
+        for (Edge edge : edges) {
+            Coordinate[] v = edge.getCoordinates();
+
+            float[] pos0 = cartesian3Degrees((float)v[0].getY(), 
(float)v[0].getX(), 0);
+            pos0[0] -= translation[0];
+            pos0[1] -= translation[1];
+            pos0[2] -= translation[2];
+            float[] pos1 = cartesian3Degrees((float)v[1].getY(), 
(float)v[1].getX(), 0);
+            pos1[0] -= translation[0];
+            pos1[1] -= translation[1];
+            pos1[2] -= translation[2];
+            float[] pos2 = cartesian3Degrees((float)v[0].getY(), 
(float)v[0].getX(), building.height());
+            pos2[0] -= translation[0];
+            pos2[1] -= translation[1];
+            pos2[2] -= translation[2];
+            float[] pos3 = cartesian3Degrees((float)v[1].getY(), 
(float)v[1].getX(), building.height());
+            pos3[0] -= translation[0];
+            pos3[1] -= translation[1];
+            pos3[2] -= translation[2];
+
+            indices.add(vertices.size()/3);
+            vertices.add(pos0[0]);
+            vertices.add(pos0[1]);
+            vertices.add(pos0[2]);
+            indices.add(vertices.size()/3);
+            vertices.add(pos1[0]);
+            vertices.add(pos1[1]);
+            vertices.add(pos1[2]);
+            indices.add(vertices.size()/3);
+            vertices.add(pos2[0]);
+            vertices.add(pos2[1]);
+            vertices.add(pos2[2]);
+
+            indices.add(vertices.size()/3);
+            vertices.add(pos2[0]);
+            vertices.add(pos2[1]);
+            vertices.add(pos2[2]);
+            indices.add(vertices.size()/3);
+            vertices.add(pos1[0]);
+            vertices.add(pos1[1]);
+            vertices.add(pos1[2]);
+            indices.add(vertices.size()/3);
+            vertices.add(pos3[0]);
+            vertices.add(pos3[1]);
+            vertices.add(pos3[2]);
+        }
+    }
+
+    /**
+     * Get a list of exterior edges from the building geometry. The exterior 
edges are the ones that are not shared by two
+     * triangles.
+     * @param triangulation
+     * @return
+     */
+    private static HashSet<Edge> getExteriorEdges(Geometry triangulation) {
+        HashSet<Edge> edges = new HashSet<>(triangulation.getNumGeometries() * 
3, 1.0f);
+        for (int i = 0; i < triangulation.getNumGeometries(); i++) {
+            Geometry triangle = triangulation.getGeometryN(i);
+            Coordinate corner1 = triangle.getCoordinates()[0];
+            Coordinate corner2 = triangle.getCoordinates()[1];
+            Coordinate corner3 = triangle.getCoordinates()[2];
+            Edge edge1 = new Edge(new Coordinate[]{corner1, corner2});
+            if(edges.contains(edge1)){
+                edges.remove(edge1);
+            } else {
+                edges.add(edge1);
+            }
+            Edge edge2 = new Edge(new Coordinate[]{corner2, corner3});
+            if(edges.contains(edge2)){
+                edges.remove(edge2);
+            } else {
+                edges.add(edge2);
+            }
+            Edge edge3 = new Edge(new Coordinate[]{corner3, corner1});
+            if(edges.contains(edge3)){
+                edges.remove(edge3);
+            } else {
+                edges.add(edge3);
+            }
+        }
+        return edges;
+    }
+
+    /**
+     * Create the roof geometry. This is simply the triangulation of the 
building geometry at the building height.
+     *
+     * @param building
+     * @param translation
+     * @param triangulation
+     * @param vertices
+     * @param indices
+     */
+    private static void createRoof(Building building, float[] translation, 
Geometry triangulation, List<Float> vertices, List<Integer> indices) {
+        for (int i = 0; i < triangulation.getNumGeometries(); i++) {
+            Geometry triangle = triangulation.getGeometryN(i);
+            Coordinate corner1 = triangle.getCoordinates()[0];
+            Coordinate corner2 = triangle.getCoordinates()[1];
+            Coordinate corner3 = triangle.getCoordinates()[2];
+
+            float[] pos0 = cartesian3Degrees((float) corner1.y, (float) 
corner1.x, building.height());
+            pos0[0] -= translation[0];
+            pos0[1] -= translation[1];
+            pos0[2] -= translation[2];
+            float[] pos1 = cartesian3Degrees((float) corner2.y, (float) 
corner2.x, building.height());
+            pos1[0] -= translation[0];
+            pos1[1] -= translation[1];
+            pos1[2] -= translation[2];
+            float[] pos2 = cartesian3Degrees((float) corner3.y, (float) 
corner3.x, building.height());
+            pos2[0] -= translation[0];
+            pos2[1] -= translation[1];
+            pos2[2] -= translation[2];
+
+            indices.add(vertices.size() / 3);
+            vertices.add(pos0[0]);
+            vertices.add(pos0[1]);
+            vertices.add(pos0[2]);
+            indices.add(vertices.size() / 3);
+            vertices.add(pos1[0]);
+            vertices.add(pos1[1]);
+            vertices.add(pos1[2]);
+            indices.add(vertices.size() / 3);
+            vertices.add(pos2[0]);
+            vertices.add(pos2[1]);
+            vertices.add(pos2[2]);
+        }
+    }
+
+    /**
+     * Create a GLB from a list of nodes.
+     *
+     * @param nodes
+     * @return
+     * @throws Exception
+     */
+    public static byte[] createGltf(List<NodeModel> nodes) throws Exception {
+        DefaultSceneModel sceneModel = new DefaultSceneModel();
+
+        for (NodeModel node : nodes) {
+            sceneModel.addNode(node);
+        }
+
+        GltfModelBuilder gltfModelBuilder = GltfModelBuilder.create();
+        gltfModelBuilder.addSceneModel(sceneModel);
+        DefaultGltfModel gltfModel = gltfModelBuilder.build();
+
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        GltfAssetV2 asset = GltfAssetsV2.createEmbedded(gltfModel);
+
+        GltfAssetWriterV2 gltfAssetWriter = new GltfAssetWriterV2();
+        gltfAssetWriter.writeBinary(asset, outputStream);
+
+        return outputStream.toByteArray();
+    }
+
+    /**
+     * Returns a Cartesian3 position from longitude and latitude values given 
in radians.
+     * Port of <a 
href="https://github.com/CesiumGS/cesium/blob/20c305e94d80a76ff750cfc2fea17d22cb7e5b58/packages/engine/Source/Core/Cartesian3.js#L914";>CesiumGS</a>
+     *
+     * @param latitude in radians
+     * @param longitude in radians
+     * @param height in meters above the ellipsoid.
+     * @return The position
+     */
+    public static float[] cartesian3FromRadians(float latitude, float 
longitude, float height) {
+        Vector3D wgs84RadiiSquared = new Vector3D(6378137.0f * 6378137.0f, 
6378137.0f * 6378137.0f, 6356752.3142451793f * 6356752.3142451793f);
+        double cosLatitude = Math.cos(latitude);
+        Vector3D scratchN = new Vector3D(cosLatitude * Math.cos(longitude), 
cosLatitude * Math.sin(longitude), Math.sin(latitude)).normalize();
+        Vector3D scratchK = new Vector3D(wgs84RadiiSquared.getX() * 
scratchN.getX(), wgs84RadiiSquared.getY() * scratchN.getY(), 
wgs84RadiiSquared.getZ() * scratchN.getZ());
+        double gamma = Math.sqrt(scratchN.dot(scratchK));
+        scratchK = scratchK.divide(gamma);
+        scratchN = new Vector3D(scratchN.getX() * height, scratchN.getY() * 
height, scratchN.getZ() * height);
+        Vector3D result = scratchK.add(scratchN);
+        return new float[]{(float) result.getX(), (float) result.getZ(), 
-(float) result.getY()};
+    }
+
+    /**
+     * Returns a Cartesian3 position from longitude and latitude values given 
in degrees.
+     * Port of <a 
href="https://github.com/CesiumGS/cesium/blob/20c305e94d80a76ff750cfc2fea17d22cb7e5b58/packages/engine/Source/Core/Cartesian3.js#L914";>CesiumGS</a>
+     *
+     * @param latitude in degrees
+     * @param longitude in degrees
+     * @param height in meters above the ellipsoid.
+     * @return The position
+     */
+    public static float[] cartesian3Degrees(float latitude, float longitude, 
float height) {
+        return cartesian3FromRadians(latitude * (float) Math.PI / 180, 
longitude * (float) Math.PI / 180, height);
+    }
+}
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/TdTilesStore.java 
b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/TdTilesStore.java
new file mode 100644
index 00000000..044206a1
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/TdTilesStore.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed 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.tdtiles;
+
+
+import org.apache.baremaps.database.tile.*;
+import org.apache.baremaps.openstreetmap.utils.GeometryUtils;
+import org.apache.baremaps.tdtiles.building.Building;
+import org.locationtech.jts.geom.Geometry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.*;
+
+/**
+ * A read-only {@code TileStore} implementation that uses the PostgreSQL to 
generate 3d tiles.
+ */
+public class TdTilesStore {
+
+  private static final Logger logger = 
LoggerFactory.getLogger(TdTilesStore.class);
+  private static final String QUERY =
+      "select st_asbinary(geom), tags -> 'buildings:height', tags -> 'height', 
tags -> 'buildings:levels'  from osm_ways where tags ? 'building' and 
st_intersects( st_force3d(geom,0), st_makeenvelope(%1$s, %2$s, %3$s, %4$s, 
4326)) LIMIT %5$s";
+
+
+  private final DataSource datasource;
+
+  public TdTilesStore(DataSource datasource) {
+    this.datasource = datasource;
+  }
+
+  public List<Building> read(float xmin, float xmax, float ymin, float ymax, 
int limit) throws TileStoreException {
+    try (Connection connection = datasource.getConnection();
+        Statement statement = connection.createStatement()) {
+
+      String sql = String.format(QUERY, ymin * 180 / (float)Math.PI, xmin * 
180 / (float)Math.PI, ymax * 180 / (float)Math.PI, xmax * 180 / (float)Math.PI, 
limit);
+      logger.debug("Executing query: {}", sql);
+
+      List<Building> buildings = new ArrayList<>();
+
+      try (ResultSet resultSet = statement.executeQuery(sql)) {
+        while (resultSet.next()) {
+          byte[] bytes = resultSet.getBytes(1);
+          Geometry geometry = GeometryUtils.deserialize(bytes);
+
+          String buildingHeight = resultSet.getString(2);
+          String height = resultSet.getString(3);
+          String buildingLevels = resultSet.getString(4);
+          float finalHeight = 10;
+          if(buildingHeight != null) {
+            finalHeight = Float.parseFloat(buildingHeight.replaceAll("[^0-9]", 
""));
+          } else if(height != null) {
+            finalHeight = Float.parseFloat(height.replaceAll("[^0-9]", ""));
+          } else if(buildingLevels != null) {
+            finalHeight = Float.parseFloat(buildingLevels.replaceAll("[^0-9]", 
"")) * 3;
+          }
+
+          buildings.add(new Building(geometry, finalHeight));
+        }
+      }
+      return buildings;
+    } catch (SQLException e) {
+      throw new TileStoreException(e);
+    }
+  }
+
+}
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/building/Building.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/building/Building.java
new file mode 100644
index 00000000..3eafff70
--- /dev/null
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/building/Building.java
@@ -0,0 +1,7 @@
+package org.apache.baremaps.tdtiles.building;
+
+import org.locationtech.jts.geom.Geometry;
+
+public record Building(Geometry geometry, float height) {
+
+}
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/subtree/Availability.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/subtree/Availability.java
new file mode 100644
index 00000000..a6ef848d
--- /dev/null
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/subtree/Availability.java
@@ -0,0 +1,4 @@
+package org.apache.baremaps.tdtiles.subtree;
+
+public record Availability(boolean constant) {
+}
\ No newline at end of file
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/subtree/Subtree.java 
b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/subtree/Subtree.java
new file mode 100644
index 00000000..ff48f0c0
--- /dev/null
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/subtree/Subtree.java
@@ -0,0 +1,7 @@
+package org.apache.baremaps.tdtiles.subtree;
+
+public record Subtree(Availability tileAvailability,
+                      Availability contentAvailability,
+                      Availability childSubtreeAvailability) {
+
+}
\ No newline at end of file
diff --git 
a/baremaps-server/src/main/java/org/apache/baremaps/server/TdTilesResources.java
 
b/baremaps-server/src/main/java/org/apache/baremaps/server/TdTilesResources.java
new file mode 100644
index 00000000..7b0e61a4
--- /dev/null
+++ 
b/baremaps-server/src/main/java/org/apache/baremaps/server/TdTilesResources.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed 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.server;
+
+import static com.google.common.net.HttpHeaders.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.sql.DataSource;
+import javax.ws.rs.GET;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Response;
+
+import de.javagl.jgltf.model.NodeModel;
+import org.apache.baremaps.tdtiles.TdTilesStore;
+import org.apache.baremaps.tdtiles.building.Building;
+import org.apache.baremaps.tdtiles.GltfBuilder;
+import org.apache.baremaps.tdtiles.subtree.Availability;
+import org.apache.baremaps.tdtiles.subtree.Subtree;
+
+@Singleton
[email protected]("/")
+public class TdTilesResources {
+  private final TdTilesStore tdTilesStore;
+
+  @Inject
+  public TdTilesResources(DataSource dataSource) {
+    this.tdTilesStore = new TdTilesStore(dataSource);
+  }
+
+  @GET
+  @javax.ws.rs.Path("/subtrees/{level}.{x}.{y}.json")
+  public Response getSubtree(@PathParam("level") int level, @PathParam("x") 
int x,
+      @PathParam("y") int y) {
+    if (level == 18) {
+        return Response.ok().entity(new Subtree(new Availability(false),new 
Availability(true),new Availability(false))).header(CONTENT_TYPE, 
"application/json").build();
+    }
+    return Response.ok().entity(new Subtree(new Availability(true),new 
Availability(true),new Availability(true))).header(CONTENT_TYPE, 
"application/json").build();
+  }
+
+  @GET
+  @javax.ws.rs.Path("/content/content_{level}__{x}_{y}.glb")
+  public Response getContent(@PathParam("level") int level, @PathParam("x") 
int x,
+      @PathParam("y") int y) throws Exception {
+    float[] coords = xyzToLatLonRadians(x, y, level);
+    int limit = level > 17 ? 1000 : 80;
+    List<Building> buildings = 
tdTilesStore.read(coords[0],coords[1],coords[2],coords[3], limit);
+    List<NodeModel> nodes = new ArrayList<>();
+    for (Building building : buildings) {
+      float tolerance = level > 17 ? 0.00001f : level >= 16 ? 0.0001f : 
0.0003f;
+      nodes.add(GltfBuilder.createNode(building, tolerance));
+    }
+    return Response.ok().entity(
+            GltfBuilder.createGltf(nodes)
+    ).build();
+  }
+
+  public static float[] xyzToLatLonRadians(int x, int y, int z) {
+    float[] answer = new float[4];
+    int  subdivision = 1 << z;
+    float yWidth = (float)Math.PI / subdivision;
+    float xWidth = 2 * (float)Math.PI / subdivision;
+    answer[0] = - (float)Math.PI / 2 + y * yWidth; // Lon
+    answer[1] = answer[0] + yWidth; // Lon max
+    answer[2] = - (float)Math.PI + xWidth * x; // Lat
+    answer[3] = answer[2] + xWidth; // Lat max
+    // Clamp to -PI/2 to PI/2
+    answer[0] = Math.max(- (float)Math.PI / 2, Math.min((float)Math.PI / 2, 
answer[0]));
+    answer[1] = Math.max(- (float)Math.PI / 2, Math.min((float)Math.PI / 2, 
answer[1]));
+    // Clamp to -PI to PI
+    answer[2] = Math.max(- (float)Math.PI, Math.min((float)Math.PI, 
answer[2]));
+    answer[3] = Math.max(- (float)Math.PI, Math.min((float)Math.PI, 
answer[3]));
+    return answer;
+  }
+
+  @GET
+  @javax.ws.rs.Path("/{path:.*}")
+  public Response get(@PathParam("path") String path) {
+    if (path.equals("") || path.endsWith("/")) {
+      path += "index.html";
+    }
+    path = String.format("tdtiles/%s", path);
+    try (InputStream inputStream = 
getClass().getClassLoader().getResourceAsStream(path)) {
+      var bytes = inputStream.readAllBytes();
+      return Response.ok().entity(bytes).build();
+    } catch (IOException e) {
+      return Response.status(404).build();
+    }
+  }
+}
diff --git a/baremaps-server/src/main/resources/tdtiles/favicon.ico 
b/baremaps-server/src/main/resources/tdtiles/favicon.ico
new file mode 100644
index 00000000..7162b07e
Binary files /dev/null and 
b/baremaps-server/src/main/resources/tdtiles/favicon.ico differ
diff --git a/baremaps-server/src/main/resources/tdtiles/index.html 
b/baremaps-server/src/main/resources/tdtiles/index.html
new file mode 100644
index 00000000..0bc4b889
--- /dev/null
+++ b/baremaps-server/src/main/resources/tdtiles/index.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <!-- Include the CesiumJS JavaScript and CSS files -->
+    <script 
src="https://cesium.com/downloads/cesiumjs/releases/1.100/Build/Cesium/Cesium.js";></script>
+    <link 
href="https://cesium.com/downloads/cesiumjs/releases/1.100/Build/Cesium/Widgets/widgets.css";
 rel="stylesheet">
+</head>
+<body>
+<div id="cesiumContainer"></div>
+<script>
+    Cesium.Ion.defaultAccessToken = 
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjYTZkYThmNi1iMDIxLTRkOTAtOTI4YS01OTY4YWIzYzYzOGUiLCJpZCI6MTE4MzI0LCJpYXQiOjE2NzEwMTU5NzZ9.qjN6GQvDXYjHsgTHQ6Y8BengL4GEPGmff_WKfLzrn0o';
+
+    // Initialize the Cesium Viewer in the HTML element with the 
`cesiumContainer` ID.
+    const viewer = new Cesium.Viewer('cesiumContainer', {
+        //terrainProvider: Cesium.createWorldTerrain()
+    });
+
+    // Create the tileset in the viewer
+    const tileset =
+        new Cesium.Cesium3DTileset({
+            url: "http://localhost:9000/tileset.json";,
+            maximumScreenSpaceError: 10,
+            //debugShowBoundingVolume: true,
+            //debugShowGeometricError: true,
+            //debugShowUrl: true,
+            //debugWireframe: true
+        });
+
+    viewer.scene.primitives.add(tileset);
+
+    const pos = Cesium.Cartesian3.fromDegrees(
+        9.5209, 47.1410, 0);
+    console.log(pos);
+
+    // Set initial position
+    viewer.camera.setView({
+        destination : Cesium.Cartesian3.fromDegrees(6.6323, 46.5197, 300)
+    });
+</script>
+</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/baremaps-server/src/main/resources/tdtiles/tileset.json 
b/baremaps-server/src/main/resources/tdtiles/tileset.json
new file mode 100644
index 00000000..84850eb8
--- /dev/null
+++ b/baremaps-server/src/main/resources/tdtiles/tileset.json
@@ -0,0 +1,24 @@
+{
+  "asset" : {
+    "version" : "1.1"
+  },
+  "geometricError" : 5000.0,
+  "root" : {
+    "boundingVolume" : {
+      "region" : [ -3.141592653589793238, -1.57079632679, 
3.141592653589793238, 1.57079632679, 0, 1000]
+    },
+    "geometricError" : 100000.0,
+    "refine" : "REPLACE",
+    "content" : {
+      "uri" : "/content/content_{level}__{x}_{y}.glb"
+    },
+    "implicitTiling" : {
+      "subdivisionScheme" : "QUADTREE",
+      "subtreeLevels" : 3,
+      "availableLevels" : 5,
+      "subtrees" : {
+        "uri" : "/subtrees/{level}.{x}.{y}.json"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/examples/tdtiles/README.md b/examples/tdtiles/README.md
new file mode 100644
index 00000000..1d541c6f
--- /dev/null
+++ b/examples/tdtiles/README.md
@@ -0,0 +1,5 @@
+# OpenStreetMap example
+
+This folder contains the required files to create and serve 3d tiles from 
OpenStreetMap data. 
+
+Refer to the [official 
documentation](https://baremaps.apache.org/examples/serve-3d-tiles/) for more 
information.
\ No newline at end of file
diff --git a/examples/tdtiles/indexes.sql b/examples/tdtiles/indexes.sql
new file mode 100644
index 00000000..6990e7b6
--- /dev/null
+++ b/examples/tdtiles/indexes.sql
@@ -0,0 +1,5 @@
+CREATE INDEX CONCURRENTLY IF NOT EXISTS osm_ways_gin ON osm_ways USING gin 
(nodes);
+CREATE INDEX CONCURRENTLY IF NOT EXISTS osm_relations_gin ON osm_relations 
USING gin (member_refs);
+CREATE INDEX CONCURRENTLY IF NOT EXISTS osm_nodes_gix ON osm_nodes USING GIST 
(geom);
+CREATE INDEX CONCURRENTLY IF NOT EXISTS osm_ways_gix ON osm_ways USING GIST 
(geom);
+CREATE INDEX CONCURRENTLY IF NOT EXISTS osm_relations_gix ON osm_relations 
USING GIST (geom);
\ No newline at end of file
diff --git a/examples/tdtiles/workflow.json b/examples/tdtiles/workflow.json
new file mode 100644
index 00000000..fa7388e9
--- /dev/null
+++ b/examples/tdtiles/workflow.json
@@ -0,0 +1,42 @@
+{
+  "steps": [
+    {
+      "id": "download",
+      "needs": [],
+      "tasks": [
+        {
+          "type": "DownloadUrl",
+          "url": 
"https://download.geofabrik.de/europe/liechtenstein-latest.osm.pbf";,
+          "path": "liechtenstein-latest.osm.pbf"
+        }
+      ]
+    },
+    {
+      "id": "import",
+      "needs": [
+        "download"
+      ],
+      "tasks": [
+        {
+          "type": "ImportOpenStreetMap",
+          "file": "liechtenstein-latest.osm.pbf",
+          "database": 
"jdbc:postgresql://localhost:5432/baremaps?&user=baremaps&password=baremaps",
+          "databaseSrid": 4326
+        }
+      ]
+    },
+    {
+      "id": "index",
+      "needs": [
+        "import"
+      ],
+      "tasks": [
+        {
+          "type": "ExecuteSql",
+          "file": "indexes.sql",
+          "database": 
"jdbc:postgresql://localhost:5432/baremaps?&user=baremaps&password=baremaps"
+        }
+      ]
+    }
+  ]
+}
diff --git a/pom.xml b/pom.xml
index ca1e9894..7c5cf2a5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -82,6 +82,7 @@
     <version.lib.jaxb-runtime>2.3.5</version.lib.jaxb-runtime>
     <version.lib.jdbi>3.24.1</version.lib.jdbi>
     <version.lib.jersey>2.35</version.lib.jersey>
+    <version.lib.jgltf>2.0.3</version.lib.jgltf>
     <version.lib.jmh>1.35</version.lib.jmh>
     <version.lib.jsqlparser>4.2</version.lib.jsqlparser>
     <version.lib.jts>1.19.0</version.lib.jts>
@@ -192,6 +193,11 @@
         <artifactId>pgbulkinsert</artifactId>
         <version>${version.lib.pgbulkinsert}</version>
       </dependency>
+      <dependency>
+        <groupId>de.javagl</groupId>
+        <artifactId>jgltf-model-builder</artifactId>
+        <version>${version.lib.jgltf}</version>
+      </dependency>
       <dependency>
         <groupId>info.picocli</groupId>
         <artifactId>picocli</artifactId>


Reply via email to