From 50d7e64fe72e6b550cee23933e498a0627ce7227 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 9 Dec 2025 16:47:32 +0100 Subject: [PATCH 01/63] Feature: Add configuration of embedded database --- .../database/DataBaseSettings.java | 92 +++++++++++++++++++ .../EmbeddedDatabaseConfiguration.java | 47 ++++++++++ .../database/EmbeddedDatabaseHandler.java | 61 ++++++++---- .../citydoctor2/database/FeatureCache.java | 12 ++- .../EmbeddedDatabaseHandlerException.java | 18 ++++ .../citygml3/Citygml3FeatureMapper.java | 2 +- .../database/EmbeddedDataBaseHandlerTest.java | 4 +- 7 files changed, 209 insertions(+), 27 deletions(-) create mode 100644 CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java create mode 100644 CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java create mode 100644 CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/exceptions/EmbeddedDatabaseHandlerException.java diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java new file mode 100644 index 00000000..bff6243b --- /dev/null +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java @@ -0,0 +1,92 @@ +package de.hft.stuttgart.citydoctor2.database; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.InvalidPathException; +import java.nio.file.Paths; +import java.util.Properties; + +public final class DataBaseSettings { + + private static final Logger logger = LogManager.getLogger(DataBaseSettings.class); + + private static final Properties props; + + private DataBaseSettings() {} + + static { + props = new Properties(); + + File propFile = new File("DBSettings.properties"); + if (propFile.exists()) { + try (BufferedReader bis = new BufferedReader(new FileReader(propFile))) { + props.load(bis); + } catch (IOException e) { + logger.error("Failed to load database settings", e); + } + } +// props.setProperty("name","cd_db"); +// props.setProperty("directory","/database/"); +// props.setProperty("poolSize","10"); +// props.setProperty("inMemoryMode","false"); +// props.setProperty("attemptFallback","true"); +// props.setProperty("debugMode","false"); + } + + public static EmbeddedDatabaseConfiguration getConfig() { + EmbeddedDatabaseConfiguration defaultConfig = new EmbeddedDatabaseConfiguration(); + if (props.isEmpty()) { + logger.debug("No database settings found, using default configuration"); + return defaultConfig; + } + String name = props.getProperty("name"); + String directory = props.getProperty("directory"); + String poolSizeString = props.getProperty("poolSize"); + String inMemoryModeString = props.getProperty("inMemoryMode"); + String attemptFallbackString = props.getProperty("attemptFallback"); + String debugModeString = props.getProperty("debugMode"); + + boolean inMemoryMode = Boolean.parseBoolean(inMemoryModeString); + boolean attemptFallback = attemptFallbackString == null || Boolean.parseBoolean(attemptFallbackString); + boolean debugMode = Boolean.parseBoolean(debugModeString); + int poolSize; + try { + poolSize = Integer.parseInt(poolSizeString); + if (poolSize < 1) { + //TODO: Localize String + logger.warn("poolSize property cannot be less than 1, using default value"); + poolSize = defaultConfig.connectionPoolSize(); + } + } catch (NumberFormatException e) { + //TODO: Localize String + logger.warn("poolSize property must be an integer, using default value"); + poolSize = defaultConfig.connectionPoolSize(); + } + + if (name == null || name.isBlank()){ + name = defaultConfig.databaseName(); + } + if (directory == null || directory.isBlank()){ + directory = defaultConfig.databaseDirectory(); + } + try { + Paths.get(directory, name); + } catch (InvalidPathException e) { + if (!inMemoryMode) { + // Only warn if in-memory mode is not being used + //TODO: Localize String + logger.warn("Properties for directory and name do not form a valid path, using default values"); + name = defaultConfig.databaseName(); + directory = defaultConfig.databaseDirectory(); + } + } + + return new EmbeddedDatabaseConfiguration(name, directory, poolSize, inMemoryMode, attemptFallback, debugMode); + } + +} diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java new file mode 100644 index 00000000..36ddc571 --- /dev/null +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java @@ -0,0 +1,47 @@ +package de.hft.stuttgart.citydoctor2.database; + +import java.io.File; + +/** + * Record containing the configuration parameters for the setup of the embedded database + * @param databaseName Name of the database + * @param databaseDirectory Directory of the database. Relative paths will be resolved from the current working directory. + * CityDoctor will create the directory if it does not exist, and will overwrite an existing + * database-file if its name matches databaseName + * @param connectionPoolSize Size of the connection pool + * @param inMemoryMode If true, CityDoctor will create the embedded database in RAM + * @param attemptFallback If true, CityDoctor will attempt to create the embedded database in RAM if creation of the + * database-file fails. Will be ignored if inMemoryMode is true + * @param debugMode If true, CityDoctor will start the embedded database in automatic mixed mode to allow access to the + * database during runtime. + */ +public record EmbeddedDatabaseConfiguration(String databaseName, String databaseDirectory, int connectionPoolSize, + boolean inMemoryMode, boolean attemptFallback, boolean debugMode) { + + private static final String IN_MEMORY_PREFIX= "jdbc:h2:mem"; + private static final String AUTO_SERVER_PARAMETER = ";AUTO_SERVER=TRUE"; + /** + * Instantiates the default configuration for the embedded database. + */ + EmbeddedDatabaseConfiguration() { + this("cd_db", File.separator + "database"+ File.separator, Runtime.getRuntime().availableProcessors(), + false, true, false); + } + + public String getInMemoryH2Url() { + return IN_MEMORY_PREFIX + databaseName; + } + + public String getH2FileUrl(){ + return databaseDirectory + databaseName; + } + + public String getH2ParametersSuffix(){ + if (debugMode){ + return AUTO_SERVER_PARAMETER; + } else { + return ";"; + } + } + +} diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java index e7820948..e32251f0 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java @@ -8,6 +8,7 @@ import de.hft.stuttgart.citydoctor2.datastructure.CityObject; import de.hft.stuttgart.citydoctor2.datastructure.Geometry; import de.hft.stuttgart.citydoctor2.datastructure.GmlId; +import de.hft.stuttgart.citydoctor2.exceptions.EmbeddedDatabaseHandlerException; import de.hft.stuttgart.citydoctor2.utils.Localization; import org.apache.commons.lang3.SerializationException; import org.apache.commons.lang3.SerializationUtils; @@ -16,6 +17,7 @@ import org.apache.logging.log4j.Logger; import org.h2gis.functions.factory.H2GISDBFactory; import org.h2gis.utilities.wrapper.DataSourceWrapper; +import javax.sql.DataSource; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -34,34 +36,54 @@ import java.util.Map; public class EmbeddedDatabaseHandler { private static final Logger logger = LogManager.getLogger(EmbeddedDatabaseHandler.class); - private static final String DB_NAME = "/database/cd_db"; + private final EmbeddedDatabaseConfiguration config; private DataSourceWrapper dataSource; - public EmbeddedDatabaseHandler(){ + public EmbeddedDatabaseHandler(EmbeddedDatabaseConfiguration dbConfig){ + config = dbConfig; + String jdbcUrl; try { - String jdbcurl = H2GISDBFactory.createDataSource(DB_NAME, - true, ";AUTO_SERVER=TRUE").getConnection().getMetaData().getURL(); - - logger.debug("1] {}",jdbcurl); - HikariDataSource ds = new HikariDataSource(); - - ds.setJdbcUrl(jdbcurl); - ds.setUsername("sa"); - ds.setPassword("sa"); - int coreCount = Runtime.getRuntime().availableProcessors(); - ds.setMaximumPoolSize(coreCount*5); - ds.setMinimumIdle(coreCount*5); - - dataSource = new DataSourceWrapper(ds); - setupFeaturesTable(); - //Cleanup hook - Runtime.getRuntime().addShutdownHook(new Thread(ds::close)); + jdbcUrl = createEmbeddedDataBase(config.getH2FileUrl()); + logger.debug("1] {}",jdbcUrl); + } catch (Exception e) { logger.fatal(Localization.getText("DatabaseHandler.setupFailure")); logger.fatal(e.getMessage()); + if (config.attemptFallback()){ + logger.info("Attempting fallback to in-memory database"); + try { + jdbcUrl = createEmbeddedDataBase(config.getInMemoryH2Url()); + } catch (Exception e1) { + throw new EmbeddedDatabaseHandlerException("Fallback to in-memory database failed, embedded database could not be created", e1); + } + logger.info("Fallback to in-memory database succeeded"); + logger.warn("The in-memory database is limited by the system's available RAM. Loading big CityGML files can lead to OutOfMemory errors."); + } else { + throw new EmbeddedDatabaseHandlerException("Embedded database could not be created"); + } + } + setupHikariPool(jdbcUrl); + setupFeaturesTable(); + } + private void setupHikariPool(String jdbcUrl){ + HikariDataSource ds = new HikariDataSource(); + ds.setJdbcUrl(jdbcUrl); + ds.setUsername("sa"); + ds.setPassword("sa"); + ds.setMaximumPoolSize(config.connectionPoolSize()); + dataSource = new DataSourceWrapper(ds); + //Cleanup hook + Runtime.getRuntime().addShutdownHook(new Thread(ds::close)); + } + private String createEmbeddedDataBase(String dbUrl) throws SQLException { + DataSource source = H2GISDBFactory.createDataSource(dbUrl, + true, config.getH2ParametersSuffix()); + try (Connection con = source.getConnection()) { + return con.getMetaData().getURL(); + } } @@ -85,6 +107,7 @@ public class EmbeddedDatabaseHandler { } catch (SQLException e) { logger.fatal(Localization.getText("DatabaseHandler.tableFailure")); logger.fatal(e.getMessage()); + throw new EmbeddedDatabaseHandlerException("Could not setup features table in database", e); } } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/FeatureCache.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/FeatureCache.java index b797f208..96d6d930 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/FeatureCache.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/FeatureCache.java @@ -42,11 +42,15 @@ public class FeatureCache implements CityObjectCache{ private final ConcurrentHashMap evictionLocks = new ConcurrentHashMap<>(); public FeatureCache() { - handler = new EmbeddedDatabaseHandler(); + this(DataBaseSettings.getConfig()); + } + + public FeatureCache(EmbeddedDatabaseConfiguration dbConfig){ + handler = new EmbeddedDatabaseHandler(dbConfig); cache = Caffeine.newBuilder().maximumSize(3000).removalListener((GmlId key, CityObject value, RemovalCause cause) -> { - if (value != null && cause.wasEvicted() && isFeatureMarshallable(value)){ - handler.marshallCityObject(value); - } + if (value != null && cause.wasEvicted() && isFeatureMarshallable(value)){ + handler.marshallCityObject(value); + } }).build(handler::unmarshallCityObject); } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/exceptions/EmbeddedDatabaseHandlerException.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/exceptions/EmbeddedDatabaseHandlerException.java new file mode 100644 index 00000000..ffc6562e --- /dev/null +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/exceptions/EmbeddedDatabaseHandlerException.java @@ -0,0 +1,18 @@ +package de.hft.stuttgart.citydoctor2.exceptions; + +import java.io.Serial; + +public class EmbeddedDatabaseHandlerException extends RuntimeException{ + + @Serial + private static final long serialVersionUID = -7848325704234347425L; + + + public EmbeddedDatabaseHandlerException(String message) { + super(message); + } + + public EmbeddedDatabaseHandlerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/Citygml3FeatureMapper.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/Citygml3FeatureMapper.java index e27555ca..4ac8c7f9 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/Citygml3FeatureMapper.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/Citygml3FeatureMapper.java @@ -166,7 +166,7 @@ public class Citygml3FeatureMapper extends ObjectWalker { private static final Logger logger = LogManager.getLogger(Citygml3FeatureMapper.class); - private static FeatureCache featureCache = new FeatureCache(); + private static final FeatureCache featureCache = new FeatureCache(); private final CityDoctorModel model; private final ParserConfiguration config; private final Path directory; diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java index 06c0bb6e..a6286e01 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java @@ -29,12 +29,10 @@ import static org.junit.Assert.fail; public class EmbeddedDataBaseHandlerTest { - private EmbeddedDatabaseHandler handler; - + private EmbeddedDatabaseHandler handler = new EmbeddedDatabaseHandler(new EmbeddedDatabaseConfiguration()); @Before public void resetDatabase() { try { - handler = new EmbeddedDatabaseHandler(); handler.setupFeaturesTable(); } catch (Exception e) { fail(e.getMessage()); -- GitLab From baac7dede9ad5bc917140a19e1fca8d0ca979cde Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Wed, 10 Dec 2025 09:19:54 +0100 Subject: [PATCH 02/63] Feature: Add tempfile-mode flag to db settings --- .../citydoctor2/database/DataBaseSettings.java | 4 +++- .../database/EmbeddedDatabaseConfiguration.java | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java index bff6243b..9ba4d2a7 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java @@ -50,10 +50,12 @@ public final class DataBaseSettings { String inMemoryModeString = props.getProperty("inMemoryMode"); String attemptFallbackString = props.getProperty("attemptFallback"); String debugModeString = props.getProperty("debugMode"); + String tempModeString = props.getProperty("tempMode"); boolean inMemoryMode = Boolean.parseBoolean(inMemoryModeString); boolean attemptFallback = attemptFallbackString == null || Boolean.parseBoolean(attemptFallbackString); boolean debugMode = Boolean.parseBoolean(debugModeString); + boolean tempMode = Boolean.parseBoolean(tempModeString); int poolSize; try { poolSize = Integer.parseInt(poolSizeString); @@ -86,7 +88,7 @@ public final class DataBaseSettings { } } - return new EmbeddedDatabaseConfiguration(name, directory, poolSize, inMemoryMode, attemptFallback, debugMode); + return new EmbeddedDatabaseConfiguration(name, directory, poolSize, inMemoryMode, attemptFallback, debugMode, tempMode); } } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java index 36ddc571..fb487934 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java @@ -14,9 +14,11 @@ import java.io.File; * database-file fails. Will be ignored if inMemoryMode is true * @param debugMode If true, CityDoctor will start the embedded database in automatic mixed mode to allow access to the * database during runtime. + * @param tempFileMode If true, CityDoctor will create the database in the system's temp directory. Will be ignored if + * inMemoryMode is true */ public record EmbeddedDatabaseConfiguration(String databaseName, String databaseDirectory, int connectionPoolSize, - boolean inMemoryMode, boolean attemptFallback, boolean debugMode) { + boolean inMemoryMode, boolean attemptFallback, boolean debugMode, boolean tempFileMode) { private static final String IN_MEMORY_PREFIX= "jdbc:h2:mem"; private static final String AUTO_SERVER_PARAMETER = ";AUTO_SERVER=TRUE"; @@ -25,7 +27,12 @@ public record EmbeddedDatabaseConfiguration(String databaseName, String database */ EmbeddedDatabaseConfiguration() { this("cd_db", File.separator + "database"+ File.separator, Runtime.getRuntime().availableProcessors(), - false, true, false); + false, true, false, false); + } + + public static EmbeddedDatabaseConfiguration getTestConfig() { + return new EmbeddedDatabaseConfiguration("cd_db", File.separator + "database"+ File.separator, Runtime.getRuntime().availableProcessors(), + true, true, true, false); } public String getInMemoryH2Url() { -- GitLab From 667cac627d1282f79d4eedc8de26acdd0138c84f Mon Sep 17 00:00:00 2001 From: Matthias Betz Date: Wed, 10 Dec 2025 14:18:30 +0100 Subject: [PATCH 03/63] write GUI check config in settings --- .../citydoctor2/CityDoctorValidation.java | 4 +-- .../citydoctor2/CityDoctorValidationCLI.java | 2 +- .../check/ValidationConfiguration.java | 35 +++++++++++++------ .../citydoctor2/check/FilterTest.java | 4 +-- .../check/ValidationConfigurationTest.java | 6 ++-- .../citydoctor2/gui/CheckDialog.java | 30 ++++++++++------ .../stuttgart/citydoctor2/gui/Settings.java | 6 ++-- .../stuttgart/citydoctor2/healer/Healer.java | 2 +- 8 files changed, 57 insertions(+), 32 deletions(-) diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidation.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidation.java index ec2d0df0..548fee23 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidation.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidation.java @@ -133,7 +133,7 @@ public class CityDoctorValidation { config = ValidationConfiguration.loadStandardValidationConfig(); config.setUseStreaming(true); } else { - config = ValidationConfiguration.loadValidationConfig(validationConfigFile.getAbsolutePath()); + config = ValidationConfiguration.loadValidationConfigFile(validationConfigFile.getAbsolutePath()); } String xmlOutputPath = null; if (xmlOutput != null) { @@ -234,7 +234,7 @@ public class CityDoctorValidation { logger.error(Localization.getText("CityDoctorValidation.notExactlyOneConfigFile")); System.exit(4); } - return ValidationConfiguration.loadValidationConfig(configFiles.get(0)); + return ValidationConfiguration.loadValidationConfigFile(configFiles.get(0)); } else { if (optional) { logger.warn(Localization.getText("CityDoctorValidation.loadingDefaultConfig")); diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidationCLI.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidationCLI.java index 9003eb7b..5c606fda 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidationCLI.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidationCLI.java @@ -47,7 +47,7 @@ public class CityDoctorValidationCLI implements Runnable { if(config == null){ valConfig=ValidationConfiguration.loadStandardValidationConfig(); } else{ - valConfig=ValidationConfiguration.loadValidationConfig(config.getAbsolutePath()); + valConfig=ValidationConfiguration.loadValidationConfigFile(config.getAbsolutePath()); } CityDoctorValidation.startValidationProcess(input, xmlReport, pdfReport, valConfig,output); } catch (CityDoctorWriteException | CityGmlParseException | IOException | InvalidGmlFileException e) { diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/ValidationConfiguration.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/ValidationConfiguration.java index dec4e670..2e12266e 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/ValidationConfiguration.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/ValidationConfiguration.java @@ -53,7 +53,6 @@ public class ValidationConfiguration implements Serializable { private static final long serialVersionUID = -8020055032177740646L; private static final Logger logger = LogManager.getLogger(ValidationConfiguration.class); - private Map globalParameters = new HashMap<>(); private boolean xmlValidation = false; private boolean useStreaming = false; @@ -62,9 +61,16 @@ public class ValidationConfiguration implements Serializable { private ParserConfiguration parserConfig; - public static ValidationConfiguration loadValidationConfig(String validationFile) throws FileNotFoundException { + public static ValidationConfiguration loadValidationConfigFile(String validationFile) throws FileNotFoundException { return loadValidationConfig(new FileInputStream(validationFile)); } + + public static ValidationConfiguration loadValidationConfigFromString(String validationString) throws FileNotFoundException { + Yaml yaml = new Yaml(new Constructor(ValidationConfiguration.class, new LoaderOptions())); + ValidationConfiguration config = yaml.load(validationString); + config.validateConfiguration(); + return config; + } public static ValidationConfiguration loadValidationConfig(InputStream stream) { Yaml yaml = new Yaml(new Constructor(ValidationConfiguration.class, new LoaderOptions())); @@ -91,20 +97,30 @@ public class ValidationConfiguration implements Serializable { config.setSchematronFilePathInGlobalParameters(CHECK_FOR_SOLID_XML); return config; } - + private ValidationConfiguration() { } public void saveAs(File f) throws IOException { + Yaml yaml = prepareDumpingOfConfig(); + try (BufferedWriter bw = new BufferedWriter(new FileWriter(f))) { + yaml.dump(this, bw); + } + } + + public String saveAsString() throws IOException { + Yaml yaml = prepareDumpingOfConfig(); + return yaml.dump(this); + } + + private Yaml prepareDumpingOfConfig() { DumperOptions options = new DumperOptions(); options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); options.setPrettyFlow(true); Representer rep = new ValidationConfigurationRepresenter(options); rep.addClassTag(ValidationConfiguration.class, Tag.MAP); Yaml yaml = new Yaml(rep); - try (BufferedWriter bw = new BufferedWriter(new FileWriter(f))) { - yaml.dump(this, bw); - } + return yaml; } public boolean isXmlValidation() { @@ -141,7 +157,7 @@ public class ValidationConfiguration implements Serializable { public Map getGlobalParameters() { return globalParameters; } - + public void setGlobalParameters(Map globalParameters) { this.globalParameters = globalParameters; } @@ -174,8 +190,7 @@ public class ValidationConfiguration implements Serializable { }); } globalParameters.putIfAbsent(GlobalParameters.MIN_VERTEX_DISTANCE, MIN_VERTEX_DISTANCE_DEFAULT); - globalParameters.putIfAbsent(GlobalParameters.NUMBER_OF_ROUNDING_PLACES, - NUMBER_OF_ROUNDING_PLACES_DEFAULT); + globalParameters.putIfAbsent(GlobalParameters.NUMBER_OF_ROUNDING_PLACES, NUMBER_OF_ROUNDING_PLACES_DEFAULT); } public int getNumberOfRoundingPlaces() { @@ -220,7 +235,7 @@ public class ValidationConfiguration implements Serializable { public void setSchematronFilePathInGlobalParameters(String string) { globalParameters.put(GlobalParameters.SCHEMATRON_PATH, string); } - + public void setRequirements(Map requirements) { this.requirements = requirements; } diff --git a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/FilterTest.java b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/FilterTest.java index b8c2d367..bc25f449 100644 --- a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/FilterTest.java +++ b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/FilterTest.java @@ -39,7 +39,7 @@ public class FilterTest { @Test public void testFilterExclude() throws CityGmlParseException, IOException, InvalidGmlFileException { ValidationConfiguration config = ValidationConfiguration - .loadValidationConfig("src/test/resources/testConfigWithExclude.yml"); + .loadValidationConfigFile("src/test/resources/testConfigWithExclude.yml"); CityDoctorModel model = CityGmlParser.parseCityGmlFile("src/test/resources/testarea.gml", new ParserConfiguration(8, false)); Checker c = new Checker(config, model); @@ -59,7 +59,7 @@ public class FilterTest { @Test public void testFilterInclude() throws CityGmlParseException, IOException, InvalidGmlFileException { ValidationConfiguration config = ValidationConfiguration - .loadValidationConfig("src/test/resources/testConfigWithInclude.yml"); + .loadValidationConfigFile("src/test/resources/testConfigWithInclude.yml"); CityDoctorModel model = CityGmlParser.parseCityGmlFile("src/test/resources/testarea.gml", new ParserConfiguration(8, false)); Checker c = new Checker(config, model); diff --git a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/ValidationConfigurationTest.java b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/ValidationConfigurationTest.java index 003dda31..6b49cc87 100644 --- a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/ValidationConfigurationTest.java +++ b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/ValidationConfigurationTest.java @@ -37,7 +37,7 @@ public class ValidationConfigurationTest { @Test public void testLoadingConfig() throws FileNotFoundException { String file = "src/test/resources/testConfig.yml"; - ValidationConfiguration config = ValidationConfiguration.loadValidationConfig(file); + ValidationConfiguration config = ValidationConfiguration.loadValidationConfigFile(file); assertEquals(8, config.getNumberOfRoundingPlaces()); assertTrue(config.getRequirements().containsKey(RequirementId.R_GE_R_TOO_FEW_POINTS.toString())); assertFalse(config.getRequirements().get(RequirementId.R_GE_P_NON_PLANAR.toString()).isEnabled()); @@ -47,7 +47,7 @@ public class ValidationConfigurationTest { @Test public void testLoadingConfigWithFilter() throws FileNotFoundException { String file = "src/test/resources/testConfigWithFilter.yml"; - ValidationConfiguration config = ValidationConfiguration.loadValidationConfig(file); + ValidationConfiguration config = ValidationConfiguration.loadValidationConfigFile(file); assertEquals(8, config.getNumberOfRoundingPlaces()); assertTrue(config.getRequirements().containsKey(RequirementId.R_GE_R_TOO_FEW_POINTS.toString())); assertFalse(config.getRequirements().get(RequirementId.R_GE_P_NON_PLANAR.toString()).isEnabled()); @@ -76,7 +76,7 @@ public class ValidationConfigurationTest { String file = "src/test/resources/testConfigVulnerability.yml"; ValidationConfiguration config = null; try { - config = ValidationConfiguration.loadValidationConfig(file); + config = ValidationConfiguration.loadValidationConfigFile(file); assertNotNull(config); fail("Config with Vulnerability should not be successfully loaded"); } catch (FileNotFoundException e){ diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CheckDialog.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CheckDialog.java index fdfd46aa..edc94a4c 100644 --- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CheckDialog.java +++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CheckDialog.java @@ -3,7 +3,6 @@ package de.hft.stuttgart.citydoctor2.gui; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -11,14 +10,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import de.hft.stuttgart.citydoctor2.parser.ProgressListener; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.scene.control.Alert; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ProgressIndicator; -import javafx.scene.control.Tooltip; -import javafx.stage.DirectoryChooser; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -32,6 +23,7 @@ import de.hft.stuttgart.citydoctor2.check.Unit; import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration; import de.hft.stuttgart.citydoctor2.checks.Checks; import de.hft.stuttgart.citydoctor2.gui.tree.TreeRequirement; +import de.hft.stuttgart.citydoctor2.parser.ProgressListener; import de.hft.stuttgart.citydoctor2.utils.Localization; import javafx.application.Platform; import javafx.event.Event; @@ -39,6 +31,7 @@ import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.control.ScrollPane; @@ -256,6 +249,15 @@ public class CheckDialog { }); schematronField.setText(ValidationConfiguration.CHECK_FOR_SOLID_XML); + + String configString = Settings.get(Settings.CONFIG); + if (configString != null) { + try { + applyConfig(ValidationConfiguration.loadValidationConfigFromString(configString)); + } catch (IOException e) { + // failed to read config, ignore as default values are still available + } + } } private void setupValueColumns(List> columns) { @@ -319,7 +321,7 @@ public class CheckDialog { if (f != null) { Settings.set(Settings.LAST_OPEN_FOLDER, f.getParent()); try { - ValidationConfiguration config = ValidationConfiguration.loadValidationConfig(f.getAbsolutePath()); + ValidationConfiguration config = ValidationConfiguration.loadValidationConfigFile(f.getAbsolutePath()); applyConfig(config); } catch (IOException e) { window.showExceptionDialog(e); @@ -440,9 +442,15 @@ public class CheckDialog { private void setupCheckButton() { checkBtn.setOnAction(ea -> { - ValidationConfiguration config = createConfig(); checkBtn.setDisable(true); cancelBtn.setDisable(true); + ValidationConfiguration config = createConfig(); + try { + String configString = config.saveAsString(); + Settings.set(Settings.CONFIG, configString); + } catch (IOException e) { + // failed to save config, ignore + } stage.setOnCloseRequest(Event::consume); Thread t = new Thread(() -> { diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/Settings.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/Settings.java index 0ce59586..7466d96c 100644 --- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/Settings.java +++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/Settings.java @@ -6,6 +6,7 @@ import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Properties; import org.apache.logging.log4j.LogManager; @@ -22,6 +23,7 @@ public class Settings { public static final String FRAME_X = "frameX"; public static final String FRAME_Y = "frameY"; public static final String LANGUAGE = "language"; + public static final String CONFIG = "config"; private static Properties props; @@ -30,14 +32,14 @@ public class Settings { File propFile = new File("GUISettings.properties"); if (propFile.exists()) { - try (BufferedReader bis = new BufferedReader(new FileReader(propFile))) { + try (BufferedReader bis = new BufferedReader(new FileReader(propFile, StandardCharsets.UTF_8))) { props.load(bis); } catch (IOException e) { logger.error("Failed to load settings", e); } } Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try (BufferedWriter bw = new BufferedWriter(new FileWriter(propFile))) { + try (BufferedWriter bw = new BufferedWriter(new FileWriter(propFile, StandardCharsets.UTF_8))) { props.store(bw, "GUI configuration"); } catch (IOException e) { logger.error("Failed to save settings", e); diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java index 98a7c883..7ac14f87 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java @@ -156,7 +156,7 @@ public class Healer { if (configFile == null) { config = ValidationConfiguration.loadStandardValidationConfig(); } else { - config = ValidationConfiguration.loadValidationConfig(configFile.getAbsolutePath()); + config = ValidationConfiguration.loadValidationConfigFile(configFile.getAbsolutePath()); } CityDoctorModel model = CityGmlParser.parseCityGmlFile(in.getAbsolutePath(), config.getParserConfiguration()); Checker c = new Checker(config, model); -- GitLab From 664abca9844c5f1d3ce21e657a66a05f1e14d1f9 Mon Sep 17 00:00:00 2001 From: Matthias Betz Date: Wed, 10 Dec 2025 14:24:24 +0100 Subject: [PATCH 04/63] disable autopro --- .../citydoctor2/gui/AutoProWindow.java | 6 +++--- .../Extensions/CityDoctorHealerGUI/pom.xml | 4 ++-- .../healer/gui/HealerController.java | 17 +++++++---------- .../citydoctor2/healer/gui/HealerToolbar.java | 1 + .../healer/genetic/gui/GeneticsGUI.java | 2 +- CityDoctorParent/pom.xml | 1 - 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/CityDoctorParent/Extensions/CityDoctorAutoPro/src/main/java/de/hft/stuttgart/citydoctor2/gui/AutoProWindow.java b/CityDoctorParent/Extensions/CityDoctorAutoPro/src/main/java/de/hft/stuttgart/citydoctor2/gui/AutoProWindow.java index 08a586a5..d8c23b22 100644 --- a/CityDoctorParent/Extensions/CityDoctorAutoPro/src/main/java/de/hft/stuttgart/citydoctor2/gui/AutoProWindow.java +++ b/CityDoctorParent/Extensions/CityDoctorAutoPro/src/main/java/de/hft/stuttgart/citydoctor2/gui/AutoProWindow.java @@ -362,7 +362,7 @@ public class AutoProWindow extends JDialog implements ItemListener { */ private void startCppAutoProMethods() throws FileNotFoundException { - List buildings = this.model.getBuildings(); + List buildings = this.model.getBuildings().toList(); List geometryList = getAllGeometriesOfSelectedBuildings(buildings); setBuildingsToOrigin(geometryList); @@ -525,12 +525,12 @@ public class AutoProWindow extends JDialog implements ItemListener { Building building = part.getParent(); building.getBuildingParts().remove(part); if (building.getBuildingParts().isEmpty() && building.getGeometries().isEmpty()) { - model.getBuildings().remove(building); + model.getBuildings().toList().remove(building); } } else if (parent instanceof Building && parent.getGeometries().isEmpty()) { Building building = (Building) parent; if (building.getBuildingParts().isEmpty()) { - model.getBuildings().remove(building); + model.getBuildings().toList().remove(building); } } } diff --git a/CityDoctorParent/Extensions/CityDoctorHealerGUI/pom.xml b/CityDoctorParent/Extensions/CityDoctorHealerGUI/pom.xml index d8113362..c75ed10f 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealerGUI/pom.xml +++ b/CityDoctorParent/Extensions/CityDoctorHealerGUI/pom.xml @@ -22,10 +22,10 @@ de.hft.stuttgart CityDoctorHealer - + org.openjfx javafx-controls diff --git a/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerController.java b/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerController.java index 5668a457..f495692c 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerController.java +++ b/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerController.java @@ -2,36 +2,33 @@ package de.hft.stuttgart.citydoctor2.healer.gui; import java.util.ArrayList; import java.util.EnumMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import de.hft.stuttgart.citydoctor2.check.Check; import de.hft.stuttgart.citydoctor2.check.CheckError; import de.hft.stuttgart.citydoctor2.check.CheckId; -import de.hft.stuttgart.citydoctor2.check.RequirementType; import de.hft.stuttgart.citydoctor2.check.Checker; import de.hft.stuttgart.citydoctor2.check.Requirement; +import de.hft.stuttgart.citydoctor2.check.RequirementType; import de.hft.stuttgart.citydoctor2.datastructure.AbstractBuilding; import de.hft.stuttgart.citydoctor2.datastructure.BoundarySurface; import de.hft.stuttgart.citydoctor2.datastructure.Building; -import de.hft.stuttgart.citydoctor2.datastructure.Installation; import de.hft.stuttgart.citydoctor2.datastructure.BuildingPart; import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel; import de.hft.stuttgart.citydoctor2.datastructure.CityObject; import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon; import de.hft.stuttgart.citydoctor2.datastructure.Geometry; +import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation; import de.hft.stuttgart.citydoctor2.datastructure.GeometryType; import de.hft.stuttgart.citydoctor2.datastructure.GmlId; +import de.hft.stuttgart.citydoctor2.datastructure.Installation; import de.hft.stuttgart.citydoctor2.datastructure.LinkedPolygon; import de.hft.stuttgart.citydoctor2.datastructure.Lod; import de.hft.stuttgart.citydoctor2.datastructure.Opening; import de.hft.stuttgart.citydoctor2.datastructure.Polygon; -import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation; -import de.hft.stuttgart.citydoctor2.gui.AutoProWindow; import de.hft.stuttgart.citydoctor2.gui.HighlightController; import de.hft.stuttgart.citydoctor2.gui.ListErrorVisitor; import de.hft.stuttgart.citydoctor2.gui.TriangulatedGeometry; @@ -86,10 +83,10 @@ public class HealerController { if (model == null) { return; } - model.createFeatureStream().forEach(CityObject::prepareForChecking); - AutoProWindow window = new AutoProWindow(model); - window.setLocationRelativeTo(null); - window.setVisible(true); +// model.createFeatureStream().forEach(CityObject::prepareForChecking); +// AutoProWindow window = new AutoProWindow(model); +// window.setLocationRelativeTo(null); +// window.setVisible(true); } public void setCurrentModel(CityDoctorModel model) { diff --git a/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerToolbar.java b/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerToolbar.java index ab129dc6..c94cfe66 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerToolbar.java +++ b/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerToolbar.java @@ -74,6 +74,7 @@ public class HealerToolbar { saveBtn.setDisable(true); saveImage.setImage(new Image(MainWindow.class.getResourceAsStream("icons/save.png"))); simplyBtn.setOnAction(ae -> controller.showSimplyfication()); + simplyBtn.setDisable(true); } public HBox getToolBar() { diff --git a/CityDoctorParent/Extensions/CityDoctorHealerGenetic/src/main/java/de/hft/stuttgart/citydoctor2/healer/genetic/gui/GeneticsGUI.java b/CityDoctorParent/Extensions/CityDoctorHealerGenetic/src/main/java/de/hft/stuttgart/citydoctor2/healer/genetic/gui/GeneticsGUI.java index c9a65976..065224ad 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealerGenetic/src/main/java/de/hft/stuttgart/citydoctor2/healer/genetic/gui/GeneticsGUI.java +++ b/CityDoctorParent/Extensions/CityDoctorHealerGenetic/src/main/java/de/hft/stuttgart/citydoctor2/healer/genetic/gui/GeneticsGUI.java @@ -329,7 +329,7 @@ public class GeneticsGUI extends View { parserConfig); currentChecker = new Checker(model1); currentChecker.runChecks(); - Building building = model1.getBuildings().toList().getFirst(); + Building building = model1.getBuildings().findFirst().get(); currentGeometry = building.getGeometries().get(0); currentFeature = building; showCurrentGeometry(); diff --git a/CityDoctorParent/pom.xml b/CityDoctorParent/pom.xml index e7728964..d69e27d2 100644 --- a/CityDoctorParent/pom.xml +++ b/CityDoctorParent/pom.xml @@ -285,7 +285,6 @@ org.apache.logging.log4j log4j-slf4j18-impl - 2.18.0 test -- GitLab From 1c6652197f7810b944aaebc1ddedd427b3467268 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Wed, 10 Dec 2025 16:36:19 +0100 Subject: [PATCH 05/63] Refactor: Rename properties --- .../citydoctor2/database/DataBaseSettings.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java index 9ba4d2a7..9784e816 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DataBaseSettings.java @@ -44,13 +44,13 @@ public final class DataBaseSettings { logger.debug("No database settings found, using default configuration"); return defaultConfig; } - String name = props.getProperty("name"); - String directory = props.getProperty("directory"); - String poolSizeString = props.getProperty("poolSize"); - String inMemoryModeString = props.getProperty("inMemoryMode"); - String attemptFallbackString = props.getProperty("attemptFallback"); - String debugModeString = props.getProperty("debugMode"); - String tempModeString = props.getProperty("tempMode"); + String name = props.getProperty("database.name"); + String directory = props.getProperty("database.directory"); + String poolSizeString = props.getProperty("database.connectionPoolSize"); + String inMemoryModeString = props.getProperty("database.inMemoryMode"); + String attemptFallbackString = props.getProperty("database.attemptFallback"); + String debugModeString = props.getProperty("database.debugMode"); + String tempModeString = props.getProperty("database.tempMode"); boolean inMemoryMode = Boolean.parseBoolean(inMemoryModeString); boolean attemptFallback = attemptFallbackString == null || Boolean.parseBoolean(attemptFallbackString); -- GitLab From d240ea97de33c8a739380ccdbe09c9c05f4231ae Mon Sep 17 00:00:00 2001 From: Matthias Betz Date: Wed, 10 Dec 2025 16:38:05 +0100 Subject: [PATCH 06/63] add workaround for unserializable Attributes and TextContent --- .../BridgeConstructiveElement.java | 34 +- .../citydoctor2/utils/CityGmlUtils.java | 339 +++--- .../java/org/xmlobjects/xml/Attributes.java | 74 ++ .../java/org/xmlobjects/xml/TextContent.java | 1049 +++++++++++++++++ .../BridgeConstructiveElementTest.java | 30 + 5 files changed, 1330 insertions(+), 196 deletions(-) create mode 100644 CityDoctorParent/CityDoctorModel/src/main/java/org/xmlobjects/xml/Attributes.java create mode 100644 CityDoctorParent/CityDoctorModel/src/main/java/org/xmlobjects/xml/TextContent.java create mode 100644 CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BridgeConstructiveElementTest.java diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/BridgeConstructiveElement.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/BridgeConstructiveElement.java index 01b892c7..b3c61c6a 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/BridgeConstructiveElement.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/BridgeConstructiveElement.java @@ -18,13 +18,10 @@ */ package de.hft.stuttgart.citydoctor2.datastructure; -import de.hft.stuttgart.citydoctor2.check.CheckableVisitor; -import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; -import de.hft.stuttgart.citydoctor2.utils.CityGmlUtils; -import javafx.scene.paint.Color; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.citygml4j.core.model.core.AbstractSpaceBoundaryProperty; +import java.io.Serial; +import java.util.ArrayList; +import java.util.List; + import org.citygml4j.core.model.deprecated.bridge.DeprecatedPropertiesOfBridgeConstructiveElement; import org.citygml4j.core.util.geometry.GeometryFactory; import org.xmlobjects.gml.model.geometry.GeometryProperty; @@ -34,14 +31,13 @@ import org.xmlobjects.gml.model.geometry.complexes.CompositeSurface; import org.xmlobjects.gml.model.geometry.primitives.Solid; import org.xmlobjects.gml.model.geometry.primitives.SolidProperty; -import java.io.Serial; -import java.util.ArrayList; -import java.util.List; +import de.hft.stuttgart.citydoctor2.check.CheckableVisitor; +import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; +import de.hft.stuttgart.citydoctor2.utils.CityGmlUtils; +import javafx.scene.paint.Color; public class BridgeConstructiveElement extends CityObject { - private static final Logger logger = LogManager.getLogger(BridgeConstructiveElement.class); - private static final String CANNOT_ADD = "Cannot add "; @Serial @@ -105,20 +101,6 @@ public class BridgeConstructiveElement extends CityObject { } } - private void reCreateBoundarySurface(GeometryFactory factory, ParserConfiguration config, BoundarySurface bs) { - if (bs.getGeometries().isEmpty()) { - for (AbstractSpaceBoundaryProperty bsp : gmlBridgeElement.getBoundaries()) { - if (bsp.getObject() != null && bsp.getObject() == bs.getGmlObject()) { - logger.warn("Found empty boundary surface: {}, removing from BridgeConstructiveElement", bs.getGmlId()); - gmlBridgeElement.getBoundaries().remove(bsp); - break; - } - } - return; - } - bs.reCreateGeometries(factory, config); - } - private void setCompositeSurfaceAccordingToLod(Geometry geom, CompositeSurface cs) { switch (geom.getLod()) { case LOD1: diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/utils/CityGmlUtils.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/utils/CityGmlUtils.java index 564e68df..33a1e3ba 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/utils/CityGmlUtils.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/utils/CityGmlUtils.java @@ -41,175 +41,174 @@ import java.util.Set; */ public final class CityGmlUtils { - - private CityGmlUtils() { - // util class - } - - public static org.xmlobjects.gml.model.geometry.primitives.Polygon createGmlPolygon(GeometryFactory factory, - Polygon cdPoly, ParserConfiguration config) { - if (cdPoly.getExteriorRing() == null) { - return null; - } - org.xmlobjects.gml.model.geometry.primitives.Polygon gmlPoly = new org.xmlobjects.gml.model.geometry.primitives.Polygon(); - // exterior ring - LinearRing extLr = cdPoly.getExteriorRing(); - if (extLr.getVertices().size() < 3) { - // this ring does not have enough points in it - // this leads to errors when exporting therefore ignore it - return null; - } - org.xmlobjects.gml.model.geometry.primitives.LinearRing gmlLr = createGmlRing(factory, config, extLr); - gmlPoly.setExterior(new AbstractRingProperty(gmlLr)); - - // interior rings - for (LinearRing lr : cdPoly.getInnerRings()) { - gmlLr = createGmlRing(factory, config, lr); - if (lr.getVertices().size() < 3) { - // this ring does not have enough points in it - // this leads to errors when exporting therefore ignore it - return null; - } - gmlPoly.getInterior().add(new AbstractRingProperty(gmlLr)); - } - gmlPoly.setId(cdPoly.getGmlId().getGmlString()); - return gmlPoly; - } - - public static CompositeSurface createGmlComposite(GeometryFactory factory, CompositeCollection comp, - ParserConfiguration config) { - List surfaces = new ArrayList<>(); - for (ConcretePolygon cd : comp.getNonRecursiveCompositeMembers()) { - surfaces.add(new SurfaceProperty(createGmlPolygon(factory, cd, config))); - } - for (CompositeCollection cc : comp.getChildComposites()) { - surfaces.add(new SurfaceProperty(createGmlComposite(factory, cc, config))); - } - if (surfaces.isEmpty()) { - return null; - } - return new CompositeSurface(surfaces); - } - - - public static org.xmlobjects.gml.model.geometry.primitives.LinearRing createGmlRing(GeometryFactory factory, - ParserConfiguration config, LinearRing lr) { - - ProjCoordinate p1 = new ProjCoordinate(); - ProjCoordinate p2 = new ProjCoordinate(); - List ringValues = new ArrayList<>(); - BasicCoordinateTransform trans = config.getOriginalTransform(); - for (Vertex v : lr.getVertices()) { - double x = v.getX(); - double y = v.getY(); - double z = v.getZ(); - if (trans != null) { - p1.x = x; - p1.y = y; - trans.transform(p1, p2); - x = p2.x; - y = p2.y; - z = z * config.getFromMeters(); - } - ringValues.add(x); - ringValues.add(y); - ringValues.add(z); - } - - org.xmlobjects.gml.model.geometry.primitives.LinearRing gmlLr = factory.createLinearRing(ringValues, 3); - gmlLr.setId(lr.getGmlId().getGmlString()); - return gmlLr; - } - - public static Solid createSolid(Geometry geom, GeometryFactory factory, ParserConfiguration config) { - if (geom.getType() != GeometryType.SOLID) { - throw new IllegalArgumentException("Only solids are allowed"); - } - - CompositeSurface comp = new CompositeSurface(); - List surfaceMember = comp.getSurfaceMembers(); - for (Polygon cdPoly : geom.getPolygons()) { - if (!cdPoly.isLink()) { - org.xmlobjects.gml.model.geometry.primitives.Polygon gmlPoly = createGmlPolygon(factory, cdPoly, config); - if (gmlPoly != null) { - surfaceMember.add(new SurfaceProperty(gmlPoly)); - } - } else { - // add reference to polygon - surfaceMember.add(new SurfaceProperty("#" + cdPoly.getGmlId().getGmlString())); - } - } - - if (surfaceMember.isEmpty()) { - return null; - } - Solid solid = new Solid(); - Shell shell = new Shell(surfaceMember); - solid.setExterior(new ShellProperty(shell)); - solid.setId(geom.getGmlId().getGmlString()); - return solid; - } - - public static MultiSurface createMultiSurface(Geometry geom, GeometryFactory factory, - ParserConfiguration config) { - if (geom.getType() != GeometryType.MULTI_SURFACE && geom.getType() != GeometryType.COMPOSITE_SURFACE) { - throw new IllegalArgumentException("This can only handle MultiSurfaces"); - } - List surfaces = new ArrayList<>(); - Set compositeCollections = new HashSet<>(); - for (Polygon cdPoly : geom.getPolygons()) { - if (cdPoly instanceof ConcretePolygon conc && conc.isCompositeMember()) { - compositeCollections.add(conc.getPartOfComposite()); - } else if (!cdPoly.isLink()) { - // is not part of a boundary surface - org.xmlobjects.gml.model.geometry.primitives.Polygon gmlPoly = createGmlPolygon(factory, cdPoly, config); - if (gmlPoly != null) { - surfaces.add(new SurfaceProperty(gmlPoly)); - } - } else { - // add reference to polygon - surfaces.add(new SurfaceProperty("#" + cdPoly.getGmlId().getGmlString())); - } - } - for (CompositeCollection cdPoly : compositeCollections) { - surfaces.add(new SurfaceProperty(createGmlComposite(factory, cdPoly, config))); - } - if (surfaces.isEmpty()) { - return null; - } - MultiSurface ms = new MultiSurface(surfaces); - ms.setId(geom.getGmlId().getGmlString()); - return ms; - } - - public static MultiSurface createMultiSurface(List polygons, GeometryFactory factory, - ParserConfiguration config) { - List surfaces = new ArrayList<>(); - for (Polygon cdPoly : polygons) { - org.xmlobjects.gml.model.geometry.primitives.Polygon gmlPoly = createGmlPolygon(factory, cdPoly, config); - if (gmlPoly != null) { - surfaces.add(new SurfaceProperty(gmlPoly)); - } - } - if (surfaces.isEmpty()) { - return null; - } - return new MultiSurface(surfaces); - } - - public static CompositeSurface createCompositeSurface(Geometry geom, GeometryFactory factory, - ParserConfiguration config) { - List surfaces = new ArrayList<>(); - for (Polygon cdPoly : geom.getPolygons()) { - org.xmlobjects.gml.model.geometry.primitives.Polygon gmlPoly = createGmlPolygon(factory, cdPoly, config); - if (gmlPoly != null) { - surfaces.add(new SurfaceProperty(gmlPoly)); - } - } - if (surfaces.isEmpty()) { - return null; - } - return new CompositeSurface(surfaces); - } + private CityGmlUtils() { + // util class + } + + public static org.xmlobjects.gml.model.geometry.primitives.Polygon createGmlPolygon(GeometryFactory factory, + Polygon cdPoly, ParserConfiguration config) { + if (cdPoly.getExteriorRing() == null) { + return null; + } + org.xmlobjects.gml.model.geometry.primitives.Polygon gmlPoly = new org.xmlobjects.gml.model.geometry.primitives.Polygon(); + // exterior ring + LinearRing extLr = cdPoly.getExteriorRing(); + if (extLr.getVertices().size() < 3) { + // this ring does not have enough points in it + // this leads to errors when exporting therefore ignore it + return null; + } + org.xmlobjects.gml.model.geometry.primitives.LinearRing gmlLr = createGmlRing(factory, config, extLr); + gmlPoly.setExterior(new AbstractRingProperty(gmlLr)); + + // interior rings + for (LinearRing lr : cdPoly.getInnerRings()) { + gmlLr = createGmlRing(factory, config, lr); + if (lr.getVertices().size() < 3) { + // this ring does not have enough points in it + // this leads to errors when exporting therefore ignore it + return null; + } + gmlPoly.getInterior().add(new AbstractRingProperty(gmlLr)); + } + gmlPoly.setId(cdPoly.getGmlId().getGmlString()); + return gmlPoly; + } + + public static CompositeSurface createGmlComposite(GeometryFactory factory, CompositeCollection comp, + ParserConfiguration config) { + List surfaces = new ArrayList<>(); + for (ConcretePolygon cd : comp.getNonRecursiveCompositeMembers()) { + surfaces.add(new SurfaceProperty(createGmlPolygon(factory, cd, config))); + } + for (CompositeCollection cc : comp.getChildComposites()) { + surfaces.add(new SurfaceProperty(createGmlComposite(factory, cc, config))); + } + if (surfaces.isEmpty()) { + return null; + } + return new CompositeSurface(surfaces); + } + + public static org.xmlobjects.gml.model.geometry.primitives.LinearRing createGmlRing(GeometryFactory factory, + ParserConfiguration config, LinearRing lr) { + + ProjCoordinate p1 = new ProjCoordinate(); + ProjCoordinate p2 = new ProjCoordinate(); + List ringValues = new ArrayList<>(); + BasicCoordinateTransform trans = config.getOriginalTransform(); + for (Vertex v : lr.getVertices()) { + double x = v.getX(); + double y = v.getY(); + double z = v.getZ(); + if (trans != null) { + p1.x = x; + p1.y = y; + trans.transform(p1, p2); + x = p2.x; + y = p2.y; + z = z * config.getFromMeters(); + } + ringValues.add(x); + ringValues.add(y); + ringValues.add(z); + } + + org.xmlobjects.gml.model.geometry.primitives.LinearRing gmlLr = factory.createLinearRing(ringValues, 3); + gmlLr.setId(lr.getGmlId().getGmlString()); + return gmlLr; + } + + public static Solid createSolid(Geometry geom, GeometryFactory factory, ParserConfiguration config) { + if (geom.getType() != GeometryType.SOLID) { + throw new IllegalArgumentException("Only solids are allowed"); + } + + CompositeSurface comp = new CompositeSurface(); + List surfaceMember = comp.getSurfaceMembers(); + for (Polygon cdPoly : geom.getPolygons()) { + if (!cdPoly.isLink()) { + org.xmlobjects.gml.model.geometry.primitives.Polygon gmlPoly = createGmlPolygon(factory, cdPoly, + config); + if (gmlPoly != null) { + surfaceMember.add(new SurfaceProperty(gmlPoly)); + } + } else { + // add reference to polygon + surfaceMember.add(new SurfaceProperty("#" + cdPoly.getGmlId().getGmlString())); + } + } + + if (surfaceMember.isEmpty()) { + return null; + } + Solid solid = new Solid(); + Shell shell = new Shell(surfaceMember); + solid.setExterior(new ShellProperty(shell)); + solid.setId(geom.getGmlId().getGmlString()); + return solid; + } + + public static MultiSurface createMultiSurface(Geometry geom, GeometryFactory factory, ParserConfiguration config) { + if (geom.getType() != GeometryType.MULTI_SURFACE && geom.getType() != GeometryType.COMPOSITE_SURFACE) { + throw new IllegalArgumentException("This can only handle MultiSurfaces"); + } + List surfaces = new ArrayList<>(); + Set compositeCollections = new HashSet<>(); + for (Polygon cdPoly : geom.getPolygons()) { + if (cdPoly instanceof ConcretePolygon conc && conc.isCompositeMember()) { + compositeCollections.add(conc.getPartOfComposite()); + } else if (!cdPoly.isLink()) { + // is not part of a boundary surface + org.xmlobjects.gml.model.geometry.primitives.Polygon gmlPoly = createGmlPolygon(factory, cdPoly, + config); + if (gmlPoly != null) { + surfaces.add(new SurfaceProperty(gmlPoly)); + } + } else { + // add reference to polygon + surfaces.add(new SurfaceProperty("#" + cdPoly.getGmlId().getGmlString())); + } + } + for (CompositeCollection cdPoly : compositeCollections) { + surfaces.add(new SurfaceProperty(createGmlComposite(factory, cdPoly, config))); + } + if (surfaces.isEmpty()) { + return null; + } + MultiSurface ms = new MultiSurface(surfaces); + ms.setId(geom.getGmlId().getGmlString()); + return ms; + } + + public static MultiSurface createMultiSurface(List polygons, GeometryFactory factory, + ParserConfiguration config) { + List surfaces = new ArrayList<>(); + for (Polygon cdPoly : polygons) { + org.xmlobjects.gml.model.geometry.primitives.Polygon gmlPoly = createGmlPolygon(factory, cdPoly, config); + if (gmlPoly != null) { + surfaces.add(new SurfaceProperty(gmlPoly)); + } + } + if (surfaces.isEmpty()) { + return null; + } + return new MultiSurface(surfaces); + } + + public static CompositeSurface createCompositeSurface(Geometry geom, GeometryFactory factory, + ParserConfiguration config) { + List surfaces = new ArrayList<>(); + for (Polygon cdPoly : geom.getPolygons()) { + org.xmlobjects.gml.model.geometry.primitives.Polygon gmlPoly = createGmlPolygon(factory, cdPoly, config); + if (gmlPoly != null) { + surfaces.add(new SurfaceProperty(gmlPoly)); + } + } + if (surfaces.isEmpty()) { + return null; + } + return new CompositeSurface(surfaces); + } } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/org/xmlobjects/xml/Attributes.java b/CityDoctorParent/CityDoctorModel/src/main/java/org/xmlobjects/xml/Attributes.java new file mode 100644 index 00000000..d4460515 --- /dev/null +++ b/CityDoctorParent/CityDoctorModel/src/main/java/org/xmlobjects/xml/Attributes.java @@ -0,0 +1,74 @@ +package org.xmlobjects.xml; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; + +public class Attributes implements Serializable { + + private static final long serialVersionUID = 8942583366234714632L; + + private final Map> attributes = new HashMap<>(); + + public boolean isEmpty() { + return attributes.isEmpty(); + } + + public void add(String namespaceURI, String localName, TextContent value) { + attributes.computeIfAbsent(namespaceURI, v -> new HashMap<>()).put(localName, value); + } + + public void add(String namespaceURI, String localName, String value) { + add(namespaceURI, localName, TextContent.of(value)); + } + + public void add(String localName, TextContent value) { + add(XMLConstants.NULL_NS_URI, localName, value); + } + + public void add(String localName, String value) { + add(localName, TextContent.of(value)); + } + + public void add(QName name, TextContent value) { + add(name.getNamespaceURI(), name.getLocalPart(), value); + } + + public void add(QName name, String value) { + add(name, TextContent.of(value)); + } + + public void addAll(String namespaceURI, Map attributes) { + this.attributes.computeIfAbsent(namespaceURI, v -> new HashMap<>()).putAll(attributes); + } + + public Map> get() { + return attributes; + } + + public Map get(String namespaceURI) { + return attributes.getOrDefault(namespaceURI, Collections.emptyMap()); + } + + public TextContent getValue(String localName) { + return getValue(XMLConstants.NULL_NS_URI, localName); + } + + public TextContent getValue(String namespaceURI, String localName) { + return get(namespaceURI).getOrDefault(localName, TextContent.empty()); + } + + public TextContent getValue(QName name) { + return getValue(name.getNamespaceURI(), name.getLocalPart()); + } + + public Attributes copy() { + Attributes copy = new Attributes(); + copy.attributes.putAll(attributes); + return copy; + } +} diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/org/xmlobjects/xml/TextContent.java b/CityDoctorParent/CityDoctorModel/src/main/java/org/xmlobjects/xml/TextContent.java new file mode 100644 index 00000000..9537ddd8 --- /dev/null +++ b/CityDoctorParent/CityDoctorModel/src/main/java/org/xmlobjects/xml/TextContent.java @@ -0,0 +1,1049 @@ +package org.xmlobjects.xml; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeConstants; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.Duration; +import javax.xml.datatype.XMLGregorianCalendar; + +public class TextContent implements Serializable { + + private static final long serialVersionUID = -3862132079185243841L; + private static final DatatypeFactory XML_TYPE_FACTORY; + private static final TextContent EMPTY = new TextContent(""); + + private static Function ZONE_OFFSET_PROVIDER = dateTime -> ZoneOffset.systemDefault().getRules().getOffset(dateTime); + private static boolean WITH_TIME_OFFSET = true; + private static boolean WITH_DATE_OFFSET = false; + + private enum Fields { YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, NANO, TIMEZONE } + private static final EnumSet DATE_TIME_FIELDS = EnumSet.allOf(Fields.class); + private static final EnumSet TIME_FIELDS = EnumSet.of(Fields.HOUR, Fields.MINUTE, Fields.SECOND, Fields.NANO, Fields.TIMEZONE); + private static final EnumSet DATE_FIELDS = EnumSet.of(Fields.YEAR, Fields.MONTH, Fields.DAY, Fields.TIMEZONE); + private static final EnumSet GYEAR_MONTH_FIELDS = EnumSet.of(Fields.YEAR, Fields.MONTH, Fields.TIMEZONE); + private static final EnumSet GMONTH_DAY_FIELDS = EnumSet.of(Fields.MONTH, Fields.DAY, Fields.TIMEZONE); + private static final EnumSet GDAY_FIELDS = EnumSet.of(Fields.DAY, Fields.TIMEZONE); + private static final EnumSet GMONTH_FIELDS = EnumSet.of(Fields.MONTH, Fields.TIMEZONE); + private static final EnumSet GYEAR_FIELDS = EnumSet.of(Fields.YEAR, Fields.TIMEZONE); + + static { + try { + XML_TYPE_FACTORY = DatatypeFactory.newInstance(); + } catch (DatatypeConfigurationException e) { + throw new RuntimeException("Failed to initialize datatype factory.", e); + } + } + + private String content; + private String trimmedContent; + private String[] tokenizedContent; + private Object value; + + private transient Function zoneOffsetProvider = ZONE_OFFSET_PROVIDER; + + private TextContent(String content) { + this.content = Objects.requireNonNull(content, "Content must not be null."); + } + + public static TextContent empty() { + return EMPTY; + } + + public static TextContent of(String content) { + return ofObject(content); + } + + public static TextContent ofList(List content) { + return ofObjectList(content); + } + + public static TextContent ofBoolean(Boolean content) { + return ofObject(content); + } + + public static TextContent ofBooleanList(List content) { + return ofObjectList(content); + } + + public static TextContent ofDouble(Double content) { + return ofObject(content); + } + + public static TextContent ofDoubleList(List content) { + return ofObjectList(content); + } + + public static TextContent ofInteger(Integer content) { + return ofObject(content); + } + + public static TextContent ofIntegerList(List content) { + return ofObjectList(content); + } + + public static TextContent ofDuration(Duration content) { + return ofObject(content); + } + + public static TextContent ofDurationList(List content) { + return ofObjectList(content); + } + + public static TextContent ofDateTime(OffsetDateTime content, boolean withOffset) { + return ofOffsetDateTime(content, DATE_TIME_FIELDS, withOffset); + } + + public static TextContent ofDateTime(OffsetDateTime content) { + return ofOffsetDateTime(content, DATE_TIME_FIELDS, WITH_TIME_OFFSET); + } + + public static TextContent ofDateTimeList(List content, boolean withOffset) { + return ofOffsetDateTimeList(content, DATE_TIME_FIELDS, withOffset); + } + + public static TextContent ofDateTimeList(List content) { + return ofOffsetDateTimeList(content, DATE_TIME_FIELDS, WITH_TIME_OFFSET); + } + + public static TextContent ofTime(OffsetDateTime content, boolean withOffset) { + return ofOffsetDateTime(content, TIME_FIELDS, withOffset); + } + + public static TextContent ofTime(OffsetDateTime content) { + return ofOffsetDateTime(content, TIME_FIELDS, WITH_TIME_OFFSET); + } + + public static TextContent ofTimeList(List content, boolean withOffset) { + return ofOffsetDateTimeList(content, TIME_FIELDS, withOffset); + } + + public static TextContent ofTimeList(List content) { + return ofOffsetDateTimeList(content, TIME_FIELDS, WITH_TIME_OFFSET); + } + + public static TextContent ofDate(OffsetDateTime content, boolean withOffset) { + return ofOffsetDateTime(content, DATE_FIELDS, withOffset); + } + + public static TextContent ofDate(OffsetDateTime content) { + return ofOffsetDateTime(content, DATE_FIELDS, WITH_DATE_OFFSET); + } + + public static TextContent ofDateList(List content, boolean withOffset) { + return ofOffsetDateTimeList(content, DATE_FIELDS, withOffset); + } + + public static TextContent ofDateList(List content) { + return ofOffsetDateTimeList(content, DATE_FIELDS, WITH_DATE_OFFSET); + } + + public static TextContent ofGYearMonth(OffsetDateTime content, boolean withOffset) { + return ofOffsetDateTime(content, GYEAR_MONTH_FIELDS, withOffset); + } + + public static TextContent ofGYearMonth(OffsetDateTime content) { + return ofOffsetDateTime(content, GYEAR_MONTH_FIELDS, WITH_DATE_OFFSET); + } + + public static TextContent ofGYearMonthList(List content, boolean withOffset) { + return ofOffsetDateTimeList(content, GYEAR_MONTH_FIELDS, withOffset); + } + + public static TextContent ofGYearMonthList(List content) { + return ofOffsetDateTimeList(content, GYEAR_MONTH_FIELDS, WITH_DATE_OFFSET); + } + + public static TextContent ofGMonthDay(OffsetDateTime content, boolean withOffset) { + return ofOffsetDateTime(content, GMONTH_DAY_FIELDS, withOffset); + } + + public static TextContent ofGMonthDay(OffsetDateTime content) { + return ofOffsetDateTime(content, GMONTH_DAY_FIELDS, WITH_DATE_OFFSET); + } + + public static TextContent ofGMonthDayList(List content, boolean withOffset) { + return ofOffsetDateTimeList(content, GMONTH_DAY_FIELDS, withOffset); + } + + public static TextContent ofGMonthDayList(List content) { + return ofOffsetDateTimeList(content, GMONTH_DAY_FIELDS, WITH_DATE_OFFSET); + } + + public static TextContent ofGDay(OffsetDateTime content, boolean withOffset) { + return ofOffsetDateTime(content, GDAY_FIELDS, withOffset); + } + + public static TextContent ofGDay(OffsetDateTime content) { + return ofOffsetDateTime(content, GDAY_FIELDS, WITH_DATE_OFFSET); + } + + public static TextContent ofGDayList(List content, boolean withOffset) { + return ofOffsetDateTimeList(content, GDAY_FIELDS, withOffset); + } + + public static TextContent ofGDayList(List content) { + return ofOffsetDateTimeList(content, GDAY_FIELDS, WITH_DATE_OFFSET); + } + + public static TextContent ofGMonth(OffsetDateTime content, boolean withOffset) { + return ofOffsetDateTime(content, GMONTH_FIELDS, withOffset); + } + + public static TextContent ofGMonth(OffsetDateTime content) { + return ofOffsetDateTime(content, GMONTH_FIELDS, WITH_DATE_OFFSET); + } + + public static TextContent ofGMonthList(List content, boolean withOffset) { + return ofOffsetDateTimeList(content, GMONTH_FIELDS, withOffset); + } + + public static TextContent ofGMonthList(List content) { + return ofOffsetDateTimeList(content, GMONTH_FIELDS, WITH_DATE_OFFSET); + } + + public static TextContent ofGYear(OffsetDateTime content, boolean withOffset) { + return ofOffsetDateTime(content, GYEAR_FIELDS, withOffset); + } + + public static TextContent ofGYear(OffsetDateTime content) { + return ofOffsetDateTime(content, GYEAR_FIELDS, WITH_DATE_OFFSET); + } + + public static TextContent ofGYearList(List content, boolean withOffset) { + return ofOffsetDateTimeList(content, GYEAR_FIELDS, withOffset); + } + + public static TextContent ofGYearList(List content) { + return ofOffsetDateTimeList(content, GYEAR_FIELDS, WITH_DATE_OFFSET); + } + + public boolean isEmpty() { + return this == EMPTY; + } + + public TextContent trim() { + content = trimmedContent(); + return this; + } + + public TextContent normalize() { + int length = content.length(); + if (length != 0) { + StringBuilder normalized = new StringBuilder(length); + for (int i = 0; i < length; i++) { + char ch = content.charAt(i); + normalized.append(isWhiteSpace(ch) ? ' ' : ch); + } + + content = normalized.toString(); + } + + return this; + } + + public TextContent collapse() { + int length = content.length(); + if (length != 0) { + char ch = 0; + int i = 0; + + for (; i < length; i++) { + ch = content.charAt(i); + if (!isWhiteSpace(ch)) + break; + } + + if (i != length) { + StringBuilder collapsed = new StringBuilder(length - i).append(ch); + boolean isWhiteSpace = false; + + for (i += 1; i < length; i++) { + ch = content.charAt(i); + if (isWhiteSpace(ch)) { + isWhiteSpace = true; + } else { + if (isWhiteSpace) { + collapsed.append(' '); + isWhiteSpace = false; + } + + collapsed.append(ch); + } + } + + content = trimmedContent = collapsed.toString(); + } else + content = trimmedContent = ""; + } + + return this; + } + + public TextContent filter(Predicate predicate) { + return predicate.test(this) ? this : EMPTY; + } + + public String get() { + return isPresent() ? content : null; + } + + public boolean isPresent() { + return this != EMPTY; + } + + public void ifPresent(Consumer action) { + if (isPresent()) + action.accept(content); + } + + @SuppressWarnings("unchecked") + public List getAsList() { + if (isListOfType(value, String.class)) { + return (List) value; + } else if (tokenizeContent() != 0) { + List strings = new ArrayList<>(tokenizedContent.length); + Collections.addAll(strings, tokenizedContent); + return setValue(strings); + } else { + return setValue(null); + } + } + + public boolean isList() { + return getAsList() != null; + } + + public void ifList(Consumer> action) { + List value = getAsList(); + if (value != null) { + action.accept(value); + } + } + + public Boolean getAsBoolean() { + return value instanceof Boolean bool ? bool : setValue(toBoolean(trimmedContent())); + } + + public boolean isBoolean() { + return getAsBoolean() != null; + } + + public void ifBoolean(Consumer action) { + Boolean value = getAsBoolean(); + if (value != null) { + action.accept(value); + } + } + + @SuppressWarnings("unchecked") + public List getAsBooleanList() { + if (isListOfType(value, Boolean.class)) { + return (List) value; + } else if (tokenizeContent() != 0) { + List booleans = new ArrayList<>(tokenizedContent.length); + for (String token : tokenizedContent) { + Boolean value = toBoolean(token); + if (value != null) { + booleans.add(value); + } else { + return setValue(null); + } + } + + return setValue(booleans); + } else { + return setValue(null); + } + } + + public boolean isBooleanList() { + return getAsBooleanList() != null; + } + + public void ifBooleanList(Consumer> action) { + List value = getAsBooleanList(); + if (value != null) { + action.accept(value); + } + } + + public Double getAsDouble() { + if (value instanceof Double doubleValue) { + return doubleValue; + } else if (!isEmpty() && !trimmedContent().isEmpty()) { + try { + return setValue(Double.parseDouble(trimmedContent)); + } catch (NumberFormatException e) { + return setValue(null); + } + } else { + return setValue(null); + } + } + + public boolean isDouble() { + return getAsDouble() != null; + } + + public void ifDouble(Consumer action) { + Double value = getAsDouble(); + if (value != null) { + action.accept(value); + } + } + + @SuppressWarnings("unchecked") + public List getAsDoubleList() { + if (isListOfType(value, Double.class)) { + return (List) value; + } else if (tokenizeContent() != 0) { + List doubles = new ArrayList<>(tokenizedContent.length); + for (String token : tokenizedContent) { + try { + doubles.add(Double.parseDouble(token)); + } catch (NumberFormatException e) { + return setValue(null); + } + } + + return setValue(doubles); + } else { + return setValue(null); + } + } + + public boolean isDoubleList() { + return getAsDoubleList() != null; + } + + public void ifDoubleList(Consumer> action) { + List value = getAsDoubleList(); + if (value != null) { + action.accept(value); + } + } + + public Integer getAsInteger() { + if (value instanceof Integer intValue) { + return intValue; + } else if (!isEmpty() && !trimmedContent().isEmpty()) { + try { + return setValue(Integer.parseInt(trimmedContent)); + } catch (NumberFormatException e) { + return setValue(null); + } + } else { + return setValue(null); + } + } + + public boolean isInteger() { + return getAsInteger() != null; + } + + public void ifInteger(Consumer action) { + Integer value = getAsInteger(); + if (value != null) { + action.accept(value); + } + } + + @SuppressWarnings("unchecked") + public List getAsIntegerList() { + if (isListOfType(value, Integer.class)) { + return (List) value; + } else if (tokenizeContent() != 0) { + List integers = new ArrayList<>(tokenizedContent.length); + for (String token : tokenizedContent) { + try { + integers.add(Integer.parseInt(token)); + } catch (NumberFormatException e) { + return setValue(null); + } + } + + return setValue(integers); + } else { + return setValue(null); + } + } + + public boolean isIntegerList() { + return getAsIntegerList() != null; + } + + public void ifIntegerList(Consumer> action) { + List value = getAsIntegerList(); + if (value != null) { + action.accept(value); + } + } + + public Duration getAsDuration() { + if (value instanceof Duration duration) { + return duration; + } else if (!isEmpty() && !trimmedContent().isEmpty()) { + try { + return setValue(XML_TYPE_FACTORY.newDuration(trimmedContent)); + } catch (Throwable e) { + return setValue(null); + } + } else { + return setValue(null); + } + } + + public boolean isDuration() { + return getAsDuration() != null; + } + + public void ifDuration(Consumer action) { + Duration value = getAsDuration(); + if (value != null) { + action.accept(value); + } + } + + @SuppressWarnings("unchecked") + public List getAsDurationList() { + if (isListOfType(value, Duration.class)) { + return (List) value; + } else if (tokenizeContent() != 0) { + List durations = new ArrayList<>(tokenizedContent.length); + for (String token : tokenizedContent) { + try { + durations.add(XML_TYPE_FACTORY.newDuration(token)); + } catch (Throwable e) { + return setValue(null); + } + } + + return setValue(durations); + } else { + return setValue(null); + } + } + + public boolean isDurationList() { + return getAsDurationList() != null; + } + + public void ifDurationList(Consumer> action) { + List value = getAsDurationList(); + if (value != null) { + action.accept(value); + } + } + + public OffsetDateTime getAsDateTime() { + return getAsOffsetDateTime("dateTime"); + } + + public boolean isDateTime() { + return getAsDateTime() != null; + } + + public void ifDateTime(Consumer action) { + OffsetDateTime value = getAsDateTime(); + if (value != null) { + action.accept(value); + } + } + + public List getAsDateTimeList() { + return getAsOffsetDateTimeList("dateTime"); + } + + public boolean isDateTimeList() { + return getAsDateTimeList() != null; + } + + public void ifDateTimeList(Consumer> action) { + List value = getAsDateTimeList(); + if (value != null) { + action.accept(value); + } + } + + public OffsetDateTime getAsTime() { + return getAsOffsetDateTime("time"); + } + + public boolean isTime() { + return getAsTime() != null; + } + + public void ifTime(Consumer action) { + OffsetDateTime value = getAsTime(); + if (value != null) { + action.accept(value); + } + } + + public List getAsTimeList() { + return getAsOffsetDateTimeList("time"); + } + + public boolean isTimeList() { + return getAsTimeList() != null; + } + + public void ifTimeList(Consumer> action) { + List value = getAsTimeList(); + if (value != null) { + action.accept(value); + } + } + + public OffsetDateTime getAsDate() { + return getAsOffsetDateTime("date"); + } + + public boolean isDate() { + return getAsDate() != null; + } + + public void ifDate(Consumer action) { + OffsetDateTime value = getAsDate(); + if (value != null) { + action.accept(value); + } + } + + public List getAsDateList() { + return getAsOffsetDateTimeList("date"); + } + + public boolean isDateList() { + return getAsDateList() != null; + } + + public void ifDateList(Consumer> action) { + List value = getAsDateList(); + if (value != null) { + action.accept(value); + } + } + + public OffsetDateTime getAsGYearMonth() { + return getAsOffsetDateTime("gYearMonth"); + } + + public boolean isGYearMonth() { + return getAsGYearMonth() != null; + } + + public void ifGYearMonth(Consumer action) { + OffsetDateTime value = getAsGYearMonth(); + if (value != null) { + action.accept(value); + } + } + + public List getAsGYearMonthList() { + return getAsOffsetDateTimeList("gYearMonth"); + } + + public boolean isGYearMonthList() { + return getAsGYearMonthList() != null; + } + + public void ifGYearMonthList(Consumer> action) { + List value = getAsGYearMonthList(); + if (value != null) { + action.accept(value); + } + } + + public OffsetDateTime getAsGMonthDay() { + return getAsOffsetDateTime("gMonthDay"); + } + + public boolean isGMonthDay() { + return getAsGMonthDay() != null; + } + + public void ifGMonthDay(Consumer action) { + OffsetDateTime value = getAsGMonthDay(); + if (value != null) { + action.accept(value); + } + } + + public List getAsGMonthDayList() { + return getAsOffsetDateTimeList("gMonthDay"); + } + + public boolean isGMonthDayList() { + return getAsGMonthDayList() != null; + } + + public void ifGMonthDayList(Consumer> action) { + List value = getAsGMonthDayList(); + if (value != null) { + action.accept(value); + } + } + + public OffsetDateTime getAsGDay() { + return getAsOffsetDateTime("gDay"); + } + + public boolean isGDay() { + return getAsGDay() != null; + } + + public void ifGDay(Consumer action) { + OffsetDateTime value = getAsGDay(); + if (value != null) { + action.accept(value); + } + } + + public List getAsGDayList() { + return getAsOffsetDateTimeList("gDay"); + } + + public boolean isGDayList() { + return getAsGDayList() != null; + } + + public void ifGDayList(Consumer> action) { + List value = getAsGDayList(); + if (value != null) { + action.accept(value); + } + } + + public OffsetDateTime getAsGMonth() { + return getAsOffsetDateTime("gMonth"); + } + + public boolean isGMonth() { + return getAsGMonth() != null; + } + + public void ifGMonth(Consumer action) { + OffsetDateTime value = getAsGMonth(); + if (value != null) { + action.accept(value); + } + } + + public List getAsGMonthList() { + return getAsOffsetDateTimeList("gMonth"); + } + + public boolean isGMonthList() { + return getAsGMonthList() != null; + } + + public void ifGMonthList(Consumer> action) { + List value = getAsGMonthList(); + if (value != null) { + action.accept(value); + } + } + + public OffsetDateTime getAsGYear() { + return getAsOffsetDateTime("gYear"); + } + + public boolean isGYear() { + return getAsGYear() != null; + } + + public void ifGYear(Consumer action) { + OffsetDateTime value = getAsGYear(); + if (value != null) { + action.accept(value); + } + } + + public List getAsGYearList() { + return getAsOffsetDateTimeList("gYear"); + } + + public boolean isGYearList() { + return getAsGYearList() != null; + } + + public void ifGYearList(Consumer> action) { + List value = getAsGYearList(); + if (value != null) { + action.accept(value); + } + } + + public TextContent withZoneOffsetProvider(Function zoneOffsetProvider) { + if (zoneOffsetProvider != null) { + this.zoneOffsetProvider = zoneOffsetProvider; + } + + return this; + } + + private OffsetDateTime getAsOffsetDateTime(String localName) { + if (value instanceof XMLGregorianCalendar calendar + && calendar.getXMLSchemaType().getLocalPart().equals(localName)) { + return toOffsetDateTime((XMLGregorianCalendar) value); + } else if (!isEmpty() && !trimmedContent().isEmpty()) { + XMLGregorianCalendar calendar = setValue(toCalendar(trimmedContent(), localName)); + return calendar != null ? toOffsetDateTime(calendar) : null; + } else { + return setValue(null); + } + } + + @SuppressWarnings("unchecked") + private List getAsOffsetDateTimeList(String localName) { + List calendars; + if (value instanceof List list + && list.stream().allMatch(v -> v instanceof XMLGregorianCalendar calendar + && calendar.getXMLSchemaType().getLocalPart().equals(localName))) { + calendars = (List) value; + } else if (tokenizeContent() != 0) { + calendars = new ArrayList<>(tokenizedContent.length); + for (String token : tokenizedContent) { + XMLGregorianCalendar value = toCalendar(token, localName); + if (value != null) { + calendars.add(value); + } else { + return setValue(null); + } + } + + setValue(calendars); + } else { + return setValue(null); + } + + List result = new ArrayList<>(calendars.size()); + for (XMLGregorianCalendar calendar : calendars) { + result.add(toOffsetDateTime(calendar)); + } + + return result; + } + + private T setValue(T value) { + this.value = value; + return value; + } + + private boolean isListOfType(Object value, Class type) { + return value instanceof List list && list.stream().allMatch(type::isInstance); + } + + private Boolean toBoolean(String value) { + if (value.isEmpty()) { + return null; + } else if ("true".equals(value) + || "1".equals(value)) { + return Boolean.TRUE; + } else if ("false".equals(value) + || "0".equals(value)) { + return Boolean.FALSE; + } else { + return null; + } + } + + private XMLGregorianCalendar toCalendar(String value, String localName) { + try { + XMLGregorianCalendar calendar = XML_TYPE_FACTORY.newXMLGregorianCalendar(value); + if (calendar.getXMLSchemaType().getLocalPart().equals(localName)) { + return calendar; + } + } catch (Throwable e) { + // + } + + return null; + } + + private OffsetDateTime toOffsetDateTime(XMLGregorianCalendar calendar) { + int day = calendar.getDay(); + int month = calendar.getMonth(); + int year = calendar.getYear(); + int hour = calendar.getHour(); + int minute = calendar.getMinute(); + int second = calendar.getSecond(); + int offset = calendar.getTimezone(); + + BigDecimal fractional = calendar.getFractionalSecond(); + int nano = fractional != null ? (int) (fractional.doubleValue() * 1e+9) : DatatypeConstants.FIELD_UNDEFINED; + + LocalDateTime dateTime = LocalDateTime.of( + year != DatatypeConstants.FIELD_UNDEFINED ? year : 0, + month != DatatypeConstants.FIELD_UNDEFINED ? month : 1, + day != DatatypeConstants.FIELD_UNDEFINED ? day : 1, + hour != DatatypeConstants.FIELD_UNDEFINED ? hour : 0, + minute != DatatypeConstants.FIELD_UNDEFINED ? minute : 0, + second != DatatypeConstants.FIELD_UNDEFINED ? second : 0, + nano != DatatypeConstants.FIELD_UNDEFINED ? nano : 0); + + return OffsetDateTime.of(dateTime, offset != DatatypeConstants.FIELD_UNDEFINED ? + ZoneOffset.ofTotalSeconds(offset * 60) : + zoneOffsetProvider.apply(dateTime)); + } + + private static TextContent ofObject(Object content) { + return content != null ? new TextContent(content.toString()) : EMPTY; + } + + private static TextContent ofObjectList(List content) { + if (content != null && !content.isEmpty()) { + StringBuilder builder = new StringBuilder(content.size()); + boolean first = true; + + for (Object object : content) { + if (object != null) { + if (!first) { + builder.append(" "); + } else { + first = false; + } + + builder.append(object); + } + } + + return new TextContent(builder.toString()); + } else { + return EMPTY; + } + } + + private static TextContent ofOffsetDateTime(OffsetDateTime content, EnumSet fields, boolean withOffset) { + XMLGregorianCalendar calendar = toCalendar(content, fields, withOffset); + return calendar != null ? new TextContent(calendar.toXMLFormat()) : EMPTY; + } + + private static TextContent ofOffsetDateTimeList(List content, EnumSet fields, boolean withOffset) { + if (content != null && !content.isEmpty()) { + StringBuilder builder = new StringBuilder(content.size()); + boolean first = true; + + for (OffsetDateTime dateTime : content) { + XMLGregorianCalendar calendar = toCalendar(dateTime, fields, withOffset); + if (calendar != null) { + if (!first) { + builder.append(" "); + } else { + first = false; + } + + builder.append(calendar.toXMLFormat()); + } + } + + return new TextContent(builder.toString()); + } else { + return EMPTY; + } + } + + private static XMLGregorianCalendar toCalendar(OffsetDateTime dateTime, EnumSet fields, boolean withOffset) { + XMLGregorianCalendar calendar = null; + if (dateTime != null) { + calendar = XML_TYPE_FACTORY.newXMLGregorianCalendar( + fields.contains(Fields.YEAR) ? dateTime.getYear() : DatatypeConstants.FIELD_UNDEFINED, + fields.contains(Fields.MONTH) ? dateTime.getMonthValue() : DatatypeConstants.FIELD_UNDEFINED, + fields.contains(Fields.DAY) ? dateTime.getDayOfMonth() : DatatypeConstants.FIELD_UNDEFINED, + fields.contains(Fields.HOUR) ? dateTime.getHour() : DatatypeConstants.FIELD_UNDEFINED, + fields.contains(Fields.MINUTE) ? dateTime.getMinute() : DatatypeConstants.FIELD_UNDEFINED, + fields.contains(Fields.SECOND) ? dateTime.getSecond() : DatatypeConstants.FIELD_UNDEFINED, + DatatypeConstants.FIELD_UNDEFINED, + withOffset && fields.contains(Fields.TIMEZONE) ? + dateTime.getOffset().getTotalSeconds() / 60 : + DatatypeConstants.FIELD_UNDEFINED); + + if (fields.contains(Fields.NANO) && dateTime.getNano() != 0) { + calendar.setFractionalSecond(BigDecimal.valueOf(dateTime.getNano(), 9).stripTrailingZeros()); + } + } + + return calendar; + } + + public static void setZoneOffsetProvider(Function zoneOffsetProvider) { + if (zoneOffsetProvider != null) { + ZONE_OFFSET_PROVIDER = zoneOffsetProvider; + } + } + + public static void serializeTimeWithOffset(boolean useTimeOffset) { + WITH_TIME_OFFSET = useTimeOffset; + } + + public static void serializeDateWithOffset(boolean useDateOffset) { + WITH_DATE_OFFSET = useDateOffset; + } + + private String trimmedContent() { + if (trimmedContent == null) { + trimmedContent = content.trim(); + } + + return trimmedContent; + } + + private int tokenizeContent() { + if (tokenizedContent == null) { + int length = content.length(); + String[] tokens = new String[(length / 2) + 1]; + int noOfTokens = 0; + int current = -1; + int next; + + do { + next = nextWhiteSpace(content, current + 1, length); + if (next != current + 1) { + tokens[noOfTokens++] = content.substring(current + 1, next); + } + + current = next; + } while (next != length); + + tokenizedContent = new String[noOfTokens]; + System.arraycopy(tokens, 0, tokenizedContent, 0, noOfTokens); + } + + return tokenizedContent.length; + } + + private int nextWhiteSpace(String value, int pos, int length) { + for (int i = pos; i < length; i++) { + if (isWhiteSpace(value.charAt(i))) { + return i; + } + } + + return length; + } + + private boolean isWhiteSpace(char ch) { + return ch == ' ' || ch == '\n' || Character.isWhitespace(ch); + } + + @Override + public String toString() { + return content; + } +} + diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BridgeConstructiveElementTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BridgeConstructiveElementTest.java new file mode 100644 index 00000000..cf7440e9 --- /dev/null +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BridgeConstructiveElementTest.java @@ -0,0 +1,30 @@ +package de.hft.stuttgart.citydoctor2.datastructure; + +import static org.junit.jupiter.api.Assertions.*; + +import org.citygml4j.core.model.construction.WallSurface; +import org.citygml4j.core.util.geometry.GeometryFactory; +import org.junit.jupiter.api.Test; + +import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; + +class BridgeConstructiveElementTest { + + @Test + void testBoundarySurfaceGeometryRecreation() { + WallSurface wallSurface = new WallSurface(); + BoundarySurface surface = new BoundarySurface(wallSurface); + surface.addGeometry(GeometryTestUtils.createDummyGeometry(GeometryType.MULTI_SURFACE, Lod.LOD2)); + + var gmlBce = new org.citygml4j.core.model.bridge.BridgeConstructiveElement(); + BridgeConstructiveElement bce = new BridgeConstructiveElement(gmlBce); + bce.addBoundarySurface(surface); + + assertNull(wallSurface.getLod2MultiSurface()); + GeometryFactory factory = GeometryFactory.newInstance(); + ParserConfiguration config = new ParserConfiguration(8, false); + bce.reCreateGeometries(factory, config); + assertNotNull(wallSurface.getLod2MultiSurface()); + } + +} -- GitLab From 46fcd0307b42485cc88490679fe0a4410d9413fd Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Thu, 11 Dec 2025 15:01:02 +0100 Subject: [PATCH 07/63] Feat: Add cross-feature reference detection --- .../citygml3/Citygml3FeatureMapper.java | 54 ++++++++++++++++--- .../citydoctor2/parser/CityGmlParser.java | 1 + 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/Citygml3FeatureMapper.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/Citygml3FeatureMapper.java index e27555ca..265fc2e8 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/Citygml3FeatureMapper.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/Citygml3FeatureMapper.java @@ -23,6 +23,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.StringJoiner; +import java.util.function.Function; import de.hft.stuttgart.citydoctor2.database.FeatureCache; import de.hft.stuttgart.citydoctor2.database.UnconnectedCache; @@ -172,10 +174,13 @@ public class Citygml3FeatureMapper extends ObjectWalker { private final Path directory; private final double neighborDistance; private Map polygonMap = new HashMap<>(); - private Map compositeMap = new HashMap<>(); + private Map compositeMap = new HashMap<>();; private List references = new ArrayList<>(); private Map vertexMap = new HashMap<>(); + private Map> unresolvedPolyRefIDMap = new HashMap<>(); + private Map parsedPolygonsIdMap = new HashMap<>(); + public Citygml3FeatureMapper(ParserConfiguration config, Path path, boolean unconnected) { this.config = config; this.directory = path.getParent(); @@ -197,6 +202,37 @@ public class Citygml3FeatureMapper extends ObjectWalker { } } + public void checkUnresolvedReferences() { + if (unresolvedPolyRefIDMap.isEmpty()) { + return; + } + boolean xLinkGlobalError = false; + List crossRefs = new ArrayList<>(); + for (Map.Entry> entry : unresolvedPolyRefIDMap.entrySet()) { + String id = entry.getKey(); + boolean emptyCrossFeatureReference = parsedPolygonsIdMap.containsKey(id); + xLinkGlobalError = xLinkGlobalError || emptyCrossFeatureReference; + if (emptyCrossFeatureReference) { + List gmlIds = entry.getValue(); + StringJoiner joiner = new StringJoiner(", "); + gmlIds.forEach(gmlId -> joiner.add(gmlId.toString())); + GmlId crossFeatureId = parsedPolygonsIdMap.get(id); + //TODO: Localize String + logger.warn("Polygon {} of Feature {} is being referenced in other Features: {}", id, crossFeatureId, joiner); + crossRefs.add(id); + } + } + if (xLinkGlobalError) { + //TODO: Localize String + logger.warn("Model has Features containing cross-feature references without their own copies of the referenced polygons"); + } + crossRefs.forEach(unresolvedPolyRefIDMap::remove); + if(!unresolvedPolyRefIDMap.isEmpty()) { + logger.warn("Model contains polygon references that could not be found"); + } + } + + @Override public void visit(AbstractSpace space) { // if we are here, an AbstractSpace thing was read that is not handled in the @@ -903,7 +939,7 @@ public class Citygml3FeatureMapper extends ObjectWalker { private void finishCityObjectConstruction(CityObject co) { parseId(co.getGmlObject(), co); co.clearGmlGeometries(); - resolveAndClearReferences(); + resolveAndClearReferences(co); updateEdgesAndVertices(co); } @@ -1092,7 +1128,7 @@ public class Citygml3FeatureMapper extends ObjectWalker { return igh; } - private void resolveAndClearReferences() { + private void resolveAndClearReferences(CityObject co) { for (ResolvableReference ref : references) { String href = ref.href(); if (href.startsWith("#")) { @@ -1107,6 +1143,9 @@ public class Citygml3FeatureMapper extends ObjectWalker { handlePolygonReference(href, geom); } } + for(String polygonId: polygonMap.keySet()){ + parsedPolygonsIdMap.put(polygonId, co.getGmlId()); + } // clear storage for polygons and vertices references = new ArrayList<>(); @@ -1117,9 +1156,12 @@ public class Citygml3FeatureMapper extends ObjectWalker { private void handlePolygonReference(String href, Geometry geom) { ConcretePolygon concPoly = polygonMap.get(href); if (concPoly == null) { - if (logger.isWarnEnabled()) { - logger.warn(Localization.getText("FeatureMapper.polygonUnreferenced"), href); - } + List featureList = unresolvedPolyRefIDMap.computeIfAbsent(href, k -> new ArrayList<>()); + GmlId featureId = geom.getParent().getTopLevelCityObject().getGmlId(); + featureList.add(featureId); +// if (logger.isWarnEnabled()) { +// logger.warn(Localization.getText("FeatureMapper.polygonUnreferenced"), href); +// } } else { LinkedPolygon lPoly = new LinkedPolygon(concPoly, geom); if (geom.getParent() instanceof BoundarySurface bs) { diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java index d94ab7ee..41005760 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java @@ -492,6 +492,7 @@ public class CityGmlParser { logger.info(Localization.getText("CityGmlParser.parsedObjects"), mapper.getModel().getNumberOfFeatures()); } + mapper.checkUnresolvedReferences(); mapper.setCityGMLVersion(version); return mapper.getModel(); } catch (Exception e) { -- GitLab From b7b0e6e6c0cb0abaca82a082765e895cf6e35c5c Mon Sep 17 00:00:00 2001 From: Matthias Betz Date: Fri, 12 Dec 2025 08:28:40 +0100 Subject: [PATCH 08/63] Add XMLValidationError and list it in global errors --- .../check/AbstractErrorVisitor.java | 5 + .../citydoctor2/check/CheckResult.java | 4 + .../citydoctor2/check/Checkable.java | 10 +- .../stuttgart/citydoctor2/check/ErrorId.java | 1 + .../citydoctor2/check/ErrorVisitor.java | 3 + .../citydoctor2/check/HealingMethod.java | 5 + .../check/QualityAdeErrorVisitor.java | 6 + .../check/error/XMLValidationError.java | 59 +++++++++ .../mapper/citygml3/GMLValidationHandler.java | 21 +++- .../citydoctor2/parser/CityGmlParser.java | 18 +-- ...sVisitor.java => ClearResultsVisitor.java} | 2 +- .../datastructure/BoundarySurfaceTest.java | 4 +- .../datastructure/GeometryTest.java | 4 +- .../datastructure/TrafficSpaceTest.java | 4 +- .../stuttgart/citydoctor2/check/Checker.java | 81 ++++++------ .../citydoctor2/gui/CityDoctorController.java | 116 ++++++++++-------- .../citydoctor2/gui/GlobalErrorVisitor.java | 7 ++ .../citydoctor2/gui/ListErrorVisitor.java | 6 + .../stuttgart/citydoctor2/gui/MainWindow.java | 17 ++- 19 files changed, 260 insertions(+), 113 deletions(-) create mode 100644 CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/error/XMLValidationError.java rename CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/utils/visitors/{ClearVisitorResultsVisitor.java => ClearResultsVisitor.java} (84%) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/AbstractErrorVisitor.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/AbstractErrorVisitor.java index 6ddc35c4..5ad1d34b 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/AbstractErrorVisitor.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/AbstractErrorVisitor.java @@ -40,6 +40,7 @@ import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError; import de.hft.stuttgart.citydoctor2.check.error.SurfaceUnfragmentedError; import de.hft.stuttgart.citydoctor2.check.error.TooFewPolygonsError; import de.hft.stuttgart.citydoctor2.check.error.UnknownCheckError; +import de.hft.stuttgart.citydoctor2.check.error.XMLValidationError; public abstract class AbstractErrorVisitor implements ErrorVisitor { @@ -206,5 +207,9 @@ public abstract class AbstractErrorVisitor implements ErrorVisitor { @Override public void visit(SolidError err) { } + + @Override + public void visit(XMLValidationError err) { + } } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckResult.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckResult.java index 154d32c0..78372b27 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckResult.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckResult.java @@ -71,4 +71,8 @@ public class CheckResult implements Serializable { return String.format("CheckResult [id=%s, status=%s, error=%s]", id, status, err); } + public boolean isParsingError() { + return false; + } + } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java index 3994bdd1..8c5da64d 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java @@ -26,7 +26,7 @@ import java.util.Map; import de.hft.stuttgart.citydoctor2.utils.CheckErrorFound; import de.hft.stuttgart.citydoctor2.utils.visitors.CheckableErrorCollector; -import de.hft.stuttgart.citydoctor2.utils.visitors.ClearVisitorResultsVisitor; +import de.hft.stuttgart.citydoctor2.utils.visitors.ClearResultsVisitor; import de.hft.stuttgart.citydoctor2.utils.visitors.ClearMetaInformationVisitor; import de.hft.stuttgart.citydoctor2.utils.visitors.ContainsAnyErrorVisitor; import de.hft.stuttgart.citydoctor2.utils.visitors.ContainsErrorVisitor; @@ -221,18 +221,18 @@ public abstract class Checkable implements Serializable { } /** - * Clears the checkResults list of this checkable. + * Clears the checkResults of this checkable unless the error occured during parsing. */ public void clearCheckResults() { - setValidated(false); - checkResults.clear(); + checkResults.entrySet().removeIf(e -> !e.getValue().isParsingError()); + setValidated(!checkResults.isEmpty()); } /** * Clears the checkResults list of this checkable and all child objects in its datastructure. */ public final void clearAllContainedCheckResults() { - this.accept(new ClearVisitorResultsVisitor()); + this.accept(new ClearResultsVisitor()); } /** diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/ErrorId.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/ErrorId.java index 41d9b3f9..be393f08 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/ErrorId.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/ErrorId.java @@ -66,6 +66,7 @@ public record ErrorId(String name) implements Serializable { public static final ErrorId SE_BS_UNFRAGMENTED = new ErrorId("SE_BS_UNFRAGMENTED"); public static final ErrorId GE_P_DEGENERATED_RING = new ErrorId("GE_P_DEGENERATED_POLYGON"); public static final ErrorId SE_POLYGON_WITHOUT_SURFACE = new ErrorId("SE_POLYGON_WITHOUT_SURFACE"); + public static final ErrorId SC_SCHEMA_VALIDATION = new ErrorId("SC_SCHEMA_VALIDATION"); diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/ErrorVisitor.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/ErrorVisitor.java index 23f95ee0..82c9a5f8 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/ErrorVisitor.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/ErrorVisitor.java @@ -58,6 +58,7 @@ import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError; import de.hft.stuttgart.citydoctor2.check.error.SurfaceUnfragmentedError; import de.hft.stuttgart.citydoctor2.check.error.TooFewPolygonsError; import de.hft.stuttgart.citydoctor2.check.error.UnknownCheckError; +import de.hft.stuttgart.citydoctor2.check.error.XMLValidationError; /** * Visitor pattern interface to access errors. This is to determine the type of @@ -149,5 +150,7 @@ public interface ErrorVisitor { public void visit(CheckError err); public void visit(SolidError err); + + public void visit(XMLValidationError err); } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/HealingMethod.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/HealingMethod.java index 3879e29e..0fe22b7d 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/HealingMethod.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/HealingMethod.java @@ -51,6 +51,7 @@ import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError; import de.hft.stuttgart.citydoctor2.check.error.SurfaceUnfragmentedError; import de.hft.stuttgart.citydoctor2.check.error.TooFewPolygonsError; import de.hft.stuttgart.citydoctor2.check.error.UnknownCheckError; +import de.hft.stuttgart.citydoctor2.check.error.XMLValidationError; /** * Visitor pattern interface used in the healing procedures. The modification @@ -199,6 +200,10 @@ public interface HealingMethod { default boolean visit(PolygonWithoutSurfaceError err, ModificationListener l) { return false; } + + default boolean visit(XMLValidationError err, ModificationListener l) { + return false; + } public HealingMethod createNew(); diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/QualityAdeErrorVisitor.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/QualityAdeErrorVisitor.java index 741dfc57..75e79035 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/QualityAdeErrorVisitor.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/QualityAdeErrorVisitor.java @@ -58,6 +58,7 @@ import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError; import de.hft.stuttgart.citydoctor2.check.error.SurfaceUnfragmentedError; import de.hft.stuttgart.citydoctor2.check.error.TooFewPolygonsError; import de.hft.stuttgart.citydoctor2.check.error.UnknownCheckError; +import de.hft.stuttgart.citydoctor2.check.error.XMLValidationError; import de.hft.stuttgart.citydoctor2.datastructure.Edge; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.Polygon; @@ -403,5 +404,10 @@ public class QualityAdeErrorVisitor extends AbstractErrorVisitor { public void visit(PolygonWithoutSurfaceError err) { // not translated } + + @Override + public void visit(XMLValidationError err) { + // not translated + } } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/error/XMLValidationError.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/error/XMLValidationError.java new file mode 100644 index 00000000..14a2afdc --- /dev/null +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/error/XMLValidationError.java @@ -0,0 +1,59 @@ +package de.hft.stuttgart.citydoctor2.check.error; + +import java.io.Serial; + +import de.hft.stuttgart.citydoctor2.check.CheckError; +import de.hft.stuttgart.citydoctor2.check.ErrorId; +import de.hft.stuttgart.citydoctor2.check.ErrorReport; +import de.hft.stuttgart.citydoctor2.check.ErrorType; +import de.hft.stuttgart.citydoctor2.check.ErrorVisitor; +import de.hft.stuttgart.citydoctor2.check.HealingMethod; +import de.hft.stuttgart.citydoctor2.check.ModificationListener; +import de.hft.stuttgart.citydoctor2.datastructure.GmlElement; + +public class XMLValidationError implements CheckError { + + @Serial + private static final long serialVersionUID = 4002356100680460598L; + + private String message; + + public XMLValidationError(String message) { + this.message = message; + } + + @Override + public ErrorType getType() { + return ErrorType.ERROR; + } + + @Override + public ErrorId getErrorId() { + return ErrorId.SC_SCHEMA_VALIDATION; + } + + @Override + public GmlElement getFeature() { + return null; + } + + @Override + public void accept(ErrorVisitor errorVisitor) { + errorVisitor.visit(this); + } + + @Override + public boolean accept(HealingMethod method, ModificationListener l) { + return method.visit(this, l); + } + + public String getMessage() { + return message; + } + + @Override + public void report(ErrorReport report) { + report.add("message", message); + } + +} diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/GMLValidationHandler.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/GMLValidationHandler.java index b020d106..9b07b931 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/GMLValidationHandler.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/mapper/citygml3/GMLValidationHandler.java @@ -28,29 +28,38 @@ import org.xml.sax.SAXParseException; public class GMLValidationHandler implements ErrorHandler { + private static final int MAX_LINE_SIZE = 200; + private static final Logger logger = LogManager.getLogger(GMLValidationHandler.class); private final List messages = new ArrayList<>(); @Override public void error(SAXParseException exception) { - String message = "[" + exception.getLineNumber() + ", " + exception.getColumnNumber() + "] " - + exception.getMessage(); + String message = constructMessage(exception); logger.error(message); messages.add(message); } + private String constructMessage(SAXParseException exception) { + String exceptionMessage = exception.getMessage(); + String message = "[line: " + exception.getLineNumber() + "] " + + exceptionMessage.substring(0, Math.min(exceptionMessage.length(), MAX_LINE_SIZE)); + if (exceptionMessage.length() > MAX_LINE_SIZE) { + message += "..."; + } + return message; + } + @Override public void warning(SAXParseException exception) { - String message = "[" + exception.getLineNumber() + ", " + exception.getColumnNumber() + "] " - + exception.getMessage(); + String message = constructMessage(exception); logger.warn(message); messages.add(message); } @Override public void fatalError(SAXParseException exception) { - String message = "[" + exception.getLineNumber() + ", " + exception.getColumnNumber() + "] " - + exception.getMessage(); + String message = constructMessage(exception); logger.fatal(message); messages.add(message); } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java index d94ab7ee..700fcd2c 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java @@ -28,6 +28,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,8 +44,6 @@ import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; -import de.hft.stuttgart.citydoctor2.datastructure.GmlId; -import de.hft.stuttgart.citydoctor2.datastructure.ImplicitGeometryHolder; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -84,8 +83,11 @@ import org.xmlobjects.schema.SchemaHandlerException; import org.xmlobjects.stream.XMLReader; import org.xmlobjects.stream.XMLReaderFactory; +import de.hft.stuttgart.citydoctor2.check.error.XMLValidationError; import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel; import de.hft.stuttgart.citydoctor2.datastructure.CityObject; +import de.hft.stuttgart.citydoctor2.datastructure.GmlId; +import de.hft.stuttgart.citydoctor2.datastructure.ImplicitGeometryHolder; import de.hft.stuttgart.citydoctor2.mapper.citygml3.Citygml3FeatureMapper; import de.hft.stuttgart.citydoctor2.mapper.citygml3.GMLValidationHandler; import de.hft.stuttgart.citydoctor2.math.Vector3d; @@ -262,11 +264,9 @@ public class CityGmlParser { throws CityGmlParseException, InvalidGmlFileException { CityGMLContext context = getContext(); Path file = Paths.get(filePath); + List validationIssues = Collections.emptyList(); if (config.getValidate()) { - List messages = validateFile(context, handler, file); - if (!messages.isEmpty()) { - throw new InvalidGmlFileException("Invalid GML File. First error: \n" + messages.get(0)); - } + validationIssues = validateFile(context, handler, file); } try { @@ -277,7 +277,11 @@ public class CityGmlParser { if (l != null) { ois.addListener(l::updateProgress); } - return readAndKeepFeatures(config, file, in, ois, verbose, unconnected); + CityDoctorModel model = readAndKeepFeatures(config, file, in, ois, verbose, unconnected); + for (String xmlError : validationIssues) { + model.addGlobalError(new XMLValidationError(xmlError)); + } + return model; } } catch (CityGMLReadException | IOException e) { throw new CityGmlParseException("Failed to read CityGML file", e); diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/utils/visitors/ClearVisitorResultsVisitor.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/utils/visitors/ClearResultsVisitor.java similarity index 84% rename from CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/utils/visitors/ClearVisitorResultsVisitor.java rename to CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/utils/visitors/ClearResultsVisitor.java index 15436a7e..0aa8f06c 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/utils/visitors/ClearVisitorResultsVisitor.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/utils/visitors/ClearResultsVisitor.java @@ -6,7 +6,7 @@ import de.hft.stuttgart.citydoctor2.check.Checkable; /** * This Visitor removes the check results from a CityObject and the objects in its datastructure. */ -public class ClearVisitorResultsVisitor extends CheckableUtilsVisitor { +public class ClearResultsVisitor extends CheckableUtilsVisitor { @Override public void check(Checkable checkable) { diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BoundarySurfaceTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BoundarySurfaceTest.java index c08b1ad7..d13efe11 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BoundarySurfaceTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BoundarySurfaceTest.java @@ -30,7 +30,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import de.hft.stuttgart.citydoctor2.utils.visitors.ClearVisitorResultsVisitor; +import de.hft.stuttgart.citydoctor2.utils.visitors.ClearResultsVisitor; import de.hft.stuttgart.citydoctor2.utils.visitors.GmlGeometryRebuilder; import de.hft.stuttgart.citydoctor2.utils.visitors.UnsetGeometriesVisitor; import org.citygml4j.core.model.construction.AbstractConstructionSurface; @@ -141,7 +141,7 @@ public class BoundarySurfaceTest { assertFalse(bs.getAllCheckResults().isEmpty()); bs.addOpening(oMock); bs.clearAllContainedCheckResults(); - verify(oMock).accept(any(ClearVisitorResultsVisitor.class)); + verify(oMock).accept(any(ClearResultsVisitor.class)); assertTrue(bs.getAllCheckResults().isEmpty()); } diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/GeometryTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/GeometryTest.java index 5c8b97cf..0b33bb45 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/GeometryTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/GeometryTest.java @@ -33,7 +33,7 @@ import java.util.ArrayList; import java.util.List; import de.hft.stuttgart.citydoctor2.check.CheckableUtilsVisitor; -import de.hft.stuttgart.citydoctor2.utils.visitors.ClearVisitorResultsVisitor; +import de.hft.stuttgart.citydoctor2.utils.visitors.ClearResultsVisitor; import org.citygml4j.core.model.construction.AbstractConstructionSurface; import org.citygml4j.core.model.construction.WallSurface; import org.citygml4j.core.util.geometry.GeometryFactory; @@ -301,7 +301,7 @@ public class GeometryTest { Polygon p = mock(Polygon.class); geom.addPolygon(p); geom.clearAllContainedCheckResults(); - verify(p).accept(any(ClearVisitorResultsVisitor.class)); + verify(p).accept(any(ClearResultsVisitor.class)); assertFalse(geom.containsError(CheckId.C_GE_P_HOLE_OUTSIDE)); } diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/TrafficSpaceTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/TrafficSpaceTest.java index 42759031..c5f848de 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/TrafficSpaceTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/TrafficSpaceTest.java @@ -6,7 +6,7 @@ import de.hft.stuttgart.citydoctor2.check.CheckId; import de.hft.stuttgart.citydoctor2.check.CheckResult; import de.hft.stuttgart.citydoctor2.check.ResultStatus; import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; -import de.hft.stuttgart.citydoctor2.utils.visitors.ClearVisitorResultsVisitor; +import de.hft.stuttgart.citydoctor2.utils.visitors.ClearResultsVisitor; import org.citygml4j.core.model.core.AbstractSpaceBoundary; import org.citygml4j.core.model.core.AbstractUnoccupiedSpace; import org.citygml4j.core.model.deprecated.core.DeprecatedPropertiesOfAbstractCityObject; @@ -91,7 +91,7 @@ public class TrafficSpaceTest { tso.clearAllContainedCheckResults(); assertFalse(tso.containsError(CheckId.C_GE_P_HOLE_OUTSIDE)); - verify(taMock).accept(any(ClearVisitorResultsVisitor.class)); + verify(taMock).accept(any(ClearResultsVisitor.class)); } @Test diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java index c863720e..29786f51 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java @@ -18,6 +18,49 @@ */ package de.hft.stuttgart.citydoctor2.check; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.xml.XMLConstants; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXResult; +import javax.xml.transform.stream.StreamSource; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import de.hft.stuttgart.citydoctor2.check.error.AttributeInvalidError; import de.hft.stuttgart.citydoctor2.check.error.AttributeMissingError; import de.hft.stuttgart.citydoctor2.check.error.AttributeValueWrongError; @@ -29,13 +72,11 @@ import de.hft.stuttgart.citydoctor2.checks.Checks; import de.hft.stuttgart.citydoctor2.checks.SvrlContentHandler; import de.hft.stuttgart.citydoctor2.checks.util.FeatureCheckedListener; import de.hft.stuttgart.citydoctor2.database.CityObjectCache; -import de.hft.stuttgart.citydoctor2.database.CityObjectCache; import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel; import de.hft.stuttgart.citydoctor2.datastructure.CityObject; import de.hft.stuttgart.citydoctor2.datastructure.FeatureType; import de.hft.stuttgart.citydoctor2.datastructure.GmlId; import de.hft.stuttgart.citydoctor2.datastructure.ImplicitGeometryHolder; -import de.hft.stuttgart.citydoctor2.datastructure.GmlId; import de.hft.stuttgart.citydoctor2.parser.CityGmlConsumer; import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException; import de.hft.stuttgart.citydoctor2.parser.CityGmlParser; @@ -59,40 +100,6 @@ import de.hft.stuttgart.quality.model.properties.RequirementProperty; import de.hft.stuttgart.quality.model.types.Checking; import de.hft.stuttgart.quality.model.types.Parameter; import de.hft.stuttgart.quality.model.types.ValidationPlan; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.xml.XMLConstants; -import javax.xml.transform.Result; -import javax.xml.transform.Source; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMResult; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.sax.SAXResult; -import javax.xml.transform.stream.StreamSource; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; /** * The main container class for checking. It contains the logic for validation, @@ -253,7 +260,7 @@ public class Checker { } handler.getFeatureErrors().forEach((k, v) -> { String trimmedId = k.trim(); - CityObject co = cache.get(new GmlId(k)); + CityObject co = cache.get(new GmlId(trimmedId)); if (co == null) { // gml id reported by schematron was not found, add to general errors handleSchematronErrorsGlobal(v); diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CityDoctorController.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CityDoctorController.java index 3d68b2fc..c01bc3d7 100644 --- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CityDoctorController.java +++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/CityDoctorController.java @@ -1,24 +1,76 @@ package de.hft.stuttgart.citydoctor2.gui; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.citygml4j.core.model.core.CityModel; + import de.hft.stuttgart.citydoctor2.check.CheckError; import de.hft.stuttgart.citydoctor2.check.Checker; import de.hft.stuttgart.citydoctor2.check.ErrorId; import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration; -import de.hft.stuttgart.citydoctor2.check.error.SchematronError; import de.hft.stuttgart.citydoctor2.database.UnconnectedCache; -import de.hft.stuttgart.citydoctor2.datastructure.*; +import de.hft.stuttgart.citydoctor2.datastructure.AbstractBuilding; +import de.hft.stuttgart.citydoctor2.datastructure.AbstractFurniture; +import de.hft.stuttgart.citydoctor2.datastructure.AbstractRoom; +import de.hft.stuttgart.citydoctor2.datastructure.BoundarySurface; +import de.hft.stuttgart.citydoctor2.datastructure.BoundingBox; +import de.hft.stuttgart.citydoctor2.datastructure.BridgeConstructiveElement; +import de.hft.stuttgart.citydoctor2.datastructure.BridgeObject; +import de.hft.stuttgart.citydoctor2.datastructure.Building; +import de.hft.stuttgart.citydoctor2.datastructure.BuildingConstructiveElement; +import de.hft.stuttgart.citydoctor2.datastructure.BuildingPart; +import de.hft.stuttgart.citydoctor2.datastructure.BuildingUnit; +import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel; +import de.hft.stuttgart.citydoctor2.datastructure.CityFurniture; +import de.hft.stuttgart.citydoctor2.datastructure.CityObject; +import de.hft.stuttgart.citydoctor2.datastructure.FeatureType; +import de.hft.stuttgart.citydoctor2.datastructure.GenericCityObject; +import de.hft.stuttgart.citydoctor2.datastructure.Geometry; +import de.hft.stuttgart.citydoctor2.datastructure.GmlId; +import de.hft.stuttgart.citydoctor2.datastructure.Installation; +import de.hft.stuttgart.citydoctor2.datastructure.Opening; +import de.hft.stuttgart.citydoctor2.datastructure.Storey; +import de.hft.stuttgart.citydoctor2.datastructure.TopLevelTransportFeature; +import de.hft.stuttgart.citydoctor2.datastructure.TrafficAreaObject; +import de.hft.stuttgart.citydoctor2.datastructure.TrafficSpaceObject; +import de.hft.stuttgart.citydoctor2.datastructure.TransportSection; +import de.hft.stuttgart.citydoctor2.datastructure.TransportationObject; +import de.hft.stuttgart.citydoctor2.datastructure.TransportationSpace; +import de.hft.stuttgart.citydoctor2.datastructure.Tunnel; +import de.hft.stuttgart.citydoctor2.datastructure.TunnelConstructiveElement; +import de.hft.stuttgart.citydoctor2.datastructure.TunnelPart; +import de.hft.stuttgart.citydoctor2.datastructure.Vegetation; +import de.hft.stuttgart.citydoctor2.datastructure.WaterObject; import de.hft.stuttgart.citydoctor2.exceptions.CityDoctorWriteException; import de.hft.stuttgart.citydoctor2.gui.table.ErrorStat; -import de.hft.stuttgart.citydoctor2.gui.tree.*; +import de.hft.stuttgart.citydoctor2.gui.tree.AddMoreButtonRunnable; +import de.hft.stuttgart.citydoctor2.gui.tree.ButtonRenderable; +import de.hft.stuttgart.citydoctor2.gui.tree.Renderable; import de.hft.stuttgart.citydoctor2.gui.tree.node.CityObjectGroupNode; import de.hft.stuttgart.citydoctor2.gui.tree.node.CityObjectNode; import de.hft.stuttgart.citydoctor2.gui.tree.node.GeometryNode; import de.hft.stuttgart.citydoctor2.gui.tree.node.OpeningNode; import de.hft.stuttgart.citydoctor2.gui.tree.node.TopLevelCityObjectNode; -import de.hft.stuttgart.citydoctor2.mapper.citygml3.GMLValidationHandler; -import de.hft.stuttgart.citydoctor2.parser.*; +import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException; +import de.hft.stuttgart.citydoctor2.parser.CityGmlParser; +import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException; +import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; +import de.hft.stuttgart.citydoctor2.parser.ProgressListener; import de.hft.stuttgart.citydoctor2.utils.Localization; -import de.hft.stuttgart.quality.adapter.types.ErrorAdapter; import javafx.application.Platform; import javafx.scene.chart.XYChart.Data; import javafx.scene.chart.XYChart.Series; @@ -29,18 +81,6 @@ import javafx.scene.control.TreeView; import javafx.scene.paint.Color; import javafx.stage.FileChooser; import javafx.stage.FileChooser.ExtensionFilter; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.citygml4j.core.model.core.CityModel; -import org.xml.sax.SAXParseException; - -import java.io.File; -import java.util.*; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Stream; public class CityDoctorController { @@ -98,40 +138,14 @@ public class CityDoctorController { }); currentChecker = null; currentConfig = new ParserConfiguration(numberOfRoundingPlaces, useValidation, lowMemory); - GMLValidationHandler handler = null; - List validationIssues = new ArrayList<>(); - if (useValidation) { - handler = new GMLValidationHandler() { - @Override - public void error(SAXParseException exception) { - if (exception.getLineNumber() >= 0) { - String s = "In line " + exception.getLineNumber() + ":"; - validationIssues.add(s); - } - validationIssues.add(exception.getMessage()); - } - - @Override - public void warning(SAXParseException exception) { - error(exception); - } - - @Override - public void fatalError(SAXParseException exception) { - error(exception); - } - }; - } - model = CityGmlParser.parseCityGmlFile(path, currentConfig, l, handler); - if (!validationIssues.isEmpty()) { - StringJoiner sj = new StringJoiner("\n"); - validationIssues.stream().forEach(sj::add); - throw new InvalidGmlFileException(sj.toString()); - } + model = CityGmlParser.parseCityGmlFile(path, currentConfig, l); + mainWindow.getClickHandler().setConfig(currentConfig); sourceFile = path; renderer.reset(); Platform.runLater(() -> { + // show errors that occured during parsing + updateGlobalErrors(); mainWindow.addFileNameToTitle(path); mainWindow.getCheckButton().setDisable(false); mainWindow.getLod1Btn().setDisable(false); @@ -1099,6 +1113,11 @@ public class CityDoctorController { for (CheckError e : model.getGlobalErrors()) { e.accept(globErrVisitor); } + if (model.getGlobalErrors().isEmpty()) { + mainWindow.getGlobalErrorsLabel().setStyle("-fx-text-fill: black;"); + } else { + mainWindow.getGlobalErrorsLabel().setStyle("-fx-text-fill: red;"); + } } void updateFeatureTrees() { @@ -1233,6 +1252,7 @@ public class CityDoctorController { case LAND -> filterLandTree(searchString); case CITY_FURNITURE -> filterCityFurnitureTree(searchString); case OTHER_CITY_OBJECT -> filterOtherCityObjectsTree(searchString); + default -> throw new IllegalArgumentException("selected tab is unknown: " + selectedTab); } diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/GlobalErrorVisitor.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/GlobalErrorVisitor.java index 287ea14d..0bfb5d97 100644 --- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/GlobalErrorVisitor.java +++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/GlobalErrorVisitor.java @@ -42,6 +42,7 @@ import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError; import de.hft.stuttgart.citydoctor2.check.error.SurfaceUnfragmentedError; import de.hft.stuttgart.citydoctor2.check.error.TooFewPolygonsError; import de.hft.stuttgart.citydoctor2.check.error.UnknownCheckError; +import de.hft.stuttgart.citydoctor2.check.error.XMLValidationError; public class GlobalErrorVisitor implements ErrorVisitor { @@ -221,5 +222,11 @@ public class GlobalErrorVisitor implements ErrorVisitor { @Override public void visit(SolidError err) { } + + @Override + public void visit(XMLValidationError err) { + mainWindow.getGlobalErrorsView().getItems().add(err.getErrorId().getIdString() + + " - " + err.getMessage()); + } } diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ListErrorVisitor.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ListErrorVisitor.java index 34f7132a..b86814a3 100644 --- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ListErrorVisitor.java +++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ListErrorVisitor.java @@ -45,6 +45,7 @@ import de.hft.stuttgart.citydoctor2.check.error.SurfaceUnfragmentedError; import de.hft.stuttgart.citydoctor2.check.error.DegeneratedRingError; import de.hft.stuttgart.citydoctor2.check.error.TooFewPolygonsError; import de.hft.stuttgart.citydoctor2.check.error.UnknownCheckError; +import de.hft.stuttgart.citydoctor2.check.error.XMLValidationError; import de.hft.stuttgart.citydoctor2.datastructure.Edge; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.Vertex; @@ -295,4 +296,9 @@ public class ListErrorVisitor implements ErrorVisitor { public void visit(SolidError err) { // nothing to display } + + @Override + public void visit(XMLValidationError err) { + // nothing to display + } } diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/MainWindow.java b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/MainWindow.java index 5fb958e1..1ca7c4f0 100644 --- a/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/MainWindow.java +++ b/CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/MainWindow.java @@ -208,6 +208,8 @@ public class MainWindow extends Application { @FXML private Label availableLabel; + + private Label globalErrorsLabel; private Image north; private Image northFlip; @@ -310,9 +312,6 @@ public class MainWindow extends Application { ViewRegistration.registerView(valView); setupViews(valView); - createLanguageSelector(); - setLabelsInCorrectLanguage(); - Scene scene = new Scene(bp, 1280, 800); createDropTarget(scene, valView); String version = Localization.getText(Localization.VERSION); @@ -544,6 +543,9 @@ public class MainWindow extends Application { setupSearchButtons(); setupShowCityComboBox(); + + createLanguageSelector(); + setLabelsInCorrectLanguage(); detailsTabPane.getSelectionModel().selectedIndexProperty() .addListener((ov, oldI, newI) -> Platform.runLater(() -> { @@ -553,6 +555,11 @@ public class MainWindow extends Application { errorView.getSelectionModel().getSelectedItem().getValue().visit(renderer); } })); + + // setup label of the global error tab for color changes + globalErrorsLabel = new Label(globalErrorsTab.getText()); + globalErrorsTab.setGraphic(globalErrorsLabel); + globalErrorsTab.setText(""); } private void setupShowCityComboBox() { @@ -1190,4 +1197,8 @@ public class MainWindow extends Application { public FeatureType getSelectedTab() { return selectedTab; } + + public Label getGlobalErrorsLabel() { + return globalErrorsLabel; + } } -- GitLab From 1fbe16e1c9a1bedc34793ede8ccf59a70047f4f3 Mon Sep 17 00:00:00 2001 From: Matthias Betz Date: Fri, 12 Dec 2025 10:18:54 +0100 Subject: [PATCH 09/63] add xml validation to streaming processing --- .../citydoctor2/checkresult/CheckReport.java | 7 +- .../citydoctor2/parser/CityGmlConsumer.java | 1 - .../citydoctor2/parser/CityGmlParser.java | 1237 +++++++++-------- .../writer/CityGMLWriterUtils.java | 2 +- .../parser/CityGml3ParserTest.java | 2 +- .../stuttgart/citydoctor2/check/Checker.java | 15 + .../citydoctor2/check/CheckerTest.java | 319 +++-- .../SimpleSolid_SrefBS_XMLInvalid.gml | 199 +++ .../testConfigWithStreamingWithValidation.yml | 46 + .../stuttgart/citydoctor2/healer/Healer.java | 2 +- 10 files changed, 1063 insertions(+), 767 deletions(-) create mode 100644 CityDoctorParent/CityDoctorValidation/src/test/resources/SimpleSolid_SrefBS_XMLInvalid.gml create mode 100644 CityDoctorParent/CityDoctorValidation/src/test/resources/testConfigWithStreamingWithValidation.yml diff --git a/CityDoctorParent/CityDoctorCheckResult/src/main/java/de/hft/stuttgart/citydoctor2/checkresult/CheckReport.java b/CityDoctorParent/CityDoctorCheckResult/src/main/java/de/hft/stuttgart/citydoctor2/checkresult/CheckReport.java index 5f6d0e84..31e2d868 100644 --- a/CityDoctorParent/CityDoctorCheckResult/src/main/java/de/hft/stuttgart/citydoctor2/checkresult/CheckReport.java +++ b/CityDoctorParent/CityDoctorCheckResult/src/main/java/de/hft/stuttgart/citydoctor2/checkresult/CheckReport.java @@ -22,6 +22,7 @@ import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.io.OutputStream; +import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -65,7 +66,11 @@ public class CheckReport { private Map featureReports; public static CheckReport load(String file) throws CheckReportParseException { - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + return load(Path.of(file)); + } + + public static CheckReport load(Path path) throws CheckReportParseException { + try (BufferedReader reader = new BufferedReader(new FileReader(path.toFile()))) { CheckReport report = (CheckReport) getContext().createUnmarshaller().unmarshal(new InputSource(reader)); enrichReportStructure(report); return report; diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlConsumer.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlConsumer.java index 0400e60d..900ce896 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlConsumer.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlConsumer.java @@ -32,6 +32,5 @@ public interface CityGmlConsumer { } -// public void endOfFile(); } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java index ceb369f9..07224105 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/parser/CityGmlParser.java @@ -102,618 +102,629 @@ import de.hft.stuttgart.quality.QualityADEModule; */ public class CityGmlParser { - private static final String CITY_OBJECT_MEMBER = "cityObjectMember"; - - private static final String WGS_84 = "EPSG:4326"; - - private static final Logger logger = LogManager.getLogger(CityGmlParser.class); - - private static final CRSFactory CRS_FACTORY = new CRSFactory(); - // EPSG:31467 - private static final Pattern P_EPSG = Pattern.compile("^(EPSG:\\d+)$"); - // urn:ogc:def:crs,crs:EPSG:6.12:31467,crs:EPSG:6.12:5783 - // or - // urn:ogc:def:crs,crs:EPSG::28992 - private static final Pattern P_OGC = Pattern.compile("urn:ogc:def:crs,crs:EPSG:[\\d\\.]*:([\\d]+)\\D*"); - - private static final Pattern P_OGC2 = Pattern.compile("urn:ogc:def:crs:EPSG:[\\d\\.]*:([\\d]+)\\D*"); - - // urn:adv:crs:DE_DHDN_3GK3*DE_DHHN92_NH - // urn:adv:crs:ETRS89_UTM32*DE_DHHN92_NH - private static final Pattern P_URN = Pattern.compile("urn:adv:crs:([^\\*]+)"); - - private static final SAXParserFactory FACTORY; - - private static Proj4FileReader csReader = new Proj4FileReader(); - private static final Pattern P_VUNITS = Pattern.compile("^\\+vunits=.+"); - private static final Pattern P_GEOIDGRID = Pattern.compile("^\\+geoidgrid=.+"); - - private static CityGMLContext context; - private static List chunkProperties = new ArrayList<>(); - - - static { - System.setProperty("javax.xml.transform.TransformerFactory", "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"); - FACTORY = SAXParserFactory.newInstance(); - try { - FACTORY.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); - } catch (SAXNotRecognizedException | SAXNotSupportedException | ParserConfigurationException e) { - logger.catching(e); - } - - chunkProperties.add(new QName(CityGMLConstants.CITYGML_1_0_CORE_NAMESPACE, CITY_OBJECT_MEMBER)); - chunkProperties.add(new QName(CityGMLConstants.CITYGML_2_0_CORE_NAMESPACE, CITY_OBJECT_MEMBER)); - chunkProperties.add(new QName(CityGMLConstants.CITYGML_3_0_CORE_NAMESPACE, CITY_OBJECT_MEMBER)); - } - - private CityGmlParser() { - } - - public static synchronized CityGMLContext getContext() { - if (context == null) { - try { - context = CityGMLContext.newInstance(CityGmlParser.class.getClassLoader()); - // also setup ades - ADERegistry adeRegistry = ADERegistry.getInstance(); - adeRegistry.loadADE(new QualityADEContext()); - } catch (CityGMLContextException e) { - logger.fatal(Localization.getText("CityGmlParser.creating4jContextFailed"), e); - throw new IllegalStateException("Unable to create citygml4j context"); - } catch (ADEException e) { - logger.fatal(Localization.getText("CityGmlParser.creatingAdePluginsFailed"), e); - throw new IllegalStateException("Unable to add ADE plugins to citygml4j"); - } - } - return context; - } - - /** - * Parses a given CityGML file and collects the contained CityObject Features in a {@link CityDoctorModel}. - *

- * The nested parse uses an {@link de.hft.stuttgart.citydoctor2.database.UnconnectedCache UnconnectedCache}, which - * is not connected to the database. - *

- * Nested parsing should only be used for accessing Features in auxiliary, small CityGML files, e.g. parsing of - * {@link de.hft.stuttgart.citydoctor2.datastructure.LibraryObject LibraryObject} files. - * - * @param filePath String path to the file - * @param config Configuration of the parser - * @return A {@link CityDoctorModel} object with the parsed Features - * @throws CityGmlParseException if parsing of the CityGML-file failed - * @throws InvalidGmlFileException if the GMLValidationHandler found an error - */ - public static CityDoctorModel nestedCityGmlFileParse(String filePath, ParserConfiguration config) - throws CityGmlParseException, InvalidGmlFileException { - return parseCityGmlFile(filePath, config, null, null, false, true); - } - - /** - * Parses a given CityGML file and collects the contained CityObject Features in a {@link CityDoctorModel}. - *

- * Features are held in a non-persistent, embedded Database with an in-memory upstream cache. Thus, strong references - * to {@link CityObject} are to be avoided, to allow for the memory management of the cache. - * - * @param filePath String path to the file - * @param config Configuration of the parser - * @return A {@link CityDoctorModel} object with the parsed Features - * @throws CityGmlParseException if parsing of the CityGML-file failed - * @throws InvalidGmlFileException if the GMLValidationHandler found an error - */ - public static CityDoctorModel parseCityGmlFile(String filePath, ParserConfiguration config) - throws CityGmlParseException, InvalidGmlFileException { - return parseCityGmlFile(filePath, config, null, null); - } - - - /** - * Parses a given CityGML file and collects the contained CityObject Features in a {@link CityDoctorModel}. - *

- * Features are held in a non-persistent, embedded Database with an in-memory upstream cache. Thus, strong references - * to {@link CityObject} are to be avoided, to allow for the memory management of the cache. - * - * @param filePath String path to the file - * @param config Configuration of the parser - * @param l Listener hook for tracking of parsing progress - * @return A {@link CityDoctorModel} object with the parsed Features - * @throws CityGmlParseException if parsing of the CityGML-file failed - * @throws InvalidGmlFileException if the GMLValidationHandler found an error - */ - public static CityDoctorModel parseCityGmlFile(String filePath, ParserConfiguration config, ProgressListener l) - throws CityGmlParseException, InvalidGmlFileException { - return parseCityGmlFile(filePath, config, l, null); - } - - /** - * Parses a given CityGML file and collects the contained CityObject Features in a {@link CityDoctorModel}. - *

- * Features are held in a non-persistent, embedded Database with an in-memory upstream cache. Thus, strong references - * to {@link CityObject} are to be avoided, to allow for the memory management of the cache. - * - * @param filePath String path to the file - * @param config Configuration of the parser - * @param l Listener hook for tracking of parsing progress - * @param handler Validation handler for GML-file validation - * @return A {@link CityDoctorModel} object with the parsed Features - * @throws CityGmlParseException if parsing of the CityGML-file failed - * @throws InvalidGmlFileException if the GMLValidationHandler found an error - */ - public static CityDoctorModel parseCityGmlFile(String filePath, ParserConfiguration config, ProgressListener l, - GMLValidationHandler handler) throws CityGmlParseException, InvalidGmlFileException { - return parseCityGmlFile(filePath, config, l, handler, true, false); - } - - /** - * Parses a given CityGML file and collects the contained CityObject Features in a {@link CityDoctorModel}. - *

- * Features are held in a non-persistent, embedded Database with an in-memory upstream cache. Thus, strong references - * to {@link CityObject} are to be avoided, to allow for the memory management of the cache. - * - * @param filePath String path to the file - * @param config Configuration of the parser - * @param l Listener hook for tracking of parsing progress - * @param handler Validation handler for GML-file validation - * @param verbose If verbosity is enabled the parser will write info logs to the logger - * @param unconnected If true the parser will use an {@link de.hft.stuttgart.citydoctor2.database.UnconnectedCache UnconnectedCache} - * to collect the parsed CityObjects - * @return A {@link CityDoctorModel} object with the parsed Features - * @throws CityGmlParseException if parsing of the CityGML-file failed - * @throws InvalidGmlFileException if the GMLValidationHandler found an error - */ - private static CityDoctorModel parseCityGmlFile(String filePath, ParserConfiguration config, ProgressListener l, - GMLValidationHandler handler, boolean verbose, boolean unconnected) - throws CityGmlParseException, InvalidGmlFileException { - CityGMLContext context = getContext(); - Path file = Paths.get(filePath); - List validationIssues = Collections.emptyList(); - if (config.getValidate()) { - validationIssues = validateFile(context, handler, file); - } - - try { - parseEpsgCodeFromFile(file, config, verbose); - CityGMLInputFactory in = context.createCityGMLInputFactory() - .withChunking(ChunkOptions.chunkByProperties(chunkProperties).skipCityModel(false)); - try (ObservedInputStream ois = new ObservedInputStream(file.toFile())) { - if (l != null) { - ois.addListener(l::updateProgress); - } - CityDoctorModel model = readAndKeepFeatures(config, file, in, ois, verbose, unconnected); - for (String xmlError : validationIssues) { - model.addGlobalError(new XMLValidationError(xmlError)); - } - return model; - } - } catch (CityGMLReadException | IOException e) { - throw new CityGmlParseException("Failed to read CityGML file", e); - } - } - - - - public static void streamCityGml(String file, ParserConfiguration config, CityGmlConsumer cityObjectConsumer, - String outputFile) throws CityGmlParseException { - Path f = Paths.get(file); - streamCityGml(f, config, null, cityObjectConsumer, outputFile); - } - - public static void streamCityGml(File file, ParserConfiguration parserConfig, CityGmlConsumer cityObjectConsumer, - String outputFile) throws CityGmlParseException { - streamCityGml(file.toPath(), parserConfig, null, cityObjectConsumer, outputFile); - } - - public static void streamCityGml(Path file, ParserConfiguration config, ProgressListener l, - CityGmlConsumer cityObjectConsumer, String outputFile) throws CityGmlParseException { - parseEpsgCodeFromFile(file, config, true); - startReadingCityGmlFile(file, config, l, cityObjectConsumer, outputFile); - } - - public static CityModel parseOnlyCityModel(File inputFile) throws CityGmlParseException { - try { - CityGMLInputFactory inputFactory = context.createCityGMLInputFactory() - .withChunking(ChunkOptions.chunkByProperties(chunkProperties).skipCityModel(false)); - try (CityGMLReader reader = inputFactory.createCityGMLReader(inputFile)) { - while (reader.hasNext()) { - AbstractFeature chunk = reader.next(); - if (chunk instanceof CityModel cModel) { - cModel.setCityObjectMembers(null); - return cModel; - } - } - } - } catch (CityGMLReadException e) { - throw new CityGmlParseException(e); - } - throw new CityGmlParseException("Did not find any CityModel in CityGML file"); - } - - private static void startReadingCityGmlFile(Path file, ParserConfiguration config, ProgressListener l, - CityGmlConsumer cityObjectConsumer, String outputFile) { - try (ObservedInputStream ois = new ObservedInputStream(file.toFile())) { - if (l != null) { - ois.addListener(l::updateProgress); - } - readAndDiscardFeatures(file, config, ois, cityObjectConsumer, outputFile); - } catch (IOException | CityGMLReadException e) { - logger.error(Localization.getText("CityGmlParser.errorReadingGmlFile"), e.getMessage()); - logger.catching(Level.ERROR, e); - } - } - - private static void readAndDiscardFeatures(Path file, ParserConfiguration config, ObservedInputStream ois, - CityGmlConsumer cityObjectConsumer, String outputFile) throws CityGMLReadException { - Citygml3FeatureMapper mapper = new Citygml3FeatureMapper(config, file, false); - readAndDiscardModel(mapper, ois, cityObjectConsumer, outputFile); - - } - - private static void readAndDiscardModel(Citygml3FeatureMapper mapper, ObservedInputStream ois, - CityGmlConsumer cityObjectConsumer, String outputFile) throws CityGMLReadException { - getContext(); - CityGMLInputFactory inputFactory = context.createCityGMLInputFactory() - .withChunking(ChunkOptions.chunkByProperties(chunkProperties).skipCityModel(false)); - CityGMLChunkWriter writer = null; - try (CityGMLReader reader = inputFactory.createCityGMLReader(ois)) { - - CityDoctorModel model = mapper.getModel(); - boolean isInitialized = false; - while (reader.hasNext()) { - AbstractFeature chunk = reader.next(); - if (writer == null) { - writer = createCityModelWriter(outputFile, reader); - } - if (!isInitialized && writer != null && reader.getParentInfo() != null - && reader.getParentInfo().getTypeName().getLocalPart().equals("CityModel")) { - FeatureInfo parentInfo = reader.getParentInfo(); - writer.withCityModelInfo(parentInfo); - isInitialized = true; - } - if (chunk instanceof AbstractCityObject ag) { - ag.accept(mapper); - drainCityModel(model, cityObjectConsumer); - writeAbstractCityObject(writer, ag); - } else if (chunk instanceof CityModel cModel) { - cModel.setCityObjectMembers(null); - mapper.setCityModel(cModel); - cityObjectConsumer.accept(cModel); - writeCityModel(writer, cModel); - } else if (writer != null) { - writer.writeMember(chunk); - } - } - - // end of stream - logger.debug("End of gml file stream"); - } catch (CityGMLReadException e) { - logger.error(Localization.getText("CityGmlParser.errorReadingGmlFile"), e.getMessage(), e); - } catch (CityGMLWriteException e) { - logger.error(Localization.getText("CityGmlParser.errorWritingGmlFile"), e.getMessage(), e); - } finally { - if (writer != null) { - try { - writer.close(); - } catch (CityGMLWriteException e) { - // ignore - } - } - } - } - - private static void writeCityModel(CityGMLChunkWriter writer, CityModel cModel) throws CityGMLWriteException { - if (writer != null) { - for (AbstractFeatureProperty featureProp : cModel.getFeatureMembers()) { - writer.writeMember(featureProp.getObject()); - } - } - } - - private static void writeAbstractCityObject(CityGMLChunkWriter writer, AbstractCityObject ag) - throws CityGMLWriteException { - if (writer != null) { - writer.writeMember(ag); - } - } - - private static CityGMLChunkWriter createCityModelWriter(String outputFile, CityGMLReader reader) - throws CityGMLWriteException { - if (outputFile == null) { - return null; - } - CityGMLContext gmlContext = CityGmlParser.getContext(); - CityGMLVersion version = CityGMLModules.getCityGMLVersion(reader.getName().getNamespaceURI()); - CityGMLOutputFactory factory = gmlContext.createCityGMLOutputFactory(version); - CityGMLChunkWriter writer = factory.createCityGMLChunkWriter(new File(outputFile), - StandardCharsets.UTF_8.name()); - writer.withPrefix("qual", QualityADEModule.NAMESPACE_URI); - writer.withSchemaLocation(QualityADEModule.NAMESPACE_URI, QualityADEModule.NAMESPACE_URI + "/qualityAde.xsd"); - writer.withIndent(" "); - writer.withDefaultPrefixes(); - writer.withDefaultSchemaLocations(); - return writer; - } - - private static CityDoctorModel readAndKeepFeatures(ParserConfiguration config, Path file, - CityGMLInputFactory inputFactory, ObservedInputStream ois, - boolean verbose, boolean unconnected) throws CityGMLReadException { - return readAndKeepModel(new Citygml3FeatureMapper(config, file, unconnected), inputFactory, ois, verbose); - } - - private static CityDoctorModel readAndKeepModel(Citygml3FeatureMapper mapper, CityGMLInputFactory inputFactory, - ObservedInputStream ois, boolean verbose) throws CityGMLReadException { - try (CityGMLReader reader = inputFactory.createCityGMLReader(ois)) { - CityGMLVersion version = null; - // model is read in chunked mode - // object members are replaced by href in model - // need to remove the refs and re-add unparsed objects - List acos = new ArrayList<>(); - CityModel cgmlModel = null; - CityDoctorModel cdModel = mapper.getModel(); - AutoCloseable parsingContext = cdModel.getCache().startParsingMode(); - try (parsingContext){ - List queuedObjects = new ArrayList<>(); - while (reader.hasNext()) { - AbstractFeature chunk = reader.next(); - version = CityGMLModules.getCityGMLVersion(reader.getName().getNamespaceURI()); - if (chunk instanceof CityModel cModel) { - cModel.setCityObjectMembers(null); - mapper.setCityModel(cModel); - mapper.setCityGMLVersion(version); - cgmlModel = cModel; - - if (!queuedObjects.isEmpty()) { - for (AbstractCityObject aco : queuedObjects) { - if (!cdModel.containsGmlId(aco.getId())) { - cgmlModel.getCityObjectMembers().add(new AbstractCityObjectProperty(aco)); - } - } - } - } else if (chunk instanceof AbstractCityObject aco) { - acos.add(aco); // getid - String id = aco.getId(); - aco.accept(mapper); - if (cgmlModel == null) { - // Add object to queue till the CityModel has been parsed - queuedObjects.add(aco); - }else if (!cdModel.containsGmlId(id)){ - cgmlModel.getCityObjectMembers().add(new AbstractCityObjectProperty(aco)); - } - } - } - - if (mapper.getModel().getCityModel() == null) { - // file does not contain a city model? - // create it for now - logger.warn(Localization.getText("CityGmlParser.missingCityModel")); - mapper.setCityModel(new CityModel()); - for (AbstractCityObject aco : queuedObjects) { - if (!cdModel.containsGmlId(aco.getId())) { - mapper.getModel().getCityModel().getCityObjectMembers().add(new AbstractCityObjectProperty(aco)); - } - } - } - - ImplicitGeometryHolder.resolveDeferredObjects(mapper.getModel().getCache()); - if (logger.isInfoEnabled() && verbose) { - logger.info(Localization.getText("CityGmlParser.parsedObjects"), - mapper.getModel().getNumberOfFeatures()); - } - mapper.checkUnresolvedReferences(); - mapper.setCityGMLVersion(version); - return mapper.getModel(); - } catch (Exception e) { - throw new CityGMLReadException(e); - } - - } - } - - private static void parseEpsgCodeFromFile(Path file, ParserConfiguration config, boolean verbose) throws CityGmlParseException { - try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file.toFile()))) { - parseEpsgCodeFromStream(bis, config, verbose); - } catch (ParserConfigurationException | SAXException | IOException e) { - throw new CityGmlParseException("Failed to read CityGML file", e); - } - } - - private static void parseEpsgCodeFromStream(InputStream is, ParserConfiguration config, boolean verbose) - throws ParserConfigurationException, SAXException { - SAXParser parser = FACTORY.newSAXParser(); - CityGmlHandler handler = new CityGmlHandler(); - try { - parser.parse(new InputSource(is), handler); - } catch (EnvelopeFoundException e) { - try { - parseCoordinateSystem(config, handler, verbose); - } catch (Exception e2) { - logEpsgParseError(e2); - } - } catch (Exception e) { - logEpsgParseError(e); - } - if (handler.getEpsg() == null && logger.isInfoEnabled() && verbose) { - logger.info(Localization.getText("CityGmlParser.missingEPSGCode")); - } - - } - - private static void logEpsgParseError(Exception e) { - logger.debug("Exception while parsing for EPSG code", e); - if (logger.isWarnEnabled()) { - logger.warn(Localization.getText("CityGmlParser.failedEPSGParse")); - } - } - - private static void parseCoordinateSystem(ParserConfiguration config, CityGmlHandler handler, boolean verbose) { - if (handler.getEpsg() == null) { - return; - } - CoordinateReferenceSystem crs = crsFromSrsName(handler.getEpsg()); - if (crs == null) { - // could not find a coordinate system for srsName - // assuming metric system - if (logger.isInfoEnabled() && verbose) { - logger.info(Localization.getText("CityGmlParser.missingEPSGCode")); - } - return; - } - if (crs.getProjection().getUnits() == Units.METRES) { - // coordinate system is in meters, do not convert - if (logger.isInfoEnabled() && verbose) { - logger.info(Localization.getText("CityGmlParser.noConversionNeeded")); - } - return; - } - parseMeterConversion(config, crs); - Vector3d low = handler.getLowerCorner(); - Vector3d up = handler.getUpperCorner(); - double centerLong = low.getX() + ((up.getX() - low.getX()) / 2); - double centerLat = low.getY() + ((up.getY() - low.getY()) / 2); - if (!crs.getName().equals(WGS_84)) { - // need to convert coordinates first to WGS84, then find UTM Zone - CoordinateReferenceSystem wgs84 = crsFromSrsName(WGS_84); - ProjCoordinate p1 = new ProjCoordinate(); - p1.setValue(centerLong, centerLat); - ProjCoordinate p2 = new ProjCoordinate(); - BasicCoordinateTransform bct = new BasicCoordinateTransform(crs, wgs84); - bct.transform(p1, p2); - centerLong = p2.x; - centerLat = p2.y; - } - int zone = (int) (31 + Math.round(centerLong / 6)); - CoordinateReferenceSystem utm; - if (centerLat < 0) { - // south - if (logger.isInfoEnabled() && verbose) { - logger.info(Localization.getText("CityGmlParser.convertCrsToUtmZoneS"), zone); - } - utm = CRS_FACTORY.createFromParameters("UTM", "+proj=utm +ellps=WGS84 +units=m +zone=" + zone + " +south"); - } else { - // north - if (logger.isInfoEnabled() && verbose) { - logger.info(Localization.getText("CityGmlParser.convertCrsToUtmZoneN"), zone); - } - utm = CRS_FACTORY.createFromParameters("UTM", "+proj=utm +ellps=WGS84 +units=m +zone=" + zone); - } - config.setCoordinateSystem(crs, utm); - } - - private static void parseMeterConversion(ParserConfiguration config, CoordinateReferenceSystem crs) { - Projection projection = crs.getProjection(); - double fromMetres = projection.getFromMetres(); - if (fromMetres > 0) { - // also transform height information - config.setFromMeters(fromMetres); - } else { - config.setFromMeters(1.0); - } - } - - /** - * The srsName (The name by which this reference system is identified) inside - * the CityGML file can have multiple formats. This method tries to parse the - * string and detect the corresponding reference system. If it is found, it - * returns a proj4j.CoordinateReferenceSystem. It throws an - * IllegalArgumentException otherwise. - *

- * This method should be able to parse any EPSG id : e.g. "EPSG:1234". German - * Citygmls might also have "DE_DHDN_3GK3" or "ETRS89_UTM32" as srsName, so - * those are also included. It isn't guaranteed that those formats are correctly - * parsed, though. - *

- * The EPSG ids and parameters are defined in resources ('nad/epsg') inside - * proj4j-0.1.0.jar. Some EPSG ids are missing though, e.g. 7415 - * - * @param srsName - * @return CoordinateReferenceSystem - */ - private static CoordinateReferenceSystem crsFromSrsName(String srsName) { - srsName = srsName.trim(); - Matcher mEPSG = P_EPSG.matcher(srsName); - if (mEPSG.find()) { - if ("EPSG:4979".contentEquals(srsName)) { - srsName = "EPSG:4236"; - } else if ("EPSG:7415".contentEquals(srsName)) { - return CRS_FACTORY.createFromParameters("EPSG:7415", - "+proj=sterea +lat_0=52.15616055555555 +lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 +y_0=463000 +ellps=bessel +towgs84=565.417,50.3319,465.552,-0.398957,0.343988,-1.8774,4.0725 +units=m +no_defs"); - } - return CRS_FACTORY.createFromName(srsName); - } - - Matcher mOGC = P_OGC.matcher(srsName); - if (mOGC.find()) { - return getCrsFromOGCSrsCode(mOGC.group(1)); - } - Matcher mOGC2 = P_OGC2.matcher(srsName); - if (mOGC2.find()) { - return getCrsFromOGCSrsCode(mOGC2.group(1)); - - } - Matcher mURN = P_URN.matcher(srsName); - // NOTE: Could use a HashMap if the switch/case becomes too long. - if (mURN.find()) { - return switch (mURN.group(1)) { - case "DE_DHDN_3GK2" -> CRS_FACTORY.createFromName("EPSG:31466"); - case "DE_DHDN_3GK3" -> CRS_FACTORY.createFromName("EPSG:31467"); - case "DE_DHDN_3GK4" -> CRS_FACTORY.createFromName("EPSG:31468"); - case "DE_DHDN_3GK5" -> CRS_FACTORY.createFromName("EPSG:31469"); - case "ETRS89_UTM32" -> CRS_FACTORY.createFromName("EPSG:25832"); - default -> null; - }; - } - if (srsName.equals("http://www.opengis.net/def/crs/EPSG/0/6697")) { - return CRS_FACTORY.createFromParameters("EPSG:6697", "+proj=longlat +ellps=GRS80 +no_defs +axis=neu"); - } - return null; - } - - private static CoordinateReferenceSystem getCrsFromOGCSrsCode(String srsCode) { - String crsName = "EPSG:" + srsCode; - List params = new ArrayList<>(Arrays.stream(csReader.getParameters(crsName)).toList()); - params.removeIf(P_VUNITS.asPredicate()); - params.removeIf(P_GEOIDGRID.asPredicate()); - return CRS_FACTORY.createFromParameters(crsName, params.toArray(new String[0])); - } - - private static List validateFile(CityGMLContext context, GMLValidationHandler handler, Path file) - throws CityGmlParseException { - if (handler == null) { - handler = new GMLValidationHandler(); - } - try { - SchemaHandler schemaHandler = new ValidationSchemaHandler(context.getDefaultSchemaHandler()); - readAdditionalSchemaDefinitions(context, file, schemaHandler); - Source[] schemas = schemaHandler.getSchemas(); - SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - schemaFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - Schema schema = schemaFactory.newSchema(schemas); - Validator validator = schema.newValidator(); - validator.setErrorHandler(handler); - validator.validate(new StreamSource(file.toFile())); - return handler.getMessages(); - } catch (SchemaHandlerException | SAXException | IOException e) { - throw new CityGmlParseException("Failed to validate CityGML file", e); - } - } - - private static void readAdditionalSchemaDefinitions(CityGMLContext context, Path file, SchemaHandler schemaHandler) - throws CityGmlParseException { - try (XMLReader reader = XMLReaderFactory.newInstance(context.getXMLObjects()) - .withSchemaHandler(schemaHandler) - .createReader(file)) { - reader.nextTag(); - } catch (Exception e) { - throw new CityGmlParseException("Failed to read file " + file.toAbsolutePath() + ".", e); - } - } - - private static void drainCityModel(CityDoctorModel model, CityGmlConsumer cityObjectConsumer) { - List id = new ArrayList<>(); - model.createFeatureStream().forEach(co -> { - cityObjectConsumer.accept(co); - id.add(co.getGmlId()); - }); - id.forEach(model::removeFeature); - - } - + private static final String CITY_OBJECT_MEMBER = "cityObjectMember"; + + private static final String WGS_84 = "EPSG:4326"; + + private static final Logger logger = LogManager.getLogger(CityGmlParser.class); + + private static final CRSFactory CRS_FACTORY = new CRSFactory(); + // EPSG:31467 + private static final Pattern P_EPSG = Pattern.compile("^(EPSG:\\d+)$"); + // urn:ogc:def:crs,crs:EPSG:6.12:31467,crs:EPSG:6.12:5783 + // or + // urn:ogc:def:crs,crs:EPSG::28992 + private static final Pattern P_OGC = Pattern.compile("urn:ogc:def:crs,crs:EPSG:[\\d\\.]*:([\\d]+)\\D*"); + + private static final Pattern P_OGC2 = Pattern.compile("urn:ogc:def:crs:EPSG:[\\d\\.]*:([\\d]+)\\D*"); + + // urn:adv:crs:DE_DHDN_3GK3*DE_DHHN92_NH + // urn:adv:crs:ETRS89_UTM32*DE_DHHN92_NH + private static final Pattern P_URN = Pattern.compile("urn:adv:crs:([^\\*]+)"); + + private static final SAXParserFactory FACTORY; + + private static Proj4FileReader csReader = new Proj4FileReader(); + private static final Pattern P_VUNITS = Pattern.compile("^\\+vunits=.+"); + private static final Pattern P_GEOIDGRID = Pattern.compile("^\\+geoidgrid=.+"); + + private static CityGMLContext context; + private static List chunkProperties = new ArrayList<>(); + + static { + System.setProperty("javax.xml.transform.TransformerFactory", + "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"); + FACTORY = SAXParserFactory.newInstance(); + try { + FACTORY.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + } catch (SAXNotRecognizedException | SAXNotSupportedException | ParserConfigurationException e) { + logger.catching(e); + } + + chunkProperties.add(new QName(CityGMLConstants.CITYGML_1_0_CORE_NAMESPACE, CITY_OBJECT_MEMBER)); + chunkProperties.add(new QName(CityGMLConstants.CITYGML_2_0_CORE_NAMESPACE, CITY_OBJECT_MEMBER)); + chunkProperties.add(new QName(CityGMLConstants.CITYGML_3_0_CORE_NAMESPACE, CITY_OBJECT_MEMBER)); + } + + private CityGmlParser() { + } + + public static synchronized CityGMLContext setupContext() { + if (context == null) { + try { + context = CityGMLContext.newInstance(CityGmlParser.class.getClassLoader()); + // also setup ades + ADERegistry adeRegistry = ADERegistry.getInstance(); + adeRegistry.loadADE(new QualityADEContext()); + } catch (CityGMLContextException e) { + logger.fatal(Localization.getText("CityGmlParser.creating4jContextFailed"), e); + throw new IllegalStateException("Unable to create citygml4j context"); + } catch (ADEException e) { + logger.fatal(Localization.getText("CityGmlParser.creatingAdePluginsFailed"), e); + throw new IllegalStateException("Unable to add ADE plugins to citygml4j"); + } + } + return context; + } + + /** + * Parses a given CityGML file and collects the contained CityObject Features in + * a {@link CityDoctorModel}. + *

+ * The nested parse uses an + * {@link de.hft.stuttgart.citydoctor2.database.UnconnectedCache + * UnconnectedCache}, which is not connected to the database. + *

+ * Nested parsing should only be used for accessing Features in auxiliary, small + * CityGML files, e.g. parsing of + * {@link de.hft.stuttgart.citydoctor2.datastructure.LibraryObject + * LibraryObject} files. + * + * @param filePath String path to the file + * @param config Configuration of the parser + * @return A {@link CityDoctorModel} object with the parsed Features + * @throws CityGmlParseException if parsing of the CityGML-file failed + * @throws InvalidGmlFileException if the GMLValidationHandler found an error + */ + public static CityDoctorModel nestedCityGmlFileParse(String filePath, ParserConfiguration config) + throws CityGmlParseException, InvalidGmlFileException { + return parseCityGmlFile(filePath, config, null, null, false, true); + } + + /** + * Parses a given CityGML file and collects the contained CityObject Features in + * a {@link CityDoctorModel}. + *

+ * Features are held in a non-persistent, embedded Database with an in-memory + * upstream cache. Thus, strong references to {@link CityObject} are to be + * avoided, to allow for the memory management of the cache. + * + * @param filePath String path to the file + * @param config Configuration of the parser + * @return A {@link CityDoctorModel} object with the parsed Features + * @throws CityGmlParseException if parsing of the CityGML-file failed + * @throws InvalidGmlFileException if the GMLValidationHandler found an error + */ + public static CityDoctorModel parseCityGmlFile(String filePath, ParserConfiguration config) + throws CityGmlParseException, InvalidGmlFileException { + return parseCityGmlFile(filePath, config, null, null); + } + + /** + * Parses a given CityGML file and collects the contained CityObject Features in + * a {@link CityDoctorModel}. + *

+ * Features are held in a non-persistent, embedded Database with an in-memory + * upstream cache. Thus, strong references to {@link CityObject} are to be + * avoided, to allow for the memory management of the cache. + * + * @param filePath String path to the file + * @param config Configuration of the parser + * @param l Listener hook for tracking of parsing progress + * @return A {@link CityDoctorModel} object with the parsed Features + * @throws CityGmlParseException if parsing of the CityGML-file failed + * @throws InvalidGmlFileException if the GMLValidationHandler found an error + */ + public static CityDoctorModel parseCityGmlFile(String filePath, ParserConfiguration config, ProgressListener l) + throws CityGmlParseException, InvalidGmlFileException { + return parseCityGmlFile(filePath, config, l, null); + } + + /** + * Parses a given CityGML file and collects the contained CityObject Features in + * a {@link CityDoctorModel}. + *

+ * Features are held in a non-persistent, embedded Database with an in-memory + * upstream cache. Thus, strong references to {@link CityObject} are to be + * avoided, to allow for the memory management of the cache. + * + * @param filePath String path to the file + * @param config Configuration of the parser + * @param l Listener hook for tracking of parsing progress + * @param handler Validation handler for GML-file validation + * @return A {@link CityDoctorModel} object with the parsed Features + * @throws CityGmlParseException if parsing of the CityGML-file failed + * @throws InvalidGmlFileException if the GMLValidationHandler found an error + */ + public static CityDoctorModel parseCityGmlFile(String filePath, ParserConfiguration config, ProgressListener l, + GMLValidationHandler handler) throws CityGmlParseException, InvalidGmlFileException { + return parseCityGmlFile(filePath, config, l, handler, true, false); + } + + /** + * Parses a given CityGML file and collects the contained CityObject Features in + * a {@link CityDoctorModel}. + *

+ * Features are held in a non-persistent, embedded Database with an in-memory + * upstream cache. Thus, strong references to {@link CityObject} are to be + * avoided, to allow for the memory management of the cache. + * + * @param filePath String path to the file + * @param config Configuration of the parser + * @param l Listener hook for tracking of parsing progress + * @param handler Validation handler for GML-file validation + * @param verbose If verbosity is enabled the parser will write info logs to + * the logger + * @param unconnected If true the parser will use an + * {@link de.hft.stuttgart.citydoctor2.database.UnconnectedCache + * UnconnectedCache} to collect the parsed CityObjects + * @return A {@link CityDoctorModel} object with the parsed Features + * @throws CityGmlParseException if parsing of the CityGML-file failed + * @throws InvalidGmlFileException if the GMLValidationHandler found an error + */ + private static CityDoctorModel parseCityGmlFile(String filePath, ParserConfiguration config, ProgressListener l, + GMLValidationHandler handler, boolean verbose, boolean unconnected) + throws CityGmlParseException, InvalidGmlFileException { + CityGMLContext context = setupContext(); + Path file = Paths.get(filePath); + List validationIssues = Collections.emptyList(); + if (config.getValidate()) { + validationIssues = validateFile(handler, file); + } + try { + parseEpsgCodeFromFile(file, config, verbose); + CityGMLInputFactory in = context.createCityGMLInputFactory() + .withChunking(ChunkOptions.chunkByProperties(chunkProperties).skipCityModel(false)); + try (ObservedInputStream ois = new ObservedInputStream(file.toFile())) { + if (l != null) { + ois.addListener(l::updateProgress); + } + CityDoctorModel model = readAndKeepFeatures(config, file, in, ois, verbose, unconnected); + for (String xmlError : validationIssues) { + model.addGlobalError(new XMLValidationError(xmlError)); + } + return model; + } + } catch (CityGMLReadException | IOException e) { + throw new CityGmlParseException("Failed to read CityGML file", e); + } + } + + public static void streamCityGml(String file, ParserConfiguration config, CityGmlConsumer cityObjectConsumer, + String outputFile) throws CityGmlParseException { + Path f = Paths.get(file); + streamCityGml(f, config, null, cityObjectConsumer, outputFile); + } + + public static void streamCityGml(File file, ParserConfiguration parserConfig, CityGmlConsumer cityObjectConsumer, + String outputFile) throws CityGmlParseException { + streamCityGml(file.toPath(), parserConfig, null, cityObjectConsumer, outputFile); + } + + public static void streamCityGml(Path file, ParserConfiguration config, ProgressListener l, + CityGmlConsumer cityObjectConsumer, String outputFile) throws CityGmlParseException { + parseEpsgCodeFromFile(file, config, true); + startReadingCityGmlFile(file, config, l, cityObjectConsumer, outputFile); + } + + public static CityModel parseOnlyCityModel(File inputFile) throws CityGmlParseException { + try { + CityGMLInputFactory inputFactory = context.createCityGMLInputFactory() + .withChunking(ChunkOptions.chunkByProperties(chunkProperties).skipCityModel(false)); + try (CityGMLReader reader = inputFactory.createCityGMLReader(inputFile)) { + while (reader.hasNext()) { + AbstractFeature chunk = reader.next(); + if (chunk instanceof CityModel cModel) { + cModel.setCityObjectMembers(null); + return cModel; + } + } + } + } catch (CityGMLReadException e) { + throw new CityGmlParseException(e); + } + throw new CityGmlParseException("Did not find any CityModel in CityGML file"); + } + + private static void startReadingCityGmlFile(Path file, ParserConfiguration config, ProgressListener l, + CityGmlConsumer cityObjectConsumer, String outputFile) { + try (ObservedInputStream ois = new ObservedInputStream(file.toFile())) { + if (l != null) { + ois.addListener(l::updateProgress); + } + readAndDiscardFeatures(file, config, ois, cityObjectConsumer, outputFile); + } catch (IOException | CityGMLReadException e) { + logger.error(Localization.getText("CityGmlParser.errorReadingGmlFile"), e.getMessage()); + logger.catching(Level.ERROR, e); + } + } + + private static void readAndDiscardFeatures(Path file, ParserConfiguration config, ObservedInputStream ois, + CityGmlConsumer cityObjectConsumer, String outputFile) throws CityGMLReadException { + Citygml3FeatureMapper mapper = new Citygml3FeatureMapper(config, file, false); + readAndDiscardModel(mapper, ois, cityObjectConsumer, outputFile, config, file); + + } + + private static void readAndDiscardModel(Citygml3FeatureMapper mapper, ObservedInputStream ois, + CityGmlConsumer cityObjectConsumer, String outputFile, ParserConfiguration config, Path file) + throws CityGMLReadException { + setupContext(); + CityGMLInputFactory inputFactory = context.createCityGMLInputFactory() + .withChunking(ChunkOptions.chunkByProperties(chunkProperties).skipCityModel(false)); + CityGMLChunkWriter writer = null; + try (CityGMLReader reader = inputFactory.createCityGMLReader(ois)) { + CityDoctorModel model = mapper.getModel(); + boolean isInitialized = false; + while (reader.hasNext()) { + AbstractFeature chunk = reader.next(); + if (writer == null) { + writer = createCityModelWriter(outputFile, reader); + } + if (!isInitialized && writer != null && reader.getParentInfo() != null + && reader.getParentInfo().getTypeName().getLocalPart().equals("CityModel")) { + FeatureInfo parentInfo = reader.getParentInfo(); + writer.withCityModelInfo(parentInfo); + isInitialized = true; + } + if (chunk instanceof AbstractCityObject ag) { + ag.accept(mapper); + drainCityModel(model, cityObjectConsumer); + writeAbstractCityObject(writer, ag); + } else if (chunk instanceof CityModel cModel) { + cModel.setCityObjectMembers(null); + mapper.setCityModel(cModel); + cityObjectConsumer.accept(cModel); + writeCityModel(writer, cModel); + } else if (writer != null) { + writer.writeMember(chunk); + } + } + + // end of stream + logger.debug("End of gml file stream"); + } catch (CityGMLReadException e) { + logger.error(Localization.getText("CityGmlParser.errorReadingGmlFile"), e.getMessage(), e); + } catch (CityGMLWriteException e) { + logger.error(Localization.getText("CityGmlParser.errorWritingGmlFile"), e.getMessage(), e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (CityGMLWriteException e) { + // ignore + } + } + } + } + + private static void writeCityModel(CityGMLChunkWriter writer, CityModel cModel) throws CityGMLWriteException { + if (writer != null) { + for (AbstractFeatureProperty featureProp : cModel.getFeatureMembers()) { + writer.writeMember(featureProp.getObject()); + } + } + } + + private static void writeAbstractCityObject(CityGMLChunkWriter writer, AbstractCityObject ag) + throws CityGMLWriteException { + if (writer != null) { + writer.writeMember(ag); + } + } + + private static CityGMLChunkWriter createCityModelWriter(String outputFile, CityGMLReader reader) + throws CityGMLWriteException { + if (outputFile == null) { + return null; + } + CityGMLContext gmlContext = CityGmlParser.setupContext(); + CityGMLVersion version = CityGMLModules.getCityGMLVersion(reader.getName().getNamespaceURI()); + CityGMLOutputFactory factory = gmlContext.createCityGMLOutputFactory(version); + CityGMLChunkWriter writer = factory.createCityGMLChunkWriter(new File(outputFile), + StandardCharsets.UTF_8.name()); + writer.withPrefix("qual", QualityADEModule.NAMESPACE_URI); + writer.withSchemaLocation(QualityADEModule.NAMESPACE_URI, QualityADEModule.NAMESPACE_URI + "/qualityAde.xsd"); + writer.withIndent(" "); + writer.withDefaultPrefixes(); + writer.withDefaultSchemaLocations(); + return writer; + } + + private static CityDoctorModel readAndKeepFeatures(ParserConfiguration config, Path file, + CityGMLInputFactory inputFactory, ObservedInputStream ois, boolean verbose, boolean unconnected) + throws CityGMLReadException { + return readAndKeepModel(new Citygml3FeatureMapper(config, file, unconnected), inputFactory, ois, verbose); + } + + private static CityDoctorModel readAndKeepModel(Citygml3FeatureMapper mapper, CityGMLInputFactory inputFactory, + ObservedInputStream ois, boolean verbose) throws CityGMLReadException { + try (CityGMLReader reader = inputFactory.createCityGMLReader(ois)) { + CityGMLVersion version = null; + // model is read in chunked mode + // object members are replaced by href in model + // need to remove the refs and re-add unparsed objects + List acos = new ArrayList<>(); + CityModel cgmlModel = null; + CityDoctorModel cdModel = mapper.getModel(); + AutoCloseable parsingContext = cdModel.getCache().startParsingMode(); + try (parsingContext) { + List queuedObjects = new ArrayList<>(); + while (reader.hasNext()) { + AbstractFeature chunk = reader.next(); + version = CityGMLModules.getCityGMLVersion(reader.getName().getNamespaceURI()); + if (chunk instanceof CityModel cModel) { + cModel.setCityObjectMembers(null); + mapper.setCityModel(cModel); + mapper.setCityGMLVersion(version); + cgmlModel = cModel; + + if (!queuedObjects.isEmpty()) { + for (AbstractCityObject aco : queuedObjects) { + if (!cdModel.containsGmlId(aco.getId())) { + cgmlModel.getCityObjectMembers().add(new AbstractCityObjectProperty(aco)); + } + } + } + } else if (chunk instanceof AbstractCityObject aco) { + acos.add(aco); // getid + String id = aco.getId(); + aco.accept(mapper); + if (cgmlModel == null) { + // Add object to queue till the CityModel has been parsed + queuedObjects.add(aco); + } else if (!cdModel.containsGmlId(id)) { + cgmlModel.getCityObjectMembers().add(new AbstractCityObjectProperty(aco)); + } + } + } + + if (mapper.getModel().getCityModel() == null) { + // file does not contain a city model? + // create it for now + logger.warn(Localization.getText("CityGmlParser.missingCityModel")); + mapper.setCityModel(new CityModel()); + for (AbstractCityObject aco : queuedObjects) { + if (!cdModel.containsGmlId(aco.getId())) { + mapper.getModel().getCityModel().getCityObjectMembers() + .add(new AbstractCityObjectProperty(aco)); + } + } + } + + ImplicitGeometryHolder.resolveDeferredObjects(mapper.getModel().getCache()); + if (logger.isInfoEnabled() && verbose) { + logger.info(Localization.getText("CityGmlParser.parsedObjects"), + mapper.getModel().getNumberOfFeatures()); + } + mapper.checkUnresolvedReferences(); + mapper.setCityGMLVersion(version); + return mapper.getModel(); + } catch (Exception e) { + throw new CityGMLReadException(e); + } + + } + } + + private static void parseEpsgCodeFromFile(Path file, ParserConfiguration config, boolean verbose) + throws CityGmlParseException { + try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file.toFile()))) { + parseEpsgCodeFromStream(bis, config, verbose); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw new CityGmlParseException("Failed to read CityGML file", e); + } + } + + private static void parseEpsgCodeFromStream(InputStream is, ParserConfiguration config, boolean verbose) + throws ParserConfigurationException, SAXException { + SAXParser parser = FACTORY.newSAXParser(); + CityGmlHandler handler = new CityGmlHandler(); + try { + parser.parse(new InputSource(is), handler); + } catch (EnvelopeFoundException e) { + try { + parseCoordinateSystem(config, handler, verbose); + } catch (Exception e2) { + logEpsgParseError(e2); + } + } catch (Exception e) { + logEpsgParseError(e); + } + if (handler.getEpsg() == null && logger.isInfoEnabled() && verbose) { + logger.info(Localization.getText("CityGmlParser.missingEPSGCode")); + } + + } + + private static void logEpsgParseError(Exception e) { + logger.debug("Exception while parsing for EPSG code", e); + if (logger.isWarnEnabled()) { + logger.warn(Localization.getText("CityGmlParser.failedEPSGParse")); + } + } + + private static void parseCoordinateSystem(ParserConfiguration config, CityGmlHandler handler, boolean verbose) { + if (handler.getEpsg() == null) { + return; + } + CoordinateReferenceSystem crs = crsFromSrsName(handler.getEpsg()); + if (crs == null) { + // could not find a coordinate system for srsName + // assuming metric system + if (logger.isInfoEnabled() && verbose) { + logger.info(Localization.getText("CityGmlParser.missingEPSGCode")); + } + return; + } + if (crs.getProjection().getUnits() == Units.METRES) { + // coordinate system is in meters, do not convert + if (logger.isInfoEnabled() && verbose) { + logger.info(Localization.getText("CityGmlParser.noConversionNeeded")); + } + return; + } + parseMeterConversion(config, crs); + Vector3d low = handler.getLowerCorner(); + Vector3d up = handler.getUpperCorner(); + double centerLong = low.getX() + ((up.getX() - low.getX()) / 2); + double centerLat = low.getY() + ((up.getY() - low.getY()) / 2); + if (!crs.getName().equals(WGS_84)) { + // need to convert coordinates first to WGS84, then find UTM Zone + CoordinateReferenceSystem wgs84 = crsFromSrsName(WGS_84); + ProjCoordinate p1 = new ProjCoordinate(); + p1.setValue(centerLong, centerLat); + ProjCoordinate p2 = new ProjCoordinate(); + BasicCoordinateTransform bct = new BasicCoordinateTransform(crs, wgs84); + bct.transform(p1, p2); + centerLong = p2.x; + centerLat = p2.y; + } + int zone = (int) (31 + Math.round(centerLong / 6)); + CoordinateReferenceSystem utm; + if (centerLat < 0) { + // south + if (logger.isInfoEnabled() && verbose) { + logger.info(Localization.getText("CityGmlParser.convertCrsToUtmZoneS"), zone); + } + utm = CRS_FACTORY.createFromParameters("UTM", "+proj=utm +ellps=WGS84 +units=m +zone=" + zone + " +south"); + } else { + // north + if (logger.isInfoEnabled() && verbose) { + logger.info(Localization.getText("CityGmlParser.convertCrsToUtmZoneN"), zone); + } + utm = CRS_FACTORY.createFromParameters("UTM", "+proj=utm +ellps=WGS84 +units=m +zone=" + zone); + } + config.setCoordinateSystem(crs, utm); + } + + private static void parseMeterConversion(ParserConfiguration config, CoordinateReferenceSystem crs) { + Projection projection = crs.getProjection(); + double fromMetres = projection.getFromMetres(); + if (fromMetres > 0) { + // also transform height information + config.setFromMeters(fromMetres); + } else { + config.setFromMeters(1.0); + } + } + + /** + * The srsName (The name by which this reference system is identified) inside + * the CityGML file can have multiple formats. This method tries to parse the + * string and detect the corresponding reference system. If it is found, it + * returns a proj4j.CoordinateReferenceSystem. It throws an + * IllegalArgumentException otherwise. + *

+ * This method should be able to parse any EPSG id : e.g. "EPSG:1234". German + * Citygmls might also have "DE_DHDN_3GK3" or "ETRS89_UTM32" as srsName, so + * those are also included. It isn't guaranteed that those formats are correctly + * parsed, though. + *

+ * The EPSG ids and parameters are defined in resources ('nad/epsg') inside + * proj4j-0.1.0.jar. Some EPSG ids are missing though, e.g. 7415 + * + * @param srsName + * @return CoordinateReferenceSystem + */ + private static CoordinateReferenceSystem crsFromSrsName(String srsName) { + srsName = srsName.trim(); + Matcher mEPSG = P_EPSG.matcher(srsName); + if (mEPSG.find()) { + if ("EPSG:4979".contentEquals(srsName)) { + srsName = "EPSG:4236"; + } else if ("EPSG:7415".contentEquals(srsName)) { + return CRS_FACTORY.createFromParameters("EPSG:7415", + "+proj=sterea +lat_0=52.15616055555555 +lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 +y_0=463000 +ellps=bessel +towgs84=565.417,50.3319,465.552,-0.398957,0.343988,-1.8774,4.0725 +units=m +no_defs"); + } + return CRS_FACTORY.createFromName(srsName); + } + + Matcher mOGC = P_OGC.matcher(srsName); + if (mOGC.find()) { + return getCrsFromOGCSrsCode(mOGC.group(1)); + } + Matcher mOGC2 = P_OGC2.matcher(srsName); + if (mOGC2.find()) { + return getCrsFromOGCSrsCode(mOGC2.group(1)); + + } + Matcher mURN = P_URN.matcher(srsName); + // NOTE: Could use a HashMap if the switch/case becomes too long. + if (mURN.find()) { + return switch (mURN.group(1)) { + case "DE_DHDN_3GK2" -> CRS_FACTORY.createFromName("EPSG:31466"); + case "DE_DHDN_3GK3" -> CRS_FACTORY.createFromName("EPSG:31467"); + case "DE_DHDN_3GK4" -> CRS_FACTORY.createFromName("EPSG:31468"); + case "DE_DHDN_3GK5" -> CRS_FACTORY.createFromName("EPSG:31469"); + case "ETRS89_UTM32" -> CRS_FACTORY.createFromName("EPSG:25832"); + default -> null; + }; + } + if (srsName.equals("http://www.opengis.net/def/crs/EPSG/0/6697")) { + return CRS_FACTORY.createFromParameters("EPSG:6697", "+proj=longlat +ellps=GRS80 +no_defs +axis=neu"); + } + return null; + } + + private static CoordinateReferenceSystem getCrsFromOGCSrsCode(String srsCode) { + String crsName = "EPSG:" + srsCode; + List params = new ArrayList<>(Arrays.stream(csReader.getParameters(crsName)).toList()); + params.removeIf(P_VUNITS.asPredicate()); + params.removeIf(P_GEOIDGRID.asPredicate()); + return CRS_FACTORY.createFromParameters(crsName, params.toArray(new String[0])); + } + + public static List validateFile(GMLValidationHandler handler, Path file) + throws CityGmlParseException { + if (handler == null) { + handler = new GMLValidationHandler(); + } + setupContext(); + try { + SchemaHandler schemaHandler = new ValidationSchemaHandler(context.getDefaultSchemaHandler()); + readAdditionalSchemaDefinitions(context, file, schemaHandler); + Source[] schemas = schemaHandler.getSchemas(); + SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + schemaFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + Schema schema = schemaFactory.newSchema(schemas); + Validator validator = schema.newValidator(); + validator.setErrorHandler(handler); + validator.validate(new StreamSource(file.toFile())); + return handler.getMessages(); + } catch (SchemaHandlerException | SAXException | IOException e) { + throw new CityGmlParseException("Failed to validate CityGML file", e); + } + } + + private static void readAdditionalSchemaDefinitions(CityGMLContext context, Path file, SchemaHandler schemaHandler) + throws CityGmlParseException { + try (XMLReader reader = XMLReaderFactory.newInstance(context.getXMLObjects()).withSchemaHandler(schemaHandler) + .createReader(file)) { + reader.nextTag(); + } catch (Exception e) { + throw new CityGmlParseException("Failed to read file " + file.toAbsolutePath() + ".", e); + } + } + + private static void drainCityModel(CityDoctorModel model, CityGmlConsumer cityObjectConsumer) { + List id = new ArrayList<>(); + model.createFeatureStream().forEach(co -> { + cityObjectConsumer.accept(co); + id.add(co.getGmlId()); + }); + id.forEach(model::removeFeature); + + } } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/writer/CityGMLWriterUtils.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/writer/CityGMLWriterUtils.java index e3b9cbfb..b73c1d97 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/writer/CityGMLWriterUtils.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/writer/CityGMLWriterUtils.java @@ -65,7 +65,7 @@ public class CityGMLWriterUtils { } public static void writeCityModel(String file, CityDoctorModel model) throws CityDoctorWriteException { - CityGMLContext gmlContext = CityGmlParser.getContext(); + CityGMLContext gmlContext = CityGmlParser.setupContext(); CityModel cModel = model.getCityModel(); CityGMLOutputFactory factory = gmlContext.createCityGMLOutputFactory(model.getCityGMLVersion()); try (CityGMLWriter writer = factory.createCityGMLWriter(new File(file))) { diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/parser/CityGml3ParserTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/parser/CityGml3ParserTest.java index 88b958f4..ad0e6057 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/parser/CityGml3ParserTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/parser/CityGml3ParserTest.java @@ -37,7 +37,7 @@ public class CityGml3ParserTest { @Test public void testGetContext() { - CityGMLContext context = CityGmlParser.getContext(); + CityGMLContext context = CityGmlParser.setupContext(); List ades = ADERegistry.getInstance().getADEs(); ADE ade = ades.get(0); assertEquals(QualityADEContext.class, ade.getClass()); diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java index 29786f51..7b3afcf3 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java @@ -65,6 +65,7 @@ import de.hft.stuttgart.citydoctor2.check.error.AttributeInvalidError; import de.hft.stuttgart.citydoctor2.check.error.AttributeMissingError; import de.hft.stuttgart.citydoctor2.check.error.AttributeValueWrongError; import de.hft.stuttgart.citydoctor2.check.error.SchematronError; +import de.hft.stuttgart.citydoctor2.check.error.XMLValidationError; import de.hft.stuttgart.citydoctor2.checkresult.utility.CheckReportWriteException; import de.hft.stuttgart.citydoctor2.checks.CheckContainer; import de.hft.stuttgart.citydoctor2.checks.CheckPrototype; @@ -918,6 +919,20 @@ public class Checker { // create reporter if available XmlStreamReporter xmlReporter = getXmlReporter(config, xmlBos, fileName); PdfStreamReporter pdfReporter = getPdfReporter(config, pdfBos, fileName); + + // XML validation + if (config.isUseStreaming() && (xmlReporter != null || pdfReporter != null)) { + List validationIssues = CityGmlParser.validateFile(null, inputFile.toPath()); + for (String xmlError : validationIssues) { + XMLValidationError validationError = new XMLValidationError(xmlError); + if (xmlReporter != null) { + xmlReporter.reportGlobalError(validationError); + } + if (pdfReporter != null) { + pdfReporter.reportGlobalError(validationError); + } + } + } // execute schematron first SvrlContentHandler handler = executeSchematronValidationIfAvailable(config, inputFile); diff --git a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/CheckerTest.java b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/CheckerTest.java index cbc489ab..8b20dd99 100644 --- a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/CheckerTest.java +++ b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/check/CheckerTest.java @@ -18,6 +18,22 @@ */ package de.hft.stuttgart.citydoctor2.check; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + import de.hft.stuttgart.citydoctor2.CityDoctorValidation; import de.hft.stuttgart.citydoctor2.checkresult.CheckReport; import de.hft.stuttgart.citydoctor2.checkresult.GlobalErrorStatistics; @@ -31,160 +47,165 @@ import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException; import de.hft.stuttgart.citydoctor2.parser.CityGmlParser; import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException; import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; -import org.apache.commons.io.FileUtils; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; /** * @author Matthias Betz */ public class CheckerTest { - @Rule - public TemporaryFolder folder = new TemporaryFolder(); - - @Test - public void testSchematron() throws CityGmlParseException, InvalidGmlFileException { - ValidationConfiguration config = ValidationConfiguration.loadStandardValidationConfig(); - config.getRequirements().get(Requirement.R_SE_BS_ROOF_UNFRAGMENTED.toString()).setEnabled(false); - config.setSchematronFilePathInGlobalParameters("src/test/resources/schematronTest.xml"); - CityDoctorModel model = CityGmlParser.parseCityGmlFile( - "src/test/resources/SimpleSolid_SrefBS_SchematronTest.gml", config.getParserConfiguration()); - Checker checker = new Checker(config, model); - checker.runChecks(); - for (Building b : model.getBuildings().toList()) { - if (b.getGmlId().getGmlString().equals("_Simple_BD.1")) { - assertTrue(b.containsAnyError()); - } else { - assertFalse(b.containsAnyError()); - } - } - assertFalse(model.getGlobalErrors().isEmpty()); - } - - @Test - public void testChecker() throws CityGmlParseException, IOException, InvalidGmlFileException, CityDoctorWriteException { - - File f = folder.newFile(); - File f2 = folder.newFile(); - try { - String[] args = new String[6]; - args[0] = "-in"; - args[1] = "src/test/resources/QA-CS-CONCOMP.gml"; - args[2] = "-xmlReport"; - args[3] = f.getAbsolutePath(); - args[4] = "-pdfReport"; - args[5] = f2.getAbsolutePath(); - CityDoctorValidation.main(args); - assertTrue(f.exists()); - assertTrue(f2.exists()); - } finally { - f.delete(); - f2.delete(); - } - } - - @Test - public void testStreaming() throws CityGmlParseException, IOException, InvalidGmlFileException, CityDoctorWriteException { - File f = folder.newFile(); - File f2 = folder.newFile(); - File f3 = folder.newFile(); - try { - String[] args = new String[10]; - args[0] = "-in"; - args[1] = "src/test/resources/testarea.gml"; - args[2] = "-config"; - args[3] = "src/test/resources/testConfigWithStreaming.yml"; - args[4] = "-pdfReport"; - args[5] = f.getAbsolutePath(); - args[6] = "-xmlReport"; - args[7] = f2.getAbsolutePath(); - args[8] = "-out"; - args[9] = f3.getAbsolutePath(); - CityDoctorValidation.main(args); - assertTrue(f.exists()); - assertTrue(f2.exists()); - assertTrue(f3.exists()); - } finally { - f.delete(); - f2.delete(); - f3.delete(); - } - } - - @Test - public void testReportCreation() throws IOException, CityGmlParseException, InvalidGmlFileException { - Path tmpDir = null; - try { - tmpDir = Files.createTempDirectory("testTmp"); - tmpDir.toFile().deleteOnExit(); - Path xmlPath = tmpDir.resolve("report.xml"); - Path pdfPath = tmpDir.resolve("report.pdf"); - File features = new File("src/test/resources/feature_types.gml"); - CityDoctorModel model = CityGmlParser.nestedCityGmlFileParse( - features.toString(), new ParserConfiguration(8, false)); - assertNotNull(model); - Checker checker = new Checker(model); - checker.runChecks(); - checker.writeXmlReport(xmlPath.toString()); - checker.writePdfReport(pdfPath.toString()); - assertTrue(Files.exists(xmlPath)); - assertTrue(Files.exists(pdfPath)); - try { - CheckReport report = CheckReport.load(xmlPath.toString()); - assertNotNull(report); - - GlobalStatistics globalStatistics = report.getGlobalStatistics(); - assertNotNull(globalStatistics); - - ModelStatistics modelStats = globalStatistics.getModelStats(); - assertNotNull(modelStats); - assertEquals(1, modelStats.getNumBuildings()); - assertEquals(1, modelStats.getNumVegetation()); - assertEquals(1, modelStats.getNumTransportation()); - assertEquals(1, modelStats.getNumTunnelObjects()); - assertEquals(1, modelStats.getNumBridgeObjects()); - assertEquals(1, modelStats.getNumWaterObjects()); - assertEquals(1, modelStats.getNumLandObjects()); - assertEquals(1, modelStats.getNumCityFurniture()); - assertEquals(1, modelStats.getNumGenericCityObjects()); - - GlobalErrorStatistics globError = globalStatistics.getGlobalErrorStats(); - assertNotNull(globError); - assertEquals(0, globError.getNumErrorBuildings()); - assertEquals(0, globError.getNumErrorVegetation()); - assertEquals(0, globError.getNumErrorTransportation()); - assertEquals(0, globError.getNumErrorTunnelObjects()); - assertEquals(0, globError.getNumErrorBridgeObjects()); - assertEquals(0, globError.getNumErrorWaterObjects()); - assertEquals(0, globError.getNumErrorLandObjects()); - assertEquals(1, globError.getNumErrorCityFurniture()); - assertEquals(1, globError.getNumErrorGenericCityObjects()); - - - } catch (CheckReportParseException e) { - fail("Report should be valid file"); - } - - - } finally { - if (tmpDir != null) { - FileUtils.deleteDirectory(tmpDir.toFile()); - } - } - } + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void testSchematron() throws CityGmlParseException, InvalidGmlFileException { + ValidationConfiguration config = ValidationConfiguration.loadStandardValidationConfig(); + config.getRequirements().get(Requirement.R_SE_BS_ROOF_UNFRAGMENTED.toString()).setEnabled(false); + config.setSchematronFilePathInGlobalParameters("src/test/resources/schematronTest.xml"); + CityDoctorModel model = CityGmlParser.parseCityGmlFile( + "src/test/resources/SimpleSolid_SrefBS_SchematronTest.gml", config.getParserConfiguration()); + Checker checker = new Checker(config, model); + checker.runChecks(); + for (Building b : model.getBuildings().toList()) { + if (b.getGmlId().getGmlString().equals("_Simple_BD.1")) { + assertTrue(b.containsAnyError()); + } else { + assertFalse(b.containsAnyError()); + } + } + assertFalse(model.getGlobalErrors().isEmpty()); + } + + @Test + public void testChecker() + throws CityGmlParseException, IOException, InvalidGmlFileException, CityDoctorWriteException { + + File f = folder.newFile(); + File f2 = folder.newFile(); + try { + String[] args = new String[6]; + args[0] = "-in"; + args[1] = "src/test/resources/QA-CS-CONCOMP.gml"; + args[2] = "-xmlReport"; + args[3] = f.getAbsolutePath(); + args[4] = "-pdfReport"; + args[5] = f2.getAbsolutePath(); + CityDoctorValidation.main(args); + assertTrue(f.exists()); + assertTrue(f2.exists()); + } finally { + f.delete(); + f2.delete(); + } + } + + @Test + public void testStreaming() + throws CityGmlParseException, IOException, InvalidGmlFileException, CityDoctorWriteException { + File f = folder.newFile(); + File f2 = folder.newFile(); + File f3 = folder.newFile(); + try { + String[] args = new String[10]; + args[0] = "-in"; + args[1] = "src/test/resources/testarea.gml"; + args[2] = "-config"; + args[3] = "src/test/resources/testConfigWithStreaming.yml"; + args[4] = "-pdfReport"; + args[5] = f.getAbsolutePath(); + args[6] = "-xmlReport"; + args[7] = f2.getAbsolutePath(); + args[8] = "-out"; + args[9] = f3.getAbsolutePath(); + CityDoctorValidation.main(args); + assertTrue(f.exists()); + assertTrue(f2.exists()); + assertTrue(f3.exists()); + } finally { + f.delete(); + f2.delete(); + f3.delete(); + } + } + + @Test + public void testStreamingWithXMLValidation() throws CityGmlParseException, IOException, InvalidGmlFileException, + CityDoctorWriteException, CheckReportParseException { + File f2 = folder.newFile(); + try { + String[] args = new String[6]; + args[0] = "-in"; + args[1] = "src/test/resources/SimpleSolid_SrefBS_XMLInvalid.gml"; + args[2] = "-config"; + args[3] = "src/test/resources/testConfigWithStreamingWithValidation.yml"; + args[4] = "-xmlReport"; + args[5] = f2.getAbsolutePath(); + CityDoctorValidation.main(args); + assertTrue(f2.exists()); + CheckReport checkReport = CheckReport.load(f2.toPath()); + assertEquals(1, checkReport.getValidationResults().getGlobalErrors().size()); + } finally { + f2.delete(); + } + } + + @Test + public void testReportCreation() throws IOException, CityGmlParseException, InvalidGmlFileException { + Path tmpDir = null; + try { + tmpDir = Files.createTempDirectory("testTmp"); + tmpDir.toFile().deleteOnExit(); + Path xmlPath = tmpDir.resolve("report.xml"); + Path pdfPath = tmpDir.resolve("report.pdf"); + File features = new File("src/test/resources/feature_types.gml"); + CityDoctorModel model = CityGmlParser.nestedCityGmlFileParse(features.toString(), + new ParserConfiguration(8, false)); + assertNotNull(model); + Checker checker = new Checker(model); + checker.runChecks(); + checker.writeXmlReport(xmlPath.toString()); + checker.writePdfReport(pdfPath.toString()); + assertTrue(Files.exists(xmlPath)); + assertTrue(Files.exists(pdfPath)); + try { + CheckReport report = CheckReport.load(xmlPath.toString()); + assertNotNull(report); + + GlobalStatistics globalStatistics = report.getGlobalStatistics(); + assertNotNull(globalStatistics); + + ModelStatistics modelStats = globalStatistics.getModelStats(); + assertNotNull(modelStats); + assertEquals(1, modelStats.getNumBuildings()); + assertEquals(1, modelStats.getNumVegetation()); + assertEquals(1, modelStats.getNumTransportation()); + assertEquals(1, modelStats.getNumTunnelObjects()); + assertEquals(1, modelStats.getNumBridgeObjects()); + assertEquals(1, modelStats.getNumWaterObjects()); + assertEquals(1, modelStats.getNumLandObjects()); + assertEquals(1, modelStats.getNumCityFurniture()); + assertEquals(1, modelStats.getNumGenericCityObjects()); + + GlobalErrorStatistics globError = globalStatistics.getGlobalErrorStats(); + assertNotNull(globError); + assertEquals(0, globError.getNumErrorBuildings()); + assertEquals(0, globError.getNumErrorVegetation()); + assertEquals(0, globError.getNumErrorTransportation()); + assertEquals(0, globError.getNumErrorTunnelObjects()); + assertEquals(0, globError.getNumErrorBridgeObjects()); + assertEquals(0, globError.getNumErrorWaterObjects()); + assertEquals(0, globError.getNumErrorLandObjects()); + assertEquals(1, globError.getNumErrorCityFurniture()); + assertEquals(1, globError.getNumErrorGenericCityObjects()); + + } catch (CheckReportParseException e) { + fail("Report should be valid file"); + } + + } finally { + if (tmpDir != null) { + FileUtils.deleteDirectory(tmpDir.toFile()); + } + } + } } diff --git a/CityDoctorParent/CityDoctorValidation/src/test/resources/SimpleSolid_SrefBS_XMLInvalid.gml b/CityDoctorParent/CityDoctorValidation/src/test/resources/SimpleSolid_SrefBS_XMLInvalid.gml new file mode 100644 index 00000000..327d9ca0 --- /dev/null +++ b/CityDoctorParent/CityDoctorValidation/src/test/resources/SimpleSolid_SrefBS_XMLInvalid.gml @@ -0,0 +1,199 @@ + + + + + + + + + 2.5 + + falsche Reihenfolge + + + + + + + + + + + + + + + + + + + + + + + + + 13.0 15.0 0.0 + 13.0 15.0 3.0 + 13.0 10.0 3.0 + 13.0 10.0 0.0 + 13.0 15.0 0.0 + + + + + + + + + + + + + + + + + + + 10.0 15.0 0.0 + 10.0 15.0 3.0 + 11.5 15.0 4.5 + 13.0 15.0 3.0 + 13.0 15.0 0.0 + 10.0 15.0 0.0 + + + + + + + + + + + + + + + + + + + 10.0 10.0 3.0 + 10.0 15.0 3.0 + 10.0 15.0 0.0 + 10.0 10.0 0.0 + 10.0 10.0 3.0 + + + + + + + + + + + + + + + + + + + 13.0 10.0 0.0 + 13.0 10.0 3.0 + 11.5 10.0 4.5 + 10.0 10.0 3.0 + 10.0 10.0 0.0 + 13.0 10.0 0.0 + + + + + + + + + + + + + + + + + + + 10.0 10.0 3.0 + 11.5 10.0 4.5 + 11.5 15.0 4.5 + 10.0 15.0 3.0 + 10.0 10.0 3.0 + + + + + + + + + + + 11.5 10.0 4.5 + 13.0 10.0 3.0 + 13.0 15.0 3.0 + 11.5 15.0 4.5 + 11.5 10.0 4.5 + + + + + + + + + + + + + + + + + + + 10.0 10.0 0.0 + 10.0 15.0 0.0 + 13.0 15.0 0.0 + 13.0 10.0 0.0 + 10.0 10.0 0.0 + + + + + + + + + + + + \ No newline at end of file diff --git a/CityDoctorParent/CityDoctorValidation/src/test/resources/testConfigWithStreamingWithValidation.yml b/CityDoctorParent/CityDoctorValidation/src/test/resources/testConfigWithStreamingWithValidation.yml new file mode 100644 index 00000000..311740e3 --- /dev/null +++ b/CityDoctorParent/CityDoctorValidation/src/test/resources/testConfigWithStreamingWithValidation.yml @@ -0,0 +1,46 @@ +globalParameters: + numberOfRoundingPlaces: 8 + schematronFilePath: src/test/resources/schematronTest.xml +useStreaming: true +requirements: + R_GE_R_TOO_FEW_POINTS: + enabled: true + R_GE_R_NOT_CLOSED: + enabled: true + R_GE_R_CONSECUTIVE_POINTS_SAME: + enabled: true + R_GE_R_SELF_INTERSECTION: + enabled: true + R_GE_S_MULTIPLE_CONNECTED_COMPONENTS: + enabled: true + R_GE_P_INTERIOR_DISCONNECTED: + enabled: true + R_GE_P_INTERSECTING_RINGS: + enabled: true + R_GE_P_NON_PLANAR: + enabled: false + parameters: + # one of ("distance", "angle", "both") + type: distance + distanceTolerance: 0.01 + angleTolerance: 0.1 + R_GE_P_HOLE_OUTSIDE: + enabled: true + R_GE_P_ORIENTATION_RINGS_SAME: + enabled: true + R_GE_P_INNER_RINGS_NESTED: + enabled: true + R_GE_S_TOO_FEW_POLYGONS: + enabled: true + R_GE_S_NOT_CLOSED: + enabled: true + R_GE_S_NON_MANIFOLD_EDGE: + enabled: true + R_GE_S_POLYGON_WRONG_ORIENTATION: + enabled: true + R_GE_S_ALL_POLYGONS_WRONG_ORIENTATION: + enabled: true + R_GE_S_NON_MANIFOLD_VERTEX: + enabled: true + R_GE_S_SELF_INTERSECTION: + enabled: true \ No newline at end of file diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java index 7ac14f87..06be694b 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java @@ -289,7 +289,7 @@ public class Healer { private byte[] writeCityGml(AbstractCityObject aco) throws CityGMLWriteException { CityModel model = new CityModel(); model.getCityObjectMembers().add(new AbstractCityObjectProperty(aco)); - CityGMLContext context = CityGmlParser.getContext(); + CityGMLContext context = CityGmlParser.setupContext(); CityGMLOutputFactory outputFactory = context.createCityGMLOutputFactory(CityGMLVersion.v2_0); ByteArrayOutputStream out = new ByteArrayOutputStream(); try (CityGMLWriter writer = outputFactory.createCityGMLWriter(out)) { -- GitLab From 2290b62b60026e2e1921af199e05f5287dcb79d6 Mon Sep 17 00:00:00 2001 From: Matthias Betz Date: Fri, 12 Dec 2025 10:22:39 +0100 Subject: [PATCH 10/63] fix usage of wrong flag for XML validation --- .../main/java/de/hft/stuttgart/citydoctor2/check/Checker.java | 2 +- .../test/resources/testConfigWithStreamingWithValidation.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java index 7b3afcf3..8e434c05 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java @@ -921,7 +921,7 @@ public class Checker { PdfStreamReporter pdfReporter = getPdfReporter(config, pdfBos, fileName); // XML validation - if (config.isUseStreaming() && (xmlReporter != null || pdfReporter != null)) { + if (config.isXmlValidation() && (xmlReporter != null || pdfReporter != null)) { List validationIssues = CityGmlParser.validateFile(null, inputFile.toPath()); for (String xmlError : validationIssues) { XMLValidationError validationError = new XMLValidationError(xmlError); diff --git a/CityDoctorParent/CityDoctorValidation/src/test/resources/testConfigWithStreamingWithValidation.yml b/CityDoctorParent/CityDoctorValidation/src/test/resources/testConfigWithStreamingWithValidation.yml index 311740e3..3d98a3e7 100644 --- a/CityDoctorParent/CityDoctorValidation/src/test/resources/testConfigWithStreamingWithValidation.yml +++ b/CityDoctorParent/CityDoctorValidation/src/test/resources/testConfigWithStreamingWithValidation.yml @@ -2,6 +2,7 @@ globalParameters: numberOfRoundingPlaces: 8 schematronFilePath: src/test/resources/schematronTest.xml useStreaming: true +xmlValidation: true requirements: R_GE_R_TOO_FEW_POINTS: enabled: true -- GitLab From 55f76a2c98fb0f6089b4ed5e95a487aa399a4b1e Mon Sep 17 00:00:00 2001 From: Matthias Betz Date: Fri, 12 Dec 2025 10:58:06 +0100 Subject: [PATCH 11/63] don't remove global parsing errors on checking --- .../java/de/hft/stuttgart/citydoctor2/check/CheckError.java | 4 ++++ .../java/de/hft/stuttgart/citydoctor2/check/CheckResult.java | 4 ---- .../java/de/hft/stuttgart/citydoctor2/check/Checkable.java | 2 +- .../citydoctor2/check/error/XMLValidationError.java | 5 +++++ .../java/de/hft/stuttgart/citydoctor2/check/Checker.java | 3 ++- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckError.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckError.java index 79567be1..fbf22ec9 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckError.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckError.java @@ -83,4 +83,8 @@ public interface CheckError extends Serializable { */ public void report(ErrorReport report); + public default boolean isParsingError() { + return false; + } + } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckResult.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckResult.java index 78372b27..154d32c0 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckResult.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/CheckResult.java @@ -71,8 +71,4 @@ public class CheckResult implements Serializable { return String.format("CheckResult [id=%s, status=%s, error=%s]", id, status, err); } - public boolean isParsingError() { - return false; - } - } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java index 8c5da64d..24ff4a8b 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java @@ -224,7 +224,7 @@ public abstract class Checkable implements Serializable { * Clears the checkResults of this checkable unless the error occured during parsing. */ public void clearCheckResults() { - checkResults.entrySet().removeIf(e -> !e.getValue().isParsingError()); + checkResults.entrySet().removeIf(e -> !e.getValue().getError().isParsingError()); setValidated(!checkResults.isEmpty()); } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/error/XMLValidationError.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/error/XMLValidationError.java index 14a2afdc..39fc78ce 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/error/XMLValidationError.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/error/XMLValidationError.java @@ -55,5 +55,10 @@ public class XMLValidationError implements CheckError { public void report(ErrorReport report) { report.add("message", message); } + + @Override + public boolean isParsingError() { + return true; + } } diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java index 8e434c05..bd69bb2d 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java @@ -700,7 +700,8 @@ public class Checker { AtomicInteger checkedCount = new AtomicInteger(0); // clear global errors - model.getGlobalErrors().clear(); + model.getGlobalErrors().removeIf(e -> !e.isParsingError()); + CityObjectCache cache = model.getCache(); logger.trace("Setting up ThreadPool"); int threadCount = Runtime.getRuntime().availableProcessors() * 5; -- GitLab From 5df59c7af19efee49998cb099f25cc5227625f12 Mon Sep 17 00:00:00 2001 From: Matthias Betz Date: Fri, 12 Dec 2025 11:06:24 +0100 Subject: [PATCH 12/63] fix error while removing results that have no error (result is ok) --- .../java/de/hft/stuttgart/citydoctor2/check/Checkable.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java index 24ff4a8b..9c67dd20 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Checkable.java @@ -224,7 +224,12 @@ public abstract class Checkable implements Serializable { * Clears the checkResults of this checkable unless the error occured during parsing. */ public void clearCheckResults() { - checkResults.entrySet().removeIf(e -> !e.getValue().getError().isParsingError()); + checkResults.entrySet().removeIf(e -> { + if (e.getValue().getError() == null) { + return true; + } + return !e.getValue().getError().isParsingError(); + }); setValidated(!checkResults.isEmpty()); } -- GitLab From 8536ec35c36c110f449a8b06eebbd77159eb2b1e Mon Sep 17 00:00:00 2001 From: Matthias Betz Date: Fri, 12 Dec 2025 13:06:42 +0100 Subject: [PATCH 13/63] move adjacency info to geometry to allow better serialization --- .../citydoctor2/datastructure/Geometry.java | 43 ++++++----- .../citydoctor2/datastructure/Vertex.java | 73 ++----------------- .../datastructure/BuildingTest.java | 4 +- .../datastructure/GeometryTest.java | 6 +- .../datastructure/LinearRingTest.java | 3 + .../citydoctor2/datastructure/VertexTest.java | 16 ---- .../geometry/DuplicatePointsCheckTest.java | 4 + .../checks/geometry/PlanarCheckTest.java | 4 +- .../checks/util/SelfIntersectionUtilTest.java | 51 +++++++++++++ 9 files changed, 98 insertions(+), 106 deletions(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/Geometry.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/Geometry.java index f2c0d419..b13e89ff 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/Geometry.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/Geometry.java @@ -18,6 +18,16 @@ */ package de.hft.stuttgart.citydoctor2.datastructure; +import java.io.Serial; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.Set; + import de.hft.stuttgart.citydoctor2.check.CheckableVisitor; import de.hft.stuttgart.citydoctor2.math.Triangle3d; import de.hft.stuttgart.citydoctor2.math.Vector3d; @@ -26,11 +36,6 @@ import de.hft.stuttgart.citydoctor2.utils.BoundingBoxCalculator; import de.hft.stuttgart.citydoctor2.utils.SerializablePair; import javafx.scene.paint.Color; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.Serial; -import java.util.*; - /** * Representation of a geometry containing the polygons and edges * @@ -55,9 +60,11 @@ public class Geometry extends GmlElement { private CityObject parent; private final List polygons = new ArrayList<>(2); - private transient List edges; - private transient Map, Edge> edgeMap; - private transient List vertices; + private List edges; + private Map, Edge> edgeMap; + private List vertices; + private Map> adjacentRingsOfVertices = new HashMap<>(); + private Orientation orientation; public Geometry(GeometryType type, Lod lod, Orientation orientation) { @@ -69,6 +76,15 @@ public class Geometry extends GmlElement { this.orientation = orientation; } + public Set getAdjacentRingsOfVertex(Vertex v) { + return adjacentRingsOfVertices.compute(v, (vertex, set) -> { + if (set == null) { + return new HashSet<>(4); + } + return set; + }); + } + public Orientation getOrientation() { return orientation; } @@ -346,18 +362,9 @@ public class Geometry extends GmlElement { return false; } - @Serial - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - in.defaultReadObject(); - edges = new ArrayList<>(); - edgeMap = new HashMap<>(); - } - public void clearMetaData() { if (vertices != null) { - for (Vertex v : vertices) { - v.clearAdjacentRings(); - } + adjacentRingsOfVertices = new HashMap<>(); vertices = null; } edges = null; diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/Vertex.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/Vertex.java index e4244007..6962bff8 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/Vertex.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/Vertex.java @@ -18,16 +18,12 @@ */ package de.hft.stuttgart.citydoctor2.datastructure; -import de.hft.stuttgart.citydoctor2.math.Vector3d; -import de.hft.stuttgart.citydoctor2.utils.SerializablePair; - -import java.io.IOException; -import java.io.ObjectInputStream; import java.io.Serial; -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Set; +import java.util.stream.Collectors; + +import de.hft.stuttgart.citydoctor2.math.Vector3d; /** * Contains the vertex information of a point in a linear ring @@ -39,7 +35,6 @@ public class Vertex extends Vector3d { @Serial private static final long serialVersionUID = -5525361920397934892L; - private transient List>> adjacentRings = new ArrayList<>(2); public Vertex(double x, double y, double z) { super(x, y, z); @@ -49,58 +44,16 @@ public class Vertex extends Vector3d { super(vec); } - private Set getAdjacentRingsWithoutNeighbor(Geometry geom) { - for (SerializablePair> adjacency : adjacentRings) { - if (adjacency.getValue0() == geom) { - return adjacency.getValue1(); - } - } - throw new IllegalStateException("Requested adjacent rings with Geometry not containing this vertex"); - } - public Set getAdjacentRings(Geometry geom) { - return getAdjacentRingsWithoutNeighbor(geom); + return geom.getAdjacentRingsOfVertex(this); } void addAdjacentRing(LinearRing ring, Geometry geom) { - findAdjacentRingsForGeometry(geom).add(ring); - } - - private Set findAdjacentRingsForGeometry(Geometry geom) { - HashSet adjacendRingsSet = null; - for (SerializablePair> adjacency : adjacentRings) { - if (adjacency.getValue0() == geom) { - adjacendRingsSet = adjacency.getValue1(); - } - } - if (adjacendRingsSet == null) { - adjacendRingsSet = new HashSet<>(4); - adjacentRings.add(new SerializablePair<>(geom, adjacendRingsSet)); - } - return adjacendRingsSet; - } - - private Set getAdjacentPolygonsWithoutNeighbor(Geometry geom) { - for (SerializablePair> adjacency : adjacentRings) { - if (adjacency.getValue0() == geom) { - Set polygons = new HashSet<>(); - for (LinearRing lr : adjacency.getValue1()) { - polygons.add(lr.getParent()); - } - return polygons; - } - } - throw new IllegalStateException("Requested adjacent polygons with Geometry not containing this vertex"); - } - - @Serial - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - in.defaultReadObject(); - clearAdjacentRings(); + getAdjacentRings(geom).add(ring); } public Set getAdjacentPolygons(Geometry geom) { - return getAdjacentPolygonsWithoutNeighbor(geom); + return getAdjacentRings(geom).stream().map(r -> r.getParent()).collect(Collectors.toCollection(HashSet::new)); } @Override @@ -127,21 +80,11 @@ public class Vertex extends Vector3d { } public void clearAdjacentRings(Geometry geometry) { - if (adjacentRings == null) { - return; - } - findAdjacentRingsForGeometry(geometry).clear(); + getAdjacentRings(geometry).clear(); } void removeAdjacency(LinearRing lr, Geometry geom) { - findAdjacentRingsForGeometry(geom).remove(lr); - } - - /** - * Remove all adjacent rings from this vertex, ignoring geometry association - */ - void clearAdjacentRings() { - adjacentRings = new ArrayList<>(2); + getAdjacentRings(geom).remove(lr); } } diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BuildingTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BuildingTest.java index 21d897fa..67eb587d 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BuildingTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/BuildingTest.java @@ -52,12 +52,12 @@ public class BuildingTest { Vertex v2 = new Vertex(2, 0, 0); LinearRing lr = new LinearRing(LinearRingType.EXTERIOR); ConcretePolygon p = new ConcretePolygon(); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD0, Orientation.OUTWARD); + geom.addPolygon(p); p.setExteriorRing(lr); lr.addVertex(v1); lr.addVertex(v2); lr.addVertex(v1); - Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD0, Orientation.OUTWARD); - geom.addPolygon(p); geom.updateEdgesAndVertices(); b.addGeometry(geom); diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/GeometryTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/GeometryTest.java index 0b33bb45..56e32c7e 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/GeometryTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/GeometryTest.java @@ -105,9 +105,9 @@ public class GeometryTest { ConcretePolygon p = new ConcretePolygon(); LinearRing lr = new LinearRing(LinearRingType.EXTERIOR); p.setExteriorRing(lr); + geom.addPolygon(p); lr.addVertex(new Vertex(0, 0, 0)); - geom.addPolygon(p); p.setPartOfSurface(bs); p.setPartOfInstallation(bi); @@ -139,9 +139,9 @@ public class GeometryTest { ConcretePolygon p = new ConcretePolygon(); LinearRing lr = new LinearRing(LinearRingType.EXTERIOR); p.setExteriorRing(lr); + geom.addPolygon(p); lr.addVertex(new Vertex(0, 0, 0)); - geom.addPolygon(p); p.setPartOfSurface(bs); bs.addGeometry(geom); @@ -165,10 +165,10 @@ public class GeometryTest { ConcretePolygon p = new ConcretePolygon(); LinearRing lr = new LinearRing(LinearRingType.EXTERIOR); p.setExteriorRing(lr); + geom.addPolygon(p); lr.addVertex(new Vertex(0, 0, 0)); bs.addGeometry(geom); - geom.addPolygon(p); p.setPartOfSurface(bs); p.setPartOfInstallation(bi); diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/LinearRingTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/LinearRingTest.java index d2c95d59..05c18798 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/LinearRingTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/LinearRingTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.*; import org.junit.Test; +import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType; import de.hft.stuttgart.citydoctor2.math.UnitVector3d; import de.hft.stuttgart.citydoctor2.math.Vector3d; @@ -35,7 +36,9 @@ public class LinearRingTest { @Test public void testIsPointInside1() { + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p = new ConcretePolygon(); + geom.addPolygon(p); LinearRing lr = new LinearRing(LinearRingType.EXTERIOR); p.setExteriorRing(lr); diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/VertexTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/VertexTest.java index 6dc58dda..989e0206 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/VertexTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/datastructure/VertexTest.java @@ -62,20 +62,4 @@ public class VertexTest { assertEquals(1, adjacentRings.size()); assertTrue(adjacentRings.contains(lr)); } - - @Test(expected = IllegalStateException.class) - public void testGetAdjacentRingsGeometryNotContainingVertex() { - Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD1, Orientation.OUTWARD); - - Polygon p = new ConcretePolygon(); - - LinearRing lr = new LinearRing(LinearRingType.EXTERIOR); - p.setExteriorRing(lr); - - Vertex v0 = new Vertex(0, 0, 0); - lr.addVertex(v0); - - v0.getAdjacentRings(geom); - } - } diff --git a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/geometry/DuplicatePointsCheckTest.java b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/geometry/DuplicatePointsCheckTest.java index 57d993b5..2977080f 100644 --- a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/geometry/DuplicatePointsCheckTest.java +++ b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/geometry/DuplicatePointsCheckTest.java @@ -45,7 +45,9 @@ public class DuplicatePointsCheckTest { @Test public void testCheckOK() { + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p = new ConcretePolygon(); + geom.addPolygon(p); LinearRing lr = new LinearRing(LinearRingType.EXTERIOR); p.setExteriorRing(lr); @@ -67,7 +69,9 @@ public class DuplicatePointsCheckTest { @Test public void testCheckConsecutivePointSame() { + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p = new ConcretePolygon(); + geom.addPolygon(p); LinearRing lr = new LinearRing(LinearRingType.EXTERIOR); p.setExteriorRing(lr); diff --git a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/geometry/PlanarCheckTest.java b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/geometry/PlanarCheckTest.java index 4ae6fe61..e260bec7 100644 --- a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/geometry/PlanarCheckTest.java +++ b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/geometry/PlanarCheckTest.java @@ -49,13 +49,13 @@ public class PlanarCheckTest { Vertex v3 = new Vertex(1, 0, 0); LinearRing lr = new LinearRing(LinearRingType.EXTERIOR); Polygon p = new ConcretePolygon(); + geom.addPolygon(p); p.setExteriorRing(lr); lr.addVertex(v0); lr.addVertex(v1); lr.addVertex(v2); lr.addVertex(v3); lr.addVertex(v0); - geom.addPolygon(p); PlanarCheck c = new PlanarCheck(); c.check(p); assertTrue(p.getCheckResult(c).getResultStatus() == ResultStatus.OK); @@ -70,6 +70,7 @@ public class PlanarCheckTest { Vertex v3 = new Vertex(1, 1, 0); LinearRing lr = new LinearRing(LinearRingType.EXTERIOR); Polygon p = new ConcretePolygon(); + geom.addPolygon(p); p.setExteriorRing(lr); lr.addVertex(v0); lr.addVertex(v1); @@ -77,7 +78,6 @@ public class PlanarCheckTest { lr.addVertex(v3); lr.addVertex(v0); lr.setParent(p); - geom.addPolygon(p); PlanarCheck c = new PlanarCheck(); c.check(p); assertTrue(p.getCheckResult(c).getResultStatus() == ResultStatus.ERROR); diff --git a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/util/SelfIntersectionUtilTest.java b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/util/SelfIntersectionUtilTest.java index 35a7c10e..2292b110 100644 --- a/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/util/SelfIntersectionUtilTest.java +++ b/CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/util/SelfIntersectionUtilTest.java @@ -57,7 +57,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -71,6 +73,7 @@ public class SelfIntersectionUtilTest { Vertex v5 = new Vertex(10, 10, 1); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); @@ -90,7 +93,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -104,6 +109,7 @@ public class SelfIntersectionUtilTest { Vertex v5 = new Vertex(10, 10, -1); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); @@ -123,7 +129,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -137,6 +145,7 @@ public class SelfIntersectionUtilTest { Vertex v5 = new Vertex(7, 7, -5); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); @@ -159,7 +168,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -173,6 +184,7 @@ public class SelfIntersectionUtilTest { Vertex v5 = new Vertex(10, 10, 0); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); @@ -192,7 +204,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -206,6 +220,7 @@ public class SelfIntersectionUtilTest { Vertex v5 = new Vertex(7, 7, -5); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); @@ -233,7 +248,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -247,6 +264,7 @@ public class SelfIntersectionUtilTest { Vertex v5 = new Vertex(7, 7, -5); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); @@ -276,7 +294,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -292,6 +312,7 @@ public class SelfIntersectionUtilTest { Vertex v7 = new Vertex(9, 2, -3); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); @@ -331,7 +352,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -359,6 +382,7 @@ public class SelfIntersectionUtilTest { Vertex v7 = new Vertex(9, 2, -3); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); @@ -406,7 +430,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -434,6 +460,7 @@ public class SelfIntersectionUtilTest { Vertex v7 = new Vertex(9, 2, -3); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); @@ -465,7 +492,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -479,6 +508,7 @@ public class SelfIntersectionUtilTest { Vertex v5 = new Vertex(10, 10, 0); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); r2.addVertex(v3); @@ -497,7 +527,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -511,6 +543,7 @@ public class SelfIntersectionUtilTest { Vertex v5 = new Vertex(9, 10, 0); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); r2.addVertex(v3); @@ -529,7 +562,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -543,6 +578,7 @@ public class SelfIntersectionUtilTest { Vertex v5 = new Vertex(10, 11, 0); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); r2.addVertex(v3); @@ -561,7 +597,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 10, 10); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -576,6 +614,7 @@ public class SelfIntersectionUtilTest { Vertex v6 = new Vertex(8, 5, 5); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); r2.addVertex(v3); @@ -600,7 +639,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 0, 0); Vertex v2 = new Vertex(10, 0, 10); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -615,6 +656,7 @@ public class SelfIntersectionUtilTest { Vertex v6 = new Vertex(8, 0, 5); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); r2.addVertex(v3); @@ -639,7 +681,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(0, 10, 0); Vertex v2 = new Vertex(0, 10, 10); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -654,6 +698,7 @@ public class SelfIntersectionUtilTest { Vertex v6 = new Vertex(0, 8, 5); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); r2.addVertex(v3); @@ -678,7 +723,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 10, 0); Vertex v2 = new Vertex(10, 0, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -693,6 +740,7 @@ public class SelfIntersectionUtilTest { Vertex v6 = new Vertex(8, 2, 0); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); r2.addVertex(v3); @@ -737,7 +785,9 @@ public class SelfIntersectionUtilTest { Vertex v1 = new Vertex(10, 10, 0); Vertex v2 = new Vertex(10, 0, 0); + Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Polygon p1 = new ConcretePolygon(); + geom.addPolygon(p1); LinearRing r1 = new LinearRing(LinearRingType.EXTERIOR); p1.setExteriorRing(r1); @@ -752,6 +802,7 @@ public class SelfIntersectionUtilTest { Vertex v6 = new Vertex(8, 2, 0); Polygon p2 = new ConcretePolygon(); + geom.addPolygon(p2); LinearRing r2 = new LinearRing(LinearRingType.EXTERIOR); p2.setExteriorRing(r2); r2.addVertex(v3); -- GitLab From 933253803142ce0d711d8f54b66963350f9cc266 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Fri, 12 Dec 2025 13:59:26 +0100 Subject: [PATCH 14/63] Fix: Fix compilation errors --- .../stuttgart/citydoctor2/healing/HealMissingSolidTest.java | 5 ++++- .../stuttgart/citydoctor2/healer/gui/HealerController.java | 5 ++--- CityDoctorParent/pom.xml | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealMissingSolidTest.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealMissingSolidTest.java index 07adf8c1..5ee9be12 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealMissingSolidTest.java +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealMissingSolidTest.java @@ -29,6 +29,8 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import de.hft.stuttgart.citydoctor2.database.UnconnectedCache; +import org.citygml4j.core.model.CityGMLVersion; import org.citygml4j.core.model.core.AbstractThematicSurface; import org.citygml4j.core.model.core.CityModel; import org.junit.Rule; @@ -70,7 +72,8 @@ public class HealMissingSolidTest { @Test public void testCreateSolid() throws CityDoctorWriteException, IOException, CityGmlParseException, InvalidGmlFileException { ValidationConfiguration config = ValidationConfiguration.loadStandardValidationConfig(); - CityDoctorModel model = new CityDoctorModel(config.getParserConfiguration(), new File("test")); + CityDoctorModel model = new CityDoctorModel(config.getParserConfiguration(), new File("test"), new UnconnectedCache()); + model.setParsedCityGMLVersion(CityGMLVersion.v3_0); model.setCityModel(new CityModel()); Building b = new Building(); model.addBuilding(b); diff --git a/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerController.java b/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerController.java index f495692c..31627f7a 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerController.java +++ b/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/java/de/hft/stuttgart/citydoctor2/healer/gui/HealerController.java @@ -538,8 +538,7 @@ public class HealerController { // if (model == null || currentFeature == null) { // return; // } - - for (Building b : model.getBuildings()) { + model.getBuildings().forEach(b -> { // collect polygons Map> availablePolygons = new EnumMap<>(Lod.class); Map> existingPolygons = new EnumMap<>(Lod.class); @@ -568,7 +567,7 @@ public class HealerController { insertSolidGeometry(availablePolygons, part, existingPolygons, Lod.LOD4); } } - } + }); } diff --git a/CityDoctorParent/pom.xml b/CityDoctorParent/pom.xml index d69e27d2..ed37dea0 100644 --- a/CityDoctorParent/pom.xml +++ b/CityDoctorParent/pom.xml @@ -303,9 +303,9 @@ Extensions/CityDoctorGUI - + Extensions/CityDoctorHealer - + Extensions/CityDoctorHealerGUI \ No newline at end of file -- GitLab From 17359af885b8b595473c6341871dd6a6fa63a7bb Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 16 Dec 2025 16:37:14 +0100 Subject: [PATCH 15/63] Build: Suppress Hikari logs in Healer --- .../src/main/resources/log4j2.xml | 18 ++++++++++++++++++ .../src/main/resources/log4j2.xml | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 CityDoctorParent/Extensions/CityDoctorHealer/src/main/resources/log4j2.xml create mode 100644 CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/resources/log4j2.xml diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/resources/log4j2.xml b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/resources/log4j2.xml new file mode 100644 index 00000000..1b1a09f3 --- /dev/null +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/resources/log4j2.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/resources/log4j2.xml b/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/resources/log4j2.xml new file mode 100644 index 00000000..1b1a09f3 --- /dev/null +++ b/CityDoctorParent/Extensions/CityDoctorHealerGUI/src/main/resources/log4j2.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file -- GitLab From 19f5db34a75b87d05ef9e6ba920385331f914faa Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 16 Dec 2025 16:38:49 +0100 Subject: [PATCH 16/63] Fix: Defer utility visitors to ConcretePolygon --- .../datastructure/LinkedPolygon.java | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/LinkedPolygon.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/LinkedPolygon.java index d915d8dc..9c858619 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/LinkedPolygon.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/datastructure/LinkedPolygon.java @@ -19,8 +19,11 @@ package de.hft.stuttgart.citydoctor2.datastructure; import de.hft.stuttgart.citydoctor2.check.Check; +import de.hft.stuttgart.citydoctor2.check.CheckError; import de.hft.stuttgart.citydoctor2.check.CheckId; import de.hft.stuttgart.citydoctor2.check.CheckResult; +import de.hft.stuttgart.citydoctor2.check.Checkable; +import de.hft.stuttgart.citydoctor2.check.CheckableUtilsVisitor; import de.hft.stuttgart.citydoctor2.check.CheckableVisitor; import de.hft.stuttgart.citydoctor2.math.Vector3d; import de.hft.stuttgart.citydoctor2.tesselation.TesselatedPolygon; @@ -62,6 +65,52 @@ public class LinkedPolygon extends Polygon { } } + @Override + public boolean hasExistingGmlId() { + return poly.hasExistingGmlId(); + } + + @Override + public void setValidated(boolean validated) { + // Linked polys themselves are not checked + } + + @Override + public boolean isValidated() { + return poly.isValidated(); + } + + //TODO: Think how to handle this, as it violates class contract if ignored + @Override + public void prepareForChecking() { + // Linked polys themselves are not checked + } + + @Override + public void clearMetaInformation() { + // Linked polys themselves are not checked + } + + @Override + public boolean hasError(CheckId checkIdentifier) { + return poly.hasError(checkIdentifier); + } + + @Override + public boolean hasAnyError() { + return poly.hasAnyError(); + } + + @Override + public void collectContainedErrors(List errors) { + poly.collectContainedErrors(errors); + } + + @Override + public void collectErrors(List errors) { + poly.collectErrors(errors); + } + @Override public CheckResult getCheckResult(Check c) { return poly.getCheckResult(c); @@ -104,8 +153,12 @@ public class LinkedPolygon extends Polygon { @Override public void accept(CheckableVisitor c) { - // linked polygons are not checked - setValidated(true); + // LinkedPolygon should ignore Checks, as their referenced ConcretePolygon must be within the same TopLevelFeature. + // Checks always start iterating from a TopLevelFeature and thus will visit the referenced ConcretePolygon. + if (!c.isValidator()){ + // Utility Visitors get passed to the ConcretePolygon, as they can start iterating from a sub-CityObject. + poly.accept(c); + } } @Override -- GitLab From 8b2f8ba8733eeaf59ea7238a1a903d05293f04ca Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 16 Dec 2025 16:40:53 +0100 Subject: [PATCH 17/63] Fix: Connect Healer with models CityObjectCache --- .../exceptions/CityObjectCacheException.java | 18 ++++++++++++++++++ .../stuttgart/citydoctor2/healer/Healer.java | 14 ++++++++++++-- .../citydoctor2/healing/TestUtil.java | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/exceptions/CityObjectCacheException.java diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/exceptions/CityObjectCacheException.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/exceptions/CityObjectCacheException.java new file mode 100644 index 00000000..02e1186a --- /dev/null +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/exceptions/CityObjectCacheException.java @@ -0,0 +1,18 @@ +package de.hft.stuttgart.citydoctor2.exceptions; + +import java.io.Serial; + +public class CityObjectCacheException extends RuntimeException{ + + @Serial + private static final long serialVersionUID = -460467145629964615L; + + public CityObjectCacheException(String message) { + super(message); + } + + public CityObjectCacheException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java index 06be694b..3eb54805 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java @@ -12,6 +12,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import de.hft.stuttgart.citydoctor2.database.CityObjectCache; +import de.hft.stuttgart.citydoctor2.exceptions.CityObjectCacheException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.citygml4j.core.ade.ADEException; @@ -64,7 +66,7 @@ public class Healer { private Checker checker; private ModificationListener l; private HealingPlan plan; - + private CityObjectCache cache; static { // CppInitializer.initCpp(); } @@ -187,6 +189,7 @@ public class Healer { } }; } + cache = checker.getModel().getCache(); this.l = l; } @@ -208,7 +211,12 @@ public class Healer { } public void healModel(CityDoctorModel model) { - model.createFeatureStream().forEach(this::healCityObject); + try (AutoCloseable checkingContext = model.getCache().startCheckingMode()){ + model.createFeatureStream().forEach(this::healCityObject); + } catch (Exception e) { + throw new CityObjectCacheException("Healer critically failed during attempt to heal the model",e); + } + } public void healCityObject(CityObject co) { @@ -348,6 +356,8 @@ public class Healer { if (checker.getConfig().getParserConfiguration().useLowMemoryConsumption()) { co.clearMetaInformation(); } + //Reinsert object into cache and release locks + cache.put(co); } } diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/TestUtil.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/TestUtil.java index 72a2450f..cfc8063b 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/TestUtil.java +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/TestUtil.java @@ -31,7 +31,7 @@ public class TestUtil { } public static CityDoctorModel loadCityModel(String path, ValidationConfiguration config) throws CityGmlParseException, IOException, InvalidGmlFileException { - return CityGmlParser.parseCityGmlFile(path, config.getParserConfiguration()); + return CityGmlParser.nestedCityGmlFileParse(path, config.getParserConfiguration()); } public static Polygon getPolygonById(String gmlId, CityDoctorModel model) { -- GitLab From 8aa306c73b8f0406c28742e33c72279944d17385 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 16 Dec 2025 16:42:14 +0100 Subject: [PATCH 18/63] Test: Update Healer test cases --- .../stuttgart/citydoctor2/healing/TestHealer.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/TestHealer.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/TestHealer.java index 63ccbd05..46910bf2 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/TestHealer.java +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/TestHealer.java @@ -150,15 +150,14 @@ public class TestHealer { Checker c = new Checker(config, model); c.runChecks(); - - Geometry geom = model.getBuildings().toList().get(0).getGeometries().get(0); + Building b = model.getBuildings().toList().get(0); + Geometry geom = b.getGeometries().get(0); Healer healer = new Healer(c); - healer.setNumberOfIterations(33); - healer.healCityObject(model.getBuildings().toList().get(0)); + healer.healCityObject(b); c.runChecks(); - assertFalse(geom.containsAnyError()); + } @Test @@ -193,7 +192,6 @@ public class TestHealer { Geometry geom = model.getBuildings().toList().get(0).getGeometries().get(0); Healer healer = new Healer(c); - healer.setNumberOfIterations(83); healer.healCityObject(model.getBuildings().toList().get(0)); c.runChecks(); @@ -213,7 +211,6 @@ public class TestHealer { Geometry geom = model.getBuildings().toList().get(0).getGeometries().get(0); Healer healer = new Healer(c); - healer.setNumberOfIterations(165); healer.healCityObject(model.getBuildings().toList().get(0)); c.runChecks(); @@ -264,7 +261,6 @@ public class TestHealer { Geometry geom = model.getBuildings().toList().get(0).getGeometries().get(0); Healer healer = new Healer(c); - healer.setNumberOfIterations(45); healer.healCityObject(model.getBuildings().toList().get(0)); c.runChecks(); @@ -282,7 +278,6 @@ public class TestHealer { Healer healer = new Healer(c); - healer.setNumberOfIterations(45); healer.healModel(model); } @@ -299,7 +294,7 @@ public class TestHealer { Geometry geom = model.getBuildings().toList().get(0).getGeometries().get(0); Healer healer = new Healer(c); - healer.setNumberOfIterations(100); + healer.healCityObject(model.getBuildings().toList().get(0)); c.runChecks(); -- GitLab From 0423dc6ae6765a0c99594e2603d16b6f5faff115 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 16 Dec 2025 16:48:18 +0100 Subject: [PATCH 19/63] Build: Exclude CGAL-tests from Maven build Adds the CppTest marker-interface to exclude Healer tests that were interfacing with CGAL from the build. These tests are needed for the development of the replacement library, but shall not cause build failures in the meantime. --- .../citydoctor2/connect/edge/CppTest.java | 8 ++++++++ .../healing/HealPlanarPolygonErrorTest.java | 8 ++++++-- .../healing/HealSolidNotClosedErrorTest.java | 14 +++++++++++--- CityDoctorParent/pom.xml | 3 +++ 4 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/connect/edge/CppTest.java diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/connect/edge/CppTest.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/connect/edge/CppTest.java new file mode 100644 index 00000000..a7d60042 --- /dev/null +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/connect/edge/CppTest.java @@ -0,0 +1,8 @@ +package de.hft.stuttgart.citydoctor2.connect.edge; + +/** + * Marker interface for excluding testcases that use the deprecated connection to the C++ library. + * The tests are kept for the Java replacement library that is currently being developed. + */ +public interface CppTest { +} diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealPlanarPolygonErrorTest.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealPlanarPolygonErrorTest.java index 5359f2d6..a0697423 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealPlanarPolygonErrorTest.java +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealPlanarPolygonErrorTest.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import de.hft.stuttgart.citydoctor2.connect.edge.CppTest; import org.junit.Assert; import org.junit.Test; @@ -31,6 +32,7 @@ import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation; import de.hft.stuttgart.citydoctor2.healer.Healer; import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException; import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException; +import org.junit.experimental.categories.Category; public class HealPlanarPolygonErrorTest { @@ -47,14 +49,14 @@ public class HealPlanarPolygonErrorTest { Assert.assertTrue(geom.containsError(CheckId.C_GE_P_NON_PLANAR)); Healer healer = new Healer(c); - healer.setNumberOfIterations(2); healer.healCityObject(model.getBuildings().toList().get(0)); Assert.assertFalse(geom.containsError(CheckId.C_GE_P_NON_PLANAR)); } - + @Test + @Category(CppTest.class) public void testNonplanarPolygon() { CppInitializer.initCpp(); Building b = new Building(); @@ -160,6 +162,7 @@ public class HealPlanarPolygonErrorTest { } @Test + @Category(CppTest.class) public void testNonplanarPolygon_Split() { CppInitializer.initCpp(); Building b = new Building(); @@ -274,6 +277,7 @@ public class HealPlanarPolygonErrorTest { } @Test + @Category(CppTest.class) public void testNonplanarPolygon_Split2() { CppInitializer.initCpp(); Building b = new Building(); diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealSolidNotClosedErrorTest.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealSolidNotClosedErrorTest.java index 06e4d9e1..c6c57847 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealSolidNotClosedErrorTest.java +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealSolidNotClosedErrorTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertTrue; import java.io.IOException; +import de.hft.stuttgart.citydoctor2.connect.edge.CppTest; import org.citygml4j.xml.writer.CityGMLWriteException; import org.junit.Assert; import org.junit.Test; @@ -33,13 +34,16 @@ import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation; import de.hft.stuttgart.citydoctor2.healer.Healer; import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException; import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException; +import org.junit.experimental.categories.Category; public class HealSolidNotClosedErrorTest { + /** * One polygon is split up in 3 but has not matching edges - */ + */ @Test + @Category(CppTest.class) public void testVertexCloseToEdge() { CppInitializer.initCpp(); Building b = new Building(); //Angelika: als Test eingefügt @@ -164,6 +168,7 @@ public class HealSolidNotClosedErrorTest { } @Test + @Category(CppTest.class) public void testMissingPolygon() { CppInitializer.initCpp(); Building b = new Building(); @@ -343,6 +348,7 @@ public class HealSolidNotClosedErrorTest { } @Test + @Category(CppTest.class) public void test2MissingPolygonsCpp() { CppInitializer.initCpp(); Building b = new Building(); @@ -420,7 +426,9 @@ public class HealSolidNotClosedErrorTest { assertNotNull(cr); assertEquals(ResultStatus.OK, cr.getResultStatus()); } - + + + /** * Missing Polygon has a self intersection * @throws IOException @@ -448,7 +456,7 @@ public class HealSolidNotClosedErrorTest { Assert.assertTrue(geom.containsError(CheckId.C_GE_S_NOT_CLOSED)); Healer healer = new Healer(c); - //healer.setNumberOfIterations(2); + healer.setNumberOfIterations(2); healer.healCityObject(model.getBuildings().toList().get(0)); Assert.assertFalse(geom.containsError(CheckId.C_GE_S_NOT_CLOSED)); diff --git a/CityDoctorParent/pom.xml b/CityDoctorParent/pom.xml index ed37dea0..a1287ba5 100644 --- a/CityDoctorParent/pom.xml +++ b/CityDoctorParent/pom.xml @@ -41,6 +41,9 @@ **/SolidSelfIntCheckFalsePositiveBigMeshTest.java + + de.hft.stuttgart.citydoctor2.connect.edge.CppTest + -- GitLab From 2e14765fb7f243fe1759d55fcaa559819d0686b6 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Thu, 18 Dec 2025 15:59:01 +0100 Subject: [PATCH 20/63] Feature: Implement DB configuration handling --- .../EmbeddedDatabaseConfiguration.java | 5 +++-- .../database/EmbeddedDatabaseHandler.java | 20 ++++++++++++++++- .../database/EmbeddedDataBaseHandlerTest.java | 22 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java index fb487934..2d07d65d 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java @@ -20,8 +20,9 @@ import java.io.File; public record EmbeddedDatabaseConfiguration(String databaseName, String databaseDirectory, int connectionPoolSize, boolean inMemoryMode, boolean attemptFallback, boolean debugMode, boolean tempFileMode) { - private static final String IN_MEMORY_PREFIX= "jdbc:h2:mem"; + private static final String IN_MEMORY_PREFIX= "mem:"; private static final String AUTO_SERVER_PARAMETER = ";AUTO_SERVER=TRUE"; + private static final String FILE_PREFIX = "file://"; /** * Instantiates the default configuration for the embedded database. */ @@ -40,7 +41,7 @@ public record EmbeddedDatabaseConfiguration(String databaseName, String database } public String getH2FileUrl(){ - return databaseDirectory + databaseName; + return FILE_PREFIX + databaseDirectory + databaseName; } public String getH2ParametersSuffix(){ diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java index e32251f0..cb0ec430 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java @@ -19,9 +19,12 @@ import org.h2gis.utilities.wrapper.DataSourceWrapper; import javax.sql.DataSource; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InvalidClassException; +import java.nio.file.Files; +import java.nio.file.Path; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -38,12 +41,23 @@ public class EmbeddedDatabaseHandler { private static final Logger logger = LogManager.getLogger(EmbeddedDatabaseHandler.class); private final EmbeddedDatabaseConfiguration config; private DataSourceWrapper dataSource; + private File tempLoc = null; public EmbeddedDatabaseHandler(EmbeddedDatabaseConfiguration dbConfig){ config = dbConfig; String jdbcUrl; try { - jdbcUrl = createEmbeddedDataBase(config.getH2FileUrl()); + if (config.inMemoryMode()) { + jdbcUrl = createEmbeddedDataBase(config.getInMemoryH2Url()); + } else if (config.tempFileMode()){ + Path tmpDir = Files.createTempDirectory(config.databaseDirectory()); + tmpDir.toFile().deleteOnExit(); + String tempFileUrl = tmpDir.resolve(config.databaseName()).toFile().getAbsolutePath(); + jdbcUrl = createEmbeddedDataBase(tempFileUrl); + tempLoc = new File(tempFileUrl); + } else { + jdbcUrl = createEmbeddedDataBase(config.getH2FileUrl()); + } logger.debug("1] {}",jdbcUrl); } catch (Exception e) { @@ -86,6 +100,10 @@ public class EmbeddedDatabaseHandler { } } + public File getTempLoc(){ + return this.tempLoc; + } + public void setupFeaturesTable() { try (Connection con = dataSource.getConnection()) { diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java index a6286e01..c0919ea7 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java @@ -24,7 +24,10 @@ import java.util.List; import static de.hft.stuttgart.citydoctor2.database.SerializationTest.assertCityObjectEquality; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class EmbeddedDataBaseHandlerTest { @@ -38,7 +41,26 @@ public class EmbeddedDataBaseHandlerTest { fail(e.getMessage()); } } + //TODO: FIX Test failures + @Test + public void testTempFileConfiguration(){ + EmbeddedDatabaseConfiguration tempFileconfig = new EmbeddedDatabaseConfiguration("database", + "inTempConfig",1, false,false, + false,true); + assertNull(handler.getTempLoc()); + EmbeddedDatabaseHandler tempHandler = new EmbeddedDatabaseHandler(tempFileconfig); + assertTrue(tempHandler.getTempLoc().exists()); + + } + @Test + public void testInMemoryConfiguration(){ + EmbeddedDatabaseConfiguration inMemoryConfig = new EmbeddedDatabaseConfiguration("database", + "irrelevant",1, true,false, + false,false); + EmbeddedDatabaseHandler memHandler = new EmbeddedDatabaseHandler(inMemoryConfig); + assertFalse(memHandler.getTempLoc().exists()); + } @Test public void testGetBboxIntersectingObjects() { -- GitLab From 182cf7937f76916bbe09fd450950cc70b13f5c9a Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Thu, 18 Dec 2025 15:59:31 +0100 Subject: [PATCH 21/63] Test: Fix test failure due to missing file --- .../healing/HealSolidNotClosedErrorTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealSolidNotClosedErrorTest.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealSolidNotClosedErrorTest.java index c6c57847..b577700d 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealSolidNotClosedErrorTest.java +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/HealSolidNotClosedErrorTest.java @@ -4,7 +4,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.io.File; import java.io.IOException; +import java.nio.file.Path; import de.hft.stuttgart.citydoctor2.connect.edge.CppTest; import org.citygml4j.xml.writer.CityGMLWriteException; @@ -439,20 +441,18 @@ public class HealSolidNotClosedErrorTest { */ @Test public void testMissingPolygon2() throws CityGmlParseException, IOException, InvalidGmlFileException { - String path = "src/test/resources/SimpleSolid_SrefBS-GE-gml-SO-0002-T0002.gml"; //bei diesem Modell stürzt die VM ab +// String path = "src/test/resources/SimpleSolid_SrefBS-GE-gml-SO-0002-T0002.gml"; //bei diesem Modell stürzt die VM ab // String path = "src/test/resources/SimpleSolid_SrefBS-GE-gml-SO-0002-T0001.gml"; //hier nur Fehlermeldung + String path = "src/test/resources/SimpleSolid_SrefBS-GE-gml-LR-0004-T0005.gml"; +// String path = "src/test/resources/SimpleSolid_SrefBS.gml"; ValidationConfiguration config = ValidationConfiguration.loadStandardValidationConfig(); CityDoctorModel model = TestUtil.loadCityModel(path, config); + Checker c = new Checker(config, model); c.runChecks(); Geometry geom = model.getBuildings().toList().get(0).getGeometries().get(0); - geom.updateVertices(); - for (Vertex v : geom.getVertices()) { - v.setX(v.getX() - 10); - v.setY(v.getY() - 10); - } Assert.assertTrue(geom.containsError(CheckId.C_GE_S_NOT_CLOSED)); Healer healer = new Healer(c); -- GitLab From 99763e56cecf449a4975878d5ac1ff8d5dc85304 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Wed, 14 Jan 2026 13:38:58 +0100 Subject: [PATCH 22/63] Refactor: Deduplicate error classes in log --- .../main/java/de/hft/stuttgart/citydoctor2/check/Checker.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java index bd69bb2d..50abb11b 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java @@ -782,7 +782,7 @@ public class Checker { private List getMissedFeatures(List> futures) throws InterruptedException{ List missedList = new ArrayList<>(); - Set errors = new HashSet<>(); + Set errors = new HashSet<>(); for (Future future : futures) { try{ GmlId gmlId = future.get(); @@ -792,7 +792,7 @@ public class Checker { } catch (ExecutionException e){ logger.debug("A Task failed due to an unexpected exception", e); logger.debug(e.getCause()); - errors.add(e); + errors.add(e.getCause().toString()); } } if (!errors.isEmpty()) { -- GitLab From 430a06301a9822b29f6069b10b409be25a3aa3f6 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Wed, 14 Jan 2026 13:43:12 +0100 Subject: [PATCH 23/63] Feat: Implement in-memory and fallback of database Adds the handling and creation of the embedded database in RAM, which is used for the in-memory and fallback modes. --- .../EmbeddedDatabaseConfiguration.java | 28 ++++++++++---- .../database/EmbeddedDatabaseHandler.java | 38 ++++++++++++------- .../database/EmbeddedDataBaseHandlerTest.java | 30 ++++++++------- 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java index 2d07d65d..5f5ec31f 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java @@ -1,6 +1,7 @@ package de.hft.stuttgart.citydoctor2.database; import java.io.File; +import java.util.StringJoiner; /** * Record containing the configuration parameters for the setup of the embedded database @@ -20,8 +21,10 @@ import java.io.File; public record EmbeddedDatabaseConfiguration(String databaseName, String databaseDirectory, int connectionPoolSize, boolean inMemoryMode, boolean attemptFallback, boolean debugMode, boolean tempFileMode) { - private static final String IN_MEMORY_PREFIX= "mem:"; - private static final String AUTO_SERVER_PARAMETER = ";AUTO_SERVER=TRUE"; + private static final String IN_MEMORY_PREFIX = "mem:"; + private static final String JDBC_DRIVER_PREFIX = "jdbc:h2:"; + private static final String AUTO_SERVER_PARAMETER = "AUTO_SERVER=TRUE"; + private static final String KEEP_ALIVE_PARAMETER = "DB_CLOSE_DELAY=-1"; private static final String FILE_PREFIX = "file://"; /** * Instantiates the default configuration for the embedded database. @@ -36,20 +39,31 @@ public record EmbeddedDatabaseConfiguration(String databaseName, String database true, true, true, false); } + public String wrapUrlWithH2JdbcDriver(String Url){ + return JDBC_DRIVER_PREFIX + Url + getH2ParametersSuffix(); + } + public String getInMemoryH2Url() { - return IN_MEMORY_PREFIX + databaseName; + return wrapUrlWithH2JdbcDriver(IN_MEMORY_PREFIX + databaseName); + } + + public String getFallbackH2Url(){ + return getInMemoryH2Url() + ";" + KEEP_ALIVE_PARAMETER; } public String getH2FileUrl(){ - return FILE_PREFIX + databaseDirectory + databaseName; + return wrapUrlWithH2JdbcDriver(databaseDirectory + databaseName); } public String getH2ParametersSuffix(){ + StringJoiner joiner = new StringJoiner(";",";",""); if (debugMode){ - return AUTO_SERVER_PARAMETER; - } else { - return ";"; + joiner.add(AUTO_SERVER_PARAMETER); + } + if (inMemoryMode){ + joiner.add(KEEP_ALIVE_PARAMETER); } + return joiner.toString(); } } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java index cb0ec430..1d27f86e 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseHandler.java @@ -14,10 +14,9 @@ import org.apache.commons.lang3.SerializationException; import org.apache.commons.lang3.SerializationUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.h2gis.functions.factory.H2GISDBFactory; import org.h2gis.utilities.wrapper.DataSourceWrapper; +import org.h2gis.functions.factory.H2GISFunctions; -import javax.sql.DataSource; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; @@ -26,6 +25,7 @@ import java.io.InvalidClassException; import java.nio.file.Files; import java.nio.file.Path; import java.sql.Connection; +import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -41,20 +41,21 @@ public class EmbeddedDatabaseHandler { private static final Logger logger = LogManager.getLogger(EmbeddedDatabaseHandler.class); private final EmbeddedDatabaseConfiguration config; private DataSourceWrapper dataSource; - private File tempLoc = null; + private File tempFileDBLocation = null; public EmbeddedDatabaseHandler(EmbeddedDatabaseConfiguration dbConfig){ config = dbConfig; String jdbcUrl; try { if (config.inMemoryMode()) { - jdbcUrl = createEmbeddedDataBase(config.getInMemoryH2Url()); + jdbcUrl = setupInMemoryDataBase(config.getInMemoryH2Url()); } else if (config.tempFileMode()){ Path tmpDir = Files.createTempDirectory(config.databaseDirectory()); tmpDir.toFile().deleteOnExit(); - String tempFileUrl = tmpDir.resolve(config.databaseName()).toFile().getAbsolutePath(); - jdbcUrl = createEmbeddedDataBase(tempFileUrl); - tempLoc = new File(tempFileUrl); + String tempFileUrl = tmpDir.resolve(config.databaseName()).toFile().getCanonicalPath(); + jdbcUrl = createEmbeddedDataBase(config.wrapUrlWithH2JdbcDriver(tempFileUrl)); + tempFileDBLocation = new File(tempFileUrl+".mv.db"); + tempFileDBLocation.deleteOnExit(); } else { jdbcUrl = createEmbeddedDataBase(config.getH2FileUrl()); } @@ -66,7 +67,7 @@ public class EmbeddedDatabaseHandler { if (config.attemptFallback()){ logger.info("Attempting fallback to in-memory database"); try { - jdbcUrl = createEmbeddedDataBase(config.getInMemoryH2Url()); + jdbcUrl = setupInMemoryDataBase(config.getFallbackH2Url()); } catch (Exception e1) { throw new EmbeddedDatabaseHandlerException("Fallback to in-memory database failed, embedded database could not be created", e1); } @@ -93,15 +94,26 @@ public class EmbeddedDatabaseHandler { } private String createEmbeddedDataBase(String dbUrl) throws SQLException { - DataSource source = H2GISDBFactory.createDataSource(dbUrl, - true, config.getH2ParametersSuffix()); - try (Connection con = source.getConnection()) { +// DataSource source = H2GISDBFactory.createDataSource("file://"+dbUrl, +// true, config.getH2ParametersSuffix()); +// try (Connection con = source.getConnection()) { +// return con.getMetaData().getURL(); +// } + try (Connection con = DriverManager.getConnection(dbUrl,"sa","sa")){ + H2GISFunctions.load(con); return con.getMetaData().getURL(); } } - public File getTempLoc(){ - return this.tempLoc; + private String setupInMemoryDataBase(String dbUrl) throws SQLException{ + try (Connection con = DriverManager.getConnection(dbUrl,"sa","sa")){ + H2GISFunctions.load(con); + return con.getMetaData().getURL(); + } + } + + public File getTempFileDBLocation(){ + return this.tempFileDBLocation; } diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java index c0919ea7..371cea82 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java @@ -16,7 +16,6 @@ import de.hft.stuttgart.citydoctor2.parser.CityGmlParser; import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException; import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; import org.citygml4j.core.model.building.AbstractBuilding; -import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -33,37 +32,38 @@ import static org.junit.Assert.fail; public class EmbeddedDataBaseHandlerTest { private EmbeddedDatabaseHandler handler = new EmbeddedDatabaseHandler(new EmbeddedDatabaseConfiguration()); - @Before - public void resetDatabase() { - try { - handler.setupFeaturesTable(); - } catch (Exception e) { - fail(e.getMessage()); - } - } - //TODO: FIX Test failures + @Test public void testTempFileConfiguration(){ EmbeddedDatabaseConfiguration tempFileconfig = new EmbeddedDatabaseConfiguration("database", "inTempConfig",1, false,false, false,true); - assertNull(handler.getTempLoc()); EmbeddedDatabaseHandler tempHandler = new EmbeddedDatabaseHandler(tempFileconfig); - assertTrue(tempHandler.getTempLoc().exists()); + assertTrue(tempHandler.getTempFileDBLocation().exists()); } @Test - public void testInMemoryConfiguration(){ + public void testInMemoryConfiguration() throws CityGmlParseException, InvalidGmlFileException { EmbeddedDatabaseConfiguration inMemoryConfig = new EmbeddedDatabaseConfiguration("database", "irrelevant",1, true,false, false,false); EmbeddedDatabaseHandler memHandler = new EmbeddedDatabaseHandler(inMemoryConfig); - assertFalse(memHandler.getTempLoc().exists()); + assertNull(memHandler.getTempFileDBLocation()); + ParserConfiguration config = new ParserConfiguration(8, false); + CityDoctorModel model = CityGmlParser.nestedCityGmlFileParse("src/test/resources/SimpleSolid_SrefBS.gml", config); + Building b = model.getBuildings().toList().get(0); + assertEquals(0, memHandler.getFeatureCount()); + memHandler.marshallCityObject(b); + assertEquals(1, memHandler.getFeatureCount()); + CityObject co = memHandler.unmarshallCityObject(b.getGmlId()); + assertNotNull(co); + assertTrue(co instanceof Building); } @Test public void testGetBboxIntersectingObjects() { + handler.setupFeaturesTable(); Building b = new Building(); Building b2 = new Building(); b2.setGmlObject(Mockito.mock(AbstractBuilding.class)); @@ -144,6 +144,7 @@ public class EmbeddedDataBaseHandlerTest { @Test public void testGetBboxBufferIntersectingObjects() { + handler.setupFeaturesTable(); Building b = new Building(); Building b2 = new Building(); b2.setGmlObject(Mockito.mock(AbstractBuilding.class)); @@ -230,6 +231,7 @@ public class EmbeddedDataBaseHandlerTest { Building b = model.getBuildings().toList().get(0); assertNotNull(b); + assertEquals(1, handler.getFeatureCount()); handler.marshallCityObject(b); assertEquals(1, handler.getFeatureCount()); -- GitLab From 9e55508420cf8c9b4d623a53cf931412ba6f5f28 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Wed, 14 Jan 2026 13:45:59 +0100 Subject: [PATCH 24/63] Fix: Fixed NPE in stream-healing --- .../java/de/hft/stuttgart/citydoctor2/healer/Healer.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java index 3eb54805..d62e505c 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java @@ -13,6 +13,7 @@ import java.util.Map; import java.util.Objects; import de.hft.stuttgart.citydoctor2.database.CityObjectCache; +import de.hft.stuttgart.citydoctor2.database.UnconnectedCache; import de.hft.stuttgart.citydoctor2.exceptions.CityObjectCacheException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -189,7 +190,12 @@ public class Healer { } }; } - cache = checker.getModel().getCache(); + if(checker.getModel() != null){ + cache = checker.getModel().getCache(); + } else { + //Stream healing is being used, use an unconnected cache + cache = new UnconnectedCache(); + } this.l = l; } -- GitLab From d3ca6500e699b19cbd216e113a4635606ad0fd42 Mon Sep 17 00:00:00 2001 From: Matthias Betz Date: Thu, 15 Jan 2026 14:22:00 +0100 Subject: [PATCH 25/63] fix solid self intersection incorrectly ignoring degenerated triangles depending on planarity settings --- .../stuttgart/citydoctor2/check/Check.java | 2 +- .../stuttgart/citydoctor2/math/Segment3d.java | 10 +-- .../citydoctor2/math/Triangle3d.java | 76 +++++++++++++++---- .../stuttgart/citydoctor2/math/Vector3d.java | 12 +-- .../stuttgart/citydoctor2/check/Checker.java | 3 +- .../citydoctor2/checks/CheckContainer.java | 2 +- .../checks/geometry/DuplicatePointsCheck.java | 15 ++-- .../geometry/InteriorDisconnectedCheck.java | 15 ++-- .../checks/geometry/NullAreaCheck.java | 17 +++-- .../checks/geometry/PlanarCheck.java | 19 +++-- .../checks/geometry/RingSelfIntCheck.java | 21 ++--- .../checks/geometry/SolidSelfIntCheck.java | 12 ++- .../checks/semantics/IsWallCheck.java | 11 ++- .../RoofSurfaceUnfragmentedCheck.java | 9 ++- .../checks/util/SelfIntersectionUtil.java | 21 +---- .../reporting/XmlValidationReporter.java | 9 --- .../reporting/pdf/PdfStreamReporter.java | 38 ---------- 17 files changed, 157 insertions(+), 135 deletions(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Check.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Check.java index 9a3b136d..36bfbeeb 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Check.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/check/Check.java @@ -345,7 +345,7 @@ public abstract non-sealed class Check implements CheckableVisitor { * @param config sometimes there are global parameters which can be used by * checks. Those are be stored in this container */ - public void init(Map params, ParserConfiguration config) { + public void init(Map> params, ParserConfiguration config) { } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Segment3d.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Segment3d.java index 87b6ebdd..fecef4ce 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Segment3d.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Segment3d.java @@ -34,8 +34,6 @@ public class Segment3d implements Serializable { private static final double PRECISION = 0.00000001; - private static final double EPSILON = 0.01; - private final Vector3d pointA; private final Vector3d pointB; @@ -178,7 +176,7 @@ public class Segment3d implements Serializable { return "Segment3d [pointA=" + pointA + ", pointB=" + pointB + "]"; } - public Vector3d intersection(Triangle3d triangle) { + public Vector3d intersection(Triangle3d triangle, double eps) { Vector3d v0 = triangle.getP1(); Vector3d v1 = triangle.getP2(); Vector3d v2 = triangle.getP3(); @@ -188,7 +186,7 @@ public class Segment3d implements Serializable { Vector3d p = direction.cross(edge2); double det = edge1.dot(p); - if (det > -EPSILON && det < EPSILON) { + if (det > -eps && det < eps) { return null; } @@ -197,7 +195,7 @@ public class Segment3d implements Serializable { Vector3d s = pointA.minus(v0); double u = invDet * s.dot(p); - if (u < EPSILON || u > (1.0 - EPSILON)) { + if (u < eps || u > (1.0 - eps)) { return null; } @@ -210,7 +208,7 @@ public class Segment3d implements Serializable { } double t = edge2.dot(q) * invDet; - if (t > EPSILON && t < 1 - EPSILON) { + if (t > eps && t < 1 - eps) { // t is in segment return pointA.plus(direction.mult(t)); } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Triangle3d.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Triangle3d.java index 4f3c2920..8e607beb 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Triangle3d.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Triangle3d.java @@ -35,6 +35,8 @@ public class Triangle3d implements Serializable { private static final long serialVersionUID = -6907333357794272435L; private static final double EPSILON = 0.0001; + + private static final double PLANAR_EPSILON = 0.0000001; private final Vector3d p1; private final Vector3d p2; private final Vector3d p3; @@ -51,7 +53,54 @@ public class Triangle3d implements Serializable { this.p3 = p3; this.partOf = partOf; } - + + public boolean hasMinExtent(double minExtent) { + Vector3d ab = p2.minus(p1); + Vector3d ac = p3.minus(p1); + Vector3d bc = p3.minus(p2); + // Find the longest edge to use as primary axis + double lenAB2 = ab.getSquaredLength(); + double lenAC2 = ac.getSquaredLength(); + double lenBC2 = bc.getSquaredLength(); + + Vector3d axisX; + if (lenAB2 >= lenAC2 && lenAB2 >= lenBC2) { + axisX = ab; + } else if (lenAC2 >= lenBC2) { + axisX = ac; + } else { + axisX = bc; + } + + axisX = axisX.normalize(); + + // Pick any vector not parallel to axisX for in-plane Y axis + Vector3d axisY = pickPerpendicular(axisX); + + // Project vertices onto axes + double minX = Math.min(p1.dot(axisX), Math.min(p2.dot(axisX), p3.dot(axisX))); + double maxX = Math.max(p1.dot(axisX), Math.max(p2.dot(axisX), p3.dot(axisX))); + + double minY = Math.min(p1.dot(axisY), Math.min(p2.dot(axisY), p3.dot(axisY))); + double maxY = Math.max(p1.dot(axisY), Math.max(p2.dot(axisY), p3.dot(axisY))); + + double extentX = maxX - minX; + double extentY = maxY - minY; + + return extentX > minExtent && extentY > minExtent; + } + + private Vector3d pickPerpendicular(Vector3d v) { + // choose smallest component to avoid near-zero cross + if (Math.abs(v.getX()) < Math.abs(v.getY()) && Math.abs(v.getX()) < Math.abs(v.getZ())) { + return new Vector3d(0, -v.getZ(), v.getY()).normalize(); + } + if (Math.abs(v.getY()) < Math.abs(v.getZ())) { + return new Vector3d(-v.getZ(), 0, v.getX()).normalize(); + } + return new Vector3d(-v.getY(), v.getX(), 0).normalize(); + } + public TesselatedPolygon getPartOf() { return partOf; } @@ -101,13 +150,13 @@ public class Triangle3d implements Serializable { double distanceP2T2 = planeT2.getSignedDistance(p2); double distanceP3T2 = planeT2.getSignedDistance(p3); - if (Math.abs(distanceP1T2) < epsilon) { + if (Math.abs(distanceP1T2) < PLANAR_EPSILON) { distanceP1T2 = 0.0; } - if (Math.abs(distanceP2T2) < epsilon) { + if (Math.abs(distanceP2T2) < PLANAR_EPSILON) { distanceP2T2 = 0.0; } - if (Math.abs(distanceP3T2) < epsilon) { + if (Math.abs(distanceP3T2) < PLANAR_EPSILON) { distanceP3T2 = 0.0; } @@ -144,20 +193,18 @@ public class Triangle3d implements Serializable { return false; } - boolean intersects = checkTriangleLineIntersection(other, p1, p2) || checkTriangleLineIntersection(other, p1, p3) - || checkTriangleLineIntersection(other, p2, p3) - || checkTriangleLineIntersection(this, other.p1, other.p2) - || checkTriangleLineIntersection(this, other.p1, other.p3) - || checkTriangleLineIntersection(this, other.p2, other.p3); - if (intersects) { - System.out.println(); - } + boolean intersects = checkTriangleLineIntersection(other, p1, p2, epsilon) + || checkTriangleLineIntersection(other, p1, p3, epsilon) + || checkTriangleLineIntersection(other, p2, p3, epsilon) + || checkTriangleLineIntersection(this, other.p1, other.p2, epsilon) + || checkTriangleLineIntersection(this, other.p1, other.p3, epsilon) + || checkTriangleLineIntersection(this, other.p2, other.p3, epsilon); return intersects; } - private boolean checkTriangleLineIntersection(Triangle3d other, Vector3d a, Vector3d b) { + private boolean checkTriangleLineIntersection(Triangle3d other, Vector3d a, Vector3d b, double eps) { Segment3d seg = new Segment3d(a, b); - Vector3d intersection1 = seg.intersection(other); + Vector3d intersection1 = seg.intersection(other, eps); return intersection1 != null; } @@ -274,5 +321,4 @@ public class Triangle3d implements Serializable { public void setPartOf(TesselatedPolygon p) { partOf = p; } - } diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Vector3d.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Vector3d.java index df1486e3..cbd7bd0c 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Vector3d.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/math/Vector3d.java @@ -46,9 +46,10 @@ public class Vector3d implements Serializable { public Vector3d() { this(0d, 0d, 0d); } - + /** * Convert JTS Coordinate class to Vector3d. + * * @param coord JTS Coordinate */ public Vector3d(Coordinate coord) { @@ -210,7 +211,8 @@ public class Vector3d implements Serializable { } /** - * normalizes this vector. This method changes the coordinates of this instance. + * returns a normalized vector in the same direction as this one. This method + * does not change the coordinates of this instance. */ public UnitVector3d normalize() { return UnitVector3d.of(this); @@ -253,9 +255,9 @@ public class Vector3d implements Serializable { @Override public String toString() { final int maxLen = 5; - return "Vector3d [coords=" + - (coords != null ? Arrays.toString(Arrays.copyOf(coords, Math.min(coords.length, maxLen))) : null) + - "]"; + return "Vector3d [coords=" + + (coords != null ? Arrays.toString(Arrays.copyOf(coords, Math.min(coords.length, maxLen))) : null) + + "]"; } @Override diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java index bd69bb2d..24e1d335 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java @@ -641,7 +641,7 @@ public class Checker { ArrayList checkList = new ArrayList<>(); for (CheckId id : enabledCheck) { Check c = checkConfig.getCheckForId(id); - c.init(parameterMap.get(id), parserConfig); + c.init(parameterMap, parserConfig); checkList.add(c); } return checkList; @@ -657,7 +657,6 @@ public class Checker { parameterMap.compute(proto.getCheckId(), (k, v) -> { if (v == null) { v = new HashMap<>(); - v.put(GlobalParameters.NUMBER_OF_ROUNDING_PLACES, config.getNumberOfRoundingPlacesAsString()); v.put(GlobalParameters.MIN_VERTEX_DISTANCE, config.getMinVertexDistanceAsString()); } v.putAll(e.getValue().getParameters()); diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/CheckContainer.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/CheckContainer.java index 7e628940..ff4b7b54 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/CheckContainer.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/CheckContainer.java @@ -62,7 +62,7 @@ public class CheckContainer extends Check { } @Override - public void init(Map parameters, ParserConfiguration config) { + public void init(Map> parameters, ParserConfiguration config) { check.init(parameters, config); } diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/DuplicatePointsCheck.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/DuplicatePointsCheck.java index 991e7f25..87f0d395 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/DuplicatePointsCheck.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/DuplicatePointsCheck.java @@ -30,10 +30,12 @@ import de.hft.stuttgart.citydoctor2.check.CheckId; import de.hft.stuttgart.citydoctor2.check.CheckResult; import de.hft.stuttgart.citydoctor2.check.RequirementType; import de.hft.stuttgart.citydoctor2.check.Checkable; +import de.hft.stuttgart.citydoctor2.check.GlobalParameters; import de.hft.stuttgart.citydoctor2.check.Requirement; import de.hft.stuttgart.citydoctor2.check.ResultStatus; import de.hft.stuttgart.citydoctor2.check.error.ConsecutivePointSameError; import de.hft.stuttgart.citydoctor2.check.error.RingDuplicatePointError; +import de.hft.stuttgart.citydoctor2.checks.Checks; import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.Vertex; @@ -52,8 +54,6 @@ import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; */ public class DuplicatePointsCheck extends Check { - private static final String EPSILON_NAME = "minVertexDistance"; - private static final List dependencies; static { @@ -66,11 +66,16 @@ public class DuplicatePointsCheck extends Check { classes.add(LinearRing.class); } - private double epsilon = 0.0001; + private double epsilon = Checks.MIN_VERTEX_DISTANCE_DEFAULT; @Override - public void init(Map params, ParserConfiguration config) { - String epsilonString = params.get(EPSILON_NAME); + public void init(Map> params, ParserConfiguration config) { + Map localParameters = params.get(getCheckId()); + if (localParameters == null) { + // no parameters + return; + } + String epsilonString = localParameters.get(GlobalParameters.MIN_VERTEX_DISTANCE); if (epsilonString != null) { epsilon = Double.parseDouble(epsilonString); } diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/InteriorDisconnectedCheck.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/InteriorDisconnectedCheck.java index 99fa9f41..7c54953c 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/InteriorDisconnectedCheck.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/InteriorDisconnectedCheck.java @@ -29,10 +29,12 @@ import de.hft.stuttgart.citydoctor2.check.Check; import de.hft.stuttgart.citydoctor2.check.CheckError; import de.hft.stuttgart.citydoctor2.check.CheckId; import de.hft.stuttgart.citydoctor2.check.CheckResult; +import de.hft.stuttgart.citydoctor2.check.GlobalParameters; import de.hft.stuttgart.citydoctor2.check.RequirementType; import de.hft.stuttgart.citydoctor2.check.Requirement; import de.hft.stuttgart.citydoctor2.check.ResultStatus; import de.hft.stuttgart.citydoctor2.check.error.PolygonInteriorDisconnectedError; +import de.hft.stuttgart.citydoctor2.checks.Checks; import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.Polygon; @@ -49,8 +51,6 @@ import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; */ public class InteriorDisconnectedCheck extends Check { - private static final String EPSILON_NAME = "minVertexDistance"; - private static final List dependencies; static { @@ -63,11 +63,16 @@ public class InteriorDisconnectedCheck extends Check { dependencies = Collections.unmodifiableList(deps); } - private double epsilon = 0.0001; + private double epsilon = Checks.MIN_VERTEX_DISTANCE_DEFAULT; @Override - public void init(Map params, ParserConfiguration config) { - String epsilonString = params.get(EPSILON_NAME); + public void init(Map> params, ParserConfiguration config) { + Map localParameters = params.get(getCheckId()); + if (localParameters == null) { + // no parameters + return; + } + String epsilonString = localParameters.get(GlobalParameters.MIN_VERTEX_DISTANCE); if (epsilonString != null) { epsilon = Double.parseDouble(epsilonString); } diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/NullAreaCheck.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/NullAreaCheck.java index fdba67d7..feec95f2 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/NullAreaCheck.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/NullAreaCheck.java @@ -44,9 +44,10 @@ import de.hft.stuttgart.citydoctor2.tesselation.TesselatedRing; public class NullAreaCheck extends Check { private static final String DELTA_NAME = "delta"; - private static final List dependencies; + private double delta = 0.0001; + static { ArrayList deps = new ArrayList<>(); deps.add(CheckId.C_GE_R_TOO_FEW_POINTS); @@ -55,12 +56,16 @@ public class NullAreaCheck extends Check { dependencies = Collections.unmodifiableList(deps); } - private double delta = 0.0001; - @Override - public void init(Map parameters, ParserConfiguration config) { - if (parameters.containsKey(DELTA_NAME)) { - delta = Double.parseDouble(parameters.get(DELTA_NAME)); + public void init(Map> params, ParserConfiguration config) { + Map localParameters = params.get(getCheckId()); + if (localParameters == null) { + // no parameters + return; + } + String epsilonString = localParameters.get(DELTA_NAME); + if (epsilonString != null) { + delta = Double.parseDouble(epsilonString); } } diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/PlanarCheck.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/PlanarCheck.java index af6dd4cc..c05cc517 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/PlanarCheck.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/PlanarCheck.java @@ -81,17 +81,22 @@ public class PlanarCheck extends Check { private double delta = 0.01; @Override - public void init(Map parameters, ParserConfiguration config) { - if (parameters.containsKey(TYPE)) { - planarCheckType = parameters.get(TYPE).toLowerCase(); + public void init(Map> parameters, ParserConfiguration config) { + Map localParameters = parameters.get(getCheckId()); + if (localParameters == null) { + // no parameters + return; + } + if (localParameters.containsKey(TYPE)) { + planarCheckType = localParameters.get(TYPE).toLowerCase(); } else { throw new IllegalStateException("Parameter " + TYPE + " is missing from parameters"); } - if (parameters.containsKey(ANGLE_TOLERANCE)) { - rad = Math.toRadians(Double.parseDouble(parameters.get(ANGLE_TOLERANCE))); + if (localParameters.containsKey(ANGLE_TOLERANCE)) { + rad = Math.toRadians(Double.parseDouble(localParameters.get(ANGLE_TOLERANCE))); } - if (parameters.containsKey(DISTANCE_TOLERANCE)) { - delta = Double.parseDouble(parameters.get(DISTANCE_TOLERANCE)); + if (localParameters.containsKey(DISTANCE_TOLERANCE)) { + delta = Double.parseDouble(localParameters.get(DISTANCE_TOLERANCE)); } } diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/RingSelfIntCheck.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/RingSelfIntCheck.java index 17889390..e10d61d1 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/RingSelfIntCheck.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/RingSelfIntCheck.java @@ -29,6 +29,7 @@ import de.hft.stuttgart.citydoctor2.check.Check; import de.hft.stuttgart.citydoctor2.check.CheckError; import de.hft.stuttgart.citydoctor2.check.CheckId; import de.hft.stuttgart.citydoctor2.check.CheckResult; +import de.hft.stuttgart.citydoctor2.check.GlobalParameters; import de.hft.stuttgart.citydoctor2.check.Requirement; import de.hft.stuttgart.citydoctor2.check.RequirementType; import de.hft.stuttgart.citydoctor2.check.ResultStatus; @@ -59,15 +60,12 @@ import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; */ public class RingSelfIntCheck extends Check { - private static final String EPSILON_NAME = "minVertexDistance"; - + private static final List dependencies; + // check requirement class for default parameters private double degeneratedRingTolerance = 0.01; private double epsilon = Checks.MIN_VERTEX_DISTANCE_DEFAULT; - - private static final List dependencies; - static { ArrayList deps = new ArrayList<>(); deps.add(CheckId.C_GE_R_TOO_FEW_POINTS); @@ -78,13 +76,18 @@ public class RingSelfIntCheck extends Check { @Override - public void init(Map parameters, ParserConfiguration config) { - String epsilonString = parameters.get(EPSILON_NAME); + public void init(Map> parameters, ParserConfiguration config) { + Map localParameters = parameters.get(getCheckId()); + if (localParameters == null) { + // no parameters + return; + } + String epsilonString = localParameters.get(GlobalParameters.MIN_VERTEX_DISTANCE); if (epsilonString != null) { epsilon = Double.parseDouble(epsilonString); } - if (parameters.containsKey(Requirement.DEGENERATED_RING_TOLERANCE)) { - degeneratedRingTolerance = Double.parseDouble(parameters.get(Requirement.DEGENERATED_RING_TOLERANCE)); + if (localParameters.containsKey(Requirement.DEGENERATED_RING_TOLERANCE)) { + degeneratedRingTolerance = Double.parseDouble(localParameters.get(Requirement.DEGENERATED_RING_TOLERANCE)); } } diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/SolidSelfIntCheck.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/SolidSelfIntCheck.java index c911a1d7..bdb5b24f 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/SolidSelfIntCheck.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/geometry/SolidSelfIntCheck.java @@ -73,10 +73,16 @@ public class SolidSelfIntCheck extends Check { } @Override - public void init(Map parameters, ParserConfiguration config) { - if (parameters.containsKey(PlanarCheck.DISTANCE_TOLERANCE)) { - delta = Double.parseDouble(parameters.get(PlanarCheck.DISTANCE_TOLERANCE)); + public void init(Map> parameters, ParserConfiguration config) { + Map planarParameters = parameters.get(CheckId.C_GE_P_NON_PLANAR); + if (planarParameters == null) { + // no parameters + return; } + planarParameters.computeIfPresent(PlanarCheck.DISTANCE_TOLERANCE, (k, v) -> { + delta = Double.parseDouble(v); + return v; + }); } @Override diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/semantics/IsWallCheck.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/semantics/IsWallCheck.java index 8481e330..a413b6ce 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/semantics/IsWallCheck.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/semantics/IsWallCheck.java @@ -75,13 +75,18 @@ public class IsWallCheck extends Check { private double upperAngleCos = Math.cos(135 * Math.PI / 180); @Override - public void init(Map params, ParserConfiguration config) { - String lowerAngleString = params.get(LOWER_ANGLE_NAME); + public void init(Map> params, ParserConfiguration config) { + Map localParameters = params.get(getCheckId()); + if (localParameters == null) { + // no parameters + return; + } + String lowerAngleString = localParameters.get(LOWER_ANGLE_NAME); if (lowerAngleString != null) { lowerAngleCos = Double.parseDouble(lowerAngleString); lowerAngleCos = Math.cos(lowerAngleCos * Math.PI / 180); } - String upperAngleString = params.get(UPPER_ANGLE_NAME); + String upperAngleString = localParameters.get(UPPER_ANGLE_NAME); if (upperAngleString != null) { upperAngleCos = Double.parseDouble(upperAngleString); upperAngleCos = Math.cos(upperAngleCos * Math.PI / 180); diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/semantics/RoofSurfaceUnfragmentedCheck.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/semantics/RoofSurfaceUnfragmentedCheck.java index 4996b70f..2de941fa 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/semantics/RoofSurfaceUnfragmentedCheck.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/semantics/RoofSurfaceUnfragmentedCheck.java @@ -60,8 +60,13 @@ public class RoofSurfaceUnfragmentedCheck extends Check { } @Override - public void init(Map params, ParserConfiguration config) { - String maxAngleString = params.get(MAX_ANGLE_DEVIATION); + public void init(Map> params, ParserConfiguration config) { + Map localParameters = params.get(getCheckId()); + if (localParameters == null) { + // no parameters + return; + } + String maxAngleString = localParameters.get(MAX_ANGLE_DEVIATION); if (maxAngleString != null) { maxAngleDeviation = Math.toRadians(Double.parseDouble(maxAngleString)); } diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/util/SelfIntersectionUtil.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/util/SelfIntersectionUtil.java index 30c57878..5519b2ba 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/util/SelfIntersectionUtil.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/checks/util/SelfIntersectionUtil.java @@ -40,8 +40,6 @@ import org.locationtech.jts.geom.impl.CoordinateArraySequence; import org.locationtech.jts.operation.overlay.OverlayOp; import org.locationtech.jts.operation.overlay.snap.SnapIfNeededOverlayOp; -import Jama.EigenvalueDecomposition; -import Jama.Matrix; import de.hft.stuttgart.citydoctor2.check.GeometrySelfIntersection; import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon; import de.hft.stuttgart.citydoctor2.datastructure.Geometry; @@ -53,10 +51,8 @@ import de.hft.stuttgart.citydoctor2.edge.EdgePolygon; import de.hft.stuttgart.citydoctor2.edge.IntersectPlanarPolygons; import de.hft.stuttgart.citydoctor2.edge.MeshSurface; import de.hft.stuttgart.citydoctor2.edge.PolygonPolygonIntersection; -import de.hft.stuttgart.citydoctor2.math.CovarianceMatrix; import de.hft.stuttgart.citydoctor2.math.MovedPolygon; import de.hft.stuttgart.citydoctor2.math.MovedRing; -import de.hft.stuttgart.citydoctor2.math.OrthogonalRegressionPlane; import de.hft.stuttgart.citydoctor2.math.Plane; import de.hft.stuttgart.citydoctor2.math.PlaneSegmentIntersection; import de.hft.stuttgart.citydoctor2.math.PlaneSegmentIntersection.Type; @@ -95,18 +91,7 @@ public class SelfIntersectionUtil { TesselatedPolygon tessPolygon = EarcutTesselator.tesselatePolygon(p); for (Iterator iterator = tessPolygon.getTriangles().iterator(); iterator.hasNext();) { Triangle3d t = iterator.next(); - List vertices = new ArrayList<>(3); - vertices.add(t.getP1()); - vertices.add(t.getP2()); - vertices.add(t.getP3()); - Vector3d centroid = CovarianceMatrix.getCentroid(vertices); - EigenvalueDecomposition ed = OrthogonalRegressionPlane.decompose(vertices, centroid); - Matrix eigenValues = ed.getD(); - double[] eigenValuesArray = new double[3]; - eigenValuesArray[0] = eigenValues.get(0, 0); - eigenValuesArray[1] = eigenValues.get(1, 1); - eigenValuesArray[2] = eigenValues.get(2, 2); - if (eigenValuesArray[1] < delta) { + if (!t.hasMinExtent(delta)) { iterator.remove(); } } @@ -126,7 +111,7 @@ public class SelfIntersectionUtil { } return intersections; } - + public static GeometrySelfIntersection doesSolidSelfIntersect(Geometry g, double epsilon) { return selfIntersectionJava(g, epsilon); } @@ -448,8 +433,8 @@ public class SelfIntersectionUtil { private static GeometrySelfIntersection doPolygonsIntersect(TesselatedPolygon p1, TesselatedPolygon p2, double epsilon) { for (int p1Index = 0; p1Index < p1.getTriangles().size(); p1Index++) { + Triangle3d t1 = p1.getTriangles().get(p1Index); for (int p2Index = 0; p2Index < p2.getTriangles().size(); p2Index++) { - Triangle3d t1 = p1.getTriangles().get(p1Index); Triangle3d t2 = p2.getTriangles().get(p2Index); if (t1.doesIntersect(t2, epsilon)) { logger.trace("{} intersects {}", t1, t2); diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/reporting/XmlValidationReporter.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/reporting/XmlValidationReporter.java index 3c550383..bb93a493 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/reporting/XmlValidationReporter.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/reporting/XmlValidationReporter.java @@ -24,16 +24,7 @@ import de.hft.stuttgart.citydoctor2.check.CheckError; import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration; import de.hft.stuttgart.citydoctor2.checkresult.utility.CheckReportWriteException; import de.hft.stuttgart.citydoctor2.checks.Checks; -import de.hft.stuttgart.citydoctor2.datastructure.BridgeObject; -import de.hft.stuttgart.citydoctor2.datastructure.Building; import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel; -import de.hft.stuttgart.citydoctor2.datastructure.CityFurniture; -import de.hft.stuttgart.citydoctor2.datastructure.CityObject; -import de.hft.stuttgart.citydoctor2.datastructure.GenericCityObject; -import de.hft.stuttgart.citydoctor2.datastructure.TransportationObject; -import de.hft.stuttgart.citydoctor2.datastructure.Tunnel; -import de.hft.stuttgart.citydoctor2.datastructure.Vegetation; -import de.hft.stuttgart.citydoctor2.datastructure.WaterObject; /** * diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/reporting/pdf/PdfStreamReporter.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/reporting/pdf/PdfStreamReporter.java index 4214bc32..a4b5c1bc 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/reporting/pdf/PdfStreamReporter.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/reporting/pdf/PdfStreamReporter.java @@ -285,7 +285,6 @@ public class PdfStreamReporter implements StreamReporter { tSection.setHeadlineColor(OK_COLOR); } writeErrorForCityObject(co, tSection); - TransportationObject to = (TransportationObject) co; } private void reportVegetation(CityObject co, boolean hasError) { @@ -389,25 +388,6 @@ public class PdfStreamReporter implements StreamReporter { } } - private void writeCheckResultForTransportationObject(TransportationObject to, Section root) { - Map results = to.getAllCheckResults(); - writeCheckResults(results.values(), root); - for (Geometry geom : to.getGeometries()) { - writeCheckResultForGeometry(geom, root); - } - } - - private void writeCheckResultForBuildingPart(BuildingPart bp, Section root) { - Map results = bp.getAllCheckResults(); - writeCheckResults(results.values(), root); - for (Geometry geom : bp.getGeometries()) { - writeCheckResultForGeometry(geom, root); - } - for (BoundarySurface bs : bp.getBoundarySurfaces()) { - writeCheckResultForBoundarySurface(bs, root); - } - } - private void writeCheckResultForAbstractBuilding(AbstractBuilding ab, Section root) { Map results = ab.getAllCheckResults(); writeCheckResults(results.values(), root); @@ -714,24 +694,6 @@ public class PdfStreamReporter implements StreamReporter { report.save(outFile); } - private void countFinishedReportBuildings() { - for (Section s : buildings.getSubSections()) { - if (!s.hasErrors()) { - numOkBuildings++; - // building has no errors, no table - continue; - } - numErrorBuildings++; - Table t = new Table(2); - t.setTableColumnWidth(75, 25); - t.setTitle("Error", "Count"); - for (Entry e : s.getStats().getErrorCounts().entrySet()) { - t.addRow(e.getKey(), e.getValue().toString()); - } - s.addTable(1, t); - } - } - @Override public void reportGlobalError(CheckError err) { AtomicInteger errorCount = errorStatistics.computeIfAbsent(err.getErrorId(), k -> new AtomicInteger(0)); -- GitLab From 11c31eb0e587f48765fefb04d0a653f9bf28d1af Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Thu, 15 Jan 2026 15:06:22 +0100 Subject: [PATCH 26/63] Refactor: Cleanup Code --- .../database/EmbeddedDatabaseConfiguration.java | 1 - .../database/EmbeddedDataBaseHandlerTest.java | 4 +--- .../stuttgart/citydoctor2/check/Checker.java | 17 ++++++----------- .../stuttgart/citydoctor2/healer/Healer.java | 4 +--- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java index 5f5ec31f..be56f08a 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java @@ -25,7 +25,6 @@ public record EmbeddedDatabaseConfiguration(String databaseName, String database private static final String JDBC_DRIVER_PREFIX = "jdbc:h2:"; private static final String AUTO_SERVER_PARAMETER = "AUTO_SERVER=TRUE"; private static final String KEEP_ALIVE_PARAMETER = "DB_CLOSE_DELAY=-1"; - private static final String FILE_PREFIX = "file://"; /** * Instantiates the default configuration for the embedded database. */ diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java index 371cea82..b795e50e 100644 --- a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDataBaseHandlerTest.java @@ -23,15 +23,13 @@ import java.util.List; import static de.hft.stuttgart.citydoctor2.database.SerializationTest.assertCityObjectEquality; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; public class EmbeddedDataBaseHandlerTest { - private EmbeddedDatabaseHandler handler = new EmbeddedDatabaseHandler(new EmbeddedDatabaseConfiguration()); + private final EmbeddedDatabaseHandler handler = new EmbeddedDatabaseHandler(new EmbeddedDatabaseConfiguration()); @Test public void testTempFileConfiguration(){ diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java index 50abb11b..acb1e967 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/check/Checker.java @@ -150,8 +150,8 @@ public class Checker { return; } File xmlFile = new File(xmlOutput); - if (xmlFile.getParentFile() != null) { - xmlFile.getParentFile().mkdirs(); + if (xmlFile.getParentFile() != null && xmlFile.getParentFile().mkdirs()) { + logger.trace("Xml dir created"); } Reporter reporter = new XmlValidationReporter(); try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(xmlFile.getAbsolutePath()))) { @@ -166,9 +166,10 @@ public class Checker { return; } File pdfFile = new File(pdfOutput); - if (pdfFile.getParentFile() != null) { - pdfFile.getParentFile().mkdirs(); + if (pdfFile.getParentFile() != null && pdfFile.getParentFile().mkdirs()){ + logger.trace("Pdf dir created"); } + Reporter reporter = new PdfReporter(); try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(pdfFile.getAbsolutePath()))) { reporter.writeReport(checkConfig, bos, model, config); @@ -243,7 +244,6 @@ public class Checker { private void handleSchematronResults(SvrlContentHandler handler) { CityObjectCache cache = model.getCache(); handleSchematronErrorsGlobal(handler.getGeneralErrors()); - Map featureMap = new HashMap<>(); boolean onlySchematron = execLayers.isEmpty(); if (onlySchematron) { CheckableUtilsVisitor visitor = new CheckableUtilsVisitor() { @@ -252,12 +252,7 @@ public class Checker { checkable.setValidated(true); } }; - model.createFeatureStream().forEach(f -> { - featureMap.put(f.getGmlId().getGmlString(), f); - f.accept(visitor); - }); - } else { - model.createFeatureStream().forEach(f -> featureMap.put(f.getGmlId().getGmlString(), f)); + model.createFeatureStream().forEach(co -> co.accept(visitor)); } handler.getFeatureErrors().forEach((k, v) -> { String trimmedId = k.trim(); diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java index d62e505c..f6b3c068 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java +++ b/CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healer/Healer.java @@ -296,9 +296,7 @@ public class Healer { * * @param aco the feature to write * @return the byte array containing the feature - * @throws ADEException - * @throws CityGMLBuilderException - * @throws CityGMLWriteException + * @throws CityGMLWriteException if writing of the CityGML file fails */ private byte[] writeCityGml(AbstractCityObject aco) throws CityGMLWriteException { CityModel model = new CityModel(); -- GitLab From 3df0f7375aaf536478fbdc0434be0fe54e332e78 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Thu, 15 Jan 2026 15:07:29 +0100 Subject: [PATCH 27/63] Test: Add run config for CityDoctorGUI --- .run/CityDoctorGUIStarter.run.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.run/CityDoctorGUIStarter.run.xml b/.run/CityDoctorGUIStarter.run.xml index b803b46b..b2912b34 100644 --- a/.run/CityDoctorGUIStarter.run.xml +++ b/.run/CityDoctorGUIStarter.run.xml @@ -13,4 +13,18 @@

Checks finding an error in a Feature, or not having their dependencies met, are not a check-failure in this + * context.

+ * @param exec the threadpool for the tasks + * @param cache the cache of the model + * @param ids the list of GmlIds to check + * @param checkedCount the number of already checked Features for tracking the total progress of validation across retries + * @param l a listener for the progress of the validation, can be null + * @return a list of Futures containing the GmlId of Features that failed to execute all checks, and contain null otherwise + * @throws InterruptedException if interrupted while invoking the tasks for checking + */ private List> runChecksOnFeatures(ExecutorService exec, CityObjectCache cache, List ids, AtomicInteger checkedCount, - ProgressListener l) + @Nullable ProgressListener l) throws InterruptedException { float featureSum = ids.size() + (float) checkedCount.get(); -- GitLab From a4e34abb2a5087e450e6ef0b1ece50dc5e8f7f08 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Mon, 23 Feb 2026 14:01:39 +0100 Subject: [PATCH 56/63] Feat: Add parsing of args for db location and config --- .../database/DatabaseSettings.java | 38 ++++++++++++---- .../EmbeddedDatabaseConfiguration.java | 3 +- .../database/DatabaseSettingsTest.java | 11 +++++ .../citydoctor2/CityDoctorValidation.java | 45 +++++++++++++++---- .../citydoctor2/CityDoctorValidationCLI.java | 23 ++++++++-- 5 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/DatabaseSettingsTest.java diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DatabaseSettings.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DatabaseSettings.java index 147e62ae..8028bac7 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DatabaseSettings.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/DatabaseSettings.java @@ -1,5 +1,6 @@ package de.hft.stuttgart.citydoctor2.database; +import de.hft.stuttgart.citydoctor2.exceptions.EmbeddedDatabaseHandlerException; import de.hft.stuttgart.citydoctor2.utils.Localization; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -9,6 +10,7 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; import java.nio.file.InvalidPathException; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Properties; @@ -16,27 +18,47 @@ public final class DatabaseSettings { private static final Logger logger = LogManager.getLogger(DatabaseSettings.class); - private static final Properties props; + private static final Properties props = new Properties(); private DatabaseSettings() {} static { - props = new Properties(); File propFile = new File("DBSettings.properties"); if (propFile.exists()) { - try (BufferedReader bis = new BufferedReader(new FileReader(propFile))) { - props.load(bis); - } catch (IOException e) { - logger.error("Failed to load database settings", e); - } + loadPropertiesFromFile(propFile); + } + } + + public static void loadPropertiesFromFile(File file) throws EmbeddedDatabaseHandlerException { + try (BufferedReader bis = new BufferedReader(new FileReader(file))) { + props.load(bis); + } catch (IOException e) { + throw new EmbeddedDatabaseHandlerException(e); + } + } + + public static void setDBLocation(String dbLocation) throws EmbeddedDatabaseHandlerException{ + Path dbPath = Paths.get(dbLocation).toAbsolutePath(); + File dbLocationFile = dbPath.toFile(); + if (!dbLocationFile.isFile()) { + dbPath = dbPath.resolve("cd_db"); + } + Path dbParentDirectoryPath = dbPath.getParent(); + if (dbParentDirectoryPath.toFile().mkdirs()){ + logger.trace("Created parent directories for database location"); + } + if (!dbParentDirectoryPath.toFile().canWrite()) { + throw new EmbeddedDatabaseHandlerException("Missing write permissions for database location:" + dbLocation); } + props.setProperty("database.name", dbPath.getFileName().toString()); + props.setProperty("database.directory", dbParentDirectoryPath.toAbsolutePath().toString()); } public static EmbeddedDatabaseConfiguration getConfig() { EmbeddedDatabaseConfiguration defaultConfig = new EmbeddedDatabaseConfiguration(); if (props.isEmpty()) { - logger.debug("No database settings found or specified, using default config"); + logger.trace("No database settings found or specified, using default config"); return defaultConfig; } String name = props.getProperty("database.name"); diff --git a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java index 15ac1d56..6a321c9a 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java +++ b/CityDoctorParent/CityDoctorModel/src/main/java/de/hft/stuttgart/citydoctor2/database/EmbeddedDatabaseConfiguration.java @@ -1,6 +1,7 @@ package de.hft.stuttgart.citydoctor2.database; import java.io.File; +import java.nio.file.Paths; import java.util.StringJoiner; /** @@ -51,7 +52,7 @@ public record EmbeddedDatabaseConfiguration(String databaseName, String database } public String getH2FileUrl(){ - return wrapUrlWithH2JdbcDriver(databaseDirectory + databaseName); + return wrapUrlWithH2JdbcDriver(Paths.get(databaseDirectory).resolve(databaseName).toString()); } public String getH2ParametersSuffix(){ diff --git a/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/DatabaseSettingsTest.java b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/DatabaseSettingsTest.java new file mode 100644 index 00000000..00ca8ccb --- /dev/null +++ b/CityDoctorParent/CityDoctorModel/src/test/java/de/hft/stuttgart/citydoctor2/database/DatabaseSettingsTest.java @@ -0,0 +1,11 @@ +package de.hft.stuttgart.citydoctor2.database; + +import org.junit.jupiter.api.Test; + +public class DatabaseSettingsTest { + + public void testSetLocation(){ + String tempdirectory = System.getProperty("java.io.tmpdir"); + DatabaseSettings.setDBLocation(tempdirectory + "/citydoctor"); + } +} diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidation.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidation.java index 548fee23..c96294b6 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidation.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidation.java @@ -23,6 +23,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; +import de.hft.stuttgart.citydoctor2.database.DatabaseSettings; import de.hft.stuttgart.citydoctor2.utils.Localization; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -70,6 +71,8 @@ public class CityDoctorValidation { String xmlOutput = getXmlOutput(argParser); String pdfOutput = getPdfOutput(argParser); String outputFile = getOutputFile(argParser, true); + lookForDbConfigFileParameter(argParser); + lookForDbLocationParameter(argParser); ValidationConfiguration config = getValidationConfig(argParser); startValidationProcess(inputFile, xmlOutput, pdfOutput, config, outputFile); @@ -81,18 +84,42 @@ public class CityDoctorValidation { if (optional) { return null; } - logger.error(Localization.getText("CityDoctorValidation.noOutputFile")); + logger.fatal(Localization.getText("CityDoctorValidation.noOutputFile")); System.exit(11); } List outFiles = argParser.getValues("out"); if (outFiles.size() != 1) { - logger.error(Localization.getText("CityDoctorValidation.notExactlyOneOutputFile")); + logger.fatal(Localization.getText("CityDoctorValidation.notExactlyOneOutputFile")); System.exit(12); } outputFile = outFiles.get(0); return outputFile; } + private static void lookForDbConfigFileParameter(ArgumentParser argParser) { + if (!argParser.containsOption("db_config")) { + return; + } + List dbConfigFiles = argParser.getValues("db_config"); + if (dbConfigFiles.size() == 1) { + DatabaseSettings.loadPropertiesFromFile(new File(dbConfigFiles.get(0))); + } else if (dbConfigFiles.size() > 1) { + logger.warn("More than one DB config file provided! Using standard config instead."); + } + } + + private static void lookForDbLocationParameter(ArgumentParser argParser) { + if (!argParser.containsOption("db_location")) { + return; + } + List dbLocations = argParser.getValues("db_location"); + if (dbLocations.size() == 1) { + DatabaseSettings.setDBLocation(dbLocations.get(0)); + } else if (dbLocations.size() > 1) { + logger.warn("More than one DB location provided! Using standard location instead."); + } + } + /** * Validates the cityGML input file with a standard validation configuration. * @@ -202,7 +229,7 @@ public class CityDoctorValidation { if (argParser.containsOption("pdfreport")) { List reportFiles = argParser.getValues("pdfreport"); if (reportFiles.size() != 1) { - logger.error(Localization.getText("CityDoctorValidation.notExactlyOnePDFOutput")); + logger.fatal(Localization.getText("CityDoctorValidation.notExactlyOnePDFOutput")); System.exit(6); } return reportFiles.get(0); @@ -214,7 +241,7 @@ public class CityDoctorValidation { if (argParser.containsOption("xmlreport")) { List reportFiles = argParser.getValues("xmlreport"); if (reportFiles.size() != 1) { - logger.error(Localization.getText("CityDoctorValidation.notExactlyOneXMLOutput")); + logger.fatal(Localization.getText("CityDoctorValidation.notExactlyOneXMLOutput")); System.exit(5); } return reportFiles.get(0); @@ -231,7 +258,7 @@ public class CityDoctorValidation { if (argParser.containsOption("config")) { List configFiles = argParser.getValues("config"); if (configFiles.size() != 1) { - logger.error(Localization.getText("CityDoctorValidation.notExactlyOneConfigFile")); + logger.fatal(Localization.getText("CityDoctorValidation.notExactlyOneConfigFile")); System.exit(4); } return ValidationConfiguration.loadValidationConfigFile(configFiles.get(0)); @@ -240,7 +267,7 @@ public class CityDoctorValidation { logger.warn(Localization.getText("CityDoctorValidation.loadingDefaultConfig")); return ValidationConfiguration.loadStandardValidationConfig(); } else { - logger.error(Localization.getText("CityDoctorValidation.noConfig")); + logger.fatal(Localization.getText("CityDoctorValidation.noConfig")); System.exit(7); return null; } @@ -252,7 +279,7 @@ public class CityDoctorValidation { if (argParser.containsOption("out")) { List outFiles = argParser.getValues("out"); if (outFiles.size() != 1) { - logger.error(Localization.getText("CityDoctorValidation.multipleOutputFiles")); + logger.fatal(Localization.getText("CityDoctorValidation.multipleOutputFiles")); System.exit(3); } return outFiles.get(0); @@ -271,12 +298,12 @@ public class CityDoctorValidation { if (optional) { return null; } - logger.error(Localization.getText("CityDoctorValidation.noInputFile")); + logger.fatal(Localization.getText("CityDoctorValidation.noInputFile")); System.exit(1); } List inFiles = argParser.getValues("in"); if (inFiles.size() != 1) { - logger.error(Localization.getText("CityDoctorValidation.notExactlyOneInputFile")); + logger.fatal(Localization.getText("CityDoctorValidation.notExactlyOneInputFile")); System.exit(2); } inputFile = inFiles.get(0); diff --git a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidationCLI.java b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidationCLI.java index 5c606fda..f909db92 100644 --- a/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidationCLI.java +++ b/CityDoctorParent/CityDoctorValidation/src/main/java/de/hft/stuttgart/citydoctor2/CityDoctorValidationCLI.java @@ -2,6 +2,7 @@ package de.hft.stuttgart.citydoctor2; import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration; +import de.hft.stuttgart.citydoctor2.database.DatabaseSettings; import de.hft.stuttgart.citydoctor2.exceptions.CityDoctorWriteException; import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException; import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException; @@ -26,20 +27,30 @@ public class CityDoctorValidationCLI implements Runnable { private File config; @Option(names = {"-x" , "--xml" , "--xmlReport"}, - description = "Create a XML validation report at target location", + description = "Creates a XML validation report at target location", defaultValue = Option.NULL_VALUE) private String xmlReport; @Option(names = {"-p" , "--pdf" , "--pdfReport"}, - description = "Create a PDF validation report at target location", + description = "Creates a PDF validation report at target location", defaultValue = Option.NULL_VALUE) private String pdfReport; @Option(names = {"-o" , "--out" , "--output"}, - description = "Create a validated copy of the CityGML input with QualityADE at target location.", + description = "Creates a validated copy of the CityGML input with QualityADE at target location.", defaultValue = Option.NULL_VALUE) private String output; + @Option(names = {"--db_location", "--db_loc"}, + description = "Creates the embedded database file at target location instead of CityDoctor's working directory.", + defaultValue = Option.NULL_VALUE) + private String dbLocation; + + @Option(names = {"-db", "--db_config", "--db_settings"}, + description = "Filepath to a .properties file for overriding the default configuration of the embedded database", + defaultValue = Option.NULL_VALUE) + private String dbConfig; + @Override public void run(){ try { @@ -49,6 +60,12 @@ public class CityDoctorValidationCLI implements Runnable { } else{ valConfig=ValidationConfiguration.loadValidationConfigFile(config.getAbsolutePath()); } + if(dbConfig != null){ + DatabaseSettings.loadPropertiesFromFile(new File(dbConfig)); + } + if (dbLocation != null) { + DatabaseSettings.setDBLocation(dbLocation); + } CityDoctorValidation.startValidationProcess(input, xmlReport, pdfReport, valConfig,output); } catch (CityDoctorWriteException | CityGmlParseException | IOException | InvalidGmlFileException e) { throw new RuntimeException(e); -- GitLab From c32d1e5f8e1f0224048017745cca4e15d788e298 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 24 Feb 2026 15:07:38 +0100 Subject: [PATCH 57/63] Feat: Add default db_location to prebuilt binaries --- .../CityDoctorValidation/src/assembly/lin/start.sh | 2 +- .../CityDoctorValidation/src/assembly/no_runtime/start.bat | 2 +- .../CityDoctorValidation/src/assembly/no_runtime/start.sh | 3 ++- .../CityDoctorValidation/src/assembly/win/start.bat | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CityDoctorParent/CityDoctorValidation/src/assembly/lin/start.sh b/CityDoctorParent/CityDoctorValidation/src/assembly/lin/start.sh index 85e23ecb..6ff38584 100644 --- a/CityDoctorParent/CityDoctorValidation/src/assembly/lin/start.sh +++ b/CityDoctorParent/CityDoctorValidation/src/assembly/lin/start.sh @@ -1,2 +1,2 @@ #!/bin/sh -./runtime/bin/java -classpath app/*:plugin/* de.hft.stuttgart.citydoctor2.CityDoctorValidation -in REKaiserwall.gml -config testConfigWithStreaming.yml -xmlReport output.xml \ No newline at end of file +./runtime/bin/java -classpath app/*:plugin/* de.hft.stuttgart.citydoctor2.CityDoctorValidation -in REKaiserwall.gml -config testConfigWithStreaming.yml -xmlReport output.xml -db_location "${TMPDIR:-/tmp}" \ No newline at end of file diff --git a/CityDoctorParent/CityDoctorValidation/src/assembly/no_runtime/start.bat b/CityDoctorParent/CityDoctorValidation/src/assembly/no_runtime/start.bat index bf042835..e1f7e4ef 100644 --- a/CityDoctorParent/CityDoctorValidation/src/assembly/no_runtime/start.bat +++ b/CityDoctorParent/CityDoctorValidation/src/assembly/no_runtime/start.bat @@ -1,2 +1,2 @@ -java -classpath app/*;plugin/* de.hft.stuttgart.citydoctor2.CityDoctorValidation -in REKaiserwall.gml -config testConfigWithStreaming.yml -xmlReport output.xml +java -classpath app/*;plugin/* de.hft.stuttgart.citydoctor2.CityDoctorValidation -in REKaiserwall.gml -config testConfigWithStreaming.yml -xmlReport output.xml -db_location %TEMP%/citydoctor/ pause \ No newline at end of file diff --git a/CityDoctorParent/CityDoctorValidation/src/assembly/no_runtime/start.sh b/CityDoctorParent/CityDoctorValidation/src/assembly/no_runtime/start.sh index 8eeb31f3..8693a997 100644 --- a/CityDoctorParent/CityDoctorValidation/src/assembly/no_runtime/start.sh +++ b/CityDoctorParent/CityDoctorValidation/src/assembly/no_runtime/start.sh @@ -1,2 +1,3 @@ #!/bin/sh -java -classpath app/*:plugin/* de.hft.stuttgart.citydoctor2.CityDoctorValidation -in REKaiserwall.gml -config testConfigWithStreaming.yml -xmlReport output.xml \ No newline at end of file +java -classpath app/*:plugin/* de.hft.stuttgart.citydoctor2.CityDoctorValidation -in REKaiserwall.gml -config testConfigWithStreaming.yml -xmlReport output.xml -db_location #!/bin/sh + ./runtime/bin/java -classpath app/*:plugin/* de.hft.stuttgart.citydoctor2.CityDoctorValidation -in REKaiserwall.gml -config testConfigWithStreaming.yml -xmlReport output.xml -db_location "${TMPDIR:-/tmp}" \ No newline at end of file diff --git a/CityDoctorParent/CityDoctorValidation/src/assembly/win/start.bat b/CityDoctorParent/CityDoctorValidation/src/assembly/win/start.bat index 52d8e109..88984883 100644 --- a/CityDoctorParent/CityDoctorValidation/src/assembly/win/start.bat +++ b/CityDoctorParent/CityDoctorValidation/src/assembly/win/start.bat @@ -1,2 +1,2 @@ -"runtime/bin/java.exe" -classpath app/*;plugin/* de.hft.stuttgart.citydoctor2.CityDoctorValidation -in REKaiserwall.gml -config testConfigWithStreaming.yml -xmlReport output.xml +"runtime/bin/java.exe" -classpath app/*;plugin/* de.hft.stuttgart.citydoctor2.CityDoctorValidation -in REKaiserwall.gml -config testConfigWithStreaming.yml -xmlReport output.xml -db_location %TEMP%/citydoctor/ pause \ No newline at end of file -- GitLab From 0fd4d2ac6cd9ba538025d995b5982550843b0433 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 24 Feb 2026 15:47:53 +0100 Subject: [PATCH 58/63] Docs: Add documentation to properties file --- .../CityDoctorGUI/DBSettings.properties | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/DBSettings.properties b/CityDoctorParent/Extensions/CityDoctorGUI/DBSettings.properties index b908a091..887cc7e7 100644 --- a/CityDoctorParent/Extensions/CityDoctorGUI/DBSettings.properties +++ b/CityDoctorParent/Extensions/CityDoctorGUI/DBSettings.properties @@ -1,9 +1,30 @@ -#Embedded database configuration -#Mon Jan 19 13:16:58 CET 2026 -#database.attemptFallback=true -#database.connectionPoolSize=22 -#database.debugMode=false -#database.directory=\\database\\ -#database.inMemoryMode=false -#database.name=cd_db -#database.tempMode=false +! Embedded database configuration +! Remove # at the beginning of a parameter line to override the value. + +! Directory for the database file. DEFAULT: \\database\\ +# database.directory=\\database\\ +! Name of the database file. DEFAULT: cd_db +# database.name=cd_db + +! Sets a limit for the number of pooled connections to the embedded database. DEFAULT: 10 +! If not overridden CityDoctor will use the number of available CPU cores as limit. +# database.connectionPoolSize=10 + +! If temp mode is enabled the database-file will be created in the System's TEMP directory. DEFAULT: false +# database.tempMode=false + +! If in-memory mode is enabled the database will be created in the Systems RAM. DEFAULT: false +! WARNING: Enabling in-memory mode can lead to out-of-memory errors when loading bigger CityGML files. +# database.inMemoryMode=false + +! If fallback is enabled CityDoctor will attempt to use the in-memory mode if database-file creation fails DEFAULT: true +# database.attemptFallback=true + +! If debug mode is enabled the embedded database will start in automatic mixed mode DEFAULT: false +! NOTE: Activating this mode will start a local server with random ports. This may trigger a Firewall request, which can be safely denied. +! NOTE: This server is only accessible locally and automatically rejects any remote connections. +# database.debugMode=false + + + + -- GitLab From 094bf4e97d9160de239945df67712ab2759e8e13 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 24 Feb 2026 16:52:34 +0100 Subject: [PATCH 59/63] Style: Improve localizations --- .../src/main/resources/CityDoctorLocalization.properties | 2 +- .../src/main/resources/CityDoctorLocalization_de.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CityDoctorParent/CityDoctorModel/src/main/resources/CityDoctorLocalization.properties b/CityDoctorParent/CityDoctorModel/src/main/resources/CityDoctorLocalization.properties index f9c69759..80316fb2 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/resources/CityDoctorLocalization.properties +++ b/CityDoctorParent/CityDoctorModel/src/main/resources/CityDoctorLocalization.properties @@ -6,7 +6,7 @@ CheckContainer.error=Unexpected exception while executing check: {} CheckDialog.availableChecksLabel=Available Checks CheckDialog.cancelBtn=Cancel CheckDialog.checkBtn=Check -CheckDialog.checkDescriptionsLabel=Documentations are available on CityDoctor's homepage +CheckDialog.checkDescriptionsLabel=Explanations for the requirements are available on CityDoctor's homepage CheckDialog.checkEnabled=Enabled CheckDialog.checkName=Name CheckDialog.checksDone=Checks done diff --git a/CityDoctorParent/CityDoctorModel/src/main/resources/CityDoctorLocalization_de.properties b/CityDoctorParent/CityDoctorModel/src/main/resources/CityDoctorLocalization_de.properties index c2c88c58..657b3ad2 100644 --- a/CityDoctorParent/CityDoctorModel/src/main/resources/CityDoctorLocalization_de.properties +++ b/CityDoctorParent/CityDoctorModel/src/main/resources/CityDoctorLocalization_de.properties @@ -6,7 +6,7 @@ CheckContainer.error=Unerwarteter Fehler bei der Ausf\u00fchrung von Pr\u00fcfun CheckDialog.availableChecksLabel=Verf\u00fcgbare Pr\u00fcfungen CheckDialog.cancelBtn=Abbrechen CheckDialog.checkBtn=Pr\u00fcfen -CheckDialog.checkDescriptionsLabel=Erkl\u00e4rungen hierzu auf der CityDoctor Homepage +CheckDialog.checkDescriptionsLabel=Beschreibungen der Pr\u00fcfungen sind auf der CityDoctor Homepage verf\u00fcgbar CheckDialog.checkEnabled=Aktiv CheckDialog.checkName=Name CheckDialog.checksDone=Pr\u00fcfung abgeschlossen -- GitLab From 31cdfd69e41404068165c2691d981b4c64d609b7 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 24 Feb 2026 16:52:55 +0100 Subject: [PATCH 60/63] Docs: Move file to parent directory --- .../CityDoctorGUI/DBSettings.properties => DBSettings.properties | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CityDoctorParent/Extensions/CityDoctorGUI/DBSettings.properties => DBSettings.properties (100%) diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/DBSettings.properties b/DBSettings.properties similarity index 100% rename from CityDoctorParent/Extensions/CityDoctorGUI/DBSettings.properties rename to DBSettings.properties -- GitLab From 09afc475b653db1a4b17e56035298b01e8309236 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 24 Feb 2026 16:53:13 +0100 Subject: [PATCH 61/63] Docs: Update readme --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 703d18a6..12e928e9 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Use [Maven](https://maven.apache.org/) to build CityDoctor2. ```bash cd CityDoctorParent -mvn install +mvn clean install -P create-binaries ``` CityDoctor2 uses the [quality ade plugin](https://transfer.hft-stuttgart.de/gitlab/betzms/qualityade). A jar is provided in a local repository for ease of use. @@ -55,14 +55,16 @@ Once built there is a CityDoctorValidation-\.zip file in the target fo There is a start script included in the binary releases of citydoctor. An example start command looks like this: ```bash -java -classpath libs/*;plugins/*;CityDoctorValidation-.jar de.hft.stuttgart.citydoctor2.CityDoctorValidation -in .gml -config .yml -xmlReport .xml -pdfReport .pdf -out .gml +java -classpath libs/*;plugins/*;CityDoctorValidation-.jar de.hft.stuttgart.citydoctor2.CityDoctorValidation -in .gml -config .yml -xmlReport .xml -pdfReport .pdf -out .gml -db_location -db_settings .properties ``` Notes: -- -xmlReport, -pdfReport and -out are optional -- If a ZIP file is given as input, CityDoctor2 will check all contained CityGML files. The -out parameter is required in - this case +- -xmlReport, -pdfReport, -out, -db_location and -db_settings are optional +- Paths will be resolved from CityDoctor's working directory +- See [DBSettings.properties](DBSettings.properties) for the formatting of the database-settings file +- The -db_location parameter takes precedence over the location set in the database-settings file +- The start scripts included in the binary releases have -db_location set to the system's temp directory ## Extension modules -- GitLab From 425e5a231f6e2e5ce53d9cf2a683d07343e0ebae Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Tue, 24 Feb 2026 16:53:26 +0100 Subject: [PATCH 62/63] Docs: update changelog --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e480264..2a0c8e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.18.2] (2026-02-24) + +### Added +- CLI parameter "-db_location" for changing the location of the database-file of the embedded database. This parameter will +override the location settings of the DB-settings file. +- CLI parameter "-db_settings" for giving a path to a DB-settings .properties file. +- Added -db_location pointing to the system's temp directory to the start scripts for the binaries +- Added buttons for (un)checking all Geometric or Semantic requirements +- Added a Hyperlink to CityDoctor's homepage with detailed explanations for each requirement +- The GUI will now show a loading spinner while waiting for Schematron to finish. + + +### Fixes +- Connected CityDoctorHealer to the embedded database +- Various small fixes and improvements to performance + ## [3.18.1] (2025-12-10) ### Hotfix -- GitLab From 45d4909d616241e96cdb279c476807d38809e238 Mon Sep 17 00:00:00 2001 From: Luna Riegel Date: Wed, 25 Feb 2026 15:38:12 +0100 Subject: [PATCH 63/63] Build: Update version number --- CityDoctorParent/CityDoctorCheckResult/pom.xml | 2 +- CityDoctorParent/CityDoctorEdge/pom.xml | 2 +- CityDoctorParent/CityDoctorModel/pom.xml | 2 +- CityDoctorParent/CityDoctorValidation/pom.xml | 2 +- CityDoctorParent/Extensions/CityDoctorAutoPro/pom.xml | 2 +- CityDoctorParent/Extensions/CityDoctorGUI/pom.xml | 2 +- CityDoctorParent/Extensions/CityDoctorHealer/pom.xml | 2 +- CityDoctorParent/Extensions/CityDoctorHealerGUI/pom.xml | 2 +- CityDoctorParent/Extensions/CityDoctorHealerGenetic/pom.xml | 2 +- CityDoctorParent/pom.xml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CityDoctorParent/CityDoctorCheckResult/pom.xml b/CityDoctorParent/CityDoctorCheckResult/pom.xml index ef9fc538..b9cc0519 100644 --- a/CityDoctorParent/CityDoctorCheckResult/pom.xml +++ b/CityDoctorParent/CityDoctorCheckResult/pom.xml @@ -6,7 +6,7 @@ de.hft.stuttgart CityDoctorParent - 3.18.1 + 3.18.2 CityDoctorCheckResult diff --git a/CityDoctorParent/CityDoctorEdge/pom.xml b/CityDoctorParent/CityDoctorEdge/pom.xml index ce045dc1..284ce845 100644 --- a/CityDoctorParent/CityDoctorEdge/pom.xml +++ b/CityDoctorParent/CityDoctorEdge/pom.xml @@ -6,7 +6,7 @@ de.hft.stuttgart CityDoctorParent - 3.18.1 + 3.18.2 CityDoctorEdge diff --git a/CityDoctorParent/CityDoctorModel/pom.xml b/CityDoctorParent/CityDoctorModel/pom.xml index 95f8ea38..e107b5be 100644 --- a/CityDoctorParent/CityDoctorModel/pom.xml +++ b/CityDoctorParent/CityDoctorModel/pom.xml @@ -6,7 +6,7 @@ de.hft.stuttgart CityDoctorParent - 3.18.1 + 3.18.2 ${project.version}-${git.commit.id.abbrev} diff --git a/CityDoctorParent/CityDoctorValidation/pom.xml b/CityDoctorParent/CityDoctorValidation/pom.xml index c9514e87..f52a83cb 100644 --- a/CityDoctorParent/CityDoctorValidation/pom.xml +++ b/CityDoctorParent/CityDoctorValidation/pom.xml @@ -4,7 +4,7 @@ de.hft.stuttgart CityDoctorParent - 3.18.1 + 3.18.2 CityDoctorValidation CityDoctorValidation diff --git a/CityDoctorParent/Extensions/CityDoctorAutoPro/pom.xml b/CityDoctorParent/Extensions/CityDoctorAutoPro/pom.xml index 5c148498..a50ad983 100644 --- a/CityDoctorParent/Extensions/CityDoctorAutoPro/pom.xml +++ b/CityDoctorParent/Extensions/CityDoctorAutoPro/pom.xml @@ -6,7 +6,7 @@ de.hft.stuttgart CityDoctorParent - 3.18.1 + 3.18.2 ../../pom.xml CityDoctorAutoPro diff --git a/CityDoctorParent/Extensions/CityDoctorGUI/pom.xml b/CityDoctorParent/Extensions/CityDoctorGUI/pom.xml index 96ca6888..233c7834 100644 --- a/CityDoctorParent/Extensions/CityDoctorGUI/pom.xml +++ b/CityDoctorParent/Extensions/CityDoctorGUI/pom.xml @@ -6,7 +6,7 @@ de.hft.stuttgart CityDoctorParent - 3.18.1 + 3.18.2 ../../pom.xml CityDoctorGUI diff --git a/CityDoctorParent/Extensions/CityDoctorHealer/pom.xml b/CityDoctorParent/Extensions/CityDoctorHealer/pom.xml index 6076712b..ee969799 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealer/pom.xml +++ b/CityDoctorParent/Extensions/CityDoctorHealer/pom.xml @@ -6,7 +6,7 @@ de.hft.stuttgart CityDoctorParent - 3.18.1 + 3.18.2 ../../pom.xml CityDoctorHealer diff --git a/CityDoctorParent/Extensions/CityDoctorHealerGUI/pom.xml b/CityDoctorParent/Extensions/CityDoctorHealerGUI/pom.xml index c75ed10f..04a0807e 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealerGUI/pom.xml +++ b/CityDoctorParent/Extensions/CityDoctorHealerGUI/pom.xml @@ -7,7 +7,7 @@ de.hft.stuttgart CityDoctorParent - 3.18.1 + 3.18.2 ../../pom.xml CityDoctorHealerGUI diff --git a/CityDoctorParent/Extensions/CityDoctorHealerGenetic/pom.xml b/CityDoctorParent/Extensions/CityDoctorHealerGenetic/pom.xml index c98ae3bc..c74e9809 100644 --- a/CityDoctorParent/Extensions/CityDoctorHealerGenetic/pom.xml +++ b/CityDoctorParent/Extensions/CityDoctorHealerGenetic/pom.xml @@ -4,7 +4,7 @@ de.hft.stuttgart CityDoctorParent - 3.18.1 + 3.18.2 ../../pom.xml CityDoctorHealerGenetic diff --git a/CityDoctorParent/pom.xml b/CityDoctorParent/pom.xml index a1287ba5..2b4bd20c 100644 --- a/CityDoctorParent/pom.xml +++ b/CityDoctorParent/pom.xml @@ -5,7 +5,7 @@ 4.0.0 de.hft.stuttgart CityDoctorParent - 3.18.1 + 3.18.2 pom CityDoctorParent -- GitLab