diff --git a/.run/tdtiles-create.run.xml b/.run/tdtiles-create.run.xml new file mode 100644 index 000000000..e795524c2 --- /dev/null +++ b/.run/tdtiles-create.run.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/.run/tdtiles-dev.run.xml b/.run/tdtiles-dev.run.xml new file mode 100644 index 000000000..e5bc2600c --- /dev/null +++ b/.run/tdtiles-dev.run.xml @@ -0,0 +1,11 @@ + + + + \ 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 000000000..67337bd73 --- /dev/null +++ b/.run/tdtiles-serve.run.xml @@ -0,0 +1,11 @@ + + + + \ 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 e931ff656..0422684c4 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 @@ -28,6 +28,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.tdtiles.TdTiles; import org.apache.baremaps.cli.workflow.Workflow; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.config.Configurator; @@ -38,7 +39,7 @@ @Command(name = "baremaps", description = "A toolkit for producing vector tiles.", versionProvider = VersionProvider.class, subcommands = {Workflow.class, Database.class, - Map.class, Geocoder.class, IpLoc.class}, + Map.class, TdTiles.class, Geocoder.class, IpLoc.class}, sortOptions = false) public class Baremaps implements Callable { 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 000000000..522f2678e --- /dev/null +++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/tdtiles/Serve.java @@ -0,0 +1,96 @@ +/* + * 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.cli.tdtiles; + + + +import static org.apache.baremaps.utils.ObjectMapperUtils.objectMapper; + +import com.linecorp.armeria.common.HttpHeaderNames; +import com.linecorp.armeria.common.HttpMethod; +import com.linecorp.armeria.server.Server; +import com.linecorp.armeria.server.annotation.JacksonResponseConverterFunction; +import com.linecorp.armeria.server.cors.CorsService; +import com.linecorp.armeria.server.file.FileService; +import com.linecorp.armeria.server.file.HttpFile; +import java.util.concurrent.Callable; +import org.apache.baremaps.cli.Options; +import org.apache.baremaps.server.TdTilesResources; +import org.apache.baremaps.utils.PostgresUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +@Command(name = "serve", description = "Start a 3d tile server.") +public class Serve implements Callable { + + 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 objectMapper = objectMapper(); + var datasource = PostgresUtils.createDataSource(database); + + var serverBuilder = Server.builder(); + serverBuilder.http(port); + + var jsonResponseConverter = new JacksonResponseConverterFunction(objectMapper); + serverBuilder.annotatedService(new TdTilesResources(datasource), jsonResponseConverter); + + var index = HttpFile.of(ClassLoader.getSystemClassLoader(), "/tdtiles/index.html"); + serverBuilder.service("/", index.asService()); + serverBuilder.serviceUnder("/", FileService.of(ClassLoader.getSystemClassLoader(), "/tdtiles")); + + serverBuilder.decorator(CorsService.builderForAnyOrigin() + .allowRequestMethods(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE, + HttpMethod.OPTIONS, HttpMethod.HEAD) + .allowRequestHeaders(HttpHeaderNames.ORIGIN, HttpHeaderNames.CONTENT_TYPE, + HttpHeaderNames.ACCEPT, HttpHeaderNames.AUTHORIZATION) + .allowCredentials() + .exposeHeaders(HttpHeaderNames.LOCATION) + .newDecorator()); + + serverBuilder.disableServerHeader(); + serverBuilder.disableDateHeader(); + + var server = serverBuilder.build(); + + var startFuture = server.start(); + startFuture.join(); + + var shutdownFuture = server.closeOnJvmShutdown(); + shutdownFuture.join(); + + 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 000000000..f378cc108 --- /dev/null +++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/tdtiles/TdTiles.java @@ -0,0 +1,33 @@ +/* + * 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.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 2b40f1306..679ccb5b8 100644 --- a/baremaps-core/pom.xml +++ b/baremaps-core/pom.xml @@ -66,6 +66,15 @@ limitations under the License. de.bytefish pgbulkinsert + + de.javagl + jgltf-model + + + de.javagl + jgltf-model-builder + ${version.lib.jgltf} + it.unimi.dsi fastutil 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 000000000..336a072cc --- /dev/null +++ b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/GltfBuilder.java @@ -0,0 +1,354 @@ +/* + * 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.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 java.io.*; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.*; +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; + +public class GltfBuilder { + + /** + * Create a node from a building. + * + * @param building + * @return + */ + public static NodeModel createNode(Building building, float tolerance) { + + // Tessellate the vector data + DelaunayTriangulationBuilder delaunayTriangulationBuilder = new DelaunayTriangulationBuilder(); + delaunayTriangulationBuilder.setSites(building.geometry()); + delaunayTriangulationBuilder.setTolerance(tolerance); + Geometry triangulation = delaunayTriangulationBuilder.getTriangles(new GeometryFactory()); + if (triangulation.getNumGeometries() == 0) { + return new DefaultNodeModel(); + } + + // Compute a translation for the origin of the building. If the building is too far from the + // origin, it will + // suffer from jittering. https://help.agi.com/AGIComponents/html/BlogPrecisionsPrecisions.htm + float[] translation = cartesian3FromDegrees((float) building.geometry().getCoordinates()[0].y, + (float) building.geometry().getCoordinates()[0].x, 0); + + // Generate the 3d vertices, indices and normals + List vertices = new ArrayList<>(); + List indices = new ArrayList<>(); + List normals = new ArrayList<>(); + createRoof(building, translation, triangulation, vertices, indices); + HashSet edges = getExteriorEdges(triangulation); + createWalls(building, translation, vertices, indices, edges); + createNormals(vertices, normals); + + // Create a mesh from the vertices, indices and 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.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 vertices, List 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 vertices, + List indices, HashSet edges) { + for (Edge edge : edges) { + Coordinate[] v = edge.getCoordinates(); + + float[] pos0 = cartesian3FromDegrees((float) v[0].getY(), (float) v[0].getX(), 0); + pos0[0] -= translation[0]; + pos0[1] -= translation[1]; + pos0[2] -= translation[2]; + float[] pos1 = cartesian3FromDegrees((float) v[1].getY(), (float) v[1].getX(), 0); + pos1[0] -= translation[0]; + pos1[1] -= translation[1]; + pos1[2] -= translation[2]; + float[] pos2 = + cartesian3FromDegrees((float) v[0].getY(), (float) v[0].getX(), building.height()); + pos2[0] -= translation[0]; + pos2[1] -= translation[1]; + pos2[2] -= translation[2]; + float[] pos3 = + cartesian3FromDegrees((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 getExteriorEdges(Geometry triangulation) { + HashSet 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 vertices, List 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 = cartesian3FromDegrees((float) corner1.y, (float) corner1.x, building.height()); + pos0[0] -= translation[0]; + pos0[1] -= translation[1]; + pos0[2] -= translation[2]; + float[] pos1 = cartesian3FromDegrees((float) corner2.y, (float) corner2.x, building.height()); + pos1[0] -= translation[0]; + pos1[1] -= translation[1]; + pos1[2] -= translation[2]; + float[] pos2 = cartesian3FromDegrees((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 GLTF binary from a list of nodes. + * + * @param nodes + * @return + * @throws Exception + */ + public static byte[] createGltf(List 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 + * CesiumGS + * + * @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 + * CesiumGS + * + * @param latitude in degrees + * @param longitude in degrees + * @param height in meters above the ellipsoid. + * @return The position + */ + public static float[] cartesian3FromDegrees(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 000000000..7c83a8258 --- /dev/null +++ b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/TdTilesStore.java @@ -0,0 +1,91 @@ +/* + * 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.tdtiles; + + + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.*; +import javax.sql.DataSource; +import org.apache.baremaps.tdtiles.building.Building; +import org.apache.baremaps.tilestore.TileStoreException; +import org.apache.baremaps.utils.GeometryUtils; +import org.locationtech.jts.geom.Geometry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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(geom, 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 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); + System.out.println(sql); + + List 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 000000000..abe69ca9d --- /dev/null +++ b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/building/Building.java @@ -0,0 +1,24 @@ +/* + * 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.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 000000000..4e5f4b4c8 --- /dev/null +++ b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/subtree/Availability.java @@ -0,0 +1,21 @@ +/* + * 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.tdtiles.subtree; + +public record Availability(boolean constant) { +} 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 000000000..18ae9d873 --- /dev/null +++ b/baremaps-core/src/main/java/org/apache/baremaps/tdtiles/subtree/Subtree.java @@ -0,0 +1,24 @@ +/* + * 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.tdtiles.subtree; + +public record Subtree(Availability tileAvailability, + Availability contentAvailability, + Availability childSubtreeAvailability) { + +} 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 000000000..b328f5c66 --- /dev/null +++ b/baremaps-server/src/main/java/org/apache/baremaps/server/TdTilesResources.java @@ -0,0 +1,109 @@ +/* + * 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.server; + +import static com.google.common.net.HttpHeaders.*; +import static io.netty.handler.codec.http.HttpHeaders.Values.APPLICATION_JSON; +import static io.netty.handler.codec.http.HttpHeaders.Values.BINARY; + +import com.linecorp.armeria.common.HttpData; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.ResponseHeaders; +import com.linecorp.armeria.server.annotation.Get; +import com.linecorp.armeria.server.annotation.Param; +import de.javagl.jgltf.model.NodeModel; +import java.util.ArrayList; +import java.util.List; +import javax.sql.DataSource; +import org.apache.baremaps.tdtiles.GltfBuilder; +import org.apache.baremaps.tdtiles.TdTilesStore; +import org.apache.baremaps.tdtiles.building.Building; +import org.apache.baremaps.tdtiles.subtree.Availability; +import org.apache.baremaps.tdtiles.subtree.Subtree; + +public class TdTilesResources { + + private static final ResponseHeaders GLB_HEADERS = ResponseHeaders.builder(200) + .add(CONTENT_TYPE, BINARY) + .add(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .build(); + + private static final ResponseHeaders JSON_HEADERS = ResponseHeaders.builder(200) + .add(CONTENT_TYPE, APPLICATION_JSON) + .add(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .build(); + + private final TdTilesStore tdTilesStore; + + public TdTilesResources(DataSource dataSource) { + this.tdTilesStore = new TdTilesStore(dataSource); + } + + @Get("regex:^/subtrees/(?[0-9]+).(?[0-9]+).(?[0-9]+).json") + public HttpResponse getSubtree(@Param("level") int level, @Param("x") int x, @Param("y") int y) { + if (level == 18) { + return HttpResponse.ofJson(JSON_HEADERS, + new Subtree(new Availability(false), new Availability(true), new Availability(false))); + } + return HttpResponse.ofJson(JSON_HEADERS, + new Subtree(new Availability(true), new Availability(true), new Availability(true))); + } + + @Get("regex:^/content/content_(?[0-9]+)__(?[0-9]+)_(?[0-9]+).glb") + public HttpResponse getContent(@Param("level") int level, @Param("x") int x, @Param("y") int y) + throws Exception { + if (level < 14) { + return HttpResponse.of(GLB_HEADERS, HttpData.wrap(GltfBuilder.createGltf(new ArrayList<>()))); + } + float[] coords = xyzToLatLonRadians(x, y, level); + List nodes = new ArrayList<>(); + int limit = level > 17 ? 1000 : level > 16 ? 200 : level > 15 ? 30 : 10; + List buildings = tdTilesStore.read(coords[0], coords[1], coords[2], coords[3], limit); + for (Building building : buildings) { + float tolerance = level > 17 ? 0.00001f : level > 15 ? 0.00002f : 0.00004f; + nodes.add(GltfBuilder.createNode(building, tolerance)); + } + return HttpResponse.of(GLB_HEADERS, HttpData.wrap(GltfBuilder.createGltf(nodes))); + } + + /** + * Convert XYZ tile coordinates to lat/lon in radians. + * + * @param x + * @param y + * @param z + * @return + */ + 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; + } +} 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 000000000..7162b07e3 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 000000000..66449d058 --- /dev/null +++ b/baremaps-server/src/main/resources/tdtiles/index.html @@ -0,0 +1,38 @@ + + + + + + + + + +
+ + + + \ 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 000000000..84850eb87 --- /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/basemap/import.js b/basemap/import.js index f5490ae73..8d546251b 100644 --- a/basemap/import.js +++ b/basemap/import.js @@ -119,7 +119,7 @@ export default { "type": "ImportOsmPbf", "file": "data/data.osm.pbf", "database": config.database, - "databaseSrid": 3857, + "databaseSrid": 4326, "replaceExisting": true, }, { diff --git a/examples/tdtiles/README.md b/examples/tdtiles/README.md new file mode 100644 index 000000000..1d541c6f8 --- /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 000000000..9cad3fc46 --- /dev/null +++ b/examples/tdtiles/indexes.sql @@ -0,0 +1,6 @@ +CREATE INDEX IF NOT EXISTS osm_ways_gin ON osm_ways USING gin (nodes); +CREATE INDEX IF NOT EXISTS osm_ways_tags_gin ON osm_ways USING gin (tags); +CREATE INDEX IF NOT EXISTS osm_relations_gin ON osm_relations USING gin (member_refs); +CREATE INDEX IF NOT EXISTS osm_nodes_gix ON osm_nodes USING GIST (geom); +CREATE INDEX IF NOT EXISTS osm_ways_gix ON osm_ways USING GIST (geom); +CREATE INDEX 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 000000000..fa7388e9f --- /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 bd1744c98..5c528afb0 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,7 @@ limitations under the License. 5.0.1 1.52 2.16.1 + 2.0.3 1.35 1.19.0 5.10.0 @@ -196,6 +197,16 @@ limitations under the License. pgbulkinsert ${version.lib.pgbulkinsert}
+ + de.javagl + jgltf-model + ${version.lib.jgltf} + + + de.javagl + jgltf-model-builder + ${version.lib.jgltf} + info.picocli picocli