diff --git a/citydb-cli/build.gradle b/citydb-cli/build.gradle index 89589aff..1a3e6c6e 100644 --- a/citydb-cli/build.gradle +++ b/citydb-cli/build.gradle @@ -5,16 +5,17 @@ plugins { } dependencies { + api project(':citydb-database') + api project(':citydb-io') + api project(':citydb-operation') api project(':citydb-plugin') + api project(':citydb-query') + api project(':citydb-tiling') api 'info.picocli:picocli:4.7.6' implementation project(':citydb-config') - implementation project(':citydb-io') + implementation project(':citydb-database-postgres') implementation project(':citydb-io-citygml') implementation project(':citydb-logging') - implementation project(':citydb-database') - implementation project(':citydb-database-postgres') - implementation project(':citydb-operation') - implementation project(':citydb-query') } processResources { diff --git a/citydb-cli/src/main/java/module-info.java b/citydb-cli/src/main/java/module-info.java index 32af54cc..1c1c32c9 100644 --- a/citydb-cli/src/main/java/module-info.java +++ b/citydb-cli/src/main/java/module-info.java @@ -1,13 +1,14 @@ module org.citydb.cli { requires org.citydb.config; requires org.citygml4j.core; - requires org.citydb.io; requires org.citydb.io.citygml; requires org.citydb.logging; - requires org.citydb.database; - requires org.citydb.operation; - requires org.citydb.query; + requires transitive org.citydb.database; + requires transitive org.citydb.io; + requires transitive org.citydb.operation; requires transitive org.citydb.plugin; + requires transitive org.citydb.query; + requires transitive org.citydb.tiling; requires transitive info.picocli; exports org.citydb.cli; diff --git a/citydb-cli/src/main/java/org/citydb/cli/Launcher.java b/citydb-cli/src/main/java/org/citydb/cli/Launcher.java index 1c1bba02..b194800c 100644 --- a/citydb-cli/src/main/java/org/citydb/cli/Launcher.java +++ b/citydb-cli/src/main/java/org/citydb/cli/Launcher.java @@ -61,6 +61,7 @@ scope = CommandLine.ScopeType.INHERIT, description = "Command-line interface for the 3D City Database.", synopsisSubcommandLabel = "COMMAND", + abbreviateSynopsis = true, mixinStandardHelpOptions = true, versionProvider = Launcher.class, showAtFileInUsageHelp = true, diff --git a/citydb-cli/src/main/java/org/citydb/cli/deleter/DeleteCommand.java b/citydb-cli/src/main/java/org/citydb/cli/deleter/DeleteCommand.java index b40560f1..97857e01 100644 --- a/citydb-cli/src/main/java/org/citydb/cli/deleter/DeleteCommand.java +++ b/citydb-cli/src/main/java/org/citydb/cli/deleter/DeleteCommand.java @@ -130,7 +130,7 @@ public Integer call() throws ExecutionException { } try { - logger.info("Querying features matching the request..."); + logger.debug("Querying features matching the request..."); logger.trace("Using SQL query:\n{}", () -> helper.getFormattedSql(executor.getSelect(), databaseManager.getAdapter())); diff --git a/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportController.java b/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportController.java index 2e67d099..d9f4d454 100644 --- a/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportController.java +++ b/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportController.java @@ -26,7 +26,9 @@ import org.citydb.cli.ExecutionException; import org.citydb.cli.common.*; import org.citydb.cli.exporter.options.QueryOptions; +import org.citydb.cli.exporter.options.TilingOptions; import org.citydb.cli.exporter.util.SequentialWriter; +import org.citydb.cli.exporter.util.TilingHelper; import org.citydb.cli.util.CommandHelper; import org.citydb.cli.util.FeatureStatistics; import org.citydb.config.Config; @@ -52,6 +54,9 @@ import org.citydb.query.executor.QueryResult; import org.citydb.query.filter.encoding.FilterParseException; import org.citydb.query.util.QueryHelper; +import org.citydb.tiling.Tile; +import org.citydb.tiling.TileIterator; +import org.citydb.tiling.Tiling; import picocli.CommandLine; import java.nio.file.Path; @@ -77,7 +82,11 @@ public abstract class ExportController implements Command { @CommandLine.ArgGroup(exclusive = false, order = Integer.MAX_VALUE, heading = "Query and filter options:%n") - private QueryOptions queryOptions; + protected QueryOptions queryOptions; + + @CommandLine.ArgGroup(exclusive = false, order = Integer.MAX_VALUE, + heading = "Tiling options:%n") + protected TilingOptions tilingOptions; @CommandLine.ArgGroup(exclusive = false, order = Integer.MAX_VALUE, heading = "Database connection options:%n") @@ -119,71 +128,97 @@ protected boolean doExport() throws ExecutionException { WriteOptions writeOptions = getWriteOptions(exportOptions, databaseManager.getAdapter()); writeOptions.getFormatOptions().set(getFormatOptions(writeOptions.getFormatOptions())); - Query query = getQuery(exportOptions); - QueryExecutor executor = helper.getQueryExecutor(query, - SqlBuildOptions.defaults().omitDistinct(true), - tempDirectory, - databaseManager.getAdapter()); - - FeatureStatistics statistics = new FeatureStatistics(databaseManager.getAdapter()); helper.logIndexStatus(Level.INFO, databaseManager.getAdapter()); initialize(exportOptions, writeOptions, databaseManager); - try (OutputFile outputFile = builder.newOutputFile(outputFileOptions.getFile()); - FeatureWriter writer = createWriter(query, ioAdapter)) { - Exporter exporter = Exporter.newInstance(); - exportOptions.setOutputFile(outputFile); - - AtomicLong counter = new AtomicLong(); - - logger.info("Exporting to {} file {}.", ioManager.getFileFormat(ioAdapter), outputFile.getFile()); - writer.initialize(outputFile, writeOptions); - - logger.info("Querying features matching the request..."); - logger.trace("Using SQL query:\n{}", () -> helper.getFormattedSql(executor.getSelect(), - databaseManager.getAdapter())); - - long sequenceId = 1; - try (QueryResult result = executor.executeQuery()) { - exporter.startSession(databaseManager.getAdapter(), exportOptions); - while (shouldRun && result.hasNext()) { - long id = result.getId(); - exporter.exportFeature(id, sequenceId++).whenComplete((feature, t) -> { - if (feature != null) { - try { - writer.write(feature, (success, e) -> { - if (success == Boolean.TRUE) { - statistics.add(feature); - long count = counter.incrementAndGet(); - if (count % 1000 == 0) { - logger.info("{} features exported.", count); - } - } else { + Query query = getQuery(exportOptions); + Tiling tiling = getTiling(exportOptions); + FeatureStatistics statistics = new FeatureStatistics(databaseManager.getAdapter()); + AtomicLong counter = new AtomicLong(); + + try { + TilingHelper tilingHelper = TilingHelper.of(tiling, query, databaseManager.getAdapter()); + if (tilingHelper.isUseTiling()) { + logger.info("Creating {} tile(s) based on provided tiling scheme.", + tilingHelper.getTileMatrix().size()); + } + + TileIterator iterator = tilingHelper.getTileMatrix().getTileIterator(); + while (iterator.hasNext()) { + Tile tile = iterator.next(); + QueryExecutor executor = helper.getQueryExecutor(tilingHelper.getTileQuery(tile), + SqlBuildOptions.defaults() + .omitDistinct(true) + .withColumn(tilingHelper.isUseTiling() ? "envelope" : null), + tempDirectory, + databaseManager.getAdapter()); + + Path file = tilingHelper.getOutputFile(outputFileOptions.getFile(), tile); + FeatureStatistics tileStatistics = new FeatureStatistics(databaseManager.getAdapter()); + + try (OutputFile outputFile = builder.newOutputFile(file); + FeatureWriter writer = createWriter(query, ioAdapter)) { + Exporter exporter = Exporter.newInstance(); + exportOptions.setOutputFile(outputFile); + + logger.info("{}Exporting to {} file {}.", getTileCounter(tilingHelper, tile), + ioManager.getFileFormat(ioAdapter), outputFile.getFile()); + writer.initialize(outputFile, writeOptions); + + logger.debug("Querying features matching the request..."); + logger.trace("Using SQL query:\n{}", () -> helper.getFormattedSql(executor.getSelect(), + databaseManager.getAdapter())); + + long sequenceId = 1; + try (QueryResult result = executor.executeQuery()) { + exporter.startSession(databaseManager.getAdapter(), exportOptions); + while (shouldRun && result.hasNext()) { + long id = result.getId(); + + if (tilingHelper.isUseTiling() && !tile.isOnTile(databaseManager.getAdapter() + .getGeometryAdapter() + .getEnvelope(result.get(rs -> rs.getObject("envelope"))))) { + continue; + } + + exporter.exportFeature(id, sequenceId++).whenComplete((feature, t) -> { + if (feature != null) { + try { + writer.write(feature, (success, e) -> { + if (success == Boolean.TRUE) { + tileStatistics.add(feature); + long count = counter.incrementAndGet(); + if (count % 1000 == 0) { + logger.info("{} features exported.", count); + } + } else { + abort(feature, id, e); + } + }); + } catch (Throwable e) { abort(feature, id, e); } - }); - } catch (Throwable e) { - abort(feature, id, e); - } - } else { - abort(null, id, t); + } else { + abort(null, id, t); + } + }); } - }); + } finally { + exporter.closeSession(); + } + } catch (Throwable e) { + logger.warn("Database export aborted due to an error."); + throw new ExecutionException("A fatal error has occurred during export.", e); + } finally { + statistics.merge(tileStatistics); + if (tilingHelper.isUseTiling()) { + logStatistics(tileStatistics, "Tile export summary:", Level.DEBUG); + } } - } finally { - exporter.closeSession(); } - } catch (Throwable e) { - logger.warn("Database export aborted due to an error."); - throw new ExecutionException("A fatal error has occurred during export.", e); } finally { databaseManager.disconnect(); - if (!statistics.isEmpty()) { - logger.info("Export summary:"); - statistics.logFeatureSummary(Level.INFO); - } else { - logger.info("No features exported."); - } + logStatistics(statistics, "Export summary:", Level.INFO); } return shouldRun; @@ -206,6 +241,12 @@ protected Query getQuery(ExportOptions exportOptions) throws ExecutionException } } + protected Tiling getTiling(ExportOptions exportOptions) throws ExecutionException { + return tilingOptions != null ? + tilingOptions.getTiling() : + exportOptions.getTiling().orElseGet(TilingHelper::noTiling); + } + protected ExportOptions getExportOptions() throws ExecutionException { ExportOptions exportOptions; try { @@ -270,6 +311,22 @@ protected WriteOptions getWriteOptions(ExportOptions exportOptions, DatabaseAdap return writeOptions; } + private String getTileCounter(TilingHelper helper, Tile tile) { + return helper.isUseTiling() ? + "[" + (tile.getRow() * helper.getTileMatrix().getColumns() + tile.getColumn() + 1) + "|" + + helper.getTileMatrix().size() + "] " : + ""; + } + + private void logStatistics(FeatureStatistics statistics, String title, Level level) { + if (!statistics.isEmpty()) { + logger.log(level, title); + statistics.logFeatureSummary(level); + } else { + logger.log(level, "No features exported."); + } + } + private void abort(Feature feature, long id, Throwable e) { synchronized (lock) { if (shouldRun) { diff --git a/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportOptions.java b/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportOptions.java index 683a8ff9..eab8ff3c 100644 --- a/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportOptions.java +++ b/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportOptions.java @@ -23,12 +23,14 @@ import org.citydb.config.SerializableConfig; import org.citydb.query.Query; +import org.citydb.tiling.Tiling; import java.util.Optional; @SerializableConfig(name = "exportOptions") public class ExportOptions extends org.citydb.operation.exporter.ExportOptions { private Query query; + private Tiling tiling; public Optional getQuery() { return Optional.ofNullable(query); @@ -38,4 +40,13 @@ public ExportOptions setQuery(Query query) { this.query = query; return this; } + + public Optional getTiling() { + return Optional.ofNullable(tiling); + } + + public ExportOptions setTiling(Tiling tiling) { + this.tiling = tiling; + return this; + } } diff --git a/citydb-cli/src/main/java/org/citydb/cli/exporter/options/TilingOptions.java b/citydb-cli/src/main/java/org/citydb/cli/exporter/options/TilingOptions.java new file mode 100644 index 00000000..58ebe8fe --- /dev/null +++ b/citydb-cli/src/main/java/org/citydb/cli/exporter/options/TilingOptions.java @@ -0,0 +1,204 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.cli.exporter.options; + +import org.citydb.cli.common.Option; +import org.citydb.database.srs.SrsUnit; +import org.citydb.model.geometry.Coordinate; +import org.citydb.model.geometry.Envelope; +import org.citydb.tiling.Tiling; +import org.citydb.tiling.options.Dimension; +import org.citydb.tiling.options.DimensionScheme; +import org.citydb.tiling.options.MatrixScheme; +import org.citydb.tiling.options.TileMatrixOrigin; +import picocli.CommandLine; + +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TilingOptions implements Option { + public enum Origin {top_left, bottom_left} + + @CommandLine.Option(names = "--tile-matrix", paramLabel = "", + description = "Export tiles in a columns x rows grid.") + private String matrixScheme; + + @CommandLine.Option(names = "--tile-dimension", paramLabel = "", + description = "Export tiles with specified width and height, aligned with the database CRS grid " + + "(default length unit of the CRS assumed).") + private String dimensionScheme; + + @CommandLine.Option(names = "--tile-extent", paramLabel = "", + description = "Extent to use for tiling (default: auto-computed).") + private String extent; + + @CommandLine.Option(names = "--tile-origin", defaultValue = "top_left", + description = "Tile indexes origin: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).") + private Origin origin; + + private Tiling tiling; + + public Tiling getTiling() { + return tiling; + } + + @Override + public void preprocess(CommandLine commandLine) throws Exception { + if (matrixScheme == null && dimensionScheme == null) { + throw new CommandLine.ParameterException(commandLine, + "Error: Either --tile-matrix or --tile-dimension is required as tiling scheme"); + } else if (matrixScheme != null && dimensionScheme != null) { + throw new CommandLine.ParameterException(commandLine, + "Error: --tile-matrix and --tile-dimension are mutually exclusive (specify only one)"); + } + + tiling = new Tiling(); + if (matrixScheme != null) { + tiling.setScheme(getMatrixScheme(commandLine)); + } else if (dimensionScheme != null) { + tiling.setScheme(getDimensionScheme(commandLine)); + } + + if (extent != null) { + tiling.setExtent(getExtent(commandLine)); + } + + if (origin != null) { + tiling.setTileMatrixOrigin(switch (origin) { + case top_left -> TileMatrixOrigin.TOP_LEFT; + case bottom_left -> TileMatrixOrigin.BOTTOM_LEFT; + }); + } + } + + private MatrixScheme getMatrixScheme(CommandLine commandLine) { + String[] parts = matrixScheme.split(","); + if (parts.length == 2) { + return MatrixScheme.of(parseMatrixValue(parts[0], commandLine), + parseMatrixValue(parts[1], commandLine)); + } else { + throw new CommandLine.ParameterException(commandLine, + "Error: The matrix tiling scheme must be in COLUMNS,ROWS format but was '" + matrixScheme + "'"); + } + } + + private DimensionScheme getDimensionScheme(CommandLine commandLine) { + String[] parts = dimensionScheme.split(","); + if (parts.length == 2) { + Pattern pattern = Pattern.compile("(-?\\d*(?:\\.\\d+)?)([a-zA-Z]+)?"); + return DimensionScheme.of(parseDimension(parts[0], pattern, commandLine), + parseDimension(parts[1], pattern, commandLine)); + } else { + throw new CommandLine.ParameterException(commandLine, + "Error: The dimension tiling scheme must be in WIDTH[UNIT],HEIGHT[UNIT] format " + + "but was '" + dimensionScheme + "'"); + } + } + + private Envelope getExtent(CommandLine commandLine) { + String[] parts = extent.split(","); + if (parts.length == 4 || parts.length == 5) { + Envelope envelope; + try { + envelope = Envelope.of(Coordinate.of(Double.parseDouble(parts[0]), Double.parseDouble(parts[1])), + Coordinate.of(Double.parseDouble(parts[2]), Double.parseDouble(parts[3]))); + } catch (NumberFormatException e) { + throw new CommandLine.ParameterException(commandLine, + "Error: The coordinates of the tiling extent must be floating point numbers but were '" + + String.join(",", parts[0], parts[1], parts[2], parts[3]) + "'"); + } + + if (parts.length == 5) { + try { + envelope.setSRID(Integer.parseInt(parts[4])); + } catch (NumberFormatException e) { + throw new CommandLine.ParameterException(commandLine, + "Error: The SRID of the tiling extent must be an integer but was '" + parts[4] + "'"); + } + } + + return envelope; + } else { + throw new CommandLine.ParameterException(commandLine, + "Error: The tiling extent must be in X_MIN,Y_MIN,X_MAX,Y_MAX[,SRID] format " + + "but was '" + extent + "'"); + } + } + + private Dimension parseDimension(String input, Pattern pattern, CommandLine commandLine) { + Matcher matcher = pattern.matcher(input); + if (matcher.find()) { + return Dimension.of(parseLength(matcher.group(1), commandLine), + parseUnit(matcher.group(2), commandLine)); + } else { + throw new CommandLine.ParameterException(commandLine, + "Error: A dimension used in the dimension tiling scheme must be in LENGTH[UNIT] format " + + "but was '" + input + "'"); + } + } + + private int parseMatrixValue(String input, CommandLine commandLine) { + try { + int value = Integer.parseInt(input); + if (value > 0) { + return value; + } + } catch (NumberFormatException e) { + // + } + + throw new CommandLine.ParameterException(commandLine, + "Error: The columns and rows values of the matrix tiling scheme must be positive integers " + + "but were '" + matrixScheme + "'"); + } + + private double parseLength(String input, CommandLine commandLine) { + try { + double value = Double.parseDouble(input); + if (value > 0) { + return value; + } + } catch (NumberFormatException e) { + // + } + + throw new CommandLine.ParameterException(commandLine, + "Error: A length used in the dimension tiling scheme must be a positive number " + + "but was '" + input + "'"); + } + + private SrsUnit parseUnit(String input, CommandLine commandLine) { + if (input != null) { + SrsUnit unit = SrsUnit.of(input); + if (unit != null) { + return unit; + } + } else { + return null; + } + + throw new CommandLine.ParameterException(commandLine, + "Error: Unsupported length unit '" + input + "'. Use one of '" + + String.join("', '", Arrays.stream(SrsUnit.values()).map(SrsUnit::toString).toList()) + "'."); + } +} diff --git a/citydb-cli/src/main/java/org/citydb/cli/exporter/util/TilingHelper.java b/citydb-cli/src/main/java/org/citydb/cli/exporter/util/TilingHelper.java new file mode 100644 index 00000000..d5218b86 --- /dev/null +++ b/citydb-cli/src/main/java/org/citydb/cli/exporter/util/TilingHelper.java @@ -0,0 +1,176 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.cli.exporter.util; + +import org.apache.logging.log4j.Logger; +import org.citydb.cli.ExecutionException; +import org.citydb.database.adapter.DatabaseAdapter; +import org.citydb.logging.LoggerManager; +import org.citydb.model.common.Namespaces; +import org.citydb.model.geometry.Coordinate; +import org.citydb.model.geometry.Envelope; +import org.citydb.query.Query; +import org.citydb.query.builder.sql.SqlBuildOptions; +import org.citydb.query.executor.QueryExecutor; +import org.citydb.query.filter.Filter; +import org.citydb.query.filter.common.Predicate; +import org.citydb.query.filter.literal.BBoxLiteral; +import org.citydb.query.filter.literal.PropertyRef; +import org.citydb.tiling.Tile; +import org.citydb.tiling.TileMatrix; +import org.citydb.tiling.Tiling; +import org.citydb.tiling.TilingException; +import org.citydb.tiling.options.MatrixScheme; + +import java.nio.file.Path; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TilingHelper { + private static final Tiling NO_TILING = Tiling.newInstance() + .setExtent(Envelope.of( + Coordinate.of(-Double.MAX_VALUE, -Double.MAX_VALUE), + Coordinate.of(Double.MAX_VALUE, Double.MAX_VALUE))) + .setScheme(MatrixScheme.of(1, 1)); + + private final Logger logger = LoggerManager.getInstance().getLogger(TilingHelper.class); + private final Tiling tiling; + private final Query query; + private final DatabaseAdapter adapter; + private final boolean useTiling; + private TileMatrix tileMatrix; + private Envelope queryExtent; + + private final Pattern tokenPattern = Pattern.compile("@(?:column|row|x_min|y_min|x_max|y_max)(?:,.+?)?@", + Pattern.CASE_INSENSITIVE); + private final Matcher matcher = Pattern.compile("").matcher(""); + + private TilingHelper(Tiling tiling, Query query, DatabaseAdapter adapter) { + this.tiling = tiling; + this.query = query; + this.adapter = adapter; + useTiling = tiling != NO_TILING; + } + + public static TilingHelper of(Tiling tiling, Query query, DatabaseAdapter adapter) throws ExecutionException { + return new TilingHelper(tiling, query, adapter).buildTileMatrix(); + } + + public static Tiling noTiling() { + return NO_TILING; + } + + public boolean isUseTiling() { + return useTiling; + } + + public TileMatrix getTileMatrix() { + return tileMatrix; + } + + public Optional getQueryExtent() { + return Optional.ofNullable(queryExtent); + } + + private TilingHelper buildTileMatrix() throws ExecutionException { + if (tiling.getExtent().isEmpty()) { + logger.info("Computing the extent of all features matching the request..."); + try { + queryExtent = QueryExecutor.builder(adapter) + .build(query, SqlBuildOptions.defaults().omitDistinct(true)) + .computeExtent(); + tiling.setExtent(queryExtent); + } catch (Exception e) { + throw new ExecutionException("Failed to compute the extent.", e); + } + } + + try { + tileMatrix = tiling.buildTileMatrix(adapter); + return this; + } catch (TilingException e) { + throw new ExecutionException("Failed to build the tile matrix.", e); + } + } + + public Query getTileQuery(Tile tile) { + if (useTiling) { + Predicate bboxFilter = PropertyRef.of("envelope", Namespaces.CORE) + .intersects(BBoxLiteral.of(tile.getExtent())); + return Query.of(query).setFilter(query.getFilter() + .map(filter -> Filter.of(bboxFilter.and(filter.getExpression()))) + .orElse(Filter.of(bboxFilter))); + } else { + return query; + } + } + + public Path getOutputFile(Path outputFile, Tile tile) { + if (useTiling) { + String file = outputFile.toString(); + List> replacements = new ArrayList<>(); + + matcher.reset(file).usePattern(tokenPattern); + while (matcher.find()) { + replacements.add(replaceToken(matcher.group(0), tile)); + } + + if (!replacements.isEmpty()) { + for (Map.Entry entry : replacements) { + file = file.replaceFirst(entry.getKey(), entry.getValue()); + } + } else if (tileMatrix == null || tileMatrix.size() > 1) { + file = getDefaultOutputFile(file, tile); + } + + return Path.of(file); + } else { + return outputFile; + } + } + + private AbstractMap.SimpleEntry replaceToken(String token, Tile tile) { + String[] parts = token.substring(1, token.length() - 1).split(","); + String format = parts.length == 2 ? parts[1].trim() : "%s"; + String replacement = String.format(Locale.ENGLISH, format, + switch (parts[0].toLowerCase(Locale.ROOT)) { + case "column" -> tile.getColumn(); + case "row" -> tile.getRow(); + case "x_min" -> tile.getExtent().getLowerCorner().getX(); + case "y_min" -> tile.getExtent().getLowerCorner().getY(); + case "x_max" -> tile.getExtent().getUpperCorner().getX(); + case "y_max" -> tile.getExtent().getUpperCorner().getY(); + default -> parts[0]; + }); + + return new AbstractMap.SimpleEntry<>(token, replacement); + } + + private String getDefaultOutputFile(String file, Tile tile) { + String suffix = "_" + tile.getColumn() + "_" + tile.getRow(); + int index = file.lastIndexOf('.'); + return index > 0 ? + file.substring(0, index) + suffix + "." + file.substring(index + 1) : + file + suffix; + } +} diff --git a/citydb-tiling/build.gradle b/citydb-tiling/build.gradle new file mode 100644 index 00000000..4737a113 --- /dev/null +++ b/citydb-tiling/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':citydb-database') + api project(':citydb-model') + implementation project(':citydb-config') +} \ No newline at end of file diff --git a/citydb-tiling/src/main/java/module-info.java b/citydb-tiling/src/main/java/module-info.java new file mode 100644 index 00000000..75659519 --- /dev/null +++ b/citydb-tiling/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module org.citydb.tiling { + requires org.citydb.config; + requires transitive org.citydb.database; + requires transitive org.citydb.model; + + exports org.citydb.tiling; + exports org.citydb.tiling.encoding; + exports org.citydb.tiling.options; +} \ No newline at end of file diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/Tile.java b/citydb-tiling/src/main/java/org/citydb/tiling/Tile.java new file mode 100644 index 00000000..a782817d --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/Tile.java @@ -0,0 +1,91 @@ +/* + * 3D City Database - The Open Source CityGML Database + * https://www.3dcitydb.org/ + * + * Copyright 2013 - 2024 + * Chair of Geoinformatics + * Technical University of Munich, Germany + * https://www.lrg.tum.de/gis/ + * + * The 3D City Database is jointly developed with the following + * cooperation partners: + * + * Virtual City Systems, Berlin + * M.O.S.S. Computer Grafik Systeme GmbH, Taufkirchen + * + * 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.citydb.tiling; + +import org.citydb.database.adapter.DatabaseAdapter; +import org.citydb.database.geometry.GeometryException; +import org.citydb.database.srs.SpatialReference; +import org.citydb.database.srs.SrsException; +import org.citydb.model.geometry.Envelope; +import org.citydb.model.geometry.Point; + +import java.sql.SQLException; + +public class Tile { + private final Envelope extent; + private final int column; + private final int row; + private final DatabaseAdapter adapter; + + Tile(Envelope extent, int column, int row, DatabaseAdapter adapter) { + this.extent = extent; + this.column = column; + this.row = row; + this.adapter = adapter; + } + + public Envelope getExtent() { + return extent; + } + + public int getColumn() { + return column; + } + + public int getRow() { + return row; + } + + public boolean isOnTile(Envelope extent) throws TilingException { + return extent != null && isOnTile(Point.of(extent.getCenter()) + .setSRID(extent.getSRID().orElse(null)) + .setSrsIdentifier(extent.getSrsIdentifier().orElse(null))); + } + + public boolean isOnTile(Point point) throws TilingException { + if (point != null) { + try { + SpatialReference reference = adapter.getGeometryAdapter().getSpatialReference(extent) + .orElse(adapter.getDatabaseMetadata().getSpatialReference()); + if (reference.getSRID() != adapter.getDatabaseMetadata().getSpatialReference().getSRID()) { + point = adapter.getGeometryAdapter().transform(point); + } + } catch (GeometryException | SrsException | SQLException e) { + throw new TilingException("Failed to transform the point geometry to the database SRS.", e); + } + + return point.getCoordinate().getX() > extent.getLowerCorner().getX() + && point.getCoordinate().getX() <= extent.getUpperCorner().getX() + && point.getCoordinate().getY() > extent.getLowerCorner().getY() + && point.getCoordinate().getY() <= extent.getUpperCorner().getY(); + } else { + return false; + } + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/TileIterator.java b/citydb-tiling/src/main/java/org/citydb/tiling/TileIterator.java new file mode 100644 index 00000000..7f64b172 --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/TileIterator.java @@ -0,0 +1,74 @@ +/* + * 3D City Database - The Open Source CityGML Database + * https://www.3dcitydb.org/ + * + * Copyright 2013 - 2024 + * Chair of Geoinformatics + * Technical University of Munich, Germany + * https://www.lrg.tum.de/gis/ + * + * The 3D City Database is jointly developed with the following + * cooperation partners: + * + * Virtual City Systems, Berlin + * M.O.S.S. Computer Grafik Systeme GmbH, Taufkirchen + * + * 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.citydb.tiling; + +import org.citydb.tiling.options.TileMatrixOrigin; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +public class TileIterator implements Iterator { + private final TileMatrix matrix; + private final TileMatrixOrigin origin; + private int column = -1; + private int row; + private boolean hasNext; + + TileIterator(TileMatrix matrix, TileMatrixOrigin origin) { + this.matrix = matrix; + this.origin = origin; + } + + @Override + public boolean hasNext() { + if (!hasNext) { + if (++column == matrix.getColumns()) { + column = 0; + row++; + } + + hasNext = row < matrix.getRows(); + } + + return hasNext; + } + + @Override + public Tile next() { + try { + if (hasNext()) { + return matrix.getTileAt(column, row, origin); + } else { + throw new NoSuchElementException(); + } + } finally { + hasNext = false; + } + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/TileMatrix.java b/citydb-tiling/src/main/java/org/citydb/tiling/TileMatrix.java new file mode 100644 index 00000000..a2672110 --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/TileMatrix.java @@ -0,0 +1,155 @@ +/* + * 3D City Database - The Open Source CityGML Database + * https://www.3dcitydb.org/ + * + * Copyright 2013 - 2024 + * Chair of Geoinformatics + * Technical University of Munich, Germany + * https://www.lrg.tum.de/gis/ + * + * The 3D City Database is jointly developed with the following + * cooperation partners: + * + * Virtual City Systems, Berlin + * M.O.S.S. Computer Grafik Systeme GmbH, Taufkirchen + * + * 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.citydb.tiling; + +import org.citydb.core.concurrent.LazyInitializer; +import org.citydb.database.adapter.DatabaseAdapter; +import org.citydb.model.geometry.Coordinate; +import org.citydb.model.geometry.Envelope; +import org.citydb.tiling.options.TileMatrixOrigin; +import org.geotools.referencing.CRS; + +public class TileMatrix { + private final Envelope extent; + private final DatabaseAdapter adapter; + private final int columns; + private final int rows; + private final double tileWidth; + private final double tileHeight; + private final double[] columnOffsets; + private final double[] rowOffsets; + private final LazyInitializer swapAxes; + private TileMatrixOrigin tileMatrixOrigin = TileMatrixOrigin.TOP_LEFT; + + TileMatrix(Coordinate lowerCorner, Coordinate upperCorner, int columns, int rows, double tileWidth, + double tileHeight, DatabaseAdapter adapter) { + this.columns = columns > 0 ? columns : 1; + this.rows = rows > 0 ? rows : 1; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + this.adapter = adapter; + + columnOffsets = computeOffsets(columns, tileWidth); + rowOffsets = computeOffsets(rows, tileHeight); + extent = Envelope.of(lowerCorner, upperCorner) + .setSRID(adapter.getDatabaseMetadata().getSpatialReference().getSRID()); + swapAxes = LazyInitializer.of(() -> adapter.getDatabaseMetadata().getSpatialReference().getDefinition() + .map(crs -> CRS.AxisOrder.NORTH_EAST.equals(CRS.getAxisOrder(crs))) + .orElse(false)); + } + + public Envelope getExtent() { + return extent; + } + + public int size() { + return columns * rows; + } + + public int getColumns() { + return columns; + } + + public int getRows() { + return rows; + } + + public double getTileWidth() { + return tileWidth; + } + + public double getTileHeight() { + return tileHeight; + } + + public TileMatrixOrigin getOrigin() { + return tileMatrixOrigin; + } + + TileMatrix setTileMatrixOrigin(TileMatrixOrigin tileMatrixOrigin) { + if (tileMatrixOrigin != null) { + this.tileMatrixOrigin = tileMatrixOrigin; + } + + return this; + } + + public TileIterator getTileIterator() { + return new TileIterator(this, getOrigin()); + } + + public TileIterator getTileIterator(TileMatrixOrigin origin) { + return new TileIterator(this, origin); + } + + public Tile getTileAt(int column, int row) { + return getTileAt(column, row, getOrigin()); + } + + public Tile getTileAt(int column, int row, TileMatrixOrigin origin) { + if (column < 0 || column >= columns || row < 0 || row >= rows) { + throw new IndexOutOfBoundsException("Tile index (" + column + "," + row + ") is out of bounds."); + } + + double minX, minY, maxX, maxY; + if (swapAxes.get()) { + minX = extent.getUpperCorner().getY(); + minY = extent.getUpperCorner().getX(); + maxX = extent.getLowerCorner().getY(); + maxY = extent.getLowerCorner().getX(); + } else { + minX = extent.getLowerCorner().getX(); + minY = extent.getLowerCorner().getY(); + maxX = extent.getUpperCorner().getX(); + maxY = extent.getUpperCorner().getY(); + } + + int rowIndex = origin == TileMatrixOrigin.BOTTOM_LEFT ? rows - row - 1 : row; + Coordinate lowerCorner = Coordinate.of( + minX + columnOffsets[column], + rowIndex == rows - 1 ? minY : maxY - rowOffsets[rowIndex + 1]); + Coordinate upperCorner = Coordinate.of( + column == columns - 1 ? maxX : minX + columnOffsets[column + 1], + maxY - rowOffsets[rowIndex]); + + return new Tile(Envelope.of(lowerCorner, upperCorner) + .setSRID(adapter.getDatabaseMetadata().getSpatialReference().getSRID()), + column, row, adapter); + } + + private double[] computeOffsets(int size, double offset) { + double[] offsets = new double[size]; + offsets[0] = 0; + for (int i = 1; i < offsets.length; i++) { + offsets[i] = offsets[i - 1] + offset; + } + + return offsets; + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/Tiling.java b/citydb-tiling/src/main/java/org/citydb/tiling/Tiling.java new file mode 100644 index 00000000..a9e7b642 --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/Tiling.java @@ -0,0 +1,106 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.tiling; + +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.annotation.JSONField; +import org.citydb.config.SerializableConfig; +import org.citydb.database.adapter.DatabaseAdapter; +import org.citydb.database.geometry.GeometryException; +import org.citydb.database.srs.SpatialReference; +import org.citydb.database.srs.SrsException; +import org.citydb.model.geometry.Envelope; +import org.citydb.tiling.encoding.ExtentReader; +import org.citydb.tiling.encoding.ExtentWriter; +import org.citydb.tiling.options.TileMatrixOrigin; + +import java.sql.SQLException; +import java.util.Objects; +import java.util.Optional; + +@SerializableConfig(name = "tiling") +public class Tiling { + @JSONField(serializeUsing = ExtentWriter.class, deserializeUsing = ExtentReader.class) + private Envelope extent; + private TilingScheme scheme; + @JSONField(serializeFeatures = JSONWriter.Feature.WriteEnumUsingToString) + private TileMatrixOrigin tileMatrixOrigin = TileMatrixOrigin.TOP_LEFT; + + public static Tiling newInstance() { + return new Tiling(); + } + + public static Tiling of(Envelope extent, TilingScheme scheme) { + return new Tiling().setExtent(extent).setScheme(scheme); + } + + public Optional getExtent() { + return Optional.ofNullable(extent); + } + + public Tiling setExtent(Envelope extent) { + this.extent = extent; + return this; + } + + public Optional getScheme() { + return Optional.ofNullable(scheme); + } + + public Tiling setScheme(TilingScheme scheme) { + this.scheme = scheme; + return this; + } + + public TileMatrixOrigin getTileMatrixOrigin() { + return tileMatrixOrigin != null ? tileMatrixOrigin : TileMatrixOrigin.TOP_LEFT; + } + + public Tiling setTileMatrixOrigin(TileMatrixOrigin tileMatrixOrigin) { + this.tileMatrixOrigin = tileMatrixOrigin; + return this; + } + + public TileMatrix buildTileMatrix(DatabaseAdapter adapter) throws TilingException { + Objects.requireNonNull(adapter, "The database adapter must not be null."); + + if (extent == null) { + throw new TilingException("No tiling extent specified."); + } else if (scheme == null) { + throw new TilingException("No tiling scheme specified."); + } + + Envelope extent; + try { + SpatialReference reference = adapter.getGeometryAdapter().getSpatialReference(this.extent) + .orElse(adapter.getDatabaseMetadata().getSpatialReference()); + extent = reference.getSRID() != adapter.getDatabaseMetadata().getSpatialReference().getSRID() ? + adapter.getGeometryAdapter().transform(this.extent) : + this.extent; + } catch (GeometryException | SrsException | SQLException e) { + throw new TilingException("Failed to transform the tiling extent to the database SRS.", e); + } + + return scheme.buildTileMatrix(extent, adapter) + .setTileMatrixOrigin(getTileMatrixOrigin()); + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/TilingException.java b/citydb-tiling/src/main/java/org/citydb/tiling/TilingException.java new file mode 100644 index 00000000..1ee035df --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/TilingException.java @@ -0,0 +1,41 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.tiling; + +public class TilingException extends Exception { + + public TilingException() { + super(); + } + + public TilingException(String message) { + super(message); + } + + public TilingException(Throwable cause) { + super(cause); + } + + public TilingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/TilingScheme.java b/citydb-tiling/src/main/java/org/citydb/tiling/TilingScheme.java new file mode 100644 index 00000000..c54dc7e6 --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/TilingScheme.java @@ -0,0 +1,43 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.tiling; + +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.annotation.JSONType; +import org.citydb.database.adapter.DatabaseAdapter; +import org.citydb.model.geometry.Coordinate; +import org.citydb.model.geometry.Envelope; +import org.citydb.tiling.options.DimensionScheme; +import org.citydb.tiling.options.MatrixScheme; + +@JSONType(serializeFeatures = JSONWriter.Feature.WriteClassName, + typeKey = "type", + seeAlso = {DimensionScheme.class, MatrixScheme.class}, + seeAlsoDefault = MatrixScheme.class) +public abstract class TilingScheme { + protected abstract TileMatrix buildTileMatrix(Envelope extent, DatabaseAdapter adapter) throws TilingException; + + protected TileMatrix buildTileMatrix(Coordinate lowerCorner, Coordinate upperCorner, int columns, int rows, + double tileWidth, double tileHeight, DatabaseAdapter adapter) { + return new TileMatrix(lowerCorner, upperCorner, columns, rows, tileWidth, tileHeight, adapter); + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/encoding/ExtentReader.java b/citydb-tiling/src/main/java/org/citydb/tiling/encoding/ExtentReader.java new file mode 100644 index 00000000..56168ce9 --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/encoding/ExtentReader.java @@ -0,0 +1,65 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.tiling.encoding; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.reader.ObjectReader; +import org.citydb.config.common.SrsReference; +import org.citydb.model.geometry.Coordinate; +import org.citydb.model.geometry.Envelope; + +import java.lang.reflect.Type; +import java.util.List; + +public class ExtentReader implements ObjectReader { + @Override + public Envelope readObject(JSONReader jsonReader, Type type, Object o, long l) { + if (jsonReader.isObject()) { + JSONObject extent = jsonReader.readJSONObject(); + Object bounds = extent.get("coordinates"); + if (bounds instanceof JSONArray value) { + List coordinates = value.stream() + .filter(Number.class::isInstance) + .map(Number.class::cast) + .map(Number::doubleValue) + .toList(); + if (coordinates.size() > 3) { + Envelope envelope = Envelope.of( + Coordinate.of(coordinates.get(0), coordinates.get(1)), + Coordinate.of(coordinates.get(2), coordinates.get(3))); + + SrsReference srs = extent.getObject("srs", SrsReference.class); + if (srs != null) { + envelope.setSRID(srs.getSRID().orElse(null)) + .setSrsIdentifier(srs.getIdentifier().orElse(null)); + } + + return envelope; + } + } + } + + return null; + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/encoding/ExtentWriter.java b/citydb-tiling/src/main/java/org/citydb/tiling/encoding/ExtentWriter.java new file mode 100644 index 00000000..c10d1786 --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/encoding/ExtentWriter.java @@ -0,0 +1,58 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.tiling.encoding; + +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.writer.ObjectWriter; +import org.citydb.config.common.SrsReference; +import org.citydb.model.geometry.Envelope; + +import java.lang.reflect.Type; +import java.util.List; + +public class ExtentWriter implements ObjectWriter { + @Override + public void write(JSONWriter jsonWriter, Object o, Object o1, Type type, long l) { + if (o instanceof Envelope extent) { + jsonWriter.startObject(); + jsonWriter.writeName("coordinates"); + jsonWriter.writeColon(); + jsonWriter.write(List.of(extent.getLowerCorner().getX(), + extent.getLowerCorner().getY(), + extent.getUpperCorner().getX(), + extent.getUpperCorner().getY())); + + if (extent.getSRID().isPresent() || extent.getSrsIdentifier().isPresent()) { + jsonWriter.writeName("srs"); + jsonWriter.writeColon(); + jsonWriter.writeAs(new SrsReference() + .setSRID(extent.getSRID().orElse(null)) + .setIdentifier(extent.getSrsIdentifier().orElse(null)), + SrsReference.class); + } + + jsonWriter.endObject(); + } else { + jsonWriter.writeNull(); + } + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/encoding/PointReader.java b/citydb-tiling/src/main/java/org/citydb/tiling/encoding/PointReader.java new file mode 100644 index 00000000..126436fe --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/encoding/PointReader.java @@ -0,0 +1,62 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.tiling.encoding; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.reader.ObjectReader; +import org.citydb.config.common.SrsReference; +import org.citydb.model.geometry.Coordinate; +import org.citydb.model.geometry.Point; + +import java.lang.reflect.Type; +import java.util.List; + +public class PointReader implements ObjectReader { + @Override + public Point readObject(JSONReader jsonReader, Type type, Object o, long l) { + if (jsonReader.isObject()) { + JSONObject extent = jsonReader.readJSONObject(); + Object bounds = extent.get("coordinates"); + if (bounds instanceof JSONArray value) { + List coordinates = value.stream() + .filter(Number.class::isInstance) + .map(Number.class::cast) + .map(Number::doubleValue) + .toList(); + if (coordinates.size() > 1) { + Point point = Point.of(Coordinate.of(coordinates.get(0), coordinates.get(1))); + SrsReference srs = extent.getObject("srs", SrsReference.class); + if (srs != null) { + point.setSRID(srs.getSRID().orElse(null)) + .setSrsIdentifier(srs.getIdentifier().orElse(null)); + } + + return point; + } + } + } + + return null; + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/encoding/PointWriter.java b/citydb-tiling/src/main/java/org/citydb/tiling/encoding/PointWriter.java new file mode 100644 index 00000000..fdfadfbc --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/encoding/PointWriter.java @@ -0,0 +1,55 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.tiling.encoding; + +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.writer.ObjectWriter; +import org.citydb.config.common.SrsReference; +import org.citydb.model.geometry.Point; + +import java.lang.reflect.Type; +import java.util.List; + +public class PointWriter implements ObjectWriter { + @Override + public void write(JSONWriter jsonWriter, Object o, Object o1, Type type, long l) { + if (o instanceof Point point) { + jsonWriter.startObject(); + jsonWriter.writeName("coordinates"); + jsonWriter.writeColon(); + jsonWriter.write(List.of(point.getCoordinate().getX(), point.getCoordinate().getY())); + + if (point.getSRID().isPresent() || point.getSrsIdentifier().isPresent()) { + jsonWriter.writeName("srs"); + jsonWriter.writeColon(); + jsonWriter.writeAs(new SrsReference() + .setSRID(point.getSRID().orElse(null)) + .setIdentifier(point.getSrsIdentifier().orElse(null)), + SrsReference.class); + } + + jsonWriter.endObject(); + } else { + jsonWriter.writeNull(); + } + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/encoding/SrsUnitReader.java b/citydb-tiling/src/main/java/org/citydb/tiling/encoding/SrsUnitReader.java new file mode 100644 index 00000000..c623eec0 --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/encoding/SrsUnitReader.java @@ -0,0 +1,37 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.tiling.encoding; + +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.reader.ObjectReader; +import org.citydb.database.srs.SrsUnit; + +import java.lang.reflect.Type; + +public class SrsUnitReader implements ObjectReader { + @Override + public SrsUnit readObject(JSONReader jsonReader, Type type, Object o, long l) { + return jsonReader.isString() ? + SrsUnit.of(jsonReader.readString()) : + null; + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/options/Dimension.java b/citydb-tiling/src/main/java/org/citydb/tiling/options/Dimension.java new file mode 100644 index 00000000..18da5712 --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/options/Dimension.java @@ -0,0 +1,61 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.tiling.options; + +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.annotation.JSONField; +import org.citydb.database.srs.SrsUnit; +import org.citydb.tiling.encoding.SrsUnitReader; + +import java.util.Optional; + +public class Dimension { + private double value; + @JSONField(serializeFeatures = JSONWriter.Feature.WriteEnumUsingToString, deserializeUsing = SrsUnitReader.class) + private SrsUnit unit; + + public static Dimension of(double value, SrsUnit unit) { + return new Dimension().setValue(value).setUnit(unit); + } + + public static Dimension of(double value) { + return of(value, null); + } + + public double getValue() { + return value; + } + + public Dimension setValue(double value) { + this.value = value; + return this; + } + + public Optional getUnit() { + return Optional.ofNullable(unit); + } + + public Dimension setUnit(SrsUnit unit) { + this.unit = unit; + return this; + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/options/DimensionScheme.java b/citydb-tiling/src/main/java/org/citydb/tiling/options/DimensionScheme.java new file mode 100644 index 00000000..e52f4102 --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/options/DimensionScheme.java @@ -0,0 +1,152 @@ +/* + * 3D City Database - The Open Source CityGML Database + * https://www.3dcitydb.org/ + * + * Copyright 2013 - 2024 + * Chair of Geoinformatics + * Technical University of Munich, Germany + * https://www.lrg.tum.de/gis/ + * + * The 3D City Database is jointly developed with the following + * cooperation partners: + * + * Virtual City Systems, Berlin + * M.O.S.S. Computer Grafik Systeme GmbH, Taufkirchen + * + * 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.citydb.tiling.options; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.alibaba.fastjson2.annotation.JSONType; +import org.citydb.database.adapter.DatabaseAdapter; +import org.citydb.database.geometry.GeometryException; +import org.citydb.database.srs.SpatialReference; +import org.citydb.database.srs.SrsException; +import org.citydb.database.util.SrsHelper; +import org.citydb.model.geometry.Coordinate; +import org.citydb.model.geometry.Envelope; +import org.citydb.model.geometry.Point; +import org.citydb.tiling.TileMatrix; +import org.citydb.tiling.TilingException; +import org.citydb.tiling.TilingScheme; +import org.citydb.tiling.encoding.PointReader; +import org.citydb.tiling.encoding.PointWriter; + +import java.sql.SQLException; +import java.util.Optional; + +@JSONType(typeName = "Dimension") +public class DimensionScheme extends TilingScheme { + private Dimension width; + private Dimension height; + @JSONField(serializeUsing = PointWriter.class, deserializeUsing = PointReader.class) + private Point gridPoint; + + public static DimensionScheme of(double width, double height) { + return of(Dimension.of(width), Dimension.of(height)); + } + + public static DimensionScheme of(Dimension width, Dimension height) { + return of(width, height, null); + } + + public static DimensionScheme of(Dimension width, Dimension height, Point gridPoint) { + return new DimensionScheme() + .setWidth(width) + .setHeight(height) + .setGridPoint(gridPoint); + } + + public Optional getHeight() { + return Optional.ofNullable(height); + } + + public DimensionScheme setHeight(Dimension height) { + this.height = height; + return this; + } + + public Optional getWidth() { + return Optional.ofNullable(width); + } + + public DimensionScheme setWidth(Dimension width) { + this.width = width; + return this; + } + + public Optional getGridPoint() { + return Optional.ofNullable(gridPoint); + } + + public DimensionScheme setGridPoint(Point gridPoint) { + this.gridPoint = gridPoint; + return this; + } + + @Override + protected TileMatrix buildTileMatrix(Envelope extent, DatabaseAdapter adapter) throws TilingException { + if (width == null) { + throw new TilingException("No tile width provided for the dimension tiling scheme."); + } else if (width.getValue() <= 0) { + throw new TilingException("The tile width must be a positive number but was " + width.getValue() + "."); + } else if (height == null) { + throw new TilingException("No tile height provided for the dimension tiling scheme."); + } else if (height.getValue() <= 0) { + throw new TilingException("The tile height must be a positive number but was " + height.getValue() + "."); + } + + double tileWidth, tileHeight; + try { + SrsHelper helper = adapter.getGeometryAdapter().getSrsHelper(); + tileWidth = helper.convert(width.getValue(), width.getUnit().orElse(null)); + tileHeight = helper.convert(height.getValue(), height.getUnit().orElse(null)); + } catch (SrsException e) { + throw new TilingException("Failed to convert tile dimension to the unit of the database SRS.", e); + } + + double gridX, gridY; + if (gridPoint != null) { + try { + SpatialReference reference = adapter.getGeometryAdapter().getSpatialReference(gridPoint) + .orElse(adapter.getDatabaseMetadata().getSpatialReference()); + Point point = reference.getSRID() != adapter.getDatabaseMetadata().getSpatialReference().getSRID() ? + adapter.getGeometryAdapter().transform(gridPoint) : + gridPoint; + gridX = point.getCoordinate().getX(); + gridY = point.getCoordinate().getY(); + } catch (GeometryException | SrsException | SQLException e) { + throw new TilingException("Failed to transform the grid point to the database SRS.", e); + } + } else { + gridX = gridY = 0; + } + + double minX = getNearestNeighbor(gridX, extent.getLowerCorner().getX(), tileWidth); + double minY = getNearestNeighbor(gridY, extent.getLowerCorner().getY(), tileHeight); + int columns = (int) Math.ceil((extent.getUpperCorner().getX() - minX) / tileWidth); + int rows = (int) Math.ceil((extent.getUpperCorner().getY() - minY) / tileHeight); + Coordinate lowerCorner = Coordinate.of(minX, minY); + Coordinate upperCorner = Coordinate.of(minX + columns * tileWidth, minY + rows * tileHeight); + + return buildTileMatrix(lowerCorner, upperCorner, columns, rows, tileWidth, tileHeight, adapter); + } + + private double getNearestNeighbor(double gridValue, double candidate, double offset) { + return gridValue >= candidate ? + gridValue - Math.ceil((gridValue - candidate) / offset) * offset : + gridValue + Math.floor((candidate - gridValue) / offset) * offset; + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/options/MatrixScheme.java b/citydb-tiling/src/main/java/org/citydb/tiling/options/MatrixScheme.java new file mode 100644 index 00000000..2ee58b06 --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/options/MatrixScheme.java @@ -0,0 +1,78 @@ +/* + * 3D City Database - The Open Source CityGML Database + * https://www.3dcitydb.org/ + * + * Copyright 2013 - 2024 + * Chair of Geoinformatics + * Technical University of Munich, Germany + * https://www.lrg.tum.de/gis/ + * + * The 3D City Database is jointly developed with the following + * cooperation partners: + * + * Virtual City Systems, Berlin + * M.O.S.S. Computer Grafik Systeme GmbH, Taufkirchen + * + * 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.citydb.tiling.options; + +import com.alibaba.fastjson2.annotation.JSONType; +import org.citydb.database.adapter.DatabaseAdapter; +import org.citydb.model.geometry.Envelope; +import org.citydb.tiling.TileMatrix; +import org.citydb.tiling.TilingException; +import org.citydb.tiling.TilingScheme; + +@JSONType(typeName = "Matrix") +public class MatrixScheme extends TilingScheme { + private int columns = 1; + private int rows = 1; + + public static MatrixScheme of(int columns, int rows) { + return new MatrixScheme().setColumns(columns).setRows(rows); + } + + public int getColumns() { + return columns; + } + + public MatrixScheme setColumns(int columns) { + this.columns = columns; + return this; + } + + public int getRows() { + return rows; + } + + public MatrixScheme setRows(int rows) { + this.rows = rows; + return this; + } + + @Override + protected TileMatrix buildTileMatrix(Envelope extent, DatabaseAdapter adapter) throws TilingException { + if (columns < 1) { + throw new TilingException("The number of columns must be a positive integer but was " + columns + "."); + } else if (rows < 1) { + throw new TilingException("The number of rows must be a positive integer but was " + rows + "."); + } + + return buildTileMatrix(extent.getLowerCorner(), extent.getUpperCorner(), columns, rows, + (extent.getUpperCorner().getX() - extent.getLowerCorner().getX()) / columns, + (extent.getUpperCorner().getY() - extent.getLowerCorner().getY()) / rows, + adapter); + } +} diff --git a/citydb-tiling/src/main/java/org/citydb/tiling/options/TileMatrixOrigin.java b/citydb-tiling/src/main/java/org/citydb/tiling/options/TileMatrixOrigin.java new file mode 100644 index 00000000..24b41598 --- /dev/null +++ b/citydb-tiling/src/main/java/org/citydb/tiling/options/TileMatrixOrigin.java @@ -0,0 +1,52 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * 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.citydb.tiling.options; + +public enum TileMatrixOrigin { + TOP_LEFT("topLeft"), + BOTTOM_LEFT("bottomLeft"); + + private final String value; + + TileMatrixOrigin(String value) { + this.value = value; + } + + public String toValue() { + return value; + } + + public static TileMatrixOrigin fromValue(String value) { + for (TileMatrixOrigin v : TileMatrixOrigin.values()) { + if (v.value.equals(value)) { + return v; + } + } + + return null; + } + + @Override + public String toString() { + return value; + } +} diff --git a/settings.gradle b/settings.gradle index fa7e902a..21d417cb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,4 +13,5 @@ include 'citydb-logging' include 'citydb-model' include 'citydb-operation' include 'citydb-plugin' -include 'citydb-query' \ No newline at end of file +include 'citydb-query' +include 'citydb-tiling' \ No newline at end of file