Commit 28c42d2d authored by Luna Riegel's avatar Luna Riegel
Browse files

!Feature: Implement GeoDB and FeatureCache

BREAKING CHANGE: Implements the saving and loading of CityObjects to a
GeoDB with in-memory caching. This allows reading of bigger CityGml
files and serves as basis for the implementation of Topological Checks.

CityDoctorModel will now return Streams instead of Lists for the
CityObject-getters (like getBuildings()). This requires a rework of all
methods calling them.
parent 286d394c
Pipeline #11965 failed with stage
in 1 minute and 3 seconds
...@@ -52,7 +52,7 @@ public abstract class Checkable implements Serializable { ...@@ -52,7 +52,7 @@ public abstract class Checkable implements Serializable {
private final Map<CheckId, CheckResult> checkResults = new HashMap<>(); private final Map<CheckId, CheckResult> checkResults = new HashMap<>();
private boolean isValidated = false; private boolean isValidated = false;
protected void setValidated(boolean validated) { public void setValidated(boolean validated) {
isValidated = validated; isValidated = validated;
} }
......
package de.hft.stuttgart.citydoctor2.database;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.GmlId;
import de.hft.stuttgart.citydoctor2.utils.Pair;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.lang.ref.SoftReference;
import java.util.concurrent.ConcurrentHashMap;
/**
* Cache for CityObjects
*/
public class CityObjectCache {
private final LoadingCache<GmlId, CityObject> cache;
public CityObjectCache() {
cache = Caffeine.newBuilder().maximumSize(1000).build(DatabaseHandler::unmarshallCityObject);
}
public void put(CityObject cityObject) {
DatabaseHandler.marshallCityObject(cityObject);
cache.put(cityObject.getGmlId(), cityObject);
}
public CityObject get(GmlId id) {
return cache.get(id);
}
public List<CityObject> get(List<GmlId> ids) {
List<CityObject> features = new ArrayList<>();
for (GmlId id : ids) {
features.add(cache.get(id));
}
return features;
}
}
package de.hft.stuttgart.citydoctor2.database;
import com.github.benmanes.caffeine.cache.Cache;
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 de.hft.stuttgart.citydoctor2.utils.visitors.CityObjectCollector;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
/**
* Cache for CityObjects
*/
public class FeatureCache{
private final LoadingCache<GmlId, CityObject> cache;
private final Cache<GmlId, CityObject> subCache;
public FeatureCache() {
cache = Caffeine.newBuilder().maximumSize(3000).removalListener((GmlId key, CityObject value, RemovalCause cause) -> {
if (value != null && cause.wasEvicted()) {
DatabaseHandler.marshallCityObject(value);
}
}).build(DatabaseHandler::unmarshallCityObject);
subCache = Caffeine.newBuilder().maximumSize(250).build();
DatabaseHandler.setupDatabase();
}
public void put(CityObject cityObject) {
DatabaseHandler.marshallCityObject(cityObject);
cache.put(cityObject.getGmlId(), cityObject);
}
public CityObject get(GmlId id) {
return cache.get(id);
}
public void replace(GmlId id, CityObject cityObject) {
cache.invalidate(id);
put(cityObject);
}
public Stream<CityObject> getStream(List<GmlId> gmlIds) {
return gmlIds.stream().map(this::get);
}
/**
* Gets a non-top-level CityObject directly. Only use this in situations where directly accessing the object is required
* and access via the top-level CityObject is impossible or highly inefficient, e.g. rendering the associated object
* of a CityObjectNode.
* @param coa the adapter object of the subpart
* @return the associated CityObject
*/
public CityObject getSubPart(SubPartAdapter coa) {
CityObject co = subCache.getIfPresent(coa.getFeatureId());
if (co == null) {
return buildAndGetSubPart(coa);
}
else return co;
}
/**
*
* @param coa
* @return
*/
private CityObject buildAndGetSubPart(SubPartAdapter coa) {
CityObject parent = cache.get(coa.getTopLevelId());
if (parent == null) {
throw new IllegalStateException("CityObjectAdapter has non-existent parent - GmlId: " + coa.getTopLevelId());
}
CityObjectCollector collector = new CityObjectCollector();
parent.accept(collector);
collector.getCityObjects().forEach(cityObject -> subCache.put(cityObject.getGmlId(), cityObject));
CityObject co = subCache.getIfPresent(coa.getFeatureId());
if (co == null) {
throw new IllegalStateException("CityObjectAdapter has non-existent feature - GmlId: " + coa.getFeatureId() +
", parent GmlId: " + coa.getTopLevelId());
}
return co;
}
public List<CityObject> get(List<GmlId> ids) {
List<CityObject> features = new ArrayList<>();
for (GmlId id : ids) {
features.add(cache.get(id));
}
return features;
}
}
package de.hft.stuttgart.citydoctor2.database;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.GmlId;
/**
* Adapter class for Lists. All function calls on objects of this
* class get passed to their respective CityObjects.
*/
public class SubPartAdapter {
// TODO: Remove CO extension and change model to stream and map objects to their respective classes
private final GmlId featureId;
private final GmlId topLevelId;
private FeatureCache cache;
public SubPartAdapter(CityObject co, FeatureCache cache) {
this.featureId = co.getGmlId();
this.topLevelId = co.getTopLevelCityObject().getGmlId();
this.cache = cache;
}
public boolean isValidated(){
return cache.get(featureId).isValidated();
}
public GmlId getFeatureId() {
return featureId;
}
public GmlId getTopLevelId() {
return topLevelId;
}
}
package de.hft.stuttgart.citydoctor2.database;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.GmlId;
public class TopLevelCityObjectAdapter {
private final GmlId featureId;
private FeatureCache cache;
public TopLevelCityObjectAdapter(CityObject co, FeatureCache cache) {
this.featureId = co.getGmlId();
this.cache = cache;
}
public boolean isValidated(){
return cache.get(featureId).isValidated();
}
}
...@@ -21,6 +21,7 @@ package de.hft.stuttgart.citydoctor2.datastructure; ...@@ -21,6 +21,7 @@ package de.hft.stuttgart.citydoctor2.datastructure;
import de.hft.stuttgart.citydoctor2.check.CheckableUtilsVisitor; import de.hft.stuttgart.citydoctor2.check.CheckableUtilsVisitor;
import de.hft.stuttgart.citydoctor2.check.CheckError; import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckableVisitor; import de.hft.stuttgart.citydoctor2.check.CheckableVisitor;
import de.hft.stuttgart.citydoctor2.database.FeatureCache;
import de.hft.stuttgart.citydoctor2.exceptions.CityDoctorWriteException; import de.hft.stuttgart.citydoctor2.exceptions.CityDoctorWriteException;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.writer.CityGMLWriterUtils; import de.hft.stuttgart.citydoctor2.writer.CityGMLWriterUtils;
...@@ -46,15 +47,15 @@ import java.util.stream.Stream; ...@@ -46,15 +47,15 @@ import java.util.stream.Stream;
public class CityDoctorModel { public class CityDoctorModel {
private static final String COULD_NOT_FIND_FEATURE = "Could not find feature: "; private static final String COULD_NOT_FIND_FEATURE = "Could not find feature: ";
private final List<Building> buildings; private final List<GmlId> buildings;
private final List<Vegetation> vegetation; private final List<GmlId> vegetation;
private final List<BridgeObject> bridges; private final List<GmlId> bridges;
private final List<CityFurniture> cityfurniture; private final List<GmlId> cityfurniture;
private final List<GenericCityObject> genericObjects; private final List<GmlId> genericObjects;
private final List<CityObject> land; private final List<GmlId> land;
private final List<TransportationObject> roads; private final List<GmlId> roads;
private final List<Tunnel> tunnels; private final List<GmlId> tunnels;
private final List<WaterObject> water; private final List<GmlId> water;
private CityModel cModel; private CityModel cModel;
private final ParserConfiguration config; private final ParserConfiguration config;
private final String fileName; private final String fileName;
...@@ -63,6 +64,7 @@ public class CityDoctorModel { ...@@ -63,6 +64,7 @@ public class CityDoctorModel {
private boolean isValidated = false; private boolean isValidated = false;
private ValidationPlan plan; private ValidationPlan plan;
private CityGMLVersion cityGMLVersion; private CityGMLVersion cityGMLVersion;
private final FeatureCache cache;
public CityDoctorModel(ParserConfiguration config, File file) { public CityDoctorModel(ParserConfiguration config, File file) {
if (config == null) { if (config == null) {
...@@ -81,6 +83,7 @@ public class CityDoctorModel { ...@@ -81,6 +83,7 @@ public class CityDoctorModel {
cityfurniture = new ArrayList<>(); cityfurniture = new ArrayList<>();
genericObjects = new ArrayList<>(); genericObjects = new ArrayList<>();
globalErrors = new ArrayList<>(); globalErrors = new ArrayList<>();
cache = new FeatureCache();
} }
public boolean isValidated() { public boolean isValidated() {
...@@ -118,20 +121,12 @@ public class CityDoctorModel { ...@@ -118,20 +121,12 @@ public class CityDoctorModel {
} }
public Stream<CityObject> createFeatureStream() { public Stream<CityObject> createFeatureStream() {
return Stream.of(buildings.stream(), vegetation.stream(), bridges.stream(), land.stream(), roads.stream(), return Stream.of(getBuildings(), getVegetation(), getBridges(), getLand(), getTransportation(),
tunnels.stream(), water.stream(), cityfurniture.stream(), genericObjects.stream()).flatMap(co -> co); getTunnels(), getWater(), getCityFurniture(), getGenericCityObjects()).flatMap(co -> co);
} }
public void accept(CheckableVisitor visitor) { public void accept(CheckableVisitor visitor) {
buildings.forEach(co -> co.accept(visitor)); createFeatureStream().forEach(cityObject -> cityObject.accept(visitor));
vegetation.forEach(co -> co.accept(visitor));
bridges.forEach(co -> co.accept(visitor));
land.forEach(co -> co.accept(visitor));
roads.forEach(co -> co.accept(visitor));
tunnels.forEach(co -> co.accept(visitor));
water.forEach(co -> co.accept(visitor));
cityfurniture.forEach(co -> co.accept(visitor));
genericObjects.forEach(co -> co.accept(visitor));
} }
public void saveAs(String file, boolean saveQualityAde) throws CityDoctorWriteException { public void saveAs(String file, boolean saveQualityAde) throws CityDoctorWriteException {
...@@ -228,106 +223,153 @@ public class CityDoctorModel { ...@@ -228,106 +223,153 @@ public class CityDoctorModel {
public Set<CheckError> collectErrors() { public Set<CheckError> collectErrors() {
List<CheckError> errors = new ArrayList<>(); List<CheckError> errors = new ArrayList<>();
collectErrorsFromList(errors, buildings); createFeatureStream().forEach(cityObject -> cityObject.collectErrors(errors));
collectErrorsFromList(errors, vegetation);
collectErrorsFromList(errors, bridges);
collectErrorsFromList(errors, land);
collectErrorsFromList(errors, roads);
collectErrorsFromList(errors, tunnels);
collectErrorsFromList(errors, water);
collectErrorsFromList(errors, cityfurniture);
collectErrorsFromList(errors, genericObjects);
return new HashSet<>(errors); return new HashSet<>(errors);
} }
private void collectErrorsFromList(List<CheckError> errors, List<? extends CityObject> cos) {
for (CityObject co : cos) {
co.collectContainedErrors(errors);
}
}
public String getFileName() { public String getFileName() {
return fileName; return fileName;
} }
public void addBridge(BridgeObject coBridge) {
bridges.add(coBridge);
}
public void addBuilding(Building coBuilding) { public void addBuilding(Building coBuilding) {
buildings.add(coBuilding); buildings.add(coBuilding.getGmlId());
cache.put(coBuilding);
} }
public List<Building> getBuildings() { public Stream<Building> getBuildings() {
return buildings; return buildings.stream().map(co -> (Building) cache.get(co));
} }
public List<BridgeObject> getBridges() { public int getBuildingCount(){
return bridges; return buildings.size();
}
public void addBridge(BridgeObject coBridge) {
bridges.add(coBridge.getGmlId());
cache.put(coBridge);
} }
public List<CityFurniture> getCityFurniture() { public Stream<BridgeObject> getBridges() {
return cityfurniture; return bridges.stream().map(co -> (BridgeObject) cache.get(co));
}
public int getBridgeCount(){
return bridges.size();
} }
public void addCityFurniture(CityFurniture coFurniture) { public void addCityFurniture(CityFurniture coFurniture) {
cityfurniture.add(coFurniture); cityfurniture.add(coFurniture.getGmlId());
cache.put(coFurniture);
}
public Stream<CityFurniture> getCityFurniture() {
return cityfurniture.stream().map(co -> (CityFurniture) cache.get(co));
} }
public List<GenericCityObject> getGenericCityObjects() { public int getCityFurnitureCount(){
return genericObjects; return cityfurniture.size();
} }
public void addGenericCityObject(GenericCityObject coGenericCityObject) { public void addGenericCityObject(GenericCityObject coGenericCityObject) {
genericObjects.add(coGenericCityObject); genericObjects.add(coGenericCityObject.getGmlId());
cache.put(coGenericCityObject);
} }
public void setCityModel(CityModel cModel) { public Stream<GenericCityObject> getGenericCityObjects() {
this.cModel = cModel; return genericObjects.stream().map(co -> (GenericCityObject) cache.get(co));
} }
public CityModel getCityModel() { public int getGenericCityObjectCount(){
return cModel; return genericObjects.size();
}
public void addTransportation(TransportationObject to) {
roads.add(to.getGmlId());
cache.put(to);
} }
public List<TransportationObject> getTransportation() { public Stream<TransportationObject> getTransportation() {
return roads; return roads.stream().map(co -> (TransportationObject) cache.get(co));
} }
public List<Tunnel> getTunnels() { public int getTransportationCount(){
return tunnels; return roads.size();
} }
public List<WaterObject> getWater() { public void addTunnel(Tunnel tunnel) {
return water; tunnels.add(tunnel.getGmlId());
cache.put(tunnel);
} }
public List<CityObject> getLand() { public Stream<Tunnel> getTunnels() {
return land; return tunnels.stream().map(co -> (Tunnel) cache.get(co));
} }
public List<Vegetation> getVegetation() { public int getTunnelCount(){
return vegetation; return tunnels.size();
} }
public void addWater(WaterObject wo) { public void addWater(WaterObject wo) {
water.add(wo); water.add(wo.getGmlId());
cache.put(wo);
}
public Stream<WaterObject> getWater() {
return water.stream().map(co -> (WaterObject) cache.get(co));
}
public int getWaterCount(){
return water.size();
} }
public void addLand(LandObject lo) { public void addLand(LandObject lo) {
land.add(lo); land.add(lo.getGmlId());
cache.put(lo);
}
public void addRelief(ReliefObject relief) {
land.add(relief.getGmlId());
cache.put(relief);
}
public void addTin(TinObject tin) {
land.add(tin.getGmlId());
cache.put(tin);
}
public Stream<CityObject> getLand() {
return land.stream().map(cache::get);
}
public int getLandCount(){
return land.size();
} }
public void addVegetation(Vegetation veg) { public void addVegetation(Vegetation veg) {
vegetation.add(veg); vegetation.add(veg.getGmlId());
cache.put(veg);
} }
public void addTransportation(TransportationObject to) { public Stream<Vegetation> getVegetation() {
roads.add(to); return vegetation.stream().map(co -> (Vegetation) cache.get(co));
} }
public void addTunnel(Tunnel tunnel) { public int getVegetationCount(){
tunnels.add(tunnel); return vegetation.size();
}
public void setCityModel(CityModel cModel) {
this.cModel = cModel;
}
public CityModel getCityModel() {
return cModel;
}
public FeatureCache getCache(){
return cache;
} }
public int getNumberOfFeatures() { public int getNumberOfFeatures() {
...@@ -362,100 +404,105 @@ public class CityDoctorModel { ...@@ -362,100 +404,105 @@ public class CityDoctorModel {
} }
private void replaceWaterObject(CityObject currentFeature, CityObject nextFeature) { private void replaceWaterObject(CityObject currentFeature, CityObject nextFeature) {
int index = water.indexOf(currentFeature); int index = water.indexOf(currentFeature.getGmlId());
if (index == -1) { if (index == -1) {
throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in water objects"); throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in water objects");
} }
water.set(index, (WaterObject) nextFeature); cache.replace(currentFeature.getGmlId(), nextFeature);
water.set(index, nextFeature.getGmlId());
} }
private void replaceLandObject(CityObject currentFeature, CityObject nextFeature) { private void replaceLandObject(CityObject currentFeature, CityObject nextFeature) {
int index = land.indexOf(currentFeature); int index = land.indexOf(currentFeature.getGmlId());
if (index == -1) { if (index == -1) {
throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in land objects"); throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in land objects");
} }
land.set(index, nextFeature); cache.replace(currentFeature.getGmlId(), nextFeature);
land.set(index, nextFeature.getGmlId());
} }
private void replaceVegetation(CityObject currentFeature, CityObject nextFeature) { private void replaceVegetation(CityObject currentFeature, CityObject nextFeature) {
int index = vegetation.indexOf(currentFeature); int index = vegetation.indexOf(currentFeature.getGmlId());
if (index == -1) { if (index == -1) {
throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in vegetation"); throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in vegetation");
} }
vegetation.set(index, (Vegetation) nextFeature); cache.replace(currentFeature.getGmlId(), nextFeature);
vegetation.set(index, nextFeature.getGmlId());
} }
private void replaceCityFurniture(CityObject currentFeature, CityObject nextFeature) { private void replaceCityFurniture(CityObject currentFeature, CityObject nextFeature) {
int index = cityfurniture.indexOf(currentFeature); int index = cityfurniture.indexOf(currentFeature.getGmlId());
if (index == -1) { if (index == -1) {
throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in vegetation"); throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in vegetation");
} }
cityfurniture.set(index, (CityFurniture) nextFeature); cache.replace(currentFeature.getGmlId(), nextFeature);
cityfurniture.set(index, nextFeature.getGmlId());
} }
private void replaceGenericCityObject(CityObject currentFeature, CityObject nextFeature) { private void replaceGenericCityObject(CityObject currentFeature, CityObject nextFeature) {
int index = genericObjects.indexOf(currentFeature); int index = genericObjects.indexOf(currentFeature.getGmlId());
if (index == -1) { if (index == -1) {
throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in generic city objects"); throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in generic city objects");
} }
genericObjects.set(index, (GenericCityObject) nextFeature); cache.replace(currentFeature.getGmlId(), nextFeature);
genericObjects.set(index, nextFeature.getGmlId());
} }
private void replaceTransportationObject(CityObject currentFeature, CityObject nextFeature) { private void replaceTransportationObject(CityObject currentFeature, CityObject nextFeature) {
int index = roads.indexOf(currentFeature); int index = roads.indexOf(currentFeature.getGmlId());
if (index == -1) { if (index == -1) {
throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in transportation objects"); throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in transportation objects");
} }
roads.set(index, (TransportationObject) nextFeature); roads.set(index, nextFeature.getGmlId());
} }
private void replaceTunnel(CityObject currentFeature, CityObject nextFeature) { private void replaceTunnel(CityObject currentFeature, CityObject nextFeature) {
int index = tunnels.indexOf(currentFeature); int index = tunnels.indexOf(currentFeature.getGmlId());
if (index == -1) { if (index == -1) {
throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in tunnels"); throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in tunnels");
} }
tunnels.set(index, (Tunnel) nextFeature); tunnels.set(index, nextFeature.getGmlId());
} }
private void replaceBridge(CityObject currentFeature, CityObject nextFeature) { private void replaceBridge(CityObject currentFeature, CityObject nextFeature) {
int index = bridges.indexOf(currentFeature); int index = bridges.indexOf(currentFeature.getGmlId());
if (index == -1) { if (index == -1) {
throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in bridges"); throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in bridges");
} }
bridges.set(index, (BridgeObject) nextFeature); bridges.set(index, nextFeature.getGmlId());
} }
private void replaceBuilding(CityObject currentFeature, CityObject nextFeature) { private void replaceBuilding(CityObject currentFeature, CityObject nextFeature) {
int index = buildings.indexOf(currentFeature); int index = buildings.indexOf(currentFeature.getGmlId());
if (index == -1) { if (index == -1) {
throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in buildings"); throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in buildings");
} }
buildings.set(index, (Building) nextFeature); buildings.set(index, nextFeature.getGmlId());
} }
public void addCityObject(CityObject co) { public void addCityObject(CityObject co) {
if (co instanceof Building b) { if (co instanceof Building b) {
buildings.add(b); buildings.add(b.getGmlId());
} else if (co instanceof BridgeObject bo) { } else if (co instanceof BridgeObject bo) {
bridges.add(bo); bridges.add(bo.getGmlId());
} else if (co instanceof TransportationObject to) { } else if (co instanceof TransportationObject to) {
roads.add(to); roads.add(to.getGmlId());
} else if (co instanceof Tunnel tu) { } else if (co instanceof Tunnel tu) {
tunnels.add(tu); tunnels.add(tu.getGmlId());
} else if (co instanceof Vegetation veg) { } else if (co instanceof Vegetation veg) {
vegetation.add(veg); vegetation.add(veg.getGmlId());
} else if (co instanceof LandObject lo) { } else if (co instanceof LandObject lo) {
land.add(lo); land.add(lo.getGmlId());
} else if (co instanceof WaterObject wo) { } else if (co instanceof WaterObject wo) {
water.add(wo); water.add(wo.getGmlId());
} else if (co instanceof ReliefObject) { } else if (co instanceof ReliefObject) {
land.add(co); land.add(co.getGmlId());
} else if (co instanceof TinObject) { } else if (co instanceof TinObject) {
land.add(co); land.add(co.getGmlId());
} else if (co instanceof CityFurniture cf) { } else if (co instanceof CityFurniture cf) {
cityfurniture.add(cf); cityfurniture.add(cf.getGmlId());
} else if (co instanceof GenericCityObject gco) { } else if (co instanceof GenericCityObject gco) {
genericObjects.add(gco); genericObjects.add(gco.getGmlId());
} }
} }
...@@ -467,11 +514,4 @@ public class CityDoctorModel { ...@@ -467,11 +514,4 @@ public class CityDoctorModel {
return cityGMLVersion; return cityGMLVersion;
} }
public void addRelief(ReliefObject relief) {
addCityObject(relief);
}
public void addTin(TinObject tin) {
addCityObject(tin);
}
} }
...@@ -759,22 +759,9 @@ public class CityGmlParser { ...@@ -759,22 +759,9 @@ public class CityGmlParser {
} }
private static void drainCityModel(CityDoctorModel model, CityGmlConsumer cityObjectConsumer) { private static void drainCityModel(CityDoctorModel model, CityGmlConsumer cityObjectConsumer) {
drainCityObjectList(model.getBuildings(), cityObjectConsumer);
drainCityObjectList(model.getBridges(), cityObjectConsumer);
drainCityObjectList(model.getVegetation(), cityObjectConsumer);
drainCityObjectList(model.getLand(), cityObjectConsumer);
drainCityObjectList(model.getTransportation(), cityObjectConsumer);
drainCityObjectList(model.getWater(), cityObjectConsumer);
drainCityObjectList(model.getTunnels(), cityObjectConsumer);
drainCityObjectList(model.getCityFurniture(), cityObjectConsumer);
drainCityObjectList(model.getGenericCityObjects(), cityObjectConsumer);
}
private static void drainCityObjectList(List<? extends CityObject> objects, CityGmlConsumer cityObjectConsumer) { model.createFeatureStream().forEach(cityObjectConsumer::accept);
for (CityObject co : objects) {
cityObjectConsumer.accept(co);
}
objects.clear();
} }
} }
...@@ -22,12 +22,12 @@ import java.io.File; ...@@ -22,12 +22,12 @@ import java.io.File;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
...@@ -141,6 +141,7 @@ public class CityGMLWriterUtils { ...@@ -141,6 +141,7 @@ public class CityGMLWriterUtils {
stats.setName(adeId); stats.setName(adeId);
statistics.getErrors().add(new ErrorProperty(stats)); statistics.getErrors().add(new ErrorProperty(stats));
} }
// TODO: Quality ADE update to CityGML 3.0
statistics.setNumErrorBuildings(new FeatureStatisticsProperty(countValidatedCityObjects(model.getBuildings()))); statistics.setNumErrorBuildings(new FeatureStatisticsProperty(countValidatedCityObjects(model.getBuildings())));
statistics.setNumErrorBridgeObjects(new FeatureStatisticsProperty(countValidatedCityObjects(model.getBridges()))); statistics.setNumErrorBridgeObjects(new FeatureStatisticsProperty(countValidatedCityObjects(model.getBridges())));
statistics.setNumErrorLandObjects(new FeatureStatisticsProperty(countValidatedCityObjects(model.getLand()))); statistics.setNumErrorLandObjects(new FeatureStatisticsProperty(countValidatedCityObjects(model.getLand())));
...@@ -152,29 +153,30 @@ public class CityGMLWriterUtils { ...@@ -152,29 +153,30 @@ public class CityGMLWriterUtils {
return val; return val;
} }
private static FeatureStatistics countValidatedCityObjects(List<? extends CityObject> cos) { private static FeatureStatistics countValidatedCityObjects(Stream<? extends CityObject> cos) {
int numChecked = 0; AtomicInteger numChecked = new AtomicInteger();
int numError = 0; AtomicInteger numError = new AtomicInteger();
for (CityObject co : cos) { cos.forEach(co -> {
if (co.isValidated()) { if (co.isValidated()) {
numChecked++; numChecked.getAndIncrement();
if (co.containsAnyError()) { if (co.containsAnyError()) {
numError++; numError.getAndIncrement();
} }
} }
} });
return new FeatureStatistics(numChecked, numError); return new FeatureStatistics(numChecked.get(), numError.get());
} }
private static void storeCityObjects(List<? extends CityObject> cos, GeometryFactory gmlFactory, CityDoctorModel model, CityModel cModel, Validation val) { private static void storeCityObjects(Stream<? extends CityObject> cos, GeometryFactory gmlFactory, CityDoctorModel model, CityModel cModel, Validation val) {
for (CityObject co : cos) { cos.forEach(co -> {
if (model.isValidated() && val != null) { if (model.isValidated() && val != null) {
QualityADEUtils.writeQualityAde(co, val); QualityADEUtils.writeQualityAde(co, val);
} }
co.reCreateGeometries(gmlFactory, model.getParserConfig()); co.reCreateGeometries(gmlFactory, model.getParserConfig());
cModel.getCityObjectMembers().add(new AbstractCityObjectProperty(co.getGmlObject())); cModel.getCityObjectMembers().add(new AbstractCityObjectProperty(co.getGmlObject()));
} });
} }
} }
...@@ -13,7 +13,7 @@ import static org.junit.Assert.assertEquals; ...@@ -13,7 +13,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
public class CityObjectCacheTest { public class FeatureCacheTest {
@BeforeClass @BeforeClass
public static void setUpBeforeClass() { public static void setUpBeforeClass() {
...@@ -36,7 +36,7 @@ public class CityObjectCacheTest { ...@@ -36,7 +36,7 @@ public class CityObjectCacheTest {
b.addGeometry(GeometryTestUtils.createDummyGeometry(GeometryType.SOLID, Lod.LOD0)); b.addGeometry(GeometryTestUtils.createDummyGeometry(GeometryType.SOLID, Lod.LOD0));
assertEquals(0, DatabaseHandler.getFeatureCount()); assertEquals(0, DatabaseHandler.getFeatureCount());
CityObjectCache cache = new CityObjectCache(); FeatureCache cache = new FeatureCache();
cache.put(b); cache.put(b);
assertEquals(1, DatabaseHandler.getFeatureCount()); assertEquals(1, DatabaseHandler.getFeatureCount());
} }
......
...@@ -45,33 +45,8 @@ public class XmlValidationReporter implements Reporter { ...@@ -45,33 +45,8 @@ public class XmlValidationReporter implements Reporter {
@Override @Override
public void writeReport(Checks checks, OutputStream filePath, CityDoctorModel model, ValidationConfiguration config) throws CheckReportWriteException { public void writeReport(Checks checks, OutputStream filePath, CityDoctorModel model, ValidationConfiguration config) throws CheckReportWriteException {
StreamReporter streamReporter = new XmlStreamReporter(filePath, model.getFileName(), config); StreamReporter streamReporter = new XmlStreamReporter(filePath, model.getFileName(), config);
for (BridgeObject bo : model.getBridges()) { model.createFeatureStream().forEach(streamReporter::report);
streamReporter.report(bo);
}
for (Building b : model.getBuildings()) {
streamReporter.report(b);
}
for (TransportationObject to : model.getTransportation()) {
streamReporter.report(to);
}
for (CityObject lo : model.getLand()) {
streamReporter.report(lo);
}
for (Vegetation v : model.getVegetation()) {
streamReporter.report(v);
}
for (WaterObject wo : model.getWater()) {
streamReporter.report(wo);
}
for (Tunnel tunnel : model.getTunnels()) {
streamReporter.report(tunnel);
}
for (CityFurniture cf : model.getCityFurniture()) {
streamReporter.report(cf);
}
for (GenericCityObject gco : model.getGenericCityObjects()) {
streamReporter.report(gco);
}
for (CheckError err : model.getGlobalErrors()) { for (CheckError err : model.getGlobalErrors()) {
streamReporter.reportGlobalError(err); streamReporter.reportGlobalError(err);
} }
......
package de.hft.stuttgart.citydoctor2.gui; package de.hft.stuttgart.citydoctor2.gui;
import de.hft.stuttgart.citydoctor2.check.CheckError; import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.Checkable;
import de.hft.stuttgart.citydoctor2.check.Checker; import de.hft.stuttgart.citydoctor2.check.Checker;
import de.hft.stuttgart.citydoctor2.check.ErrorId; import de.hft.stuttgart.citydoctor2.check.ErrorId;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration; import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
...@@ -48,6 +49,8 @@ import java.util.*; ...@@ -48,6 +49,8 @@ import java.util.*;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
public class CityDoctorController { public class CityDoctorController {
...@@ -79,6 +82,8 @@ public class CityDoctorController { ...@@ -79,6 +82,8 @@ public class CityDoctorController {
private AtomicInteger cityFurnitureChunkNr = new AtomicInteger(0); private AtomicInteger cityFurnitureChunkNr = new AtomicInteger(0);
private AtomicInteger genericCityObjectsChunkNr = new AtomicInteger(0); private AtomicInteger genericCityObjectsChunkNr = new AtomicInteger(0);
public CityDoctorController(MainWindow mainWindow, HighlightController highlightController, Renderer renderer) { public CityDoctorController(MainWindow mainWindow, HighlightController highlightController, Renderer renderer) {
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
this.highlightController = highlightController; this.highlightController = highlightController;
...@@ -205,6 +210,10 @@ public class CityDoctorController { ...@@ -205,6 +210,10 @@ public class CityDoctorController {
return zipArchive; return zipArchive;
} }
public static int getMaxFeaturesPerChunk(){
return MAX_FEATURES_PER_CHUNK;
}
public void loadZipEntry(CityGmlZipEntry entry) { public void loadZipEntry(CityGmlZipEntry entry) {
if (!entry.isLoaded()) { if (!entry.isLoaded()) {
entry.loadEntry(currentConfig); entry.loadEntry(currentConfig);
...@@ -260,15 +269,15 @@ public class CityDoctorController { ...@@ -260,15 +269,15 @@ public class CityDoctorController {
} }
private void setupFeatureTabs() { private void setupFeatureTabs() {
mainWindow.setDisableOfFeatureTab(FeatureType.BUILDING, model.getBuildings().isEmpty()); mainWindow.setDisableOfFeatureTab(FeatureType.BUILDING, model.getBuildingCount() == 0);
mainWindow.setDisableOfFeatureTab(FeatureType.VEGETATION, model.getVegetation().isEmpty()); mainWindow.setDisableOfFeatureTab(FeatureType.VEGETATION, model.getVegetationCount() == 0);
mainWindow.setDisableOfFeatureTab(FeatureType.TRANSPORTATION, model.getTransportation().isEmpty()); mainWindow.setDisableOfFeatureTab(FeatureType.TRANSPORTATION, model.getTransportationCount() == 0);
mainWindow.setDisableOfFeatureTab(FeatureType.TUNNEL, model.getTunnels().isEmpty()); mainWindow.setDisableOfFeatureTab(FeatureType.TUNNEL, model.getTunnelCount() == 0);
mainWindow.setDisableOfFeatureTab(FeatureType.BRIDGE, model.getBridges().isEmpty()); mainWindow.setDisableOfFeatureTab(FeatureType.BRIDGE, model.getBridgeCount() == 0);
mainWindow.setDisableOfFeatureTab(FeatureType.WATER, model.getWater().isEmpty()); mainWindow.setDisableOfFeatureTab(FeatureType.WATER, model.getWaterCount() == 0);
mainWindow.setDisableOfFeatureTab(FeatureType.LAND, model.getLand().isEmpty()); mainWindow.setDisableOfFeatureTab(FeatureType.LAND, model.getLandCount() == 0);
mainWindow.setDisableOfFeatureTab(FeatureType.CITY_FURNITURE, model.getCityFurniture().isEmpty()); mainWindow.setDisableOfFeatureTab(FeatureType.CITY_FURNITURE, model.getCityFurnitureCount() == 0);
mainWindow.setDisableOfFeatureTab(FeatureType.GENERIC_CITY_OBJECT, model.getGenericCityObjects().isEmpty()); mainWindow.setDisableOfFeatureTab(FeatureType.GENERIC_CITY_OBJECT, model.getGenericCityObjectCount() == 0);
} }
public void buildTrees() { public void buildTrees() {
...@@ -281,7 +290,7 @@ public class CityDoctorController { ...@@ -281,7 +290,7 @@ public class CityDoctorController {
buildWater(model); buildWater(model);
buildLand(model); buildLand(model);
buildCityFurniture(model); buildCityFurniture(model);
buildOtherCityObjects(model); buildGenericCityObjects(model);
buildTunnel(model); buildTunnel(model);
} }
...@@ -298,122 +307,130 @@ public class CityDoctorController { ...@@ -298,122 +307,130 @@ public class CityDoctorController {
} }
private void buildLand(CityDoctorModel model) { private void buildLand(CityDoctorModel model) {
if (model.getLand().isEmpty()) { if (model.getLandCount() == 0) {
return; return;
} }
TreeView<Renderable> landView = mainWindow.getTerrainView(); TreeView<Renderable> landView = mainWindow.getTerrainView();
TreeItem<Renderable> landRoot = new TreeItem<>(new CityObjectGroupNode(model.getLand(), "Land", TreeItem<Renderable> landRoot = new TreeItem<>(new CityObjectGroupNode(Collections.emptyList(), "Land",
Color.BROWN)); Color.BROWN));
landRoot.setExpanded(true); landRoot.setExpanded(true);
landView.setRoot(landRoot); landView.setRoot(landRoot);
buildLandFromList(model.getLand(), landView.getRoot()); Supplier<Stream<CityObject>> supply = model::getLand;
addMoreButtonIfNecessary(model.getLand(), landView, landRoot, landChunkNr); buildLandTreeFromList(supply, landView.getRoot());
addMoreButtonForLand(landView, landRoot, landChunkNr, supply, model.getLandCount());
} }
private void buildLandFromList(List<CityObject> list, TreeItem<Renderable> root) { private void buildLandTreeFromList(Supplier<Stream<CityObject>> supply, TreeItem<Renderable> root) {
int landChunk = landChunkNr.get(); int landChunk = landChunkNr.get();
for (int i = landChunk * MAX_FEATURES_PER_CHUNK; i < (landChunk + 1) * MAX_FEATURES_PER_CHUNK supply.get().skip((long) landChunk * MAX_FEATURES_PER_CHUNK).limit(MAX_FEATURES_PER_CHUNK).forEach(land ->{
&& i < list.size(); i++) {
CityObject land = list.get(i);
CityObjectNode node = new CityObjectNode(land); CityObjectNode node = new CityObjectNode(land);
TreeItem<Renderable> item = new TreeItem<>(node); TreeItem<Renderable> item = new TreeItem<>(node);
item.setExpanded(true); item.setExpanded(true);
root.getChildren().add(item); root.getChildren().add(item);
createGeometryNodes(land, item); createGeometryNodes(land, item);
});
}
} }
private void buildCityFurniture(CityDoctorModel model) { private void buildCityFurniture(CityDoctorModel model) {
if (model.getCityFurniture().isEmpty()) { if (model.getCityFurnitureCount() == 0) {
return; return;
} }
TreeView<Renderable> cityFurnitureView = mainWindow.getCityFurnitureView(); TreeView<Renderable> cityFurnitureView = mainWindow.getCityFurnitureView();
TreeItem<Renderable> cityFurnitureRoot = new TreeItem<>(new CityObjectGroupNode(model.getCityFurniture(), TreeItem<Renderable> cityFurnitureRoot = new TreeItem<>(new CityObjectGroupNode(Collections.emptyList(),
"City Furniture", Color.BLUEVIOLET)); "City Furniture", Color.BLUEVIOLET));
cityFurnitureRoot.setExpanded(true); cityFurnitureRoot.setExpanded(true);
cityFurnitureView.setRoot(cityFurnitureRoot); cityFurnitureView.setRoot(cityFurnitureRoot);
buildCityFurnitureTreeFromList(model.getCityFurniture(), cityFurnitureView.getRoot()); Supplier<Stream<CityFurniture>> supply = model::getCityFurniture;
addMoreButtonIfNecessary(model.getCityFurniture(), cityFurnitureView, cityFurnitureRoot, cityFurnitureChunkNr); buildCityFurnitureTreeFromList(supply, cityFurnitureView.getRoot());
addMoreButtonForCityFurniture(cityFurnitureView, cityFurnitureRoot, cityFurnitureChunkNr, supply,
model.getCityFurnitureCount());
} }
private void buildOtherCityObjects(CityDoctorModel model) { private void buildGenericCityObjects(CityDoctorModel model) {
if (model.getGenericCityObjects().isEmpty()) { if (model.getGenericCityObjectCount() == 0) {
return; return;
} }
TreeView<Renderable> otherObjectsView = mainWindow.getGenericCityObjectView(); TreeView<Renderable> otherObjectsView = mainWindow.getGenericCityObjectView();
TreeItem<Renderable> otherObjectsRoot = new TreeItem<>(new CityObjectGroupNode(model.getGenericCityObjects(), TreeItem<Renderable> otherObjectsRoot = new TreeItem<>(new CityObjectGroupNode(Collections.emptyList(),
"Generic CityObjects", Color.DARKGRAY)); "Generic CityObjects", Color.DARKGRAY));
otherObjectsRoot.setExpanded(true); otherObjectsRoot.setExpanded(true);
otherObjectsView.setRoot(otherObjectsRoot); otherObjectsView.setRoot(otherObjectsRoot);
buildGenericCityObjectTreeFromList(model.getGenericCityObjects(), otherObjectsView.getRoot()); Supplier<Stream<GenericCityObject>> supply = model::getGenericCityObjects;
addMoreButtonIfNecessary(model.getGenericCityObjects(), otherObjectsView, otherObjectsRoot, genericCityObjectsChunkNr); buildGenericCityObjectTreeFromList(supply, otherObjectsView.getRoot());
addMoreButtonForGenericCityObjects( otherObjectsView, otherObjectsRoot, genericCityObjectsChunkNr, supply,
model.getGenericCityObjectCount());
} }
private void buildWater(CityDoctorModel model) { private void buildWater(CityDoctorModel model) {
if (model.getWater().isEmpty()) { if (model.getWaterCount() == 0) {
return; return;
} }
TreeView<Renderable> waterView = mainWindow.getWaterView(); TreeView<Renderable> waterView = mainWindow.getWaterView();
TreeItem<Renderable> waterRoot = new TreeItem<>(new CityObjectGroupNode(model.getWater(), "Water", TreeItem<Renderable> waterRoot = new TreeItem<>(new CityObjectGroupNode(Collections.emptyList(), "Water",
Color.LIGHTSKYBLUE)); Color.LIGHTSKYBLUE));
waterRoot.setExpanded(true); waterRoot.setExpanded(true);
waterView.setRoot(waterRoot); waterView.setRoot(waterRoot);
buildWaterTreeFromList(model.getWater(), waterRoot); Supplier<Stream<WaterObject>> supply = model::getWater;
addMoreButtonIfNecessary(model.getWater(), waterView, waterRoot, waterChunkNr); buildWaterTreeFromList(supply, waterRoot);
addMoreButtonForWater(waterView, waterRoot, waterChunkNr, supply, model.getWaterCount());
} }
private void buildBridges(CityDoctorModel model) { private void buildBridges(CityDoctorModel model) {
if (model.getBridges().isEmpty()) { if (model.getBridgeCount() == 0) {
return; return;
} }
TreeView<Renderable> bridgeView = mainWindow.getBridgeView(); TreeView<Renderable> bridgeView = mainWindow.getBridgeView();
TreeItem<Renderable> bridgeRoot = new TreeItem<>(new CityObjectGroupNode(model.getBridges(), "Bridges", TreeItem<Renderable> bridgeRoot = new TreeItem<>(new CityObjectGroupNode(Collections.emptyList(), "Bridges",
Color.CORAL)); Color.CORAL));
bridgeRoot.setExpanded(true); bridgeRoot.setExpanded(true);
bridgeView.setRoot(bridgeRoot); bridgeView.setRoot(bridgeRoot);
buildBridgeTreeFromList(model.getBridges(), bridgeView.getRoot()); Supplier<Stream<BridgeObject>> supply = model::getBridges;
addMoreButtonIfNecessary(model.getBridges(), bridgeView, bridgeRoot, bridgeChunkNr); buildBridgeTreeFromList(supply, bridgeView.getRoot());
addMoreButtonForBridges(bridgeView, bridgeRoot, bridgeChunkNr, supply, model.getBridgeCount());
} }
private void buildTransportation(CityDoctorModel model) { private void buildTransportation(CityDoctorModel model) {
if (model.getTransportation().isEmpty()) { if (model.getTransportationCount() == 0) {
return; return;
} }
TreeView<Renderable> transView = mainWindow.getTransportationView(); TreeView<Renderable> transView = mainWindow.getTransportationView();
TreeItem<Renderable> transRoot = new TreeItem<>(new CityObjectGroupNode(model.getTransportation(), TreeItem<Renderable> transRoot = new TreeItem<>(new CityObjectGroupNode(Collections.emptyList(),
"Transportation", Color.YELLOW)); "Transportation", Color.YELLOW));
transRoot.setExpanded(true); transRoot.setExpanded(true);
transView.setRoot(transRoot); transView.setRoot(transRoot);
buildTransportationTreeFromList(model.getTransportation(), transView.getRoot()); Supplier<Stream<TransportationObject>> supply = model::getTransportation;
addMoreButtonIfNecessary(model.getTransportation(), transView, transRoot, transportationChunkNr); buildTransportationTreeFromList(supply, transView.getRoot());
addMoreButtonForTransportation(transView, transRoot, transportationChunkNr, supply,
model.getTransportationCount());
} }
private void buildTunnel(CityDoctorModel model) { private void buildTunnel(CityDoctorModel model) {
if (model.getTunnels().isEmpty()) { if (model.getTunnelCount() == 0) {
return; return;
} }
TreeView<Renderable> tunnelView = mainWindow.getTunnelView(); TreeView<Renderable> tunnelView = mainWindow.getTunnelView();
TreeItem<Renderable> tunnelRoot = new TreeItem<>(new CityObjectGroupNode(model.getTunnels(), "Tunnel", TreeItem<Renderable> tunnelRoot = new TreeItem<>(new CityObjectGroupNode(Collections.emptyList(), "Tunnel",
Color.SLATEGRAY)); Color.SLATEGRAY));
tunnelRoot.setExpanded(true); tunnelRoot.setExpanded(true);
tunnelView.setRoot(tunnelRoot); tunnelView.setRoot(tunnelRoot);
buildTunnelTreeFromList(model.getTunnels(), tunnelRoot); Supplier<Stream<Tunnel>> supply = model::getTunnels;
addMoreButtonIfNecessary(model.getTunnels(), tunnelView, tunnelRoot, tunnelChunkNr); buildTunnelTreeFromList(supply, tunnelRoot);
addMoreButtonForTunnel(tunnelView, tunnelRoot, tunnelChunkNr, supply, model.getTunnelCount());
} }
private void buildVegetation(CityDoctorModel model) { private void buildVegetation(CityDoctorModel model) {
if (model.getVegetation().isEmpty()) { if (model.getVegetationCount() == 0) {
return; return;
} }
TreeView<Renderable> vegetationsView = mainWindow.getVegetationView(); TreeView<Renderable> vegetationsView = mainWindow.getVegetationView();
TreeItem<Renderable> vegRoot = new TreeItem<>(new CityObjectGroupNode(model.getVegetation(), TreeItem<Renderable> vegRoot = new TreeItem<>(new CityObjectGroupNode(Collections.emptyList(),
"Vegetation", Color.LIGHTGREEN)); "Vegetation", Color.LIGHTGREEN));
vegRoot.setExpanded(true); vegRoot.setExpanded(true);
vegetationsView.setRoot(vegRoot); vegetationsView.setRoot(vegRoot);
buildVegetationTreeFromList(model.getVegetation(), vegRoot); Supplier<Stream<Vegetation>> supply = model::getVegetation;
addMoreButtonIfNecessary(model.getVegetation(), vegetationsView, vegRoot, vegetationChunkNr); buildVegetationTreeFromList(supply, vegRoot);
addMoreButtonForVegetation(vegetationsView, vegRoot, vegetationChunkNr, supply, model.getVegetationCount());
} }
private void addMoreButtonIfNecessary(List<? extends CityObject> list, TreeView<Renderable> view, private void addMoreButtonIfNecessary(List<? extends CityObject> list, TreeView<Renderable> view,
...@@ -429,7 +446,7 @@ public class CityDoctorController { ...@@ -429,7 +446,7 @@ public class CityDoctorController {
chunkCounter.incrementAndGet(); chunkCounter.incrementAndGet();
int visibleFeatures = countVisibleNodes(view); int visibleFeatures = countVisibleNodes(view);
buildTreeFromList(list, root, chunkCounter); // buildTreeFromList(list, root, chunkCounter);
updateTree(root); updateTree(root);
if (selectedIndex >= 0) { if (selectedIndex >= 0) {
view.getSelectionModel().select(selectedIndex); view.getSelectionModel().select(selectedIndex);
...@@ -445,17 +462,35 @@ public class CityDoctorController { ...@@ -445,17 +462,35 @@ public class CityDoctorController {
} }
} }
private void moreButton(CityDoctorModel model, TreeView<Renderable> view, TreeItem<Renderable> root,
AtomicInteger chunkCounter, Supplier<Stream<CityObject>> streamSupplier){
int featureCount = model.getBuildingCount();
if ((chunkCounter.get() + 1) * MAX_FEATURES_PER_CHUNK < featureCount) {
AddMoreButtonRunnable run = new AddMoreButtonRunnable(featureCount, view, root, chunkCounter) {
@Override
public void buildNodeTreeCall() {
buildTreeFromList(streamSupplier, root, chunkCounter);
updateTree(root);
}
};
//add button
root.getChildren().add(new TreeItem<>(new ButtonRenderable(run)));
}
}
private void buildBuildings(CityDoctorModel model) { private void buildBuildings(CityDoctorModel model) {
if (model.getBuildings().isEmpty()) { if (model.getBuildingCount() == 0) {
return; return;
} }
//TODO: Continue Supplier implementation
TreeView<Renderable> buildingsView = mainWindow.getBuildingsView(); TreeView<Renderable> buildingsView = mainWindow.getBuildingsView();
TreeItem<Renderable> root = new TreeItem<>(new CityObjectGroupNode(model.getBuildings(), "Buildings", TreeItem<Renderable> root = new TreeItem<>(new CityObjectGroupNode(Collections.emptyList(), "Buildings",
Color.WHITE)); Color.WHITE));
root.setExpanded(true); root.setExpanded(true);
buildingsView.setRoot(root); buildingsView.setRoot(root);
buildBuildingTreeFromList(model.getBuildings(), root); Supplier<Stream<Building>> supply = model::getBuildings;
addMoreButtonToBuildingsIfNecessary(model.getBuildings(), buildingsView, root, buildingChunkNr); buildBuildingTreeFromList(supply, root);
addMoreButtonForBuildings(buildingsView, root, buildingChunkNr, supply, model.getBuildingCount());
} }
...@@ -471,7 +506,7 @@ public class CityDoctorController { ...@@ -471,7 +506,7 @@ public class CityDoctorController {
root.getChildren().remove(root.getChildren().size() - 1); root.getChildren().remove(root.getChildren().size() - 1);
chunkCounter.getAndIncrement(); chunkCounter.getAndIncrement();
int visibleFeatures = countVisibleNodes(view); int visibleFeatures = countVisibleNodes(view);
buildBuildingTreeFromList(list, root); // buildBuildingTreeFromList(list, root);
updateTree(root); updateTree(root);
if (selectedIndex >= 0) { if (selectedIndex >= 0) {
view.getSelectionModel().select(selectedIndex); view.getSelectionModel().select(selectedIndex);
...@@ -487,6 +522,141 @@ public class CityDoctorController { ...@@ -487,6 +522,141 @@ public class CityDoctorController {
} }
} }
private void addMoreButtonForBuildings(TreeView<Renderable> view, TreeItem<Renderable> root,
AtomicInteger chunkCounter, Supplier<Stream<Building>> supply, int featureCount) {
if ((chunkCounter.get() + 1) * MAX_FEATURES_PER_CHUNK < featureCount) {
AddMoreButtonRunnable run = new AddMoreButtonRunnable(featureCount, view, root, chunkCounter) {
@Override
public void buildNodeTreeCall() {
buildBuildingTreeFromList(supply, root);
updateTree(root);
}
};
//add button
root.getChildren().add(new TreeItem<>(new ButtonRenderable(run)));
}
}
private void addMoreButtonForBridges(TreeView<Renderable> view, TreeItem<Renderable> root,
AtomicInteger chunkCounter, Supplier<Stream<BridgeObject>> supply, int featureCount){
if ((chunkCounter.get() + 1) * MAX_FEATURES_PER_CHUNK < featureCount) {
AddMoreButtonRunnable run = new AddMoreButtonRunnable(featureCount, view, root, chunkCounter) {
@Override
public void buildNodeTreeCall() {
buildBridgeTreeFromList(supply, root);
updateTree(root);
}
};
//add button
root.getChildren().add(new TreeItem<>(new ButtonRenderable(run)));
}
}
private void addMoreButtonForVegetation(TreeView<Renderable> view, TreeItem<Renderable> root,
AtomicInteger chunkCounter, Supplier<Stream<Vegetation>> supply, int featureCount){
if ((chunkCounter.get() + 1) * MAX_FEATURES_PER_CHUNK < featureCount) {
AddMoreButtonRunnable run = new AddMoreButtonRunnable(featureCount, view, root, chunkCounter) {
@Override
public void buildNodeTreeCall() {
buildVegetationTreeFromList(supply, root);
updateTree(root);
}
};
//add button
root.getChildren().add(new TreeItem<>(new ButtonRenderable(run)));
}
}
private void addMoreButtonForTunnel(TreeView<Renderable> view, TreeItem<Renderable> root,
AtomicInteger chunkCounter, Supplier<Stream<Tunnel>> supply, int featureCount){
if ((chunkCounter.get() + 1) * MAX_FEATURES_PER_CHUNK < featureCount) {
AddMoreButtonRunnable run = new AddMoreButtonRunnable(featureCount, view, root, chunkCounter) {
@Override
public void buildNodeTreeCall() {
buildTunnelTreeFromList(supply, root);
updateTree(root);
}
};
//add button
root.getChildren().add(new TreeItem<>(new ButtonRenderable(run)));
}
}
private void addMoreButtonForTransportation(TreeView<Renderable> view, TreeItem<Renderable> root,
AtomicInteger chunkCounter, Supplier<Stream<TransportationObject>> supply, int featureCount){
if ((chunkCounter.get() + 1) * MAX_FEATURES_PER_CHUNK < featureCount) {
AddMoreButtonRunnable run = new AddMoreButtonRunnable(featureCount, view, root, chunkCounter) {
@Override
public void buildNodeTreeCall() {
buildTransportationTreeFromList(supply, root);
updateTree(root);
}
};
//add button
root.getChildren().add(new TreeItem<>(new ButtonRenderable(run)));
}
}
private void addMoreButtonForLand(TreeView<Renderable> view, TreeItem<Renderable> root,
AtomicInteger chunkCounter, Supplier<Stream<CityObject>> supply, int featureCount){
if ((chunkCounter.get() + 1) * MAX_FEATURES_PER_CHUNK < featureCount) {
AddMoreButtonRunnable run = new AddMoreButtonRunnable(featureCount, view, root, chunkCounter) {
@Override
public void buildNodeTreeCall() {
buildLandTreeFromList(supply, root);
updateTree(root);
}
};
//add button
root.getChildren().add(new TreeItem<>(new ButtonRenderable(run)));
}
}
private void addMoreButtonForWater(TreeView<Renderable> view, TreeItem<Renderable> root,
AtomicInteger chunkCounter, Supplier<Stream<WaterObject>> supply, int featureCount){
if ((chunkCounter.get() + 1) * MAX_FEATURES_PER_CHUNK < featureCount) {
AddMoreButtonRunnable run = new AddMoreButtonRunnable(featureCount, view, root, chunkCounter) {
@Override
public void buildNodeTreeCall() {
buildWaterTreeFromList(supply, root);
updateTree(root);
}
};
//add button
root.getChildren().add(new TreeItem<>(new ButtonRenderable(run)));
}
}
private void addMoreButtonForCityFurniture(TreeView<Renderable> view, TreeItem<Renderable> root,
AtomicInteger chunkCounter, Supplier<Stream<CityFurniture>> supply, int featureCount){
if ((chunkCounter.get() + 1) * MAX_FEATURES_PER_CHUNK < featureCount) {
AddMoreButtonRunnable run = new AddMoreButtonRunnable(featureCount, view, root, chunkCounter) {
@Override
public void buildNodeTreeCall() {
buildCityFurnitureTreeFromList(supply, root);
updateTree(root);
}
};
//add button
root.getChildren().add(new TreeItem<>(new ButtonRenderable(run)));
}
}
private void addMoreButtonForGenericCityObjects(TreeView<Renderable> view, TreeItem<Renderable> root,
AtomicInteger chunkCounter, Supplier<Stream<GenericCityObject>> supply, int featureCount){
if ((chunkCounter.get() + 1) * MAX_FEATURES_PER_CHUNK < featureCount) {
AddMoreButtonRunnable run = new AddMoreButtonRunnable(featureCount, view, root, chunkCounter) {
@Override
public void buildNodeTreeCall() {
buildGenericCityObjectTreeFromList(supply, root);
updateTree(root);
}
};
//add button
root.getChildren().add(new TreeItem<>(new ButtonRenderable(run)));
}
}
private static int countVisibleNodes(TreeView<Renderable> view) { private static int countVisibleNodes(TreeView<Renderable> view) {
return countVisibleNodes(view.getRoot()) + 1; return countVisibleNodes(view.getRoot()) + 1;
} }
...@@ -501,17 +671,16 @@ public class CityDoctorController { ...@@ -501,17 +671,16 @@ public class CityDoctorController {
return visibleFeatures; return visibleFeatures;
} }
private void buildTreeFromList(List<? extends CityObject> cos, TreeItem<Renderable> root, private void buildTreeFromList(Supplier<Stream<CityObject>> supply, TreeItem<Renderable> root,
AtomicInteger chunkCounter) { AtomicInteger chunkCounter) {
int chunk = chunkCounter.get(); int buildingChunk = buildingChunkNr.get();
for (int i = chunk * MAX_FEATURES_PER_CHUNK; i < (chunk + 1) * MAX_FEATURES_PER_CHUNK && i < cos.size(); i++) { supply.get().skip((long) buildingChunk * MAX_FEATURES_PER_CHUNK).limit(MAX_FEATURES_PER_CHUNK).forEach(co ->{
CityObject co = cos.get(i);
CityObjectNode node = new CityObjectNode(co); CityObjectNode node = new CityObjectNode(co);
TreeItem<Renderable> nodeItem = new TreeItem<>(node); TreeItem<Renderable> nodeItem = new TreeItem<>(node);
nodeItem.setExpanded(true); nodeItem.setExpanded(true);
root.getChildren().add(nodeItem); root.getChildren().add(nodeItem);
createGeometryNodes(co, nodeItem); createGeometryNodes(co, nodeItem);
} });
} }
private <T extends CityObject> void buildTreeFromListWithProducer(List<T> cos, TreeItem<Renderable> root, private <T extends CityObject> void buildTreeFromListWithProducer(List<T> cos, TreeItem<Renderable> root,
...@@ -582,11 +751,9 @@ public class CityDoctorController { ...@@ -582,11 +751,9 @@ public class CityDoctorController {
return nodeItem; return nodeItem;
} }
private void buildBuildingTreeFromList(List<Building> list, TreeItem<Renderable> root) { private void buildBuildingTreeFromList(Supplier<Stream<Building>> supply, TreeItem<Renderable> root) {
int buildingChunk = buildingChunkNr.get(); int buildingChunk = buildingChunkNr.get();
for (int i = buildingChunk * MAX_FEATURES_PER_CHUNK; i < (buildingChunk + 1) * MAX_FEATURES_PER_CHUNK supply.get().skip((long) buildingChunk * MAX_FEATURES_PER_CHUNK).limit(MAX_FEATURES_PER_CHUNK).forEach(b ->{
&& i < list.size(); i++) {
Building b = list.get(i);
TreeItem<Renderable> item = buildTopLevelCityObjectNode(b, root, b.getBoundarySurfaces()); TreeItem<Renderable> item = buildTopLevelCityObjectNode(b, root, b.getBoundarySurfaces());
createBuildingInstallationNodes(b, item); createBuildingInstallationNodes(b, item);
...@@ -596,14 +763,12 @@ public class CityDoctorController { ...@@ -596,14 +763,12 @@ public class CityDoctorController {
createBuildingStoreyNodes(b, item); createBuildingStoreyNodes(b, item);
createBuildingUnitNodes(b, item); createBuildingUnitNodes(b, item);
createBuildingPartNodes(b, item); createBuildingPartNodes(b, item);
} });
} }
private void buildBridgeTreeFromList(List<BridgeObject> list, TreeItem<Renderable> root) { private void buildBridgeTreeFromList(Supplier<Stream<BridgeObject>> supply, TreeItem<Renderable> root) {
int bridgeChunk = bridgeChunkNr.get(); int bridgeChunk = bridgeChunkNr.get();
for (int i = bridgeChunk * MAX_FEATURES_PER_CHUNK; i < (bridgeChunk + 1) * MAX_FEATURES_PER_CHUNK supply.get().skip((long) bridgeChunk * MAX_FEATURES_PER_CHUNK).limit(MAX_FEATURES_PER_CHUNK).forEach(bridge ->{
&& i < list.size(); i++) {
BridgeObject bridge = list.get(i);
TreeItem<Renderable> item = buildTopLevelCityObjectNode(bridge, root, bridge.getBoundarySurfaces()); TreeItem<Renderable> item = buildTopLevelCityObjectNode(bridge, root, bridge.getBoundarySurfaces());
createBridgeRoomNodes(bridge, item); createBridgeRoomNodes(bridge, item);
...@@ -611,55 +776,46 @@ public class CityDoctorController { ...@@ -611,55 +776,46 @@ public class CityDoctorController {
createBridgeFurnitureNodes(bridge, item); createBridgeFurnitureNodes(bridge, item);
createBridgeConstructiveElementsNodes(bridge, item); createBridgeConstructiveElementsNodes(bridge, item);
createBridgePartNodes(bridge, item); createBridgePartNodes(bridge, item);
} });
} }
private void buildTransportationTreeFromList(List<TransportationObject> list, TreeItem<Renderable> root) { private void buildTransportationTreeFromList(Supplier<Stream<TransportationObject>> supply, TreeItem<Renderable> root) {
int transportChunk = transportationChunkNr.get(); int transportChunk = transportationChunkNr.get();
for (int i = transportChunk * MAX_FEATURES_PER_CHUNK; i < (transportChunk + 1) * MAX_FEATURES_PER_CHUNK supply.get().skip((long) transportChunk * MAX_FEATURES_PER_CHUNK).limit(MAX_FEATURES_PER_CHUNK).forEach(trans ->{
&& i < list.size(); i++) { TreeItem<Renderable> item = buildTopLevelCityObjectNode(trans, root);
TransportationObject t = list.get(i); createTransportSectionNodes(trans, item);
TreeItem<Renderable> item = buildTopLevelCityObjectNode(t, root); createTrafficSpacesNodes(trans, item);
createTransportSectionNodes(t, item); });
createTrafficSpacesNodes(t, item);
}
} }
private void buildVegetationTreeFromList(List<Vegetation> list, TreeItem<Renderable> root) { private void buildVegetationTreeFromList(Supplier<Stream<Vegetation>> supply, TreeItem<Renderable> root) {
int vegetationChunk = vegetationChunkNr.get(); int vegetationChunk = vegetationChunkNr.get();
for (int i = vegetationChunk * MAX_FEATURES_PER_CHUNK; i < (vegetationChunk + 1) * MAX_FEATURES_PER_CHUNK supply.get().skip((long) vegetationChunk * MAX_FEATURES_PER_CHUNK).limit(MAX_FEATURES_PER_CHUNK).forEach(veg ->{
&& i < list.size(); i++) { buildTopLevelCityObjectNode(veg, root);
Vegetation v = list.get(i); });
buildTopLevelCityObjectNode(v, root);
}
} }
private void buildWaterTreeFromList(List<WaterObject> list, TreeItem<Renderable> root) { private void buildWaterTreeFromList(Supplier<Stream<WaterObject>> supply, TreeItem<Renderable> root) {
int waterChunk = waterChunkNr.get(); int waterChunk = waterChunkNr.get();
for (int i = waterChunk * MAX_FEATURES_PER_CHUNK; i < (waterChunk + 1) * MAX_FEATURES_PER_CHUNK supply.get().skip((long) waterChunk * MAX_FEATURES_PER_CHUNK).limit(MAX_FEATURES_PER_CHUNK).forEach(wat ->{
&& i < list.size(); i++) { buildTopLevelCityObjectNode(wat, root);
WaterObject w = list.get(i); });
buildTopLevelCityObjectNode(w, root);
}
} }
private void buildCityFurnitureTreeFromList(List<CityFurniture> list, TreeItem<Renderable> root) { private void buildCityFurnitureTreeFromList(Supplier<Stream<CityFurniture>> supply, TreeItem<Renderable> root) {
int cityFurnChunk = cityFurnitureChunkNr.get(); int cityFurnChunk = cityFurnitureChunkNr.get();
for (int i = cityFurnChunk * MAX_FEATURES_PER_CHUNK; i < (cityFurnChunk + 1) * MAX_FEATURES_PER_CHUNK supply.get().skip((long) cityFurnChunk * MAX_FEATURES_PER_CHUNK).limit(MAX_FEATURES_PER_CHUNK).forEach(cf ->{
&& i < list.size(); i++) {
CityFurniture cf = list.get(i);
buildTopLevelCityObjectNode(cf, root); buildTopLevelCityObjectNode(cf, root);
} });
} }
private void buildGenericCityObjectTreeFromList(List<GenericCityObject> list, TreeItem<Renderable> root) { private void buildGenericCityObjectTreeFromList(Supplier<Stream<GenericCityObject>> supply, TreeItem<Renderable> root) {
int genericObjectChunk = genericCityObjectsChunkNr.get(); int genericObjectChunk = genericCityObjectsChunkNr.get();
for (int i = genericObjectChunk * MAX_FEATURES_PER_CHUNK; i < (genericObjectChunk + 1) * MAX_FEATURES_PER_CHUNK supply.get().skip((long) genericObjectChunk * MAX_FEATURES_PER_CHUNK).limit(MAX_FEATURES_PER_CHUNK).forEach(gen ->{
&& i < list.size(); i++) { buildTopLevelCityObjectNode(gen, root);
GenericCityObject goc = list.get(i); });
buildTopLevelCityObjectNode(goc, root);
}
} }
private void createTransportSectionNodes(TransportationObject t, TreeItem<Renderable> root) { private void createTransportSectionNodes(TransportationObject t, TreeItem<Renderable> root) {
...@@ -733,10 +889,9 @@ public class CityDoctorController { ...@@ -733,10 +889,9 @@ public class CityDoctorController {
} }
private void buildTunnelTreeFromList(List<Tunnel> list, TreeItem<Renderable> root) { private void buildTunnelTreeFromList(Supplier<Stream<Tunnel>> supply, TreeItem<Renderable> root) {
int tunnelChunk = tunnelChunkNr.get(); int tunnelChunk = tunnelChunkNr.get();
for (int i = tunnelChunk * MAX_FEATURES_PER_CHUNK; i < (tunnelChunk + 1) * MAX_FEATURES_PER_CHUNK && i < list.size(); i++) { supply.get().skip((long) tunnelChunk * MAX_FEATURES_PER_CHUNK).limit(MAX_FEATURES_PER_CHUNK).forEach(tunnel ->{
Tunnel tunnel = list.get(i);
TreeItem<Renderable> item = buildTopLevelCityObjectNode(tunnel, root); TreeItem<Renderable> item = buildTopLevelCityObjectNode(tunnel, root);
createTunnelHollowNodes(tunnel, item); createTunnelHollowNodes(tunnel, item);
...@@ -744,7 +899,8 @@ public class CityDoctorController { ...@@ -744,7 +899,8 @@ public class CityDoctorController {
createTunnelFurnitureNodes(tunnel, item); createTunnelFurnitureNodes(tunnel, item);
createTunnelConstructiveElementsNodes(tunnel, item); createTunnelConstructiveElementsNodes(tunnel, item);
createTunnelPartNodes(tunnel, item); createTunnelPartNodes(tunnel, item);
} });
} }
private void createTunnelInstallationNodes(Tunnel tunnel, TreeItem<Renderable> root) { private void createTunnelInstallationNodes(Tunnel tunnel, TreeItem<Renderable> root) {
...@@ -1236,15 +1392,13 @@ public class CityDoctorController { ...@@ -1236,15 +1392,13 @@ public class CityDoctorController {
private Map<ErrorId, ErrorStat> getErrorStats() { private Map<ErrorId, ErrorStat> getErrorStats() {
Map<ErrorId, ErrorStat> stats = new HashMap<>(); Map<ErrorId, ErrorStat> stats = new HashMap<>();
addErrorStats(stats, model.getBridges()); List<CheckError> errors = new ArrayList<>();
addErrorStats(stats, model.getBuildings()); model.createFeatureStream().forEach(co -> co.collectContainedErrors(errors));
addErrorStats(stats, model.getLand()); Set<CheckError> filteredErrors = new HashSet<>(errors);
addErrorStats(stats, model.getTunnels()); for (CheckError error : filteredErrors) {
addErrorStats(stats, model.getTransportation()); ErrorStat stat = stats.computeIfAbsent(error.getErrorId(), ErrorStat::new);
addErrorStats(stats, model.getVegetation()); stat.incrementCount();
addErrorStats(stats, model.getWater()); }
addErrorStats(stats, model.getCityFurniture());
addErrorStats(stats, model.getGenericCityObjects());
return stats; return stats;
} }
...@@ -1258,6 +1412,8 @@ public class CityDoctorController { ...@@ -1258,6 +1412,8 @@ public class CityDoctorController {
ErrorStat stat = stats.computeIfAbsent(error.getErrorId(), ErrorStat::new); ErrorStat stat = stats.computeIfAbsent(error.getErrorId(), ErrorStat::new);
stat.incrementCount(); stat.incrementCount();
} }
} }
public void export(CityObject co) { public void export(CityObject co) {
...@@ -1326,89 +1482,106 @@ public class CityDoctorController { ...@@ -1326,89 +1482,106 @@ public class CityDoctorController {
} }
private void filterBuildingTree(String searchString) { private void filterBuildingTree(String searchString) {
List<Building> foundBuildings = filterFeatures(searchString, model.getBuildings()); Supplier<Stream<Building>> foundBuildings = () -> model.getBuildings().
filter(co -> featureFilter(co, searchString));
int featureCount = (int) foundBuildings.get().count();
TreeView<Renderable> buildingsView = mainWindow.getBuildingsView(); TreeView<Renderable> buildingsView = mainWindow.getBuildingsView();
TreeItem<Renderable> root = buildingsView.getRoot(); TreeItem<Renderable> root = buildingsView.getRoot();
root.getChildren().clear(); root.getChildren().clear();
buildBuildingTreeFromList(foundBuildings, root); buildBuildingTreeFromList(foundBuildings, root);
updateTree(root); updateTree(root);
addMoreButtonToBuildingsIfNecessary(foundBuildings, buildingsView, root, buildingChunkNr); addMoreButtonForBuildings(buildingsView, root, buildingChunkNr, foundBuildings, featureCount);
} }
private void filterBridgeTree(String searchString) { private void filterBridgeTree(String searchString) {
List<BridgeObject> foundBridges = filterFeatures(searchString, model.getBridges()); Supplier<Stream<BridgeObject>> foundBridges = () -> model.getBridges().
filter(co -> featureFilter(co, searchString));
int featureCount = (int) foundBridges.get().count();
TreeView<Renderable> bridgeView = mainWindow.getBridgeView(); TreeView<Renderable> bridgeView = mainWindow.getBridgeView();
TreeItem<Renderable> root = bridgeView.getRoot(); TreeItem<Renderable> root = bridgeView.getRoot();
root.getChildren().clear(); root.getChildren().clear();
buildBridgeTreeFromList(foundBridges, root); buildBridgeTreeFromList(foundBridges, root);
updateTree(root); updateTree(root);
addMoreButtonIfNecessary(foundBridges, bridgeView, root, bridgeChunkNr); addMoreButtonForBridges(bridgeView, root, bridgeChunkNr, foundBridges, featureCount);
} }
private void filterTunnelTree(String searchString) { private void filterTunnelTree(String searchString) {
List<Tunnel> foundTunnel = filterFeatures(searchString, model.getTunnels()); Supplier<Stream<Tunnel>> foundTunnel = () -> model.getTunnels().filter(co -> featureFilter(co, searchString));
int featureCount = (int) foundTunnel.get().count();
TreeView<Renderable> tunnelView = mainWindow.getTunnelView(); TreeView<Renderable> tunnelView = mainWindow.getTunnelView();
TreeItem<Renderable> root = tunnelView.getRoot(); TreeItem<Renderable> root = tunnelView.getRoot();
root.getChildren().clear(); root.getChildren().clear();
buildTunnelTreeFromList(foundTunnel, root); buildTunnelTreeFromList(foundTunnel, root);
updateTree(root); updateTree(root);
addMoreButtonIfNecessary(foundTunnel, tunnelView, root, tunnelChunkNr); addMoreButtonForTunnel(tunnelView, root, tunnelChunkNr, foundTunnel, featureCount);
} }
private void filterVegetationTree(String searchString) { private void filterVegetationTree(String searchString) {
List<Vegetation> foundVegetation = filterFeatures(searchString, model.getVegetation()); Supplier<Stream<Vegetation>> foundVegetation = () -> model.getVegetation().
filter(co -> featureFilter(co, searchString));
int featureCount = (int) foundVegetation.get().count();
TreeView<Renderable> vegetationView = mainWindow.getVegetationView(); TreeView<Renderable> vegetationView = mainWindow.getVegetationView();
TreeItem<Renderable> root = vegetationView.getRoot(); TreeItem<Renderable> root = vegetationView.getRoot();
root.getChildren().clear(); root.getChildren().clear();
buildVegetationTreeFromList(foundVegetation, root); buildVegetationTreeFromList(foundVegetation, root);
updateTree(root); updateTree(root);
addMoreButtonIfNecessary(foundVegetation, vegetationView, root, vegetationChunkNr); addMoreButtonForVegetation(vegetationView, root, vegetationChunkNr, foundVegetation, featureCount);
} }
private void filterTransportationTree(String searchString) { private void filterTransportationTree(String searchString) {
List<TransportationObject> foundTransportation = filterFeatures(searchString, model.getTransportation()); Supplier<Stream<TransportationObject>> foundTransportation = () -> model.getTransportation().
filter(co -> featureFilter(co, searchString));
int featureCount = (int) foundTransportation.get().count();
TreeView<Renderable> transportationView = mainWindow.getTransportationView(); TreeView<Renderable> transportationView = mainWindow.getTransportationView();
TreeItem<Renderable> root = transportationView.getRoot(); TreeItem<Renderable> root = transportationView.getRoot();
root.getChildren().clear(); root.getChildren().clear();
buildTransportationTreeFromList(foundTransportation, root); buildTransportationTreeFromList(foundTransportation, root);
updateTree(root); updateTree(root);
addMoreButtonIfNecessary(foundTransportation, transportationView, root, transportationChunkNr); addMoreButtonForTransportation(transportationView, root, transportationChunkNr,
foundTransportation, featureCount);
} }
private void filterWaterTree(String searchString) { private void filterWaterTree(String searchString) {
List<WaterObject> foundWater = filterFeatures(searchString, model.getWater()); Supplier<Stream<WaterObject>> foundWater = () -> model.getWater().filter(co -> featureFilter(co, searchString));
int featureCount = (int) foundWater.get().count();
TreeView<Renderable> waterView = mainWindow.getWaterView(); TreeView<Renderable> waterView = mainWindow.getWaterView();
TreeItem<Renderable> root = waterView.getRoot(); TreeItem<Renderable> root = waterView.getRoot();
root.getChildren().clear(); root.getChildren().clear();
buildWaterTreeFromList(foundWater, root); buildWaterTreeFromList(foundWater, root);
addMoreButtonIfNecessary(foundWater, waterView, root, waterChunkNr); addMoreButtonForWater(waterView, root, waterChunkNr, foundWater, featureCount);
} }
private void filterLandTree(String searchString) { private void filterLandTree(String searchString) {
List<CityObject> foundLand = filterFeatures(searchString, model.getLand()); Supplier<Stream<CityObject>> foundLand = () -> model.getLand().filter(co -> featureFilter(co, searchString));
int featureCount = (int) foundLand.get().count();
TreeView<Renderable> landView = mainWindow.getTerrainView(); TreeView<Renderable> landView = mainWindow.getTerrainView();
TreeItem<Renderable> root = landView.getRoot(); TreeItem<Renderable> root = landView.getRoot();
root.getChildren().clear(); root.getChildren().clear();
buildLandFromList(foundLand, root); buildLandTreeFromList(foundLand, root);
addMoreButtonIfNecessary(foundLand, landView, root, landChunkNr); addMoreButtonForLand(landView, root, landChunkNr, foundLand, featureCount);
} }
private void filterCityFurnitureTree(String searchString) { private void filterCityFurnitureTree(String searchString) {
List<CityFurniture> foundCityFurniture = filterFeatures(searchString, model.getCityFurniture()); Supplier<Stream<CityFurniture>> foundCityFurniture = () -> model.getCityFurniture().
filter(co -> featureFilter(co, searchString));
int featureCount = (int) foundCityFurniture.get().count();
TreeView<Renderable> cityFurnitureView = mainWindow.getCityFurnitureView(); TreeView<Renderable> cityFurnitureView = mainWindow.getCityFurnitureView();
TreeItem<Renderable> root = cityFurnitureView.getRoot(); TreeItem<Renderable> root = cityFurnitureView.getRoot();
root.getChildren().clear(); root.getChildren().clear();
buildCityFurnitureTreeFromList(foundCityFurniture, root); buildCityFurnitureTreeFromList(foundCityFurniture, root);
addMoreButtonIfNecessary(foundCityFurniture, cityFurnitureView, root, cityFurnitureChunkNr); addMoreButtonForCityFurniture(cityFurnitureView, root, cityFurnitureChunkNr, foundCityFurniture, featureCount);
} }
private void filterGenericCityObjectTree(String searchString) { private void filterGenericCityObjectTree(String searchString) {
List<GenericCityObject> foundGenericCityObject = filterFeatures(searchString, model.getGenericCityObjects()); Supplier<Stream<GenericCityObject>> foundGenericCityObject = () -> model.getGenericCityObjects().
filter(co ->featureFilter(co, searchString));
int featureCount = (int) foundGenericCityObject.get().count();
TreeView<Renderable> genericCityObjectView = mainWindow.getGenericCityObjectView(); TreeView<Renderable> genericCityObjectView = mainWindow.getGenericCityObjectView();
TreeItem<Renderable> root = genericCityObjectView.getRoot(); TreeItem<Renderable> root = genericCityObjectView.getRoot();
root.getChildren().clear(); root.getChildren().clear();
buildGenericCityObjectTreeFromList(foundGenericCityObject, root); buildGenericCityObjectTreeFromList(foundGenericCityObject, root);
addMoreButtonIfNecessary(foundGenericCityObject, genericCityObjectView, root, genericCityObjectsChunkNr); addMoreButtonForGenericCityObjects(genericCityObjectView, root, genericCityObjectsChunkNr,
foundGenericCityObject, featureCount);
} }
private <T extends CityObject> List<T> filterFeatures(String searchString, List<T> features) { private <T extends CityObject> List<T> filterFeatures(String searchString, List<T> features) {
...@@ -1421,6 +1594,10 @@ public class CityDoctorController { ...@@ -1421,6 +1594,10 @@ public class CityDoctorController {
return foundFeatures; return foundFeatures;
} }
private boolean featureFilter(CityObject co, String searchString) {
return co.getGmlId().getGmlString().contains(searchString);
}
public void resetSearch(FeatureType selectedTab) { public void resetSearch(FeatureType selectedTab) {
if (model == null) { if (model == null) {
return; return;
...@@ -1455,94 +1632,174 @@ public class CityDoctorController { ...@@ -1455,94 +1632,174 @@ public class CityDoctorController {
} }
mainWindow.resetSearchBar(); mainWindow.resetSearchBar();
root.getChildren().clear(); root.getChildren().clear();
List<Building> errorBuildings = new ArrayList<>();
for (Building b : model.getBuildings()) {
if (b.containsAnyError()) {
errorBuildings.add(b);
}
}
resetFeatureChunks(); resetFeatureChunks();
buildBuildingTreeFromList(errorBuildings, root); List<GmlId> foundBuildings = new ArrayList<>();
model.getBuildings().filter(Checkable::containsAnyError).forEach(co -> foundBuildings.add(co.getGmlId()));
Supplier<Stream<Building>> supply = () -> model.getCache().getStream(foundBuildings).map(co -> (Building) co);
buildBuildingTreeFromList(supply, root);
updateTree(root); updateTree(root);
addMoreButtonToBuildingsIfNecessary(errorBuildings, buildingsView, root, buildingChunkNr); addMoreButtonForBuildings(buildingsView, root, buildingChunkNr, supply, foundBuildings.size());
} }
public void fillTreeViewWithErrorVegetation() { public void fillTreeViewWithErrorVegetation() {
TreeView<Renderable> vegetationView = mainWindow.getVegetationView();
TreeItem<Renderable> root = vegetationView.getRoot();
if (model == null) { if (model == null) {
return; return;
} }
fillTreeViewWithErrorCityObjects(mainWindow.getVegetationView(), model.getVegetation(), vegetationChunkNr); if (root == null) {
return;
}
mainWindow.resetSearchBar();
root.getChildren().clear();
resetFeatureChunks();
List<GmlId> foundVegetation = new ArrayList<>();
model.getVegetation().filter(Checkable::containsAnyError).forEach(co -> foundVegetation.add(co.getGmlId()));
Supplier<Stream<Vegetation>> supply = () -> model.getCache().getStream(foundVegetation).map(co -> (Vegetation) co);
buildVegetationTreeFromList(supply, root);
updateTree(root);
addMoreButtonForVegetation(vegetationView, root, vegetationChunkNr, supply, foundVegetation.size());
} }
public void fillTreeViewWithErrorBridges() { public void fillTreeViewWithErrorBridges() {
TreeView<Renderable> bridgeView = mainWindow.getBridgeView();
TreeItem<Renderable> root = bridgeView.getRoot();
if (model == null) { if (model == null) {
return; return;
} }
fillTreeViewWithErrorCityObjects(mainWindow.getBridgeView(), model.getBridges(), bridgeChunkNr); if (root == null) {
return;
}
mainWindow.resetSearchBar();
root.getChildren().clear();
resetFeatureChunks();
List<GmlId> foundBridge = new ArrayList<>();
model.getBridges().filter(Checkable::containsAnyError).forEach(co -> foundBridge.add(co.getGmlId()));
Supplier<Stream<BridgeObject>> supply = () -> model.getCache().getStream(foundBridge).map(co -> (BridgeObject) co);
buildBridgeTreeFromList(supply, root);
updateTree(root);
addMoreButtonForBridges(bridgeView, root, bridgeChunkNr, supply, foundBridge.size());
} }
public void fillTreeViewWithErrorLand() { public void fillTreeViewWithErrorLand() {
TreeView<Renderable> landView = mainWindow.getTerrainView();
TreeItem<Renderable> root = landView.getRoot();
if (model == null) { if (model == null) {
return; return;
} }
fillTreeViewWithErrorCityObjects(mainWindow.getTerrainView(), model.getLand(), landChunkNr); if (root == null) {
return;
}
mainWindow.resetSearchBar();
root.getChildren().clear();
resetFeatureChunks();
List<GmlId> foundLand = new ArrayList<>();
model.getLand().filter(Checkable::containsAnyError).forEach(co -> foundLand.add(co.getGmlId()));
Supplier<Stream<CityObject>> supply = () -> model.getCache().getStream(foundLand).map(co -> (CityObject) co);
buildLandTreeFromList(supply, root);
updateTree(root);
addMoreButtonForLand(landView, root, landChunkNr, supply, foundLand.size());
} }
public void fillTreeViewWithErrorCityFurniture() { public void fillTreeViewWithErrorCityFurniture() {
TreeView<Renderable> cityFurnitureView = mainWindow.getCityFurnitureView();
TreeItem<Renderable> root = cityFurnitureView.getRoot();
if (model == null) { if (model == null) {
return; return;
} }
fillTreeViewWithErrorCityObjects(mainWindow.getCityFurnitureView(), if (root == null) {
model.getCityFurniture(), cityFurnitureChunkNr); return;
}
mainWindow.resetSearchBar();
root.getChildren().clear();
resetFeatureChunks();
List<GmlId> foundCityFurniture = new ArrayList<>();
model.getCityFurniture().filter(Checkable::containsAnyError).forEach(co -> foundCityFurniture.add(co.getGmlId()));
Supplier<Stream<CityFurniture>> supply = () -> model.getCache().getStream(foundCityFurniture).map(co -> (CityFurniture) co);
buildCityFurnitureTreeFromList(supply, root);
updateTree(root);
addMoreButtonForCityFurniture(cityFurnitureView, root, cityFurnitureChunkNr, supply, foundCityFurniture.size());
} }
public void fillTreeViewWithErrorGenericCityObjects() { public void fillTreeViewWithErrorGenericCityObjects() {
TreeView<Renderable> genericCityObjectsView = mainWindow.getGenericCityObjectView();
TreeItem<Renderable> root = genericCityObjectsView.getRoot();
if (model == null) { if (model == null) {
return; return;
} }
fillTreeViewWithErrorCityObjects(mainWindow.getGenericCityObjectView(), if (root == null) {
model.getGenericCityObjects(), genericCityObjectsChunkNr); return;
}
mainWindow.resetSearchBar();
root.getChildren().clear();
resetFeatureChunks();
List<GmlId> foundGenericCityObjects = new ArrayList<>();
model.getGenericCityObjects().filter(Checkable::containsAnyError).forEach(co -> foundGenericCityObjects.add(co.getGmlId()));
Supplier<Stream<GenericCityObject>> supply = () -> model.getCache().getStream(foundGenericCityObjects).map(co -> (GenericCityObject) co);
buildGenericCityObjectTreeFromList(supply, root);
updateTree(root);
addMoreButtonForGenericCityObjects(genericCityObjectsView, root, genericCityObjectsChunkNr, supply, foundGenericCityObjects.size());
} }
public void fillTreeViewWithErrorTransportation() { public void fillTreeViewWithErrorTransportation() {
TreeView<Renderable> transportationView = mainWindow.getTransportationView();
TreeItem<Renderable> root = transportationView.getRoot();
if (model == null) { if (model == null) {
return; return;
} }
fillTreeViewWithErrorCityObjects(mainWindow.getTransportationView(), model.getTransportation(), if (root == null) {
transportationChunkNr); return;
}
mainWindow.resetSearchBar();
root.getChildren().clear();
resetFeatureChunks();
List<GmlId> foundTransportation = new ArrayList<>();
model.getTransportation().filter(Checkable::containsAnyError).forEach(co -> foundTransportation.add(co.getGmlId()));
Supplier<Stream<TransportationObject>> supply = () -> model.getCache().getStream(foundTransportation).map(co -> (TransportationObject) co);
buildTransportationTreeFromList(supply, root);
updateTree(root);
addMoreButtonForTransportation(transportationView, root, transportationChunkNr, supply, foundTransportation.size());
} }
public void fillTreeViewWithErrorTunnel() { public void fillTreeViewWithErrorTunnel() {
TreeView<Renderable> tunnelView = mainWindow.getTunnelView();
TreeItem<Renderable> root = tunnelView.getRoot();
if (model == null) { if (model == null) {
return; return;
} }
fillTreeViewWithErrorCityObjects(mainWindow.getTunnelView(), model.getTunnels(), tunnelChunkNr); if (root == null) {
return;
}
mainWindow.resetSearchBar();
root.getChildren().clear();
resetFeatureChunks();
List<GmlId> foundTunnel = new ArrayList<>();
model.getTunnels().filter(Checkable::containsAnyError).forEach(co -> foundTunnel.add(co.getGmlId()));
Supplier<Stream<Tunnel>> supply = () -> model.getCache().getStream(foundTunnel).map(co -> (Tunnel) co);
buildTunnelTreeFromList(supply, root);
updateTree(root);
addMoreButtonForTunnel(tunnelView, root, tunnelChunkNr, supply, foundTunnel.size());
} }
public void fillTreeViewWithErrorWater() { public void fillTreeViewWithErrorWater() {
TreeView<Renderable> waterView = mainWindow.getWaterView();
TreeItem<Renderable> root = waterView.getRoot();
if (model == null) { if (model == null) {
return; return;
} }
fillTreeViewWithErrorCityObjects(mainWindow.getWaterView(), model.getWater(), waterChunkNr);
}
private void fillTreeViewWithErrorCityObjects(TreeView<Renderable> treeView, List<? extends CityObject> objects,
AtomicInteger chunkCounter) {
TreeItem<Renderable> root = treeView.getRoot();
if (root == null) { if (root == null) {
return; return;
} }
mainWindow.resetSearchBar(); mainWindow.resetSearchBar();
root.getChildren().clear(); root.getChildren().clear();
List<CityObject> errorObjects = new ArrayList<>(); resetFeatureChunks();
for (CityObject co : objects) { List<GmlId> foundWater = new ArrayList<>();
if (co.containsAnyError()) { model.getWater().filter(Checkable::containsAnyError).forEach(co -> foundWater.add(co.getGmlId()));
errorObjects.add(co); Supplier<Stream<WaterObject>> supply = () -> model.getCache().getStream(foundWater).map(co -> (WaterObject) co);
} buildWaterTreeFromList(supply, root);
}
buildTreeFromList(errorObjects, root, chunkCounter);
updateTree(root); updateTree(root);
addMoreButtonIfNecessary(errorObjects, treeView, root, chunkCounter); addMoreButtonForWater(waterView, root, tunnelChunkNr, supply, foundWater.size());
} }
public void fillTreeViewFromFeatures(FeatureType selectedTab) { public void fillTreeViewFromFeatures(FeatureType selectedTab) {
...@@ -1584,7 +1841,7 @@ public class CityDoctorController { ...@@ -1584,7 +1841,7 @@ public class CityDoctorController {
updateTree(mainWindow.getCityFurnitureView().getRoot()); updateTree(mainWindow.getCityFurnitureView().getRoot());
break; break;
case GENERIC_CITY_OBJECT: case GENERIC_CITY_OBJECT:
buildOtherCityObjects(model); buildGenericCityObjects(model);
updateTree(mainWindow.getGenericCityObjectView().getRoot()); updateTree(mainWindow.getGenericCityObjectView().getRoot());
break; break;
default: default:
...@@ -1673,6 +1930,9 @@ public class CityDoctorController { ...@@ -1673,6 +1930,9 @@ public class CityDoctorController {
case TRANSPORTATION: case TRANSPORTATION:
fillTreeViewWithErrorTransportation(); fillTreeViewWithErrorTransportation();
break; break;
case TUNNEL:
fillTreeViewWithErrorTunnel();
break;
case WATER: case WATER:
fillTreeViewWithErrorWater(); fillTreeViewWithErrorWater();
break; break;
......
...@@ -13,6 +13,7 @@ import javafx.scene.paint.PhongMaterial; ...@@ -13,6 +13,7 @@ import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.*; import javafx.scene.shape.*;
import java.util.*; import java.util.*;
import java.util.stream.Stream;
public class TriangulatedGeometry { public class TriangulatedGeometry {
...@@ -98,7 +99,7 @@ public class TriangulatedGeometry { ...@@ -98,7 +99,7 @@ public class TriangulatedGeometry {
private static void addPolygonDataFromCityObjects(List<? extends CityObject> cos, private static void addPolygonDataFromCityObjects(Stream<? extends CityObject> cos,
TriangulatedGeometry triGeom, Color color, List<ViewFilter> filters) { TriangulatedGeometry triGeom, Color color, List<ViewFilter> filters) {
CityObjectCollector collector = new CityObjectCollector(); CityObjectCollector collector = new CityObjectCollector();
cos.forEach(cityObject -> cityObject.accept(collector)); cos.forEach(cityObject -> cityObject.accept(collector));
......
package de.hft.stuttgart.citydoctor2.gui.tree;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.gui.CityDoctorController;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class AddMoreButtonRunnable implements Runnable{
private int featureCount;
private final TreeView<Renderable> view;
private final TreeItem<Renderable> root;
private final AtomicInteger chunkCounter;
private static final int MAX_FEATURES_PER_CHUNK = CityDoctorController.getMaxFeaturesPerChunk();
protected AddMoreButtonRunnable(int featureCount, TreeView<Renderable> view,
TreeItem<Renderable> root, AtomicInteger chunkCounter){
this.featureCount = featureCount;
this.view = view;
this.root = root;
this.chunkCounter = chunkCounter;
}
@Override
public void run() {
// 29 is the height of one item
int numberOfDisplayedItems = (int) (view.getHeight() / 29);
int selectedIndex = view.getSelectionModel().getSelectedIndex();
// remove button node
root.getChildren().remove(root.getChildren().size() - 1);
chunkCounter.getAndIncrement();
int visibleFeatures = countVisibleNodes(view);
buildNodeTreeCall();
if (selectedIndex >= 0) {
view.getSelectionModel().select(selectedIndex);
}
if ((chunkCounter.get() + 1) * MAX_FEATURES_PER_CHUNK < featureCount) {
root.getChildren().add(new TreeItem<>(new ButtonRenderable(this)));
}
view.scrollTo(visibleFeatures - numberOfDisplayedItems + 1);
}
/**
* Function hook for the buildTree call of the view
*/
public abstract void buildNodeTreeCall();
private static int countVisibleNodes(TreeView<Renderable> view) {
return countVisibleNodes(view.getRoot()) + 1;
}
private static int countVisibleNodes(TreeItem<Renderable> root) {
int visibleFeatures = root.getChildren().size();
for (TreeItem<Renderable> child : root.getChildren()) {
if (!child.getChildren().isEmpty() && child.isExpanded()) {
visibleFeatures += countVisibleNodes(child);
}
}
return visibleFeatures;
}
}
...@@ -12,9 +12,11 @@ public class CityObjectNode extends Renderable { ...@@ -12,9 +12,11 @@ public class CityObjectNode extends Renderable {
private static final Logger logger = LogManager.getLogger(CityObjectNode.class); private static final Logger logger = LogManager.getLogger(CityObjectNode.class);
private final CityObject co; private final CityObject co;
private String text;
public CityObjectNode(CityObject co) { public CityObjectNode(CityObject co) {
this.co = co; this.co = co;
this.text = co.getDisplayText();
} }
@Override @Override
...@@ -30,7 +32,7 @@ public class CityObjectNode extends Renderable { ...@@ -30,7 +32,7 @@ public class CityObjectNode extends Renderable {
@Override @Override
public String getText() { public String getText() {
return co.getDisplayText(); return text;
} }
@Override @Override
......
...@@ -689,9 +689,7 @@ public class HealerController { ...@@ -689,9 +689,7 @@ public class HealerController {
// triangle area is too small, ignore // triangle area is too small, ignore
return; return;
} }
for (Building b : model.getBuildings()) { model.getBuildings().forEach(building -> MeshGenerator.replaceGeometryWithMeshedGeometry(building, area));
MeshGenerator.replaceGeometryWithMeshedGeometry(b, area);
}
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// no real number was inserted, do nothing // no real number was inserted, do nothing
} }
......
...@@ -329,7 +329,7 @@ public class GeneticsGUI extends View { ...@@ -329,7 +329,7 @@ public class GeneticsGUI extends View {
parserConfig); parserConfig);
currentChecker = new Checker(model1); currentChecker = new Checker(model1);
currentChecker.runChecks(); currentChecker.runChecks();
Building building = model1.getBuildings().get(0); Building building = model1.getBuildings().toList().getFirst();
currentGeometry = building.getGeometries().get(0); currentGeometry = building.getGeometries().get(0);
currentFeature = building; currentFeature = building;
showCurrentGeometry(); showCurrentGeometry();
......
...@@ -298,7 +298,7 @@ ...@@ -298,7 +298,7 @@
<!--CityDoctor2 Extension Modules--> <!--CityDoctor2 Extension Modules-->
<module>Extensions/CityDoctorGUI</module> <module>Extensions/CityDoctorGUI</module>
<module>Extensions/CityDoctorAutoPro</module> <!-- <module>Extensions/CityDoctorAutoPro</module>-->
<module>Extensions/CityDoctorHealer</module> <module>Extensions/CityDoctorHealer</module>
<module>Extensions/CityDoctorHealerGenetic</module> <module>Extensions/CityDoctorHealerGenetic</module>
<module>Extensions/CityDoctorHealerGUI</module> <module>Extensions/CityDoctorHealerGUI</module>
......
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