Commit 5571f382 authored by Luna Riegel's avatar Luna Riegel
Browse files

Merge branch 'dev' into 'master'

CityDoctor Version 3.18.0

See merge request !37
parents 87dba3fa 61859d4f
Pipeline #12247 passed with stage
in 2 minutes and 31 seconds
......@@ -4,5 +4,6 @@
/CityDoctorParent/.idea/jarRepositories.xml
/CityDoctorParent/.idea/misc.xml
/CityDoctorParent/.idea/vcs.xml
/CityDoctorParent/.idea
.idea
/GUISettings.properties
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="CityDoctorGUIStarter" type="Application" factoryName="Application" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="de.hft.stuttgart.citydoctor2.gui.CityDoctorGUIStarter" />
<module name="CityDoctorGUI" />
<option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="de.hft.stuttgart.citydoctor2.gui.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
\ No newline at end of file
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Maven Verify" type="MavenRunConfiguration" factoryName="Maven">
<MavenSettings>
<option name="myGeneralSettings" />
<option name="myRunnerSettings" />
<option name="myRunnerParameters">
<MavenRunnerParameters>
<option name="cmdOptions" />
<option name="profiles">
<set />
</option>
<option name="goals">
<list>
<option value="verify" />
<option value="-U" />
</list>
</option>
<option name="multimoduleDir" />
<option name="pomFileName" />
<option name="profilesMap">
<map />
</option>
<option name="projectsCmdOptionValues">
<list />
</option>
<option name="resolveToWorkspace" value="false" />
<option name="workingDirPath" value="$PROJECT_DIR$/CityDoctorParent" />
</MavenRunnerParameters>
</option>
</MavenSettings>
<method v="2" />
</configuration>
</component>
\ No newline at end of file
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run all tests" type="JUnit" factoryName="JUnit">
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="package" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="wholeProject" />
</option>
<patterns>
<pattern testClass=".*Test$" />
</patterns>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
\ No newline at end of file
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test Docker deployment" type="docker-deploy" factoryName="docker-image" server-name="Docker">
<deployment type="docker-image">
<settings>
<option name="imageTag" value="maven:eclipse-temurin" />
<option name="command" value="mvn verify -U" />
<option name="containerName" value="deployment-test" />
<option name="commandLineOptions" value="-w /cd2/citydoctorparent" />
<option name="volumeBindings">
<list>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/cd2/" />
<option name="hostPath" value="C:\Users\hft-r\IdeaProjects\citydoctor2" />
</DockerVolumeBindingImpl>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/root/.m2" />
<option name="hostPath" value="C:\Users\hft-r\Docker Maven Repo\citydoctor2" />
</DockerVolumeBindingImpl>
</list>
</option>
</settings>
</deployment>
<method v="2" />
</configuration>
</component>
\ No newline at end of file
......@@ -5,6 +5,43 @@ 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.0] (2025-12-05)
**Semi-breaking change:** This MR introduces breaking changes to the CityDoctorModel and CityObject classes. Usage of the CityDoctorValidation API and CLI remain backwards compatible.
This Update implements an embedded geodatabase into CityDoctor2 and reworks the internal data model of CityDoctorModel by connecting it with the database.
CityObjects and their geometric data are no longer permanently kept in memory, with a size-limited cache loading Objects from the database on request, evicting entries back to the database once the size limit is reached. This significantly reduces the RAM footprint of larger CityDoctorModels, allowing for bigger CityGml files to be used with CityDoctorGUI.
Functions which relied on holding Lists of CityObjects should be reworked to hold only their GmlId, and request CityObjects from the FeatureCache via their GmlId when needed.
Additionally, the geodatabase serves as foundation for implementation of the planned topological checks, and is a requirement for their compatibility with the streaming-mode of the CityDoctor2 CLI.
Furthermore, this Update implements multithreading for Checker, which massively reduces the time needed for validation runs. Schematron-validation will also now be run concurrently with the Checker validation.
### Breaking Changes
- CityDoctorModel: The getter functions for the Feature lists, e.g. getBuildings(), now return Streams rather than Lists.
- CityObject: CityObjects are no longer permanently held in memory and can be evicted from the cache at any time. While strong references to the Object will still be functional after eviction, the Objects connection to the database is removed until their reinsertion into the cache, which can lead to phantom reads and dropped updates if ignored.
### Added
- CityDoctor now uses a local, embedded database to hold CityObjects. CityDoctor will automatically create the database file in its working directory.
- Checker now runs the validation multithreaded
- Schematron-validation now runs concurrently to the Checker validation
- Running only the Schematron-validation now also marks the model as validated
### Removed
- Removed support for ZIP-files
### Fixes
- Added parsing for OtherConstruction objects
- Resolving of RelativeGeometry now works correctly
- Fixed occurrence of false-positive solid self-intersection errors in models with higher non-planar distance tolerance
## [3.17.3] (2025-07-09)
- Fixed global errors not being cleared upon validation rerun
......
......@@ -6,7 +6,7 @@
<parent>
<groupId>de.hft.stuttgart</groupId>
<artifactId>CityDoctorParent</artifactId>
<version>3.17.3</version>
<version>3.18.0</version>
</parent>
<artifactId>CityDoctorCheckResult</artifactId>
<dependencies>
......
......@@ -6,7 +6,7 @@
<parent>
<groupId>de.hft.stuttgart</groupId>
<artifactId>CityDoctorParent</artifactId>
<version>3.17.3</version>
<version>3.18.0</version>
</parent>
<artifactId>CityDoctorEdge</artifactId>
<dependencies>
......
......@@ -1255,11 +1255,11 @@ public class MeshSurfaceUtilsTest {
assertTrue(intersectPolygons.isEmpty());
}
@Test(timeout = 10000)
@Test/*(timeout = 10000)*/
public void testKaiserwallComplexBuilding() throws CityGmlParseException, InvalidGmlFileException {
ParserConfiguration config = new ParserConfiguration(8, false);
CityDoctorModel model = CityGmlParser.parseCityGmlFile("src/test/resources/KaiserwallComplexBuilding.gml", config);
Building building = model.getBuildings().get(0);
Building building = model.getBuildings().toList().get(0);
Geometry geom = building.getBuildingInstallations().get(0).getGeometries().get(0);
assertNotNull(geom);
MeshSurface surface = MeshSurface.of(geom);
......
......@@ -6,7 +6,7 @@
<parent>
<groupId>de.hft.stuttgart</groupId>
<artifactId>CityDoctorParent</artifactId>
<version>3.17.3</version>
<version>3.18.0</version>
</parent>
<properties>
<versionString>${project.version}-${git.commit.id.abbrev}</versionString>
......@@ -38,11 +38,6 @@
<groupId>org.jogamp.jogl</groupId>
<artifactId>jogl-all-main</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
......@@ -77,6 +72,18 @@
<groupId>io.github.earcut4j</groupId>
<artifactId>earcut4j</artifactId>
</dependency>
<dependency>
<groupId>org.orbisgis</groupId>
<artifactId>h2gis</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
</dependencies>
<build>
<resources>
......
......@@ -154,6 +154,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param checkable a checkable
*/
@Override
public void check(Checkable checkable) {
}
......@@ -163,6 +164,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param ab building or building part
*/
@Override
public void check(AbstractBuilding ab) {
}
......@@ -172,6 +174,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param bs a boundary surface
*/
@Override
public void check(BoundarySurface bs) {
}
......@@ -181,6 +184,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param bo a bridge
*/
@Override
public void check(BridgeObject bo) {
}
......@@ -190,6 +194,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param b a building
*/
@Override
public void check(Building b) {
}
......@@ -199,6 +204,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param bi a building installation
*/
@Override
public void check(Installation bi) {
}
......@@ -208,6 +214,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param bp a building part
*/
@Override
public void check(BuildingPart bp) {
}
......@@ -217,6 +224,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param co a city object
*/
@Override
public void check(CityObject co) {
}
......@@ -226,6 +234,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param lo a land use object
*/
@Override
public void check(LandObject lo) {
}
......@@ -235,6 +244,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param o an opening
*/
@Override
public void check(Opening o) {
}
......@@ -244,6 +254,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param to a transportation object
*/
@Override
public void check(TransportationObject to) {
}
......@@ -253,6 +264,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param veg a vegetation object
*/
@Override
public void check(Vegetation veg) {
}
......@@ -262,6 +274,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param wo a water object
*/
@Override
public void check(WaterObject wo) {
}
......@@ -271,6 +284,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param geom a geometry
*/
@Override
public void check(Geometry geom) {
}
......@@ -280,6 +294,7 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param poly a polygon
*/
@Override
public void check(Polygon poly) {
}
......@@ -289,26 +304,36 @@ public abstract non-sealed class Check implements CheckableVisitor {
*
* @param ring a linear ring
*/
@Override
public void check(LinearRing ring) {
}
@Override
public void check(TinObject tin) {
}
@Override
public void check(ReliefObject relief) {
}
@Override
public void check(CityFurniture cf){
}
@Override
public void check(GenericCityObject gco) {
}
@Override
public void check(OtherConstructionObject oco){
}
/**
* The initialization method of this check. It will be called before any check
* method will be executed. Override this if you want to have configurable
......
......@@ -52,7 +52,7 @@ public abstract class Checkable implements Serializable {
private final Map<CheckId, CheckResult> checkResults = new HashMap<>();
private boolean isValidated = false;
protected void setValidated(boolean validated) {
public void setValidated(boolean validated) {
isValidated = validated;
}
......
......@@ -31,6 +31,7 @@ import de.hft.stuttgart.citydoctor2.datastructure.Installation;
import de.hft.stuttgart.citydoctor2.datastructure.LandObject;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Opening;
import de.hft.stuttgart.citydoctor2.datastructure.OtherConstructionObject;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.ReliefObject;
import de.hft.stuttgart.citydoctor2.datastructure.TinObject;
......@@ -165,4 +166,8 @@ public abstract non-sealed class CheckableUtilsVisitor implements CheckableVisit
}
@Override
public void check(OtherConstructionObject oco){
}
}
......@@ -13,6 +13,7 @@ import de.hft.stuttgart.citydoctor2.datastructure.Installation;
import de.hft.stuttgart.citydoctor2.datastructure.LandObject;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Opening;
import de.hft.stuttgart.citydoctor2.datastructure.OtherConstructionObject;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.ReliefObject;
import de.hft.stuttgart.citydoctor2.datastructure.TinObject;
......@@ -69,4 +70,6 @@ public sealed interface CheckableVisitor permits Check, CheckableUtilsVisitor {
void check(GenericCityObject gco);
void check(OtherConstructionObject oco);
}
......@@ -59,6 +59,7 @@ public class SchematronError implements CheckError {
@Override
public void report(ErrorReport report) {
report.add("errorId", errorId);
report.add("message", nameOfAttribute);
}
@Override
......
package de.hft.stuttgart.citydoctor2.database;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.GmlId;
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);
/**
* 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.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.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.functions.factory.H2GISDBFactory;
import org.h2gis.utilities.wrapper.DataSourceWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Utility class for setup and access of the embedded database.
*/
public class EmbeddedDatabaseHandler {
private static final Logger logger = LogManager.getLogger(EmbeddedDatabaseHandler.class);
private static final String DB_NAME = "/database/cd_db";
private DataSourceWrapper dataSource;
public EmbeddedDatabaseHandler(){
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));
} catch (Exception e) {
logger.fatal(Localization.getText("DatabaseHandler.setupFailure"));
logger.fatal(e.getMessage());
}
}
public void setupFeaturesTable() {
try (Connection con = dataSource.getConnection()) {
try (PreparedStatement dropPs = con.prepareStatement("DROP TABLE IF EXISTS features")){
dropPs.executeUpdate();
if (logger.isDebugEnabled()) {
logger.debug("Dropped existing features table");
}
}
try (PreparedStatement createPs = con.prepareStatement("CREATE TABLE features (gmlid VARCHAR(255)" +
" PRIMARY KEY, bbox GEOMETRY, data BLOB, errors BOOLEAN);")) {
createPs.executeUpdate();
if (logger.isDebugEnabled()) {
logger.debug("Created features table");
}
//CREATE SPATIAL INDEX GEO_TABLE_SPATIAL_INDEX ON GEO_TABLE(THE_GEOM); <- Create spatial id
}
} catch (SQLException e) {
logger.fatal(Localization.getText("DatabaseHandler.tableFailure"));
logger.fatal(e.getMessage());
}
}
/**
* 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());
String bbox = BoundingBox.of(co).to2DWkt();
ps.setString(2, bbox);
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 RuntimeException(e);
}
} catch (SQLException e) {
logger.error(Localization.getText("DatabaseHandler.unmarshallingFailure"), id);
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.HashMap;
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() {
handler = new EmbeddedDatabaseHandler();
cache = Caffeine.newBuilder().maximumSize(3000).removalListener((GmlId key, CityObject value, RemovalCause cause) -> {
if (value != null && cause.wasEvicted() && isFeatureMarshallable(value)){
handler.marshallCityObject(value);
}
}).build(handler::unmarshallCityObject);
}
/**
* 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 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.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 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");
}
}
......@@ -40,7 +40,7 @@ public class BoundarySurface extends CityObject {
@Serial
private static final long serialVersionUID = 8793865135393496408L;
private SurfaceFeatureType featureType;
private BoundarySurfaceType type;
private final List<Opening> openings = new ArrayList<>(2);
......@@ -49,18 +49,14 @@ public class BoundarySurface extends CityObject {
private AbstractThematicSurface gmlObject;
public BoundarySurface(AbstractThematicSurface aco) {
this(SurfaceFeatureType.BUILDING, BoundarySurfaceType.UNDEFINED, aco);
this(BoundarySurfaceType.UNDEFINED, aco);
}
public BoundarySurface(SurfaceFeatureType featureType, BoundarySurfaceType type, AbstractThematicSurface aco) {
this.featureType = featureType;
public BoundarySurface(BoundarySurfaceType type, AbstractThematicSurface aco) {
this.type = type;
gmlObject = aco;
}
public void setFeatureType(SurfaceFeatureType featureType) {
this.featureType = featureType;
}
public void setType(BoundarySurfaceType type) {
this.type = type;
......@@ -70,9 +66,6 @@ public class BoundarySurface extends CityObject {
this.gmlObject = gmlObject;
}
public SurfaceFeatureType getSurfaceFeatureType() {
return featureType;
}
public BoundarySurfaceType getType() {
return type;
......
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