Commit 297334ab authored by Matthias Betz's avatar Matthias Betz
Browse files

Add design spec for OpenGL 3D view



Replaces the JavaFX SubScene/MeshView rendering with an OpenGL viewport
(openglfx-jogl, GL 3.3 core), single global VBO, GPU color-picking, and
baked fake shading. Windows/Linux targets; JavaFX 3D path removed.

Co-Authored-By: default avatarClaude Opus 4.8 <noreply@anthropic.com>
parents 5c2847ef bd40b396
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);
}
@Override
public boolean isParsingError() {
return true;
}
}
package de.hft.stuttgart.citydoctor2.database;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.GmlId;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
public interface CityObjectCache {
/**
* Inserts a CityObject into this cache. Overwrites any existing CityObject with the same GmlID.
* @param cityObject
*/
void put(CityObject cityObject);
/**
* Gets a CityObject from this cache by their GmlID
* @param id GmlID of the requested Object
* @return the associated CityObject, or null if no Object has a matching GmlID
*/
CityObject get(GmlId id);
Collection<CityObject> getAll(List<GmlId> ids);
/**
* Replaces a CityObject with another one
* @param id GmlID of the CityObject that is to be replaced
* @param cityObject the replacement
*/
void replace(GmlId id, CityObject cityObject);
/**
* Returns a Stream of CityObjects from a list of GmlIDs
* @param ids the list of GmlIDs
* @return CityObject Stream of the IDs
*/
Stream<CityObject> getStream(List<GmlId> ids);
/**
* Returns the total count of entries.
* @return the count
*/
int getTotalFeatureCount();
/**
* Resets the Cache to its initial state
*/
void resetCache();
/**
* Returns a list of GmlIDs, whose associated CityObjects were found to contain at least one error.
* <p/>
* Requires Validation to be run first, as unvalidated CityObjects are of unknown state.
* @return list of GmlIDs which were found to contain an error
*/
List<GmlId> getErrorFeatureIds();
/**
* Sets this cache into parsing mode, returning an AutoClosable representing the scoped-state of parsing operation.
* @return an AutoClosable representing the scoped-state for parsing
*/
AutoCloseable startParsingMode();
/**
* Sets this cache into checking mode, returning an AutoClosable representing the scoped-state of checking operation.
* @return an AutoClosable representing the scoped-state for checking
*/
AutoCloseable startCheckingMode();
}
package de.hft.stuttgart.citydoctor2.database;
import com.github.benmanes.caffeine.cache.CacheLoader;
import de.hft.stuttgart.citydoctor2.exceptions.EmbeddedDatabaseHandlerException;
import org.jspecify.annotations.NonNull;
import java.util.Map;
import java.util.Set;
public class DatabaseCacheLoader<K, V> implements CacheLoader<K, V> {
private final Unmarshaller<K, V> unmarshaller;
private final BatchUnmarshaller<K, V> batchUnmarshaller;
public DatabaseCacheLoader(Unmarshaller<K,V> unmarshaller, BatchUnmarshaller<K, V> batchUnmarshaller){
this.unmarshaller = unmarshaller;
this.batchUnmarshaller = batchUnmarshaller;
}
@Override
public @NonNull V load(@NonNull K key) throws EmbeddedDatabaseHandlerException {
return unmarshaller.unmarshall(key);
}
@Override
@NonNull
public Map<K, V> loadAll(@NonNull Set<? extends K> keys) throws EmbeddedDatabaseHandlerException {
return batchUnmarshaller.unmarshall(keys);
}
public interface Unmarshaller<K, V> {
V unmarshall(K key) throws EmbeddedDatabaseHandlerException;
}
public interface BatchUnmarshaller<K, V> {
Map<K, V> unmarshall(Set<? extends K> keys) throws EmbeddedDatabaseHandlerException;
}
}
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;
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.Path;
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 = new Properties();
private DatabaseSettings() {}
static {
File propFile = new File("DBSettings.properties");
if (propFile.exists()) {
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.trace("No database settings found or specified, using default config");
return defaultConfig;
}
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);
boolean debugMode = Boolean.parseBoolean(debugModeString);
boolean tempMode = Boolean.parseBoolean(tempModeString);
int poolSize = defaultConfig.connectionPoolSize();
if (poolSizeString != null && !poolSizeString.isBlank()){
try {
int size = Integer.parseInt(poolSizeString);
if (size < 1) {
logger.warn(Localization.getText("DatabaseSettings.poolSizeLessThanOne"));
} else poolSize = size;
} catch (NumberFormatException e) {
logger.warn(Localization.getText("DatabaseSettings.poolSizeNaN"));
}
}
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
logger.warn(Localization.getText("DatabaseSettings.invalidPath"));
name = defaultConfig.databaseName();
directory = defaultConfig.databaseDirectory();
}
}
return new EmbeddedDatabaseConfiguration(name, directory, poolSize, inMemoryMode, attemptFallback, debugMode, tempMode);
}
}
package de.hft.stuttgart.citydoctor2.database;
import java.io.File;
import java.nio.file.Paths;
import java.util.StringJoiner;
/**
* 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.
* @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 tempFileMode) {
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";
/**
* Instantiates the default configuration for the embedded database.
*/
EmbeddedDatabaseConfiguration() {
this("cd_db", File.separator + "database"+ File.separator, Runtime.getRuntime().availableProcessors(),
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 wrapUrlWithH2JdbcDriver(String url){
return JDBC_DRIVER_PREFIX + url + getH2ParametersSuffix();
}
public String getInMemoryH2Url() {
return wrapUrlWithH2JdbcDriver(IN_MEMORY_PREFIX + databaseName);
}
public String getFallbackH2Url(){
return getInMemoryH2Url() + ";" + KEEP_ALIVE_PARAMETER;
}
public String getH2FileUrl(){
return wrapUrlWithH2JdbcDriver(Paths.get(databaseDirectory).resolve(databaseName).toString());
}
public String getH2ParametersSuffix(){
StringJoiner joiner = new StringJoiner(";",";","");
if (debugMode){
joiner.add(AUTO_SERVER_PARAMETER);
}
if (inMemoryMode){
joiner.add(KEEP_ALIVE_PARAMETER);
}
return joiner.toString();
}
}
package de.hft.stuttgart.citydoctor2.database;
import com.zaxxer.hikari.HikariDataSource;
import de.hft.stuttgart.citydoctor2.check.CheckableUtilsVisitor;
import de.hft.stuttgart.citydoctor2.datastructure.BoundingBox;
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;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.h2gis.utilities.wrapper.DataSourceWrapper;
import org.h2gis.functions.factory.H2GISFunctions;
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.Array;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Utility class for setup and access of the embedded database.
*/
public class EmbeddedDatabaseHandler {
private static final Logger logger = LogManager.getLogger(EmbeddedDatabaseHandler.class);
private final EmbeddedDatabaseConfiguration config;
private DataSourceWrapper dataSource;
private File tempFileDBLocation = null;
public EmbeddedDatabaseHandler(EmbeddedDatabaseConfiguration dbConfig){
config = dbConfig;
String jdbcUrl;
try {
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().getCanonicalPath();
jdbcUrl = createEmbeddedDataBase(config.wrapUrlWithH2JdbcDriver(tempFileUrl));
tempFileDBLocation = new File(tempFileUrl+".mv.db");
tempFileDBLocation.deleteOnExit();
} else {
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.getFallbackH2Url());
} 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));
}
// Embedded database can only ever be accessed by the local machine, suppress linter-warning about password leak
@SuppressWarnings("java:S6437")
private String createEmbeddedDataBase(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;
}
public void setupFeaturesTable() {
try (Connection con = dataSource.getConnection()) {
try (PreparedStatement dropPs = con.prepareStatement("DROP TABLE IF EXISTS features")){
dropPs.executeUpdate();
logger.trace("Dropped existing features table");
}
try (PreparedStatement dropIndexPs = con.prepareStatement("DROP INDEX IF EXISTS FEATURES_SPATIAL_INDEX")){
dropIndexPs.executeUpdate();
logger.trace("Dropped existing spatial index");
}
try (PreparedStatement createPs = con.prepareStatement("CREATE TABLE features (gmlid VARCHAR(255)" +
" PRIMARY KEY, bbox GEOMETRY, data BLOB, errors BOOLEAN);")) {
createPs.executeUpdate();
logger.trace("Created features table");
}
try (PreparedStatement createIndexPs = con.prepareStatement("CREATE SPATIAL INDEX FEATURES_SPATIAL_INDEX ON features(bbox);")){
createIndexPs.executeUpdate();
logger.trace("Created spatial index");
}
} catch (SQLException e) {
logger.fatal(Localization.getText("DatabaseHandler.tableFailure"));
logger.fatal(e.getMessage());
throw new EmbeddedDatabaseHandlerException("Could not setup features table in database", e);
}
}
/**
* Saves a CityObject to the embedded database, updating the entry if it already exists.
* @param co the CityObject
*/
public void marshallCityObject(CityObject co) {
try (Connection con = dataSource.getConnection()) {
try (PreparedStatement ps = con.prepareStatement("MERGE INTO features VALUES (?, ?, ?, ?)")) {
ps.setString(1, co.getGmlId().toString());
ps.setString(2, co.getBbox().to2DWkt());
ps.setBoolean(4, co.containsAnyError());
if (logger.isDebugEnabled()) {
logger.debug("Attempting to serialize CityObject: {} ", co);
}
try {
byte[] bytes = SerializationUtils.serialize(co);
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ps.setBinaryStream(3, bis, bytes.length);
ps.executeUpdate();
} catch (SerializationException e) {
logger.error(e.getMessage());
logger.error(co.getGmlId());
}
}
} catch (SQLException e) {
logger.error(Localization.getText("DatabaseHandler.marshallingFailure"), co.getGmlId());
logger.error(e.getMessage());
}
}
/**
* Tries to save all CityObjects in a Map to the embedded database as a batched statement, updating already existing entries.
* @param map a Map containing the CityObjects
* @return true if the batched statement was successfully created and commited, false otherwise
*/
public boolean tryMarshallCityObjectMap(Map<GmlId, CityObject> map) {
// Id String for error logging
String id = "";
try (Connection con = dataSource.getConnection()) {
try (PreparedStatement ps = con.prepareStatement("MERGE INTO features VALUES (?, ?, ?, ?)")) {
con.setAutoCommit(false);
for (Map.Entry<GmlId, CityObject> entry : map.entrySet()) {
CityObject co = entry.getValue();
id = entry.getKey().toString();
ps.setString(1, id);
String bbox = BoundingBox.of(co).to2DWkt();
ps.setString(2, bbox);
if (logger.isTraceEnabled()) {
logger.trace("Attempting to serialize CityObject: {} ", co);
}
ps.setBoolean(4, co.containsAnyError());
try {
byte[] bytes = SerializationUtils.serialize(co);
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ps.setBinaryStream(3, bis, bytes.length);
ps.addBatch();
} catch (SerializationException e) {
logger.error(Localization.getText("DatabaseHandler.serializationFailure"),co.getGmlId());
logger.error(e.getMessage());
}
}
ps.executeBatch();
con.commit();
} catch (SQLException e) {
con.rollback();
logger.error(Localization.getText("DatabaseHandler.batchCommitFailure"), id);
logger.error(e.getMessage());
return false;
} finally{
con.setAutoCommit(true);
}
} catch (SQLException e){
logger.error(Localization.getText("DatabaseHandler.connectionFailure"));
logger.error(e.getMessage());
return false;
}
return true;
}
/**
* Retrieves a CityObject from the embedded database.
* @param id the gmlId of the requested Feature
* @return the Feature, or null if no entry was found
*/
public CityObject unmarshallCityObject(GmlId id) {
try (Connection con = dataSource.getConnection()) {
try (PreparedStatement ps = con.prepareStatement(
"SELECT data FROM features WHERE gmlid = ?")) {
ps.setString(1, id.toString());
ResultSet rs = ps.executeQuery();
if (rs.next()) {
try (InputStream is = rs.getBinaryStream("data")) {
CityObject co = SerializationUtils.deserialize(is);
// Rebuild the adjacency maps
co.accept(new CheckableUtilsVisitor() {
@Override
public void check(Geometry geom) {
geom.updateVertices();
}
});
return co;
} catch (InvalidClassException | ClassCastException | SerializationException e){
String msg = String.format("Deserialization of CityObject \"%s\" failed",id);
logger.error(msg,e);
}
}
} catch (IOException e) {
throw new EmbeddedDatabaseHandlerException(e);
}
} catch (SQLException e) {
logger.error(Localization.getText("DatabaseHandler.unmarshallingFailure"), id);
logger.error(e.getMessage());
}
return null;
}
public Map<GmlId, CityObject> unmarshallAllIds(Set<? extends GmlId> ids){
try (Connection con = dataSource.getConnection()) {
try (PreparedStatement ps = con.prepareStatement(
"SELECT data FROM features WHERE ARRAY_CONTAINS(? ,gmlid)")) {
String[] idStrings = ids.stream().map(Objects::toString).toArray(String[]::new);
Array array = con.createArrayOf("VARCHAR(255)", idStrings);
ps.setArray(1, array);
ResultSet rs = ps.executeQuery();
boolean encounteredError = false;
Map<GmlId, CityObject> objects= new HashMap<>();
while (rs.next()) {
try (InputStream is = rs.getBinaryStream("data")) {
CityObject co = SerializationUtils.deserialize(is);
// Rebuild the adjacency maps
co.accept(new CheckableUtilsVisitor() {
@Override
public void check(Geometry geom) {
geom.updateVertices();
}
});
objects.put(co.getGmlId(),co);
} catch (InvalidClassException | ClassCastException | SerializationException e){
//Cant get the id of the failed Feature, as the deserialization didn't work.
encounteredError = true;
}
}
if (encounteredError){
logger.error("Deserialization of one or more Features failed");
}
return objects;
} catch (IOException e) {
throw new EmbeddedDatabaseHandlerException(e);
}
} catch (SQLException e) {
logger.error(Localization.getText("DatabaseHandler.unmarshallingFailure"), ids);
logger.error(e.getMessage());
}
return null;
}
/**
* Retrieves a List of GmlIds of CityObjects that contain any {@link de.hft.stuttgart.citydoctor2.check.CheckError CheckError}.
* @return a List of GmlIds of CityObjects containing any CheckError
*/
public List<GmlId> getIdsOfAllCityObjectsWithErrors(){
List<GmlId> ids = new ArrayList<>();
try (Connection con = dataSource.getConnection()){
try (PreparedStatement ps = con.prepareStatement("SELECT gmlid FROM features WHERE errors = TRUE")) {
ResultSet rs = ps.executeQuery();
while (rs.next()) {
ids.add(new GmlId(rs.getString("gmlid")));
}
}
} catch (SQLException e) {
logger.error(Localization.getText("DatabaseHandler.fetchErrorIdsFailure"));
logger.error(e.getMessage());
}
return ids;
}
/**
* Retrieves a List of GmlIds of CityObjects that are candidates for a buffer collision with a given CityObject(subject),
* meaning the candidate's bbox intersects the subject's buffered bbox.
* @param subject the subject CityObject
* @param bufferSize the buffer distance
* @return a List of GmlIds of buffer collision candidates
*/
public List<GmlId> getBufferCollisionCandidates(CityObject subject, double bufferSize) {
return getBufferCollisionCandidates(subject.getGmlId(), bufferSize);
}
/**
* Retrieves a List of GmlIds of CityObjects that are candidates for a buffer collision with a given CityObject(subject),
* meaning the candidate's bbox intersects the subject's buffered bbox.
* @param subjectId the subject CityObject's GmlId
* @param bufferSize the buffer distance
* @return a List of GmlIds of buffer collision candidates
*/
public List<GmlId> getBufferCollisionCandidates(GmlId subjectId, double bufferSize) {
List<GmlId> intersectingObjects = new ArrayList<>();
try (Connection con = dataSource.getConnection()) {
try (PreparedStatement ps = con.prepareStatement("SELECT gmlid FROM features AS tab1" +
" WHERE ST_INTERSECTS(ST_BUFFER(tab1.bbox,?), (SELECT bbox FROM features WHERE gmlid = ?)) " +
"AND tab1.gmlid <> ?")) {
ps.setDouble(1, bufferSize);
ps.setString(2, subjectId.toString());
ps.setString(3, subjectId.toString());
ResultSet rs = ps.executeQuery();
while (rs.next()) {
intersectingObjects.add(new GmlId(rs.getString("gmlid")));
}
}
} catch (SQLException e) {
logger.error(Localization.getText("DatabaseHandler.getBufferIntersectFailure"),subjectId);
logger.debug(e.getMessage());
}
return intersectingObjects;
}
/**
* Retrieves a List of GmlIds of CityObjects that are candidates for a geometry collision with a given CityObject(subject),
* meaning the bounding boxes of candidate and subject intersect.
* @param subject the subject CityObject
* @return a List of GmlIds of buffer collision candidates
*/
public List<GmlId> getGeometryCollisionCandidates(CityObject subject) {
return getGeometryCollisionCandidates(subject.getGmlId());
}
/**
* Retrieves a List of GmlIds of CityObjects that are candidates for a geometry collision with a given CityObject(subject),
* meaning the bounding boxes of candidate and subject intersect.
* @param subjectId the subject CityObject
* @return a List of GmlIds of buffer collision candidates
*/
public List<GmlId> getGeometryCollisionCandidates(GmlId subjectId) {
List<GmlId> intersectingObjects = new ArrayList<>();
try (Connection con = dataSource.getConnection()) {
try (PreparedStatement ps = con.prepareStatement("SELECT gmlid FROM features AS tab1" +
" WHERE ST_INTERSECTS(tab1.bbox, (SELECT bbox FROM features WHERE gmlid = ?)) AND tab1.gmlid <> ?"))
{
ps.setString(1, subjectId.toString());
ps.setString(2, subjectId.toString());
ResultSet rs = ps.executeQuery();
while (rs.next()) {
intersectingObjects.add(new GmlId(rs.getString("gmlid")));
}
}
} catch (SQLException e) {
logger.error(Localization.getText("DatabaseHandler.getBBoxIntersectFailure"),subjectId);
logger.debug(e.getMessage());
}
return intersectingObjects;
}
/**
* Returns the number of top-level CityObjects entries that are currently in the embedded database.
* @return the count of top-level CityObjects in the database
*/
public int getFeatureCount() {
int count = -1;
try (Connection con = dataSource.getConnection()) {
try (PreparedStatement ps = con.prepareStatement("SELECT COUNT(gmlid) AS nr FROM features")) {
ResultSet rs = ps.executeQuery();
if (rs.next()) {
count = rs.getInt("nr");
}
}
} catch (SQLException e) {
logger.error(Localization.getText("DatabaseHandler.getFeatureCountFailure"));
logger.debug(e.getMessage());
}
return count;
}
}
package de.hft.stuttgart.citydoctor2.database;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.RemovalCause;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.GmlId;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
/**
* A managed cache for top-level CityObjects (Features). This cache is connected to the embedded database and will
* automatically load missing CityObjects from it. This cache is size limited and will evict entries using the
* Window Tiny LFU policy.
* <p>
* While this class is not a singleton, it is advised to keep an Instance of this class for the whole lifecycle and
* reset and reuse it instead of creating new Objects, as the initialization is quite heavy and resets the embedded
* database.
* <p/>
* <p>
* Avoid holding long-time strong references to CityObjects returned by the cache, as eviction will sever the
* connection between reference and cache, which will lead to conflicting Object states.
* <p/>
*/
public class FeatureCache implements CityObjectCache{
private static final Logger logger = LogManager.getLogger(FeatureCache.class);
private final LoadingCache<GmlId, CityObject> cache;
private final EmbeddedDatabaseHandler handler;
private final AtomicInteger putsCounter = new AtomicInteger(0);
private CacheState state = CacheState.IDLE;
private final ConcurrentHashMap<Thread, GmlId> evictionLocks = new ConcurrentHashMap<>();
public FeatureCache() {
this(DatabaseSettings.getConfig());
}
public FeatureCache(EmbeddedDatabaseConfiguration dbConfig){
handler = new EmbeddedDatabaseHandler(dbConfig);
DatabaseCacheLoader.Unmarshaller<GmlId, CityObject> single = handler::unmarshallCityObject;
DatabaseCacheLoader.BatchUnmarshaller<GmlId, CityObject> batch = handler::unmarshallAllIds;
cache = Caffeine.newBuilder().maximumSize(3000).removalListener((GmlId key, CityObject value, RemovalCause cause) -> {
if (cause.wasEvicted() && isFeatureMarshallable(value)){
handler.marshallCityObject(value);
}
}).build(new DatabaseCacheLoader<>(single, batch));
}
/**
* Checks if the CityObject is currently being checked by a Checker-thread to prevent concurrent modifications by the
* Marshaller on eviction.
* @param co the CityObject
* @return true if the CityObject can be marshalled, false otherwise
*/
private boolean isFeatureMarshallable(CityObject co) {
return switch (state){
case IDLE, PARSING -> true;
case CHECKING -> ! (evictionLocks.containsValue(co.getGmlId()));
};
}
@Override
public void put(CityObject cityObject) {
cache.put(cityObject.getGmlId(), cityObject);
switch (state){
case IDLE:
break;
case PARSING:
int count = putsCounter.incrementAndGet();
if (count >= 2500) {
batchMarshall();
}
break;
case CHECKING:
evictionLocks.remove(Thread.currentThread());
break;
}
}
private void batchMarshall(){
logger.trace("Setting up batch Marshaller");
Map<GmlId, CityObject> cacheContent= new HashMap<>(cache.asMap());
cache.invalidateAll();
putsCounter.set(0);
Thread batcher = new Thread(() -> {
logger.trace("Entered batch Marshaller");
boolean success = handler.tryMarshallCityObjectMap(cacheContent);
if (!success) {
logger.trace("Batch marshalling in Thread failed, falling back to one-by-one marshalling");
for (Map.Entry<GmlId, CityObject> entry : cacheContent.entrySet()) {
handler.marshallCityObject(entry.getValue());
}
}
logger.trace("Batch marshalling done");
});
batcher.start();
logger.trace("Batch Marshaller started");
}
@Override
public CityObject get(GmlId id) {
if (state == CacheState.CHECKING){
evictionLocks.put(Thread.currentThread(), id);
}
return cache.get(id);
}
@Override
public Collection<CityObject> getAll(List<GmlId> ids){
Map<GmlId, CityObject> map = cache.getAll(new HashSet<>(ids));
return map.values();
}
@Override
public void replace(GmlId id, CityObject cityObject) {
cache.invalidate(id);
cache.put(cityObject.getGmlId(), cityObject);
}
@Override
public Stream<CityObject> getStream(List<GmlId> gmlIds) {
return gmlIds.stream().map(this::get);
}
@Override
public int getTotalFeatureCount() {
return handler.getFeatureCount();
}
@Override
public void resetCache(){
logger.trace("Resetting cache");
cache.invalidateAll();
putsCounter.set(0);
handler.setupFeaturesTable();
}
@Override
public List<GmlId> getErrorFeatureIds() {
return handler.getIdsOfAllCityObjectsWithErrors();
}
/**
* Synchronizes CityObject states by updating Database with cache content.
*/
public void synchronize(){
HashMap<GmlId, CityObject> cacheContent= new HashMap<>(cache.asMap());
boolean success = handler.tryMarshallCityObjectMap(cacheContent);
if (!success) {
logger.trace("Batch marshalling failed, falling back to one-by-one marshalling");
for (Map.Entry<GmlId, CityObject> entry : cacheContent.entrySet()) {
handler.marshallCityObject(entry.getValue());
}
}
}
/**
* Signals the cache that parsing is beginning.
*/
private boolean beginParsing(){
return state.changeState(this, CacheState.PARSING);
}
/**
* Signals the cache that parsing has finished.
*/
private void finishParsing() {
if (state == CacheState.PARSING && state.changeState(this, CacheState.IDLE)){
return;
}
throw new IllegalStateException("Cache failed to exit parsing mode");
}
/**
* Signals the cache that checking is beginning.
*/
private boolean beginChecking(){
return state.changeState(this, CacheState.CHECKING);
}
/**
* Signals the cache that checking has finished.
*/
private void finishChecking() {
if (state == CacheState.CHECKING && state.changeState(this, CacheState.IDLE)){
return;
}
throw new IllegalStateException("Cache failed to exit checking mode");
}
@Override
public AutoCloseable startParsingMode(){
if (this.beginParsing()){
return this::finishParsing;
} else throw new IllegalStateException("Cache is not in IDLE state, cannot start parsing mode");
}
@Override
public AutoCloseable startCheckingMode(){
if (this.beginChecking()){
return this::finishChecking;
} else throw new IllegalStateException("Cache is not in IDLE state, cannot start checking mode");
}
protected void setState(CacheState state) {
this.state = state;
}
public CacheState getState() {
return state;
}
public enum CacheState {
IDLE {
@Override
protected boolean changeState(FeatureCache cache, CacheState nextState) {
if (cache.getState() != this){
logger.debug("FeatureCache invoked changeState() of IDLE state while cache state is {}", cache.getState());
return false;
}
this.onExit(cache);
cache.setState(nextState);
nextState.onEntry(cache);
return true;
}
@Override
public void onEntry(FeatureCache cache) {
logger.trace("FeatureCache: Entering IDLE state");
// Idle state does nothing on entry
}
@Override
protected void onExit(FeatureCache cache) {
logger.trace("FeatureCache: Leaving IDLE state");
// Idle state does nothing on exit
}
}, PARSING {
@Override
protected boolean changeState(FeatureCache cache, CacheState nextState) {
if (cache.getState() != this){
logger.debug("FeatureCache invoked changeState() of PARSING state while cache state is {}", cache.getState());
return false;
}
boolean success = false;
switch (nextState){
case IDLE:
this.onExit(cache);
cache.setState(CacheState.IDLE);
success = true;
break;
case PARSING:
logger.debug("Tried to set cache to PARSING state while already parsing");
break;
case CHECKING:
logger.debug("Tried to set cache to CHECKING state while parsing");
break;
}
return success;
}
@Override
public void onEntry(FeatureCache cache) {
logger.trace("FeatureCache: Entering PARSING state");
cache.resetCache();
}
@Override
protected void onExit(FeatureCache cache) {
logger.trace("FeatureCache: Leaving PARSING state");
cache.synchronize();
}
}, CHECKING {
@Override
protected boolean changeState(FeatureCache cache, CacheState nextState) {
if (cache.getState() != this){
logger.debug("FeatureCache invoked changeState() of CHECKING state while cache state is {}", cache.getState());
return false;
}
boolean success = false;
switch (nextState){
case IDLE:
this.onExit(cache);
cache.setState(CacheState.IDLE);
success = true;
break;
case PARSING:
logger.debug("Tried to set cache to PARSING state while checking");
break;
case CHECKING:
logger.debug("Tried to set cache to CHECKING state while already checking");
break;
}
return success;
}
@Override
public void onEntry(FeatureCache cache) {
logger.trace("FeatureCache: Entering CHECKING state");
}
@Override
protected void onExit(FeatureCache cache) {
logger.trace("FeatureCache: Leaving CHECKING state");
cache.synchronize();
}
};
/**
* Signals the statemachine to change the state of the FeatureCache. Returns true if the state successfully
* changed, false otherwise with the FeatureCache remaining in its current state.
* @param cache the FeatureCache instance
* @param nextState the requested next state
* @return true if the cache successfully changed state, false otherwise
*/
protected abstract boolean changeState(FeatureCache cache, CacheState nextState);
protected abstract void onEntry(FeatureCache cache);
protected abstract void onExit(FeatureCache cache);
}
}
package de.hft.stuttgart.citydoctor2.database;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.GmlId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
/**
* An adapter implementation for CityObjectCache, wrapping the Interface around a simple HashMap.
* <p>
* This class is intended to allow parsing or checking {@link CityDoctorModel CityDoctorModels} in specific cases where the model
* must not use the {@link FeatureCache}, like resolving of {@link de.hft.stuttgart.citydoctor2.datastructure.LibraryObject LibraryObject}
* files or mocking of the Interface in unit tests.
*
*/
public class UnconnectedCache implements CityObjectCache{
private final Map<GmlId, CityObject> cache;
private boolean idle = true;
public UnconnectedCache() {
cache = new HashMap<>();
}
@Override
public void put(CityObject cityObject) {
cache.put(cityObject.getGmlId(), cityObject);
}
@Override
public CityObject get(GmlId id) {
return cache.get(id);
}
@Override
public Collection<CityObject> getAll(List<GmlId> ids) {
return ids.stream().map(cache::get).toList();
}
@Override
public void replace(GmlId id, CityObject cityObject) {
cache.put(id, cityObject);
}
@Override
public Stream<CityObject> getStream(List<GmlId> ids) {
return cache.values().stream().filter(c -> ids.contains(c.getGmlId()));
}
@Override
public int getTotalFeatureCount() {
return cache.size();
}
public void resetCache(){
cache.clear();
}
@Override
public List<GmlId> getErrorFeatureIds() {
List<GmlId> errorFeatureIds = new ArrayList<>();
cache.forEach((gmlId, cityObject) ->{
if (cityObject.containsAnyError()){
errorFeatureIds.add(cityObject.getGmlId());
}
});
return errorFeatureIds;
}
@Override
public AutoCloseable startParsingMode() {
if (idle){
idle = false;
return ()-> idle=true;
} else throw new IllegalStateException("Cache is not idle, could not start parsing mode");
}
@Override
public AutoCloseable startCheckingMode() {
if (idle){
idle = false;
return ()-> idle=true;
} else throw new IllegalStateException("Cache is not idle, could not start checking mode");
}
}
package de.hft.stuttgart.citydoctor2.datastructure; package de.hft.stuttgart.citydoctor2.datastructure;
import de.hft.stuttgart.citydoctor2.check.Check; import de.hft.stuttgart.citydoctor2.check.CheckableVisitor;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckId;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.utils.CityGmlUtils; import de.hft.stuttgart.citydoctor2.utils.CityGmlUtils;
import de.hft.stuttgart.citydoctor2.utils.CopyHandler; import javafx.scene.paint.Color;
import de.hft.stuttgart.citydoctor2.utils.Copyable;
import org.citygml4j.core.util.geometry.GeometryFactory; import org.citygml4j.core.util.geometry.GeometryFactory;
import org.xmlobjects.gml.model.geometry.aggregates.MultiSurface; import org.xmlobjects.gml.model.geometry.aggregates.MultiSurface;
import org.xmlobjects.gml.model.geometry.aggregates.MultiSurfaceProperty; import org.xmlobjects.gml.model.geometry.aggregates.MultiSurfaceProperty;
...@@ -32,7 +29,7 @@ public abstract class AbstractFurniture extends CityObject { ...@@ -32,7 +29,7 @@ public abstract class AbstractFurniture extends CityObject {
private org.citygml4j.core.model.construction.AbstractFurniture af; private org.citygml4j.core.model.construction.AbstractFurniture af;
@Override @Override
public void accept(Check c) { public void accept(CheckableVisitor c) {
super.accept(c); super.accept(c);
if (c.canExecute(this)) { if (c.canExecute(this)) {
c.check(this); c.check(this);
...@@ -43,53 +40,13 @@ public abstract class AbstractFurniture extends CityObject { ...@@ -43,53 +40,13 @@ public abstract class AbstractFurniture extends CityObject {
} }
@Override @Override
public void collectContainedErrors(List<CheckError> errors) { public org.citygml4j.core.model.construction.AbstractFurniture getGmlObject() {
super.collectContainedErrors(errors); return af;
for (BoundarySurface boundarySurface : boundarySurfaceList) {
boundarySurface.collectContainedErrors(errors);
}
}
@Override
public void clearAllContainedCheckResults() {
super.clearAllContainedCheckResults();
for (BoundarySurface boundarySurface : boundarySurfaceList) {
boundarySurface.clearAllContainedCheckResults();
}
}
@Override
public boolean containsError(CheckId checkIdentifier) {
boolean hasError = super.containsError(checkIdentifier);
if (hasError) {
return true;
}
for (BoundarySurface boundarySurface : boundarySurfaceList) {
if (boundarySurface.containsError(checkIdentifier)) {
return true;
}
}
return false;
}
@Override
public boolean containsAnyError() {
boolean hasError = super.containsAnyError();
if (hasError) {
return true;
}
for (BoundarySurface boundarySurface : boundarySurfaceList) {
if (boundarySurface.containsAnyError()) {
return true;
}
}
return false;
} }
@Override @Override
public org.citygml4j.core.model.construction.AbstractFurniture getGmlObject() { public CityObject getTopLevelCityObject(){
return af; return parent.getTopLevelCityObject();
} }
public void addBoundarySurface(BoundarySurface boundarySurface) { public void addBoundarySurface(BoundarySurface boundarySurface) {
...@@ -97,12 +54,12 @@ public abstract class AbstractFurniture extends CityObject { ...@@ -97,12 +54,12 @@ public abstract class AbstractFurniture extends CityObject {
boundarySurface.setParent(this); boundarySurface.setParent(this);
} }
public List<BoundarySurface> getBoundarySurfaceList() { public List<BoundarySurface> getBoundarySurfaces() {
return boundarySurfaceList; return boundarySurfaceList;
} }
@Override @Override
public void reCreateGeometries(GeometryFactory factory, ParserConfiguration config) { public void rebuildGeometries(GeometryFactory factory, ParserConfiguration config) {
for (Geometry geom : getGeometries()) { for (Geometry geom : getGeometries()) {
if (geom instanceof ImplicitGeometryHolder) { if (geom instanceof ImplicitGeometryHolder) {
continue; continue;
...@@ -129,6 +86,11 @@ public abstract class AbstractFurniture extends CityObject { ...@@ -129,6 +86,11 @@ public abstract class AbstractFurniture extends CityObject {
return parent; return parent;
} }
@Override
public Color getRenderColor() {
return parent.getRenderColor();
}
@Override @Override
public void unsetGmlGeometries() { public void unsetGmlGeometries() {
af.setLod0MultiSurface(null); af.setLod0MultiSurface(null);
...@@ -172,32 +134,9 @@ public abstract class AbstractFurniture extends CityObject { ...@@ -172,32 +134,9 @@ public abstract class AbstractFurniture extends CityObject {
} }
@Override
public void prepareForChecking() {
super.prepareForChecking();
for (BoundarySurface boundarySurface : boundarySurfaceList) {
boundarySurface.prepareForChecking();
}
}
@Override
public void clearMetaInformation() {
super.clearMetaInformation();
for (BoundarySurface boundarySurface : boundarySurfaceList) {
boundarySurface.clearMetaInformation();
}
}
@Override @Override
public FeatureType getFeatureType() { public FeatureType getFeatureType() {
return FeatureType.FURNITURE; return FeatureType.FURNITURE;
} }
@Override
public void fillValues(Copyable original, CopyHandler handler) {
super.fillValues(original, handler);
AbstractFurniture originalAf = (AbstractFurniture) original;
af = originalAf.af;
}
} }
...@@ -20,16 +20,23 @@ package de.hft.stuttgart.citydoctor2.datastructure; ...@@ -20,16 +20,23 @@ package de.hft.stuttgart.citydoctor2.datastructure;
import de.hft.stuttgart.citydoctor2.math.Vector3d; import de.hft.stuttgart.citydoctor2.math.Vector3d;
import de.hft.stuttgart.citydoctor2.utils.BoundingBoxCalculator; import de.hft.stuttgart.citydoctor2.utils.BoundingBoxCalculator;
import de.hft.stuttgart.citydoctor2.utils.visitors.MinMaxExtentVisitor;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale;
/** /**
* An axis aligned bounding box represented by its two corners * An axis aligned bounding box represented by its two corners
* *
* @author Matthias Betz * @author Matthias Betz
*/ */
public class BoundingBox { public class BoundingBox implements Serializable {
@Serial
private static final long serialVersionUID = 1998769586245522486L;
private final Vector3d[] bbox; private final Vector3d[] bbox;
...@@ -47,6 +54,12 @@ public class BoundingBox { ...@@ -47,6 +54,12 @@ public class BoundingBox {
return BoundingBoxCalculator.calculateBoundingBoxFromPoints(points); return BoundingBoxCalculator.calculateBoundingBoxFromPoints(points);
} }
public static BoundingBox of(CityObject co) {
MinMaxExtentVisitor mima = new MinMaxExtentVisitor();
co.accept(mima);
return BoundingBox.of(mima.getMinMaxExtent());
}
/** /**
* Creates an axis aligned bounding box of the whole model. * Creates an axis aligned bounding box of the whole model.
* *
...@@ -161,4 +174,45 @@ public class BoundingBox { ...@@ -161,4 +174,45 @@ public class BoundingBox {
public double getDiagonalLength() { public double getDiagonalLength() {
return bbox[1].getDistance(bbox[0]); return bbox[1].getDistance(bbox[0]);
} }
/**
* Returns whether this BoundinBox is undefined, meaning that one or all corners are infinite.
* @return True if the BoundingBox is undefined, false otherwise
*/
public boolean isUndefined(){
double x1 = bbox[0].getX();
double y1 = bbox[0].getY();
double x2 = bbox[1].getX();
double y2 = bbox[1].getY();
return (Double.isInfinite(x1) || Double.isInfinite(y1) || Double.isInfinite(x2) || Double.isInfinite(y2));
}
/**
* Returns an axis-aligned 2D polygon projection of this bbox as a WKT String.
* <p/>
* An undefined bbox will return an empty polygon.
* @return The 2D polygon WKT for this bbox
*/
public String to2DWkt() {
if (this.isUndefined()){
return "POLYGON EMPTY";
}
double x1 = bbox[0].getX();
double y1 = bbox[0].getY();
double x2 = bbox[1].getX();
double y2 = bbox[1].getY();
double minX = Math.min(x1, x2);
double minY = Math.min(y1, y2);
double maxX = Math.max(x1, x2);
double maxY = Math.max(y1, y2);
return String.format(Locale.ROOT,"POLYGON((%.3f %.3f, %.3f %.3f, %.3f %.3f, %.3f %.3f, %.3f %.3f))",
minX, minY,
maxX, minY,
maxX, maxY,
minX, maxY,
minX, minY
);
}
} }
package de.hft.stuttgart.citydoctor2.datastructure; package de.hft.stuttgart.citydoctor2.datastructure;
import de.hft.stuttgart.citydoctor2.utils.CopyHandler; import javafx.scene.paint.Color;
import de.hft.stuttgart.citydoctor2.utils.Copyable;
import java.io.Serial; import java.io.Serial;
...@@ -31,20 +30,13 @@ public class BridgeRoom extends AbstractRoom { ...@@ -31,20 +30,13 @@ public class BridgeRoom extends AbstractRoom {
} }
@Override @Override
public void fillValues(Copyable original, CopyHandler handler) { public CityObject getTopLevelCityObject() {
super.fillValues(original, handler); return parent.getTopLevelCityObject();
BridgeRoom oRoom = (BridgeRoom) original;
parent = handler.getCopyInstance(oRoom.getParent());
} }
@Override @Override
public void collectInstances(CopyHandler handler) { public Color getRenderColor() {
super.collectInstances(handler); return parent.getRenderColor();
handler.addInstance(parent);
} }
@Override
public Copyable createCopyInstance() {
return new BridgeRoom();
}
} }
package de.hft.stuttgart.citydoctor2.datastructure; package de.hft.stuttgart.citydoctor2.datastructure;
import de.hft.stuttgart.citydoctor2.utils.Copyable;
import org.citygml4j.core.model.bridge.BridgeFurniture; import org.citygml4j.core.model.bridge.BridgeFurniture;
import java.io.Serial; import java.io.Serial;
...@@ -13,9 +12,4 @@ public class BridgeRoomFurniture extends AbstractFurniture { ...@@ -13,9 +12,4 @@ public class BridgeRoomFurniture extends AbstractFurniture {
public void setGmlObject(BridgeFurniture gmlObject) { public void setGmlObject(BridgeFurniture gmlObject) {
super.setGmlObject(gmlObject); super.setGmlObject(gmlObject);
} }
@Override
public Copyable createCopyInstance() {
return new BridgeRoomFurniture();
}
} }
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment