Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature validity filter #53

Merged
merged 3 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions citydb-cli/src/main/java/org/citydb/cli/common/ValidityOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* citydb-tool - Command-line tool for the 3D City Database
* https://www.3dcitydb.org/
*
* Copyright 2022-2025
* 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.common;

import org.citydb.core.time.TimeHelper;
import org.citydb.database.schema.ValidityReference;
import org.citydb.operation.exporter.options.ValidityMode;
import org.citydb.query.QueryHelper;
import org.citydb.query.filter.operation.BooleanExpression;
import picocli.CommandLine;

import java.time.OffsetDateTime;

public class ValidityOptions implements Option {
public enum Mode {valid, invalid, all}

public enum Reference {database, real_world}

@CommandLine.Option(names = {"-M", "--validity"}, defaultValue = "valid",
description = "Process features by validity: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).")
private Mode mode;

@CommandLine.Option(names = {"-T", "--validity-at"},
description = "Check validity at a specific point in time. If provided, the time must be in " +
"<YYYY-MM-DD> or <YYYY-MM-DDThh:mm:ss[(+|-)hh:mm]> format.")
private String time;

@CommandLine.Option(names = "--validity-reference", paramLabel = "<source>", defaultValue = "database",
description = "Validity time reference: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).")
private Reference reference;

@CommandLine.Option(names = "--lenient-validity",
description = "Ignore incomplete validity intervals of features.")
private boolean lenient;

private OffsetDateTime at;

public BooleanExpression getValidityFilterExpression() {
ValidityReference reference = switch (this.reference) {
case database -> ValidityReference.DATABASE;
case real_world -> ValidityReference.REAL_WORLD;
};

return switch (mode) {
case valid -> at != null ?
QueryHelper.validAt(at, reference, lenient) :
QueryHelper.isValid(reference);
case invalid -> at != null ?
QueryHelper.invalidAt(at, reference) :
QueryHelper.isInvalid(reference);
case all -> null;
};
}

public org.citydb.operation.exporter.options.ValidityOptions getExportValidityOptions() {
return new org.citydb.operation.exporter.options.ValidityOptions()
.setMode(switch (mode) {
case valid -> ValidityMode.VALID;
case invalid -> ValidityMode.INVALID;
case all -> ValidityMode.ALL;
})
.setReference(switch (reference) {
case database -> ValidityReference.DATABASE;
case real_world -> ValidityReference.REAL_WORLD;
})
.setAt(at)
.setLenient(lenient);
}

@Override
public void preprocess(CommandLine commandLine) throws Exception {
if (time != null) {
if (mode == Mode.all) {
throw new CommandLine.ParameterException(commandLine,
"Error: The validity mode '" + mode + "' does not take a time");
} else {
try {
at = OffsetDateTime.parse(time, TimeHelper.VALIDITY_TIME_FORMATTER);
} catch (Exception e) {
throw new CommandLine.ParameterException(commandLine,
"The validity time must be in YYYY-MM-DD or YYYY-MM-DDThh:mm:ss[(+|-)hh:mm] " +
"format but was '" + time + "'");
}
}
}
}
}
30 changes: 15 additions & 15 deletions citydb-cli/src/main/java/org/citydb/cli/deleter/DeleteCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
import org.citydb.cli.ExecutionException;
import org.citydb.cli.common.Command;
import org.citydb.cli.common.ConfigOption;
import org.citydb.cli.common.ConnectionOptions;
import org.citydb.cli.common.IndexOptions;
import org.citydb.cli.common.*;
import org.citydb.cli.deleter.options.MetadataOptions;
import org.citydb.cli.deleter.options.QueryOptions;
import org.citydb.cli.util.CommandHelper;
Expand Down Expand Up @@ -85,6 +82,10 @@ enum Mode {delete, terminate}
heading = "Query and filter options:%n")
private QueryOptions queryOptions;

@CommandLine.ArgGroup(exclusive = false,
heading = "Time-based feature history options:%n")
protected ValidityOptions validityOptions;

@CommandLine.ArgGroup(exclusive = false,
heading = "Database connection options:%n")
private ConnectionOptions connectionOptions;
Expand Down Expand Up @@ -192,17 +193,16 @@ private Query getQuery(DeleteOptions deleteOptions) throws ExecutionException {
try {
Query query = queryOptions != null ?
queryOptions.getQuery() :
deleteOptions.getQuery().orElseGet(QueryHelper::getAllTopLevelFeatures);

if (mode == Mode.terminate) {
BooleanExpression isValid = QueryHelper.isValid(ValidityReference.DATABASE);
query.setFilter(Filter.of(query.getFilter()
.map(Filter::getExpression)
.map(expression -> (BooleanExpression) Operators.and(expression, isValid))
.orElse(isValid)));
}

return query;
deleteOptions.getQuery().orElseGet(Query::new);
BooleanExpression validity = validityOptions != null ?
validityOptions.getValidityFilterExpression() :
QueryHelper.isValid(ValidityReference.DATABASE);

return validity != null ?
query.setFilter(query.getFilter()
.map(filter -> Filter.of(Operators.and(validity, filter.getExpression())))
.orElse(Filter.of(validity))) :
query;
} catch (FilterParseException e) {
throw new ExecutionException("Failed to parse the provided CQL2 filter expression.", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@
import org.citydb.query.builder.sql.SqlBuildOptions;
import org.citydb.query.executor.QueryExecutor;
import org.citydb.query.executor.QueryResult;
import org.citydb.query.filter.Filter;
import org.citydb.query.filter.encoding.FilterParseException;
import org.citydb.query.filter.operation.BooleanExpression;
import org.citydb.query.filter.operation.Operators;
import org.citydb.util.tiling.Tile;
import org.citydb.util.tiling.TileIterator;
import org.citydb.util.tiling.Tiling;
Expand Down Expand Up @@ -89,6 +92,10 @@ public abstract class ExportController implements Command {
heading = "Query and filter options:%n")
protected QueryOptions queryOptions;

@CommandLine.ArgGroup(exclusive = false, order = Integer.MAX_VALUE,
heading = "Time-based feature history options:%n")
protected ValidityOptions validityOptions;

@CommandLine.ArgGroup(exclusive = false, order = Integer.MAX_VALUE,
heading = "Tiling options:%n")
protected TilingOptions tilingOptions;
Expand Down Expand Up @@ -238,10 +245,18 @@ private FeatureWriter createWriter(OutputFile file, WriteOptions options, Query

protected Query getQuery(ExportOptions exportOptions) throws ExecutionException {
try {
return queryOptions != null ?
Query query = queryOptions != null ?
queryOptions.getQuery() :
exportOptions.getQuery().orElseGet(() ->
QueryHelper.getValidTopLevelFeatures(ValidityReference.DATABASE));
exportOptions.getQuery().orElseGet(Query::new);
BooleanExpression validity = validityOptions != null ?
validityOptions.getValidityFilterExpression() :
QueryHelper.isValid(ValidityReference.DATABASE);

return validity != null ?
query.setFilter(query.getFilter()
.map(filter -> Filter.of(Operators.and(validity, filter.getExpression())))
.orElse(Filter.of(validity))) :
query;
} catch (FilterParseException e) {
throw new ExecutionException("Failed to parse the provided CQL2 filter expression.", e);
}
Expand Down Expand Up @@ -285,6 +300,10 @@ protected ExportOptions getExportOptions() throws ExecutionException {
}
}

if (validityOptions != null) {
exportOptions.setValidityOptions(validityOptions.getExportValidityOptions());
}

return exportOptions;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@
package org.citydb.cli.exporter.options;

import org.citydb.cli.common.*;
import org.citydb.database.schema.ValidityReference;
import org.citydb.query.Query;
import org.citydb.query.QueryHelper;
import org.citydb.query.filter.Filter;
import org.citydb.query.filter.encoding.FilterParseException;
import picocli.CommandLine;

Expand Down Expand Up @@ -57,8 +54,6 @@ public Query getQuery() throws FilterParseException {
if (filterOptions != null) {
query.setFilter(filterOptions.getFilter());
query.setFilterSrs(filterOptions.getFilterCrs());
} else {
query.setFilter(Filter.of(QueryHelper.isValid(ValidityReference.DATABASE)));
}

if (sortingOptions != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ protected PreparedStatement getDeleteStatement(Connection connection) throws SQL
helper.getOptions().getLineage().ifPresent(lineage ->
stmt.append(", lineage = '").append(lineage).append("'"));

stmt.append(" where id = ? and termination_date is null");
stmt.append(" where id = ?");
return connection.prepareStatement(stmt.toString());
} else {
return connection.prepareCall("{call citydb_pkg.delete_feature(?, ?)}");
Expand All @@ -69,7 +69,7 @@ protected void executeBatch(Long[] ids) throws SQLException {
.orElse(helper.getAdapter().getConnectionDetails().getUser());

for (long id : ids) {
OffsetDateTime now = OffsetDateTime.now();
OffsetDateTime now = OffsetDateTime.now().withNano(0);
stmt.setObject(1, helper.getOptions().getTerminationDate().orElse(now));
stmt.setObject(2, now);
stmt.setString(3, updatingPerson);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@
import org.citydb.operation.exporter.feature.FeatureHierarchyExporter;
import org.citydb.operation.exporter.geometry.ImplicitGeometryExporter;
import org.citydb.operation.exporter.options.LodOptions;
import org.citydb.operation.exporter.util.LodFilter;
import org.citydb.operation.exporter.util.Postprocessor;
import org.citydb.operation.exporter.util.SurfaceDataMapper;
import org.citydb.operation.exporter.util.TableHelper;
import org.citydb.operation.exporter.options.ValidityOptions;
import org.citydb.operation.exporter.util.*;
import org.citydb.sqlbuilder.query.Selection;
import org.citydb.sqlbuilder.schema.Column;

Expand All @@ -54,6 +52,7 @@ public class ExportHelper {
private final Connection connection;
private final SchemaMapping schemaMapping;
private final SpatialReference targetSrs;
private final ValidityFilter validityFilter;
private final LodFilter lodFilter;
private final Postprocessor postprocessor;
private final TableHelper tableHelper;
Expand All @@ -72,6 +71,7 @@ public class ExportHelper {
schemaMapping = adapter.getSchemaAdapter().getSchemaMapping();
targetSrs = adapter.getGeometryAdapter().getSpatialReference(options.getTargetSrs().orElse(null))
.orElse(adapter.getDatabaseMetadata().getSpatialReference());
validityFilter = new ValidityFilter(options.getValidityOptions().orElseGet(ValidityOptions::new));
lodFilter = new LodFilter(options.getLodOptions().orElseGet(LodOptions::new));
postprocessor = new Postprocessor(this);
tableHelper = new TableHelper(this);
Expand All @@ -93,6 +93,10 @@ public Connection getConnection() {
return connection;
}

public ValidityFilter getValidityFilter() {
return validityFilter;
}

public LodFilter getLodFilter() {
return lodFilter;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.citydb.model.encoding.Matrix3x4Writer;
import org.citydb.operation.exporter.options.AppearanceOptions;
import org.citydb.operation.exporter.options.LodOptions;
import org.citydb.operation.exporter.options.ValidityOptions;

import java.io.IOException;
import java.nio.file.Files;
Expand All @@ -50,6 +51,7 @@ public class ExportOptions {
private SrsReference targetSrs;
@JSONField(serializeUsing = Matrix3x4Writer.class, deserializeUsing = Matrix3x4Reader.class)
private Matrix3x4 affineTransform;
private ValidityOptions validityOptions;
private LodOptions lodOptions;
private AppearanceOptions appearanceOptions;

Expand Down Expand Up @@ -102,6 +104,15 @@ public ExportOptions setAffineTransform(Matrix3x4 affineTransform) {
return this;
}

public Optional<ValidityOptions> getValidityOptions() {
return Optional.ofNullable(validityOptions);
}

public ExportOptions setValidityOptions(ValidityOptions validityOptions) {
this.validityOptions = validityOptions;
return this;
}

public Optional<LodOptions> getLodOptions() {
return Optional.ofNullable(lodOptions);
}
Expand Down
Loading