diff --git a/citydb-cli/src/main/java/org/citydb/cli/common/ValidityOptions.java b/citydb-cli/src/main/java/org/citydb/cli/common/ValidityOptions.java new file mode 100644 index 00000000..932562c9 --- /dev/null +++ b/citydb-cli/src/main/java/org/citydb/cli/common/ValidityOptions.java @@ -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 " + + " or format.") + private String time; + + @CommandLine.Option(names = "--validity-reference", paramLabel = "", 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 + "'"); + } + } + } + } +} 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 d00a3fd8..31777b42 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 @@ -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; @@ -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; @@ -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); } 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 f361526b..6cffaf98 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 @@ -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; @@ -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; @@ -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); } @@ -285,6 +300,10 @@ protected ExportOptions getExportOptions() throws ExecutionException { } } + if (validityOptions != null) { + exportOptions.setValidityOptions(validityOptions.getExportValidityOptions()); + } + return exportOptions; } diff --git a/citydb-cli/src/main/java/org/citydb/cli/exporter/options/QueryOptions.java b/citydb-cli/src/main/java/org/citydb/cli/exporter/options/QueryOptions.java index 10faec71..7cd2065e 100644 --- a/citydb-cli/src/main/java/org/citydb/cli/exporter/options/QueryOptions.java +++ b/citydb-cli/src/main/java/org/citydb/cli/exporter/options/QueryOptions.java @@ -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; @@ -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) { diff --git a/citydb-operation/src/main/java/org/citydb/operation/deleter/feature/FeatureDeleter.java b/citydb-operation/src/main/java/org/citydb/operation/deleter/feature/FeatureDeleter.java index 7825522e..fb234cdc 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/deleter/feature/FeatureDeleter.java +++ b/citydb-operation/src/main/java/org/citydb/operation/deleter/feature/FeatureDeleter.java @@ -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(?, ?)}"); @@ -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); diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportHelper.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportHelper.java index 897e0df1..5611bda2 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportHelper.java +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportHelper.java @@ -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; @@ -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; @@ -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); @@ -93,6 +93,10 @@ public Connection getConnection() { return connection; } + public ValidityFilter getValidityFilter() { + return validityFilter; + } + public LodFilter getLodFilter() { return lodFilter; } diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportOptions.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportOptions.java index b786be98..cd8f3fcf 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportOptions.java +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportOptions.java @@ -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; @@ -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; @@ -102,6 +104,15 @@ public ExportOptions setAffineTransform(Matrix3x4 affineTransform) { return this; } + public Optional getValidityOptions() { + return Optional.ofNullable(validityOptions); + } + + public ExportOptions setValidityOptions(ValidityOptions validityOptions) { + this.validityOptions = validityOptions; + return this; + } + public Optional getLodOptions() { return Optional.ofNullable(lodOptions); } diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/hierarchy/HierarchyBuilder.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/hierarchy/HierarchyBuilder.java index 47582a15..d4c12655 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/exporter/hierarchy/HierarchyBuilder.java +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/hierarchy/HierarchyBuilder.java @@ -23,6 +23,7 @@ import org.citydb.model.feature.Feature; import org.citydb.model.property.Attribute; +import org.citydb.model.property.DataType; import org.citydb.model.property.Property; import org.citydb.model.property.PropertyDescriptor; import org.citydb.operation.exporter.ExportException; @@ -37,6 +38,7 @@ import org.citydb.operation.exporter.property.PropertyStub; import org.citydb.operation.exporter.util.LodFilter; import org.citydb.operation.exporter.util.TableHelper; +import org.citydb.operation.exporter.util.ValidityFilter; import java.sql.ResultSet; import java.sql.SQLException; @@ -45,16 +47,18 @@ public class HierarchyBuilder { private final long rootId; private final ExportHelper helper; + private final ValidityFilter validityFilter; private final LodFilter lodFilter; private final TableHelper tableHelper; private final PropertyBuilder propertyBuilder; private final Hierarchy hierarchy = new Hierarchy(); - private final List propertyStubs = new ArrayList<>(); + private final Map> propertyStubs = new HashMap<>(); private final boolean exportAppearances; private HierarchyBuilder(long rootId, ExportHelper helper) { this.rootId = rootId; this.helper = helper; + validityFilter = helper.getValidityFilter(); lodFilter = helper.getLodFilter(); tableHelper = helper.getTableHelper(); propertyBuilder = new PropertyBuilder(helper); @@ -73,6 +77,7 @@ public HierarchyBuilder initialize(ResultSet rs) throws ExportException, SQLExce Set appearanceIds = new HashSet<>(); Set addressIds = new HashSet<>(); Set implicitGeometryIds = new HashSet<>(); + Map referees = new HashMap<>(); while (rs.next()) { long featureId = rs.getLong("val_feature_id"); @@ -109,15 +114,32 @@ public HierarchyBuilder initialize(ResultSet rs) throws ExportException, SQLExce PropertyStub propertyStub = tableHelper.getOrCreateExporter(PropertyExporter.class) .doExport(parentFeatureId, rs); if (propertyStub != null) { - propertyStubs.add(propertyStub); + propertyStubs.computeIfAbsent(parentFeatureId, v -> new ArrayList<>()).add(propertyStub); + if (propertyStub.getDataType() == DataType.FEATURE_PROPERTY) { + referees.merge(featureId, 1, Integer::sum); + } } } } if (!featureIds.isEmpty()) { - tableHelper.getOrCreateExporter(FeatureExporter.class) - .doExport(featureIds) - .forEach(hierarchy::addFeature); + Set removedFeatureIds = new HashSet<>(); + for (Map.Entry entry : tableHelper.getOrCreateExporter(FeatureExporter.class) + .doExport(featureIds).entrySet()) { + long featureId = entry.getKey(); + Feature feature = entry.getValue(); + if (!removedFeatureIds.contains(featureId)) { + if (featureId != rootId && !validityFilter.filter(feature)) { + removeFeature(featureId, removedFeatureIds, referees); + } else { + hierarchy.addFeature(featureId, feature); + } + } + } + + if (!removedFeatureIds.isEmpty()) { + hierarchy.getFeatures().keySet().removeAll(removedFeatureIds); + } } if (!geometryIds.isEmpty()) { @@ -152,12 +174,14 @@ public Hierarchy build() { if (root != null) { helper.lookupAndPut(root); - Iterator iterator = propertyStubs.iterator(); - while (iterator.hasNext()) { - PropertyStub propertyStub = iterator.next(); - hierarchy.addProperty(propertyStub.getDescriptor().getId(), - propertyBuilder.build(propertyStub, hierarchy)); - iterator.remove(); + for (List propertyStubs : this.propertyStubs.values()) { + Iterator iterator = propertyStubs.iterator(); + while (iterator.hasNext()) { + PropertyStub propertyStub = iterator.next(); + hierarchy.addProperty(propertyStub.getDescriptor().getId(), + propertyBuilder.build(propertyStub, hierarchy)); + iterator.remove(); + } } for (Property property : hierarchy.getProperties().values()) { @@ -179,4 +203,19 @@ public Hierarchy build() { return hierarchy; } + + private void removeFeature(long featureId, Set removedFeatureIds, Map referees) { + removedFeatureIds.add(featureId); + List propertyStubs = this.propertyStubs.remove(featureId); + if (propertyStubs != null) { + for (PropertyStub propertyStub : propertyStubs) { + if (propertyStub.getDataType() == DataType.FEATURE_PROPERTY) { + long nestedFeatureId = propertyStub.getFeatureId(); + if (referees.merge(nestedFeatureId, -1, Integer::sum) == 0) { + removeFeature(nestedFeatureId, removedFeatureIds, referees); + } + } + } + } + } } diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/options/ValidityMode.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/options/ValidityMode.java new file mode 100644 index 00000000..c543344a --- /dev/null +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/options/ValidityMode.java @@ -0,0 +1,53 @@ +/* + * 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.operation.exporter.options; + +public enum ValidityMode { + VALID("valid"), + INVALID("invalid"), + ALL("all"); + + private final String value; + + ValidityMode(String value) { + this.value = value; + } + + public String toValue() { + return value; + } + + public static ValidityMode fromValue(String value) { + for (ValidityMode v : ValidityMode.values()) { + if (v.value.equals(value)) { + return v; + } + } + + return null; + } + + @Override + public String toString() { + return value; + } +} diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/options/ValidityOptions.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/options/ValidityOptions.java new file mode 100644 index 00000000..4d105645 --- /dev/null +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/options/ValidityOptions.java @@ -0,0 +1,76 @@ +/* + * 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.operation.exporter.options; + +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.annotation.JSONField; +import org.citydb.config.encoding.ValidityTimeReader; +import org.citydb.database.schema.ValidityReference; + +import java.time.OffsetDateTime; +import java.util.Optional; + +public class ValidityOptions { + @JSONField(serializeFeatures = JSONWriter.Feature.WriteEnumUsingToString) + private ValidityMode mode; + @JSONField(deserializeUsing = ValidityTimeReader.class) + private OffsetDateTime at; + @JSONField(serializeFeatures = JSONWriter.Feature.WriteEnumUsingToString) + private ValidityReference reference; + private boolean lenient; + + public ValidityMode getMode() { + return mode != null ? mode : ValidityMode.VALID; + } + + public ValidityOptions setMode(ValidityMode mode) { + this.mode = mode; + return this; + } + + public Optional getAt() { + return Optional.ofNullable(at); + } + + public ValidityOptions setAt(OffsetDateTime at) { + this.at = at; + return this; + } + + public ValidityReference getReference() { + return reference != null ? reference : ValidityReference.DATABASE; + } + + public ValidityOptions setReference(ValidityReference reference) { + this.reference = reference; + return this; + } + + public boolean isLenient() { + return lenient; + } + + public ValidityOptions setLenient(boolean lenient) { + this.lenient = lenient; + return this; + } +} diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/util/ValidityFilter.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/util/ValidityFilter.java new file mode 100644 index 00000000..896c8451 --- /dev/null +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/util/ValidityFilter.java @@ -0,0 +1,69 @@ +/* + * 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.operation.exporter.util; + +import org.citydb.database.schema.ValidityReference; +import org.citydb.model.feature.Feature; +import org.citydb.operation.exporter.options.ValidityOptions; + +import java.time.OffsetDateTime; +import java.util.Objects; + +public class ValidityFilter { + private final Mode mode; + private final ValidityReference reference; + private final OffsetDateTime timestamp; + private final boolean lenient; + + private enum Mode { + VALID, + VALID_AT, + INVALID, + INVALID_AT, + ALL + } + + public ValidityFilter(ValidityOptions options) { + Objects.requireNonNull(options, "The validity filter options must not be null."); + reference = options.getReference(); + timestamp = options.getAt().orElse(null); + lenient = options.isLenient(); + mode = switch (options.getMode()) { + case VALID -> timestamp != null ? Mode.VALID_AT : Mode.VALID; + case INVALID -> timestamp != null ? Mode.INVALID_AT : Mode.INVALID; + case ALL -> Mode.ALL; + }; + } + + public boolean filter(Feature feature) { + return switch (mode) { + case VALID -> reference.to(feature).isEmpty(); + case VALID_AT -> ((lenient && reference.from(feature).isEmpty()) + || reference.from(feature).map(from -> !from.isAfter(timestamp)).orElse(false)) + && (reference.to(feature).isEmpty() + || reference.to(feature).map(to -> to.isAfter(timestamp)).orElse(false)); + case INVALID -> reference.to(feature).isPresent(); + case INVALID_AT -> reference.to(feature).map(to -> !to.isAfter(timestamp)).orElse(false); + case ALL -> true; + }; + } +} diff --git a/citydb-query/src/main/java/org/citydb/query/QueryHelper.java b/citydb-query/src/main/java/org/citydb/query/QueryHelper.java index 94634004..77d7dde9 100644 --- a/citydb-query/src/main/java/org/citydb/query/QueryHelper.java +++ b/citydb-query/src/main/java/org/citydb/query/QueryHelper.java @@ -44,32 +44,24 @@ public static BooleanExpression isValid(ValidityReference reference) { return PropertyRef.of(reference.to()).isNull(); } - public static BooleanExpression wasValidAt(OffsetDateTime timestamp, ValidityReference reference) { - return wasValidAt(timestamp, reference, false); + public static BooleanExpression validAt(OffsetDateTime timestamp, ValidityReference reference) { + return validAt(timestamp, reference, false); } - public static BooleanExpression wasValidAt(OffsetDateTime timestamp, ValidityReference reference, boolean lenient) { - return wasValidBetween(timestamp, timestamp, reference, lenient); - } - - public static BooleanExpression wasValidBetween(OffsetDateTime lowerBound, OffsetDateTime upperBound, ValidityReference reference) { - return wasValidBetween(lowerBound, upperBound, reference, false); - } - - public static BooleanExpression wasValidBetween(OffsetDateTime lowerBound, OffsetDateTime upperBound, ValidityReference reference, boolean lenient) { + public static BooleanExpression validAt(OffsetDateTime timestamp, ValidityReference reference, boolean lenient) { PropertyRef from = PropertyRef.of(reference.from()); PropertyRef to = PropertyRef.of(reference.to()); return Operators.and(lenient ? - from.isNull().or(from.le(TimestampLiteral.of(upperBound))) : - from.le(TimestampLiteral.of(upperBound)), - isValid(reference).or(to.gt(TimestampLiteral.of(lowerBound)))); + from.isNull().or(from.le(TimestampLiteral.of(timestamp))) : + from.le(TimestampLiteral.of(timestamp)), + isValid(reference).or(to.gt(TimestampLiteral.of(timestamp)))); } public static BooleanExpression isInvalid(ValidityReference reference) { return PropertyRef.of(reference.to()).isNotNull(); } - public static BooleanExpression wasInvalidAt(OffsetDateTime timestamp, ValidityReference reference) { + public static BooleanExpression invalidAt(OffsetDateTime timestamp, ValidityReference reference) { return PropertyRef.of(reference.to()).le(TimestampLiteral.of(timestamp)); } }