Skip to content

Commit 8f98366

Browse files
authored
Merge pull request #53 from 3dcitydb/feature-validity-filter
Add feature validity filter
2 parents 6b489c9 + bfddfc9 commit 8f98366

File tree

12 files changed

+419
-55
lines changed

12 files changed

+419
-55
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* citydb-tool - Command-line tool for the 3D City Database
3+
* https://www.3dcitydb.org/
4+
*
5+
* Copyright 2022-2025
6+
* virtualcitysystems GmbH, Germany
7+
* https://vc.systems/
8+
*
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
*/
21+
22+
package org.citydb.cli.common;
23+
24+
import org.citydb.core.time.TimeHelper;
25+
import org.citydb.database.schema.ValidityReference;
26+
import org.citydb.operation.exporter.options.ValidityMode;
27+
import org.citydb.query.QueryHelper;
28+
import org.citydb.query.filter.operation.BooleanExpression;
29+
import picocli.CommandLine;
30+
31+
import java.time.OffsetDateTime;
32+
33+
public class ValidityOptions implements Option {
34+
public enum Mode {valid, invalid, all}
35+
36+
public enum Reference {database, real_world}
37+
38+
@CommandLine.Option(names = {"-M", "--validity"}, defaultValue = "valid",
39+
description = "Process features by validity: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).")
40+
private Mode mode;
41+
42+
@CommandLine.Option(names = {"-T", "--validity-at"},
43+
description = "Check validity at a specific point in time. If provided, the time must be in " +
44+
"<YYYY-MM-DD> or <YYYY-MM-DDThh:mm:ss[(+|-)hh:mm]> format.")
45+
private String time;
46+
47+
@CommandLine.Option(names = "--validity-reference", paramLabel = "<source>", defaultValue = "database",
48+
description = "Validity time reference: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).")
49+
private Reference reference;
50+
51+
@CommandLine.Option(names = "--lenient-validity",
52+
description = "Ignore incomplete validity intervals of features.")
53+
private boolean lenient;
54+
55+
private OffsetDateTime at;
56+
57+
public BooleanExpression getValidityFilterExpression() {
58+
ValidityReference reference = switch (this.reference) {
59+
case database -> ValidityReference.DATABASE;
60+
case real_world -> ValidityReference.REAL_WORLD;
61+
};
62+
63+
return switch (mode) {
64+
case valid -> at != null ?
65+
QueryHelper.validAt(at, reference, lenient) :
66+
QueryHelper.isValid(reference);
67+
case invalid -> at != null ?
68+
QueryHelper.invalidAt(at, reference) :
69+
QueryHelper.isInvalid(reference);
70+
case all -> null;
71+
};
72+
}
73+
74+
public org.citydb.operation.exporter.options.ValidityOptions getExportValidityOptions() {
75+
return new org.citydb.operation.exporter.options.ValidityOptions()
76+
.setMode(switch (mode) {
77+
case valid -> ValidityMode.VALID;
78+
case invalid -> ValidityMode.INVALID;
79+
case all -> ValidityMode.ALL;
80+
})
81+
.setReference(switch (reference) {
82+
case database -> ValidityReference.DATABASE;
83+
case real_world -> ValidityReference.REAL_WORLD;
84+
})
85+
.setAt(at)
86+
.setLenient(lenient);
87+
}
88+
89+
@Override
90+
public void preprocess(CommandLine commandLine) throws Exception {
91+
if (time != null) {
92+
if (mode == Mode.all) {
93+
throw new CommandLine.ParameterException(commandLine,
94+
"Error: The validity mode '" + mode + "' does not take a time");
95+
} else {
96+
try {
97+
at = OffsetDateTime.parse(time, TimeHelper.VALIDITY_TIME_FORMATTER);
98+
} catch (Exception e) {
99+
throw new CommandLine.ParameterException(commandLine,
100+
"The validity time must be in YYYY-MM-DD or YYYY-MM-DDThh:mm:ss[(+|-)hh:mm] " +
101+
"format but was '" + time + "'");
102+
}
103+
}
104+
}
105+
}
106+
}

citydb-cli/src/main/java/org/citydb/cli/deleter/DeleteCommand.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@
2424
import org.apache.logging.log4j.Level;
2525
import org.apache.logging.log4j.Logger;
2626
import org.citydb.cli.ExecutionException;
27-
import org.citydb.cli.common.Command;
28-
import org.citydb.cli.common.ConfigOption;
29-
import org.citydb.cli.common.ConnectionOptions;
30-
import org.citydb.cli.common.IndexOptions;
27+
import org.citydb.cli.common.*;
3128
import org.citydb.cli.deleter.options.MetadataOptions;
3229
import org.citydb.cli.deleter.options.QueryOptions;
3330
import org.citydb.cli.util.CommandHelper;
@@ -85,6 +82,10 @@ enum Mode {delete, terminate}
8582
heading = "Query and filter options:%n")
8683
private QueryOptions queryOptions;
8784

85+
@CommandLine.ArgGroup(exclusive = false,
86+
heading = "Time-based feature history options:%n")
87+
protected ValidityOptions validityOptions;
88+
8889
@CommandLine.ArgGroup(exclusive = false,
8990
heading = "Database connection options:%n")
9091
private ConnectionOptions connectionOptions;
@@ -192,17 +193,16 @@ private Query getQuery(DeleteOptions deleteOptions) throws ExecutionException {
192193
try {
193194
Query query = queryOptions != null ?
194195
queryOptions.getQuery() :
195-
deleteOptions.getQuery().orElseGet(QueryHelper::getAllTopLevelFeatures);
196-
197-
if (mode == Mode.terminate) {
198-
BooleanExpression isValid = QueryHelper.isValid(ValidityReference.DATABASE);
199-
query.setFilter(Filter.of(query.getFilter()
200-
.map(Filter::getExpression)
201-
.map(expression -> (BooleanExpression) Operators.and(expression, isValid))
202-
.orElse(isValid)));
203-
}
204-
205-
return query;
196+
deleteOptions.getQuery().orElseGet(Query::new);
197+
BooleanExpression validity = validityOptions != null ?
198+
validityOptions.getValidityFilterExpression() :
199+
QueryHelper.isValid(ValidityReference.DATABASE);
200+
201+
return validity != null ?
202+
query.setFilter(query.getFilter()
203+
.map(filter -> Filter.of(Operators.and(validity, filter.getExpression())))
204+
.orElse(Filter.of(validity))) :
205+
query;
206206
} catch (FilterParseException e) {
207207
throw new ExecutionException("Failed to parse the provided CQL2 filter expression.", e);
208208
}

citydb-cli/src/main/java/org/citydb/cli/exporter/ExportController.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@
5555
import org.citydb.query.builder.sql.SqlBuildOptions;
5656
import org.citydb.query.executor.QueryExecutor;
5757
import org.citydb.query.executor.QueryResult;
58+
import org.citydb.query.filter.Filter;
5859
import org.citydb.query.filter.encoding.FilterParseException;
60+
import org.citydb.query.filter.operation.BooleanExpression;
61+
import org.citydb.query.filter.operation.Operators;
5962
import org.citydb.util.tiling.Tile;
6063
import org.citydb.util.tiling.TileIterator;
6164
import org.citydb.util.tiling.Tiling;
@@ -89,6 +92,10 @@ public abstract class ExportController implements Command {
8992
heading = "Query and filter options:%n")
9093
protected QueryOptions queryOptions;
9194

95+
@CommandLine.ArgGroup(exclusive = false, order = Integer.MAX_VALUE,
96+
heading = "Time-based feature history options:%n")
97+
protected ValidityOptions validityOptions;
98+
9299
@CommandLine.ArgGroup(exclusive = false, order = Integer.MAX_VALUE,
93100
heading = "Tiling options:%n")
94101
protected TilingOptions tilingOptions;
@@ -238,10 +245,18 @@ private FeatureWriter createWriter(OutputFile file, WriteOptions options, Query
238245

239246
protected Query getQuery(ExportOptions exportOptions) throws ExecutionException {
240247
try {
241-
return queryOptions != null ?
248+
Query query = queryOptions != null ?
242249
queryOptions.getQuery() :
243-
exportOptions.getQuery().orElseGet(() ->
244-
QueryHelper.getValidTopLevelFeatures(ValidityReference.DATABASE));
250+
exportOptions.getQuery().orElseGet(Query::new);
251+
BooleanExpression validity = validityOptions != null ?
252+
validityOptions.getValidityFilterExpression() :
253+
QueryHelper.isValid(ValidityReference.DATABASE);
254+
255+
return validity != null ?
256+
query.setFilter(query.getFilter()
257+
.map(filter -> Filter.of(Operators.and(validity, filter.getExpression())))
258+
.orElse(Filter.of(validity))) :
259+
query;
245260
} catch (FilterParseException e) {
246261
throw new ExecutionException("Failed to parse the provided CQL2 filter expression.", e);
247262
}
@@ -285,6 +300,10 @@ protected ExportOptions getExportOptions() throws ExecutionException {
285300
}
286301
}
287302

303+
if (validityOptions != null) {
304+
exportOptions.setValidityOptions(validityOptions.getExportValidityOptions());
305+
}
306+
288307
return exportOptions;
289308
}
290309

citydb-cli/src/main/java/org/citydb/cli/exporter/options/QueryOptions.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@
2222
package org.citydb.cli.exporter.options;
2323

2424
import org.citydb.cli.common.*;
25-
import org.citydb.database.schema.ValidityReference;
2625
import org.citydb.query.Query;
27-
import org.citydb.query.QueryHelper;
28-
import org.citydb.query.filter.Filter;
2926
import org.citydb.query.filter.encoding.FilterParseException;
3027
import picocli.CommandLine;
3128

@@ -57,8 +54,6 @@ public Query getQuery() throws FilterParseException {
5754
if (filterOptions != null) {
5855
query.setFilter(filterOptions.getFilter());
5956
query.setFilterSrs(filterOptions.getFilterCrs());
60-
} else {
61-
query.setFilter(Filter.of(QueryHelper.isValid(ValidityReference.DATABASE)));
6257
}
6358

6459
if (sortingOptions != null) {

citydb-operation/src/main/java/org/citydb/operation/deleter/feature/FeatureDeleter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ protected PreparedStatement getDeleteStatement(Connection connection) throws SQL
5151
helper.getOptions().getLineage().ifPresent(lineage ->
5252
stmt.append(", lineage = '").append(lineage).append("'"));
5353

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

7171
for (long id : ids) {
72-
OffsetDateTime now = OffsetDateTime.now();
72+
OffsetDateTime now = OffsetDateTime.now().withNano(0);
7373
stmt.setObject(1, helper.getOptions().getTerminationDate().orElse(now));
7474
stmt.setObject(2, now);
7575
stmt.setString(3, updatingPerson);

citydb-operation/src/main/java/org/citydb/operation/exporter/ExportHelper.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,8 @@
3535
import org.citydb.operation.exporter.feature.FeatureHierarchyExporter;
3636
import org.citydb.operation.exporter.geometry.ImplicitGeometryExporter;
3737
import org.citydb.operation.exporter.options.LodOptions;
38-
import org.citydb.operation.exporter.util.LodFilter;
39-
import org.citydb.operation.exporter.util.Postprocessor;
40-
import org.citydb.operation.exporter.util.SurfaceDataMapper;
41-
import org.citydb.operation.exporter.util.TableHelper;
38+
import org.citydb.operation.exporter.options.ValidityOptions;
39+
import org.citydb.operation.exporter.util.*;
4240
import org.citydb.sqlbuilder.query.Selection;
4341
import org.citydb.sqlbuilder.schema.Column;
4442

@@ -54,6 +52,7 @@ public class ExportHelper {
5452
private final Connection connection;
5553
private final SchemaMapping schemaMapping;
5654
private final SpatialReference targetSrs;
55+
private final ValidityFilter validityFilter;
5756
private final LodFilter lodFilter;
5857
private final Postprocessor postprocessor;
5958
private final TableHelper tableHelper;
@@ -72,6 +71,7 @@ public class ExportHelper {
7271
schemaMapping = adapter.getSchemaAdapter().getSchemaMapping();
7372
targetSrs = adapter.getGeometryAdapter().getSpatialReference(options.getTargetSrs().orElse(null))
7473
.orElse(adapter.getDatabaseMetadata().getSpatialReference());
74+
validityFilter = new ValidityFilter(options.getValidityOptions().orElseGet(ValidityOptions::new));
7575
lodFilter = new LodFilter(options.getLodOptions().orElseGet(LodOptions::new));
7676
postprocessor = new Postprocessor(this);
7777
tableHelper = new TableHelper(this);
@@ -93,6 +93,10 @@ public Connection getConnection() {
9393
return connection;
9494
}
9595

96+
public ValidityFilter getValidityFilter() {
97+
return validityFilter;
98+
}
99+
96100
public LodFilter getLodFilter() {
97101
return lodFilter;
98102
}

citydb-operation/src/main/java/org/citydb/operation/exporter/ExportOptions.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.citydb.model.encoding.Matrix3x4Writer;
3333
import org.citydb.operation.exporter.options.AppearanceOptions;
3434
import org.citydb.operation.exporter.options.LodOptions;
35+
import org.citydb.operation.exporter.options.ValidityOptions;
3536

3637
import java.io.IOException;
3738
import java.nio.file.Files;
@@ -50,6 +51,7 @@ public class ExportOptions {
5051
private SrsReference targetSrs;
5152
@JSONField(serializeUsing = Matrix3x4Writer.class, deserializeUsing = Matrix3x4Reader.class)
5253
private Matrix3x4 affineTransform;
54+
private ValidityOptions validityOptions;
5355
private LodOptions lodOptions;
5456
private AppearanceOptions appearanceOptions;
5557

@@ -102,6 +104,15 @@ public ExportOptions setAffineTransform(Matrix3x4 affineTransform) {
102104
return this;
103105
}
104106

107+
public Optional<ValidityOptions> getValidityOptions() {
108+
return Optional.ofNullable(validityOptions);
109+
}
110+
111+
public ExportOptions setValidityOptions(ValidityOptions validityOptions) {
112+
this.validityOptions = validityOptions;
113+
return this;
114+
}
115+
105116
public Optional<LodOptions> getLodOptions() {
106117
return Optional.ofNullable(lodOptions);
107118
}

0 commit comments

Comments
 (0)