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&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>