Commit 706f5731 authored by Eric Duminil's avatar Eric Duminil
Browse files

RegionChooser: Wild test to see if it could run as a standalone project.

parent 41fcf416
Showing with 742 additions and 1 deletion
+742 -1
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.fx.ide.jdt.core.JAVAFX_CONTAINER"/> <classpathentry kind="con" path="org.eclipse.fx.ide.jdt.core.JAVAFX_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/> <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
<classpathentry combineaccessrules="false" kind="src" path="/GeoLibs"/> <classpathentry kind="lib" path="/GeoLibs/lib/proj4j-0.1.0.jar" sourcepath="/GeoLibs/lib/proj4j-0.1.0-sources.jar"/>
<classpathentry kind="lib" path="/GeoLibs/lib/citygml4j-2.10.2.jar" sourcepath="/GeoLibs/lib/citygml4j-2.10.2.zip"/>
<classpathentry kind="lib" path="/GeoLibs/lib/vtd-xml_2_13_1.jar"/>
<classpathentry kind="lib" path="/GeoLibs/lib/jts-core-1.16.1.jar" sourcepath="/GeoLibs/lib/jts-core-1.16.1-sources.jar"/>
<classpathentry kind="output" path="bin"/> <classpathentry kind="output" path="bin"/>
</classpath> </classpath>
package eu.simstadt.geo;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Polygon;
import org.osgeo.proj4j.BasicCoordinateTransform;
import org.osgeo.proj4j.CRSFactory;
import org.osgeo.proj4j.CoordinateReferenceSystem;
import org.osgeo.proj4j.ProjCoordinate;
public class GeoUtils
{
private static final CRSFactory CRS_FACTORY = new CRSFactory();
public static final CoordinateReferenceSystem WGS84 = CRS_FACTORY.createFromName("EPSG:4326");
private static final Pattern srsNamePattern = Pattern.compile("(?i)(?<=srsName=[\"'])[^\"']+(?=[\"'])");
private GeoUtils() {
// only static use
}
/**
* Writes down the input coordinates in a string, without "." or "-". E.g. "N48_77__E9_18" for (48.77, 9.18) and
* "S12_345__W6_789" for (-12.345, -6.789).
*
* Useful for caching geolocated information. The process should be reversible.
*
* WARNING: LATITUDE FIRST!
*
* @param latitude
* @param longitude
* @return Filename friendly string for given coordinates
*/
public static String coordinatesForFilename(double latitude, double longitude) {
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance();
symbols.setDecimalSeparator('.');
DecimalFormat df = new DecimalFormat("+#.######;-#.######", symbols);
String lat = df.format(latitude).replace('-', 'S').replace('+', 'N');
String lon = df.format(longitude).replace('-', 'W').replace('+', 'E');
return (lat + "__" + lon).replace('.', '_');
}
/**
* The srsName (The name by which this reference system is identified) inside the CityGML file can have multiple
* formats. This method tries to parse the string and detect the corresponding reference system. If it is found, it
* returns a proj4j.CoordinateReferenceSystem. It throws an IllegalArgumentException otherwise.
*
* This method should be able to parse any EPSG id : e.g. "EPSG:1234". German Citygmls might also have "DE_DHDN_3GK3"
* or "ETRS89_UTM32" as srsName, so those are also included. It isn't guaranteed that those formats are correctly
* parsed, though.
*
* The EPSG ids and parameters are defined in resources ('nad/epsg') inside proj4j-0.1.0.jar. Some EPSG ids are
* missing though, e.g. 7415
*
* @param srsName
* @return CoordinateReferenceSystem
*/
public static CoordinateReferenceSystem crsFromSrsName(String srsName) {
// EPSG:31467
Pattern pEPSG = Pattern.compile("^(EPSG:\\d+)$");
Matcher mEPSG = pEPSG.matcher(srsName);
if (mEPSG.find()) {
return CRS_FACTORY.createFromName(srsName);
}
// urn:ogc:def:crs,crs:EPSG:6.12:31467,crs:EPSG:6.12:5783
// or
// urn:ogc:def:crs,crs:EPSG::28992
Pattern pOGC = Pattern.compile("urn:ogc:def:crs(?:,crs)?:EPSG:[\\d\\.]*:([\\d]+)\\D*");
Matcher mOGC = pOGC.matcher(srsName);
if (mOGC.find()) {
return CRS_FACTORY.createFromName("EPSG:" + mOGC.group(1));
}
// urn:adv:crs:DE_DHDN_3GK3*DE_DHHN92_NH
// urn:adv:crs:ETRS89_UTM32*DE_DHHN92_NH
Pattern pURN = Pattern.compile("urn:adv:crs:([^\\*]+)");
Matcher mURN = pURN.matcher(srsName);
//NOTE: Could use a HashMap if the switch/case becomes too long.
if (mURN.find()) {
switch (mURN.group(1)) {
case "DE_DHDN_3GK2":
return CRS_FACTORY.createFromName("EPSG:31466");
case "DE_DHDN_3GK3":
return CRS_FACTORY.createFromName("EPSG:31467");
case "DE_DHDN_3GK4":
return CRS_FACTORY.createFromName("EPSG:31468");
case "DE_DHDN_3GK5":
return CRS_FACTORY.createFromName("EPSG:31469");
case "ETRS89_UTM32":
return CRS_FACTORY.createFromName("EPSG:25832");
default:
// nothing found
}
}
throw new IllegalArgumentException("Unknown srsName format: " + srsName);
}
/**
* Converts a jts.geom.Polygon from one CoordinateReferenceSystem to another.
*
* NOTE: It would be easier with org.geotools.referencing.CRS instead of Proj4J
*
* @param polygonInOriginalCRS
* @param originalCRS
* @param newCRS
* @return
*/
public static Polygon changePolygonCRS(Polygon polygonInOriginalCRS, CoordinateReferenceSystem originalCRS,
CoordinateReferenceSystem newCRS) {
GeometryFactory geometryFactory = new GeometryFactory();
Coordinate[] convexHullcoordinates = polygonInOriginalCRS.getCoordinates();
BasicCoordinateTransform transformToWgs84 = new BasicCoordinateTransform(originalCRS, newCRS);
for (int i = 0; i < convexHullcoordinates.length; i++) {
ProjCoordinate wgs84Coordinate = transformToWgs84.transform(
new ProjCoordinate(convexHullcoordinates[i].x, convexHullcoordinates[i].y), new ProjCoordinate());
convexHullcoordinates[i] = new Coordinate(wgs84Coordinate.x, wgs84Coordinate.y);
}
return geometryFactory.createPolygon(convexHullcoordinates);
}
/**
*
* Fast scan of the 50 first lines of a Citygml file to look for srsName. It might not be as reliable as parsing the
* whole CityGML, but it should be much faster and use much less memory. For a more reliable solution, use
* GeoCoordinatesAccessor. This solution can be convenient for Web services, RegionChooser or HullExtractor.
*
* @param citygmlPath
* @return
* @throws IOException
*/
public static CoordinateReferenceSystem crsFromCityGMLHeader(Path citygmlPath) throws IOException {
Optional<String> line = Files.lines(citygmlPath).limit(50).filter(srsNamePattern.asPredicate()).findFirst();
if (line.isPresent()) {
Matcher matcher = srsNamePattern.matcher(line.get());
matcher.find();
return crsFromSrsName(matcher.group());
} else {
throw new IllegalArgumentException("No srsName found in the header of " + citygmlPath);
}
}
/**
* The haversine formula determines the great-circle distance between two points on a sphere given their longitudes
* and latitudes. https://en.wikipedia.org/wiki/Haversine_formula
*
* @param latitude1
* @param longitude1
* @param latitude2
* @param longitude2
* @return distance in kilometer
*/
public static Double greatCircleDistance(Double latitude1, Double longitude1, Double latitude2, Double longitude2) {
final int earthRadius = 6371; // [km]. Radius of the earth
double latDistance = Math.toRadians(latitude2 - latitude1);
double lonDistance = Math.toRadians(longitude2 - longitude1);
double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2) + Math.cos(Math.toRadians(latitude1))
* Math.cos(Math.toRadians(latitude2)) * Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return earthRadius * c;
}
/**
* Finds every CityGML in every .proj folder in a repository.
*
* @param repository
* @return a stream of CityGML Paths
* @throws IOException
*/
public static Stream<Path> everyCityGML(Path repository) throws IOException {
return Files.walk(repository, 2)
.sorted()
.filter(
p -> Files.isRegularFile(p) &&
p.getParent().toString().endsWith(".proj") &&
p.toString().toLowerCase().endsWith(".gml"));
}
}
package eu.simstadt.geo.fast_xml_parser;
import com.ximpleware.AutoPilot;
import com.ximpleware.NavException;
import com.ximpleware.VTDNav;
import com.ximpleware.XPathEvalException;
import com.ximpleware.XPathParseException;
public class BuildingXmlNode
{
private int buildingOffset;
private int buildingLength;
private VTDNav navigator;
private AutoPilot coordinatesFinder;
public Double x;
public Double xMin;
public Double xMax;
public Double yMin;
public Double yMax;
public Double y;
private int coordinatesCount = 0;
public BuildingXmlNode(VTDNav navigator, int buildingOffset, int buildingLength)
throws XPathParseException, XPathEvalException, NavException {
this.navigator = navigator;
this.coordinatesFinder = new AutoPilot(navigator);
this.buildingLength = buildingLength;
this.buildingOffset = buildingOffset;
extractCoordinates(); //NOTE: Should it be done lazily? Is there any reason to extract a BuildingXmlNode without coordinates?
}
public boolean hasCoordinates() {
return coordinatesCount > 0;
}
private void extractCoordinates()
throws XPathParseException, XPathEvalException, NavException {
double xTotal = 0;
double yTotal = 0;
double tempX;
double tempY;
double tempXMin = Double.MAX_VALUE;
double tempXMax = Double.MIN_VALUE;
double tempYMin = Double.MAX_VALUE;
double tempYMax = Double.MIN_VALUE;
coordinatesFinder.selectXPath(".//posList|.//pos");
while (coordinatesFinder.evalXPath() != -1) {
long offsetAndLength = navigator.getContentFragment();
int coordinatesOffset = (int) offsetAndLength;
int coordinatesLength = (int) (offsetAndLength >> 32);
String posList = navigator.toRawString(coordinatesOffset, coordinatesLength);
String[] coordinates = posList.trim().split("\\s+");
for (int k = 0; k < coordinates.length; k = k + 3) {
coordinatesCount++;
tempX = Double.valueOf(coordinates[k]);
tempY = Double.valueOf(coordinates[k + 1]);
if (tempX < tempXMin) {
tempXMin = tempX;
}
if (tempY < tempYMin) {
tempYMin = tempY;
}
if (tempX > tempXMax) {
tempXMax = tempX;
}
if (tempY > tempYMax) {
tempYMax = tempY;
}
xTotal += tempX;
yTotal += tempY;
}
}
this.xMin = tempXMin;
this.xMax = tempXMax;
this.yMin = tempYMin;
this.yMax = tempYMax;
this.x = xTotal / coordinatesCount;
this.y = yTotal / coordinatesCount;
}
public String toString() {
try {
return navigator.toRawString(buildingOffset, buildingLength);
} catch (NavException ex) {
return "";
}
}
}
package eu.simstadt.geo.fast_xml_parser;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.logging.Logger;
import com.ximpleware.AutoPilot;
import com.ximpleware.NavException;
import com.ximpleware.VTDGen;
import com.ximpleware.VTDNav;
import com.ximpleware.XPathEvalException;
import com.ximpleware.XPathParseException;
public class CityGmlIterator implements Iterable<BuildingXmlNode>
{
private static final Logger LOGGER = Logger.getLogger(CityGmlIterator.class.getName());
private AutoPilot buildingsFinder;
private VTDNav navigator;
private int buildingOffset = 0;
private int buildingLength = 0;
private Path citygmlPath;
/*
* Simple class to parse a CityGML and extract cityObjectMember XML nodes and their coordinates. No other attribute
* is extracted. Not suitable for building simulation, perfect for quick and dirty extract of coordinates (e.g. for
* RegionChooser or HullExtractor). It should be fast and not use much memory. A SaxParser would use even less memory
* but might be harder to code and possibly slower to run.
*
* For a more complete and more robust (but slower) implementation, use eu.simstadt.geo.GeoCoordinatesAccessor
*
* Based on VTD XML, it provides a Building iterator.
*
*/
public CityGmlIterator(Path citygmlPath) throws XPathParseException {
this.citygmlPath = citygmlPath;
VTDGen parser = new VTDGen();
parser.parseFile(citygmlPath.toString(), false);
this.navigator = parser.getNav();
this.buildingsFinder = new AutoPilot(navigator);
buildingsFinder.selectXPath("/CityModel/cityObjectMember[Building]"); //TODO: Check it's the only correct possibility. //FIXME: BuildingPart too!
}
@Override
public Iterator<BuildingXmlNode> iterator() {
return new Iterator<BuildingXmlNode>() {
@Override
public boolean hasNext() {
try {
return buildingsFinder.evalXPath() != -1;
} catch (XPathEvalException | NavException ex) {
LOGGER.warning("Error while parsing " + citygmlPath);
return false;
}
}
@Override
public BuildingXmlNode next() {
try {
long offsetAndLength = navigator.getElementFragment();
buildingOffset = (int) offsetAndLength;
buildingLength = (int) (offsetAndLength >> 32);
return new BuildingXmlNode(navigator, buildingOffset, buildingLength);
} catch (NavException | NumberFormatException | XPathParseException | XPathEvalException ex) {
LOGGER.warning("Error while parsing " + citygmlPath);
}
return null;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Returns Header of the Citygml, from the start of the file to the current building. This method needs to be called
* directly after the first building has been found.
*
* @return Citygml header
* @throws NavException
*/
public String getHeader() throws NavException {
return navigator.toRawString(0, buildingOffset);
}
/**
* Returns footer of the Citygml, from the end of the last building to the end of file. This method needs to be
* called after the last building has been found.
*
* @return Citygml footer
* @throws NavException
*/
public Object getFooter() throws IOException, NavException {
int footerOffset = buildingOffset + buildingLength;
int footerLength = (int) (Files.size(citygmlPath) - footerOffset);
return navigator.toRawString(footerOffset, footerLength);
}
}
package eu.simstadt.geo.fast_xml_parser;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.logging.Logger;
import org.locationtech.jts.algorithm.ConvexHull;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Polygon;
import org.osgeo.proj4j.CoordinateReferenceSystem;
import com.ximpleware.XPathParseException;
import eu.simstadt.geo.GeoUtils;
public class ConvexHullCalculator
{
private static final Logger LOGGER = Logger.getLogger(ConvexHullCalculator.class.getName());
/**
* Relatively fast method to extract a convex hull in WGS84 for any Citygml file for which CRS is known and whose
* size is less than 2GB (VTD XML limitation). It iterates over every building, gets the bounding box as 4
* coordinates and calculates a convex hull of all those coordinates at the end of the iteration. It finally converts
* this convex hull to WGS84 coordinates.
*
* The convex hull is needed for RegionChooser. No SimstadtModel or CityDoctor model is needed, thanks to
* eu.simstadt.geo.fast_xml_parser.CityGmlIterator.
*
* E.g. : POLYGON ((9.219282617376651 48.876828283254675, 9.2175568365387 48.87695546490524, 9.213228008654541
* 48.87741235218009, 9.21293830332426 48.8774437308139, 9.212628150503749 48.87995232037036, 9.21263222062228
* 48.880912500705406, 9.213601288895058 48.88138432918416, 9.213701498914048 48.8813841435154, 9.217160380858063
* 48.88092123714512, 9.217396359933812 48.88085245173012, 9.217558989421159 48.880784074008496, 9.219328059150042
* 48.879894270949485, 9.219367632516049 48.876847095121995, 9.219367549551574 48.87682812171015, 9.219282617376651
* 48.876828283254675))
*
* for "20140218_Gruenbuehl_LOD2.gml"
*
* @param citygmlPath
* @return convex Hull as jts.geom.Geometry, with the originalCRS written in userData.
*/
public static Geometry calculateFromCityGML(Path citygmlPath) throws XPathParseException, IOException {
GeometryFactory geometryFactory = new GeometryFactory();
ArrayList<Coordinate> allPoints = new ArrayList<>();
CityGmlIterator citygml = new CityGmlIterator(citygmlPath);
for (BuildingXmlNode buildingXmlNode : citygml) {
if (buildingXmlNode.hasCoordinates()) {
allPoints.add(new Coordinate(buildingXmlNode.xMin, buildingXmlNode.yMin));
allPoints.add(new Coordinate(buildingXmlNode.xMin, buildingXmlNode.yMax));
allPoints.add(new Coordinate(buildingXmlNode.xMax, buildingXmlNode.yMin));
allPoints.add(new Coordinate(buildingXmlNode.xMax, buildingXmlNode.yMax));
}
}
Coordinate[] pointsArray = allPoints.toArray(new Coordinate[allPoints.size()]);
ConvexHull ch = new org.locationtech.jts.algorithm.ConvexHull(pointsArray, geometryFactory);
// Convert convex hull in original coordinates to WGS84 coordinates.
// NOTE: It's faster to convert to WGS84 once the convex hull is calculated, because there are fewer points
Polygon originalConvexHull = (Polygon) ch.getConvexHull();
CoordinateReferenceSystem originalCRS = GeoUtils.crsFromCityGMLHeader(citygmlPath);
Polygon convexHull = GeoUtils.changePolygonCRS(originalConvexHull, originalCRS, GeoUtils.WGS84);
convexHull.setUserData(originalCRS.toString());
return convexHull;
}
/**
* Finds every CityGML in a folder recursively, extract their convex hull and send them to callback. Used by
* RegionChooser Javascript to display the CityGMLs.
*
* If hull has been extracted, get it from cache, calculate it otherwise.
*
* @param repository
* @param callback
* @throws IOException
*/
public static void extractHullsForEveryCityGML(Path repository, Consumer<String> callback) throws IOException {
LOGGER.info("Parsing " + repository);
GeoUtils.everyCityGML(repository)
.map(gmlPath -> {
LOGGER.fine("Importing " + repository.relativize(gmlPath));
try {
Path kmlPath = getHullPath(repository, gmlPath);
if (Files.exists(kmlPath)) {
LOGGER.fine("Using cache from " + repository.relativize(kmlPath));
return new String(Files.readAllBytes(kmlPath), StandardCharsets.UTF_8);
} else {
LOGGER.info("No hull found in cache for " + repository.relativize(gmlPath) + ". Creating it!");
return calculateConvexHullPlacemark(gmlPath, kmlPath, false);
}
} catch (IOException ex) {
ex.printStackTrace();
}
return null;
})
.filter(Objects::nonNull)
.forEach(hullKML -> {
if (hullKML.contains("<LinearRing>")) {
callback.accept(hullKML);
} else {
LOGGER.warning("Cache is empty!");
}
});
}
/**
* Writes a KML cache file for the convex hull of a CityGML file and returns the KML content as string. Depending on
* throwIfError, the method will either ignore any problem (and write the message inside the KML file (preventing the
* process from happening again next time) or stop the process, without returning or caching anything.
*
* @param gmlPath
* @param kmlPath
* @param throwIfError
* @return the KML content as a String
*/
public static String calculateConvexHullPlacemark(Path gmlPath, Path kmlPath, boolean throwIfError) {
String result;
try {
Geometry convexHull = ConvexHullCalculator.calculateFromCityGML(gmlPath);
//NOTE: Header and footer with <kml><Document> don't seem to be needed. They're easier to concatenate without.
StringBuilder kmlPlacemark = new StringBuilder();
kmlPlacemark.append(" <Placemark>\n");
kmlPlacemark.append(" <name>");
kmlPlacemark.append(gmlPath.getFileName());
kmlPlacemark.append("</name>\n");
kmlPlacemark.append(" <ExtendedData>\n");
kmlPlacemark.append(" <Data name=\"project\">\n");
kmlPlacemark.append(" <value>");
kmlPlacemark.append(gmlPath.getParent().getFileName().toString().replace(".proj", ""));
kmlPlacemark.append("</value>\n");
kmlPlacemark.append(" </Data>\n");
kmlPlacemark.append(" <Data name=\"srsName\">\n");
kmlPlacemark.append(" <value>");
kmlPlacemark.append(convexHull.getUserData());
kmlPlacemark.append("</value>\n");
kmlPlacemark.append(" </Data>\n");
kmlPlacemark.append(" </ExtendedData>\n");
kmlPlacemark.append(" <Polygon>\n");
kmlPlacemark.append(" <outerBoundaryIs>\n");
kmlPlacemark.append(" <LinearRing>\n");
kmlPlacemark.append(" <tessellate>1</tessellate>\n");
kmlPlacemark.append(" <coordinates>");
for (Coordinate coordinate : convexHull.getCoordinates()) {
kmlPlacemark.append(coordinate.x);
kmlPlacemark.append(",");
kmlPlacemark.append(coordinate.y);
kmlPlacemark.append(",0 ");
}
kmlPlacemark.append("</coordinates>\n");
kmlPlacemark.append(" </LinearRing>\n");
kmlPlacemark.append(" </outerBoundaryIs>\n");
kmlPlacemark.append(" </Polygon>\n");
kmlPlacemark.append(" </Placemark>\n");
result = kmlPlacemark.toString();
} catch (Exception ex) {
// Something went wrong.
LOGGER.severe("Couldn't calculate hull : " + ex.getMessage());
// Either throw again:
if (throwIfError) {
throw new RuntimeException(ex);
}
// Or write the error message inside a comment in the KML cache file. It could be dangerous (the file doesn't contain any geometry)
// but will prevent the same process from happening next time again. This could be desirable if the Error happens at the end of a 2GB file.
StringWriter errors = new StringWriter();
ex.printStackTrace(new PrintWriter(errors));
result = "<kml>\n<!--\n" + errors.toString() + "\n-->\n</kml>";
}
try (BufferedWriter writer = Files.newBufferedWriter(kmlPath)) {
writer.write(result);
} catch (IOException ex) {
ex.printStackTrace();
}
LOGGER.info("Convex hull written to cache.");
return result;
}
/**
* For a given CityGML Path, returns the Path at which KML hull should be cached. If needed it creates the '.cache'
* folder and hides it. The '.cache' folder isn't specific to the project: every kml cache file is written inside the
* same repository '.cache' folder.
*
* @param repository
* @param citygmlPath
*
* @return KML Path
* @throws IOException
*/
private static Path getHullPath(Path repository, Path citygmlPath) throws IOException {
String kmlFilename = citygmlPath.getFileName().toString().replaceAll("(?i)\\.gml$", ".kml");
Path cacheFolder = repository.resolve(".cache/");
Path hullsFolder = cacheFolder.resolve("hulls/");
Files.createDirectories(hullsFolder);
try {
Files.setAttribute(cacheFolder, "dos:hidden", true);
} catch (UnsupportedOperationException ex) {
// do nothing for non-windows os
}
return hullsFolder.resolve(kmlFilename);
}
}
package eu.simstadt.geo.fast_xml_parser;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.Test;
import com.ximpleware.NavException;
import com.ximpleware.XPathEvalException;
import com.ximpleware.XPathParseException;
public class CitygmlParserTests
{
private void testNoNanInCoordinates(Path citygmlPath)
throws NumberFormatException, XPathParseException, NavException, XPathEvalException, IOException {
CityGmlIterator buildingXmlNodes = new CityGmlIterator(citygmlPath);
for (BuildingXmlNode buildingXmlNode : buildingXmlNodes) {
assertTrue("Buildings should have coordinates", buildingXmlNode.hasCoordinates());
assertFalse("Coordinate should be a double", Double.isNaN(buildingXmlNode.x));
assertFalse("Coordinate should be a double", Double.isNaN(buildingXmlNode.y));
assertFalse("Coordinate should be a double", Double.isNaN(buildingXmlNode.xMax));
assertFalse("Coordinate should be a double", Double.isNaN(buildingXmlNode.yMax));
assertFalse("Coordinate should be a double", Double.isNaN(buildingXmlNode.xMin));
assertFalse("Coordinate should be a double", Double.isNaN(buildingXmlNode.yMin));
assertTrue("Coordinates Min/Max should be plausible", buildingXmlNode.xMax > buildingXmlNode.x);
assertTrue("Coordinates Min/Max should be plausible", buildingXmlNode.yMax > buildingXmlNode.y);
assertTrue("Coordinates Min/Max should be plausible", buildingXmlNode.xMin < buildingXmlNode.x);
assertTrue("Coordinates Min/Max should be plausible", buildingXmlNode.yMin < buildingXmlNode.y);
}
}
@Test
public void testExtractCoordsFromStuttgart()
throws NumberFormatException, XPathParseException, NavException, XPathEvalException, IOException {
Path repo = Paths.get("../TestRepository");
Path citygmlPath = repo.resolve("Stuttgart.proj/Stuttgart_LOD0_LOD1_buildings_and_trees.gml");
testNoNanInCoordinates(citygmlPath);
}
@Test
public void testExtractCoordsFromGruenbuehl() throws Throwable {
Path repo = Paths.get("../TestRepository");
Path citygmlPath = repo.resolve("Gruenbuehl.proj/20140218_Gruenbuehl_LOD2_1building.gml");
testNoNanInCoordinates(citygmlPath);
}
@Test
public void testExtractCoordsFromMunich() throws Throwable {
Path repo = Paths.get("../TestRepository");
Path citygmlPath = repo.resolve("Muenchen.proj/Munich_v_1_0_0.gml");
testNoNanInCoordinates(citygmlPath);
}
@Test
public void testExtractCoordsFromNYC() throws Throwable {
Path repo = Paths.get("../TestRepository");
Path citygmlPath = repo.resolve("NewYork.proj/ManhattanSmall.gml");
testNoNanInCoordinates(citygmlPath);
}
@Test
public void testExtractNoCoordsFromEmptyBuilding() throws Throwable {
Path repo = Paths.get("../TestRepository");
Path citygmlPath = repo.resolve("Ensource.proj/Stöckach_empty_buildings.gml");
CityGmlIterator buildingXmlNodes = new CityGmlIterator(citygmlPath);
int counter = 0;
for (BuildingXmlNode buildingXmlNode : buildingXmlNodes) {
assertFalse("Empty Buildings shouldn't have coordinates", buildingXmlNode.hasCoordinates());
assertTrue("Coordinate should be a Nan", Double.isNaN(buildingXmlNode.x));
assertTrue("Coordinate should be a Nan", Double.isNaN(buildingXmlNode.y));
counter++;
}
assertEquals("3 buildings should have been analyzed", counter, 3);
}
}
package eu.simstadt.geo.fast_xml_parser;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import eu.simstadt.geo.GeoUtils;
public class ConvexHullCalculatorTests
{
private static final GeometryFactory gf = new GeometryFactory();
private static final Path repository = Paths.get("../TestRepository");
@Test
public void testExtractConvexHullFromOneBuilding() throws Throwable {
Path citygmlPath = repository.resolve("Gruenbuehl.proj/20140218_Gruenbuehl_LOD2_1building.gml");
Geometry hull = ConvexHullCalculator.calculateFromCityGML(citygmlPath);
assertEquals(hull.getCoordinates().length, 4 + 1); // Convex hull of a building should be a closed rectangle
Point someBuildingPoint = gf.createPoint(new Coordinate(9.216845, 48.878196)); // WGS84
assertTrue("Hull should contain every building point", hull.contains(someBuildingPoint));
}
@Test
public void testExtractConvexHullFromOneSmallRegion() throws Throwable {
Path citygmlPath = repository.resolve("Gruenbuehl.proj/Gruenbuehl_LOD2_ALKIS_1010.gml");
Geometry hull = ConvexHullCalculator.calculateFromCityGML(citygmlPath);
assertTrue(hull.getCoordinates().length > 4); // Convex hull should have at least 4 corners
// Point somewhereBetweenBuildings = gf.createPoint(new Coordinate(3515883.6668538367, 5415843.300640578)); // Original coordinates, GSK3
Point somewhereBetweenBuildings = gf.createPoint(new Coordinate(9.21552249084, 48.87980446)); // WGS84
assertTrue("Hull should contain region between buildings", hull.contains(somewhereBetweenBuildings));
}
@Test
public void testExtractConvexHullFromStoeckachNoBuildingPart() throws Throwable {
Path citygmlPath = repository.resolve("Ensource.proj/Stöckach_überarbeitete GML-NoBuildingPart.gml");
Geometry hull = ConvexHullCalculator.calculateFromCityGML(citygmlPath);
assertTrue(hull.getCoordinates().length > 4); // Convex hull should have at least 4 corners
Point somewhereBetweenBuildings = gf.createPoint(new Coordinate(9.195212, 48.789062)); // WGS84
assertTrue("Hull should contain region between buildings", hull.contains(somewhereBetweenBuildings));
}
@Test
public void testExtractConvexHullFromEveryCitygmlInRepository() throws Throwable {
int minHullCount = 70;
//NOTE: Should cache be deleted first?
// Files.walk(repository.resolve(".cache/hulls"), 1).forEach(System.out::println);
long gmlCount = GeoUtils.everyCityGML(repository).count();
AtomicInteger hullCount = new AtomicInteger(0);
ConvexHullCalculator.extractHullsForEveryCityGML(repository, kmlHull -> {
assertTrue("KML hull should contain project name", kmlHull.contains("Data name=\"project\""));
assertTrue("KML hull should contain srs name", kmlHull.contains("Data name=\"srsName\""));
assertTrue("KML hull should contain epsg id", kmlHull.contains("<value>EPSG:"));
assertTrue("KML hull should contain coordinates", kmlHull.contains("<coordinates>"));
hullCount.getAndIncrement();
});
assertTrue("At least " + minHullCount + " citygmls should be present in repository", gmlCount >= minHullCount);
assertTrue("At least " + minHullCount + " hulls should have been calculated", hullCount.get() >= minHullCount);
}
}
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