diff --git a/.gitignore b/.gitignore index f190e4bc19841cf1aa289fc09f5b260ea70f0b82..eb2353e6a5bbacc39950c1f4c8d831003d13c863 100644 --- a/.gitignore +++ b/.gitignore @@ -272,3 +272,4 @@ $RECYCLE.BIN/ *.lnk # End of https://www.gitignore.io/api/java,maven,macos,linux,eclipse,windows,netbeans,intellij +/- diff --git a/buildConfig/RegionChooser.bat b/buildConfig/RegionChooser.bat index f28ebeed8e7a29d6734480426babef3e73350303..430ee7995efae8d4eb9422d91d568444d52ef8ca 100644 --- a/buildConfig/RegionChooser.bat +++ b/buildConfig/RegionChooser.bat @@ -1 +1,2 @@ -java -classpath lib/* -Xms512m -Xmx2g -Djava.util.logging.config.file=logging.properties eu.simstadt.regionchooser.RegionChooserFX \ No newline at end of file +java -classpath lib/* -Xms512m -Xmx2g -Djava.util.logging.config.file=logging.properties eu.simstadt.regionchooser.RegionChooserFX +REM TODO: Update, similar to RegionChooser.sh \ No newline at end of file diff --git a/buildConfig/RegionChooser.command b/buildConfig/RegionChooser.command index 95ccbc8fc351c686086e0ac2d571b287c7713bfb..02a02bd34072239804923e54a14d2b3406ec0281 100755 --- a/buildConfig/RegionChooser.command +++ b/buildConfig/RegionChooser.command @@ -1,3 +1,9 @@ #!/bin/bash -cd "$(dirname "$0")" # set the current working directory to the directory this script is in -java -d64 -classpath lib/*: -Xms512m -Xmx2g -Djava.util.logging.config.file=logging.properties eu.simstadt.regionchooser.RegionChooserFX +if [[ -z "$1" ]] + then + echo No parameter, launching RegionChooser GUI + java -classpath 'lib/*' eu.simstadt.regionchooser.RegionChooserFX + else + echo Launching RegionChooserCLI "$@" + java -classpath 'lib/*' eu.simstadt.regionchooser.RegionChooserCLI "$@" +fi \ No newline at end of file diff --git a/buildConfig/RegionChooser.sh b/buildConfig/RegionChooser.sh index 2696b380a4fee8df427606f73ab348825d887d77..26d7058eee79a814fc43574395adca82cec0e56d 100644 --- a/buildConfig/RegionChooser.sh +++ b/buildConfig/RegionChooser.sh @@ -1,2 +1,9 @@ #!/bin/bash -java -classpath 'lib/*' -Xms512m -Xmx2g -Djava.util.logging.config.file=logging.properties eu.simstadt.regionchooser.RegionChooserFX \ No newline at end of file +if [[ -z "$1" ]] + then + echo No parameter, launching RegionChooser GUI + java -classpath 'lib/*' eu.simstadt.regionchooser.RegionChooserFX + else + echo Launching RegionChooserCLI "$@" + java -classpath 'lib/*' eu.simstadt.regionchooser.RegionChooserCLI "$@" +fi diff --git a/pom.xml b/pom.xml index 7aecf17a04bb3ef14e1085f758308e2af52d76c9..19f1d2170d85d9061d24f5de0bc019e1a5e7e4e1 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ <groupId>eu.simstadt</groupId> <artifactId>region-chooser</artifactId> - <version>0.2.3-SNAPSHOT</version> + <version>0.2.9-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> @@ -27,6 +27,12 @@ <scope>test</scope> </dependency> + <dependency> + <groupId>info.picocli</groupId> + <artifactId>picocli</artifactId> + <version>4.6.3</version> + </dependency> + <!-- https://mvnrepository.com/artifact/org.osgeo/proj4j --> <dependency> <groupId>org.osgeo</groupId> diff --git a/src/main/java/eu/simstadt/regionchooser/RegionChooserBrowser.java b/src/main/java/eu/simstadt/regionchooser/RegionChooserBrowser.java index 92bf767a2e1c775d97ef0810e2bf962df713a03b..5f6f157a8c2b071d0d49efe6b65604edb9c1babf 100644 --- a/src/main/java/eu/simstadt/regionchooser/RegionChooserBrowser.java +++ b/src/main/java/eu/simstadt/regionchooser/RegionChooserBrowser.java @@ -8,9 +8,9 @@ import java.nio.file.Paths; import java.util.logging.Logger; import java.util.prefs.Preferences; +import java.util.stream.Stream; import org.locationtech.jts.io.ParseException; import com.ximpleware.NavException; -import com.ximpleware.XPathEvalException; import com.ximpleware.XPathParseException; import eu.simstadt.regionchooser.fast_xml_parser.ConvexHullCalculator; import javafx.application.Platform; @@ -22,6 +22,7 @@ import javafx.scene.layout.Region; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; +import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; import netscape.javascript.JSObject; @@ -30,6 +31,7 @@ public class RegionChooserBrowser extends Region { private static final Logger LOGGER = Logger.getLogger(RegionChooserBrowser.class.getName()); + private static final String PREF_RECENT_REPOSITORY = "RECENT_REPOSITORY"; /** * JavaFX Backend for RegionChooser. Inside simstadt_openlayers.js frontend, this class is available as `fxapp`. @@ -40,16 +42,16 @@ public class RegionChooserBrowser extends Region public JavaScriptFXBridge() { Preferences userPrefs = Preferences.userRoot().node("/eu/simstadt/desktop"); - String repoString = userPrefs.get("RECENT_REPOSITORY", "../TestRepository"); + String repoString = userPrefs.get(PREF_RECENT_REPOSITORY, "../TestRepository"); repo = Paths.get(repoString); } /** * Launches a background thread in which the hull gets extracted for every CityGML file. The hull gets sent back * to the JS app in order to be displayed. - * + * * NOTE: To be very honest, I don't really understand concurrency in JavaFX. Eric - * + * */ public void refreshHulls() { //NOTE: Could add progress bar? @@ -62,24 +64,54 @@ public Void call() throws IOException { } }; - task.setOnRunning(e -> jsApp.call("display", "Importing citgyml. Please wait.")); + task.setOnRunning(e -> { + jsApp.call("display", "Importing citgyml. Please wait."); + jsApp.call("showRepositoryName", repo.getFileName().toString()); + jsApp.call("init"); + }); task.setOnSucceeded(e -> jsApp.call("ready")); new Thread(task).start(); } - public void downloadRegionFromCityGML(String wktPolygon, String project, String citygml, String srsName) - throws IOException, ParseException, XPathParseException, NavException, XPathEvalException { - StringBuilder sb = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, srsName, - citygmlPath(project, citygml)); + /** + * This method is called from Javascript, with a prepared wktPolygon written in local coordinates. + */ + public int downloadRegionFromCityGMLs(String wktPolygon, String project, String csvCitygmls, String srsName) + throws IOException, ParseException, XPathParseException, NavException { + // It doesn't seem possible to pass arrays or list from JS to Java. So csvCitygmls contains names separated by ; + Path[] paths = Stream.of(csvCitygmls.split(";")).map(s -> citygmlPath(project, s)).toArray(Path[]::new); - File buildingIdsFile = selectSaveFileWithDialog(project, citygml, "selected_region"); - if (buildingIdsFile != null) { - try (BufferedWriter writer = Files.newBufferedWriter(buildingIdsFile.toPath())) { - //NOTE: isn't there a better way?? - writer.write(sb.toString()); - } + + File outputFile = selectSaveFileWithDialog(project, + csvCitygmls.replace(";", "_").replace(".gml", ""), "selected_region"); + + int count; + try (BufferedWriter gmlWriter = Files.newBufferedWriter(outputFile.toPath())) { + count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, srsName, gmlWriter, paths); + } + return count; + } + + + public void selectRepository() { + Preferences userPrefs = Preferences.userRoot().node("/eu/simstadt/desktop"); + String currentRepo = userPrefs.get(PREF_RECENT_REPOSITORY, "../TestRepository"); + + DirectoryChooser fileChooser = new DirectoryChooser(); + Stage mainStage = (Stage) RegionChooserBrowser.this.getScene().getWindow(); + fileChooser.setTitle("Select Repository"); + fileChooser.setInitialDirectory(new File(currentRepo)); + File repoLocation = fileChooser.showDialog(mainStage); + + if (repoLocation != null) { + repo = repoLocation.toPath(); + userPrefs.put(PREF_RECENT_REPOSITORY, repo.toAbsolutePath().toString()); + LOGGER.info("Repository was set to " + repo); + refreshHulls(); + } else { + LOGGER.warning("No repository chosen."); } } diff --git a/src/main/java/eu/simstadt/regionchooser/RegionChooserCLI.java b/src/main/java/eu/simstadt/regionchooser/RegionChooserCLI.java new file mode 100644 index 0000000000000000000000000000000000000000..5bee332f1ff943ab071e3a6fc45bc0c1439831b3 --- /dev/null +++ b/src/main/java/eu/simstadt/regionchooser/RegionChooserCLI.java @@ -0,0 +1,140 @@ +package eu.simstadt.regionchooser; + +import java.io.BufferedWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Scanner; +import java.util.concurrent.Callable; +import org.osgeo.proj4j.CoordinateReferenceSystem; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; + + +/** + * Command Line Interface for RegionChooser. Could be useful to extract large regions on server, or automate the process + * from batch/python scripts. + * + */ + +// Usage: region_chooser [-hlV] [-e=31467] -o=output.gml -w=polygon.wkt -i=input. +// gml[,input.gml...] [-i=input.gml[,input.gml...]]... +// Extracts a region from one or more citygmls. +// -i, --input=input.gml[,input.gml...] +// Citygml files to extract from +// -o, --output=output.gml Output file +// -e, --epsg=31467 EPSG id for coordinate reference system +// -l, --local Are WKT coordinates in local CRS? +// -w, --wkt=polygon.wkt File containing WKT polygon, or - for stdin +// -h, --help Show this help message and exit. +// -V, --version Print version information and exit. + + +// Example: +// --input CGSC_Repository/Würzburg.proj/LoD2_566_5516_2_BY.gml,CGSC_Repository/Würzburg.proj/LoD2_568_5516_2_BY.gml +// --output ./output.gml +// --wkt ./grombuhl.txt + + +@Command(name = "region_chooser", mixinStandardHelpOptions = true, version = "regionChooser 0.2.9", description = "Extracts a region from one or more citygmls.", sortOptions = false) +class RegionChooserCLI implements Callable<Integer> +{ + @Spec + CommandSpec spec; + + //TODO: Add --gui? + + @Option(names = { "-i", + "--input" }, required = true, split = ",", description = "Citygml files to extract from", paramLabel = "input.gml") + //TODO: Allow folders too? + Path[] citygmls; + + @Option(names = { "-o", + "--output" }, required = true, description = "Output file", paramLabel = "output.gml") + Path outputCityGML; + + @Option(names = { "-e", + "--epsg" }, description = "EPSG id for coordinate reference system.\nWill use the one from input.gml if unspecified.", paramLabel = "31467") + Integer espgId; + + @Option(names = { "-l", + "--local" }, description = "Are WKT coordinates in local CRS?\nCoordinates are in WGS84 if unspecified.", paramLabel = "local_coordinates?") + boolean localCoordinates; + + @Option(names = { "-w", + "--wkt" }, required = true, description = "File containing WKT polygon, or - for stdin", paramLabel = "polygon.wkt") + String wktFile = "-"; + + @Override + public Integer call() throws Exception { + CoordinateReferenceSystem localCRS; + + if (espgId == null) { + localCRS = RegionChooserUtils.crsFromCityGMLHeader(citygmls[0]); + } else { + localCRS = RegionChooserUtils.crsFromSrsName("EPSG:" + espgId); + } + logInfo("Coordinate system: " + localCRS); + + String wktPolygon; + + if (wktFile.equals("-")) { + if (System.in.available() == 0) { + throw new IllegalArgumentException("Please provide \"POLYGON((x1 y1, x2 y2, ...))\" to standard input."); + } else { + wktPolygon = getInput(); + } + } else { + wktPolygon = new String(Files.readAllBytes(Paths.get(wktFile)), StandardCharsets.UTF_8); + if (wktPolygon.isEmpty()) { + throw new IllegalArgumentException("Please write \"POLYGON((x1 y1, x2 y2, ...))\" inside " + wktFile); + } + } + + if (!localCoordinates) { + // WKT coordinates are in WGS84, so should be first converted to srsName + wktPolygon = RegionChooserUtils.wktPolygonToLocalCRS(wktPolygon, localCRS); + } + + logInfo("WKT Polygon expressed in local coordinates: " + wktPolygon); + + int count; + + if (outputCityGML.toString().equals("-")) { + logInfo("CityGML written to stdout."); + PrintWriter stdOut = spec.commandLine().getOut(); + count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, localCRS.toString(), stdOut, citygmls); + } else { + try (BufferedWriter gmlWriter = Files.newBufferedWriter(outputCityGML)) { + count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, localCRS.toString(), gmlWriter, + citygmls); + } + } + + logInfo("Found buildings : " + count); + + return 0; + } + + private void logInfo(String message) { + spec.commandLine().getErr().println(message); + } + + private static String getInput() { + try (Scanner myObj = new Scanner(System.in)) { + return myObj.nextLine(); + } + } + + // this example implements Callable, so parsing, error handling and handling user + // requests for usage help or version help can be done with one line of code. + public static void main(String... args) { + int exitCode = new CommandLine(new RegionChooserCLI()).execute(args); + System.exit(exitCode); + } +} \ No newline at end of file diff --git a/src/main/java/eu/simstadt/regionchooser/RegionChooserFX.java b/src/main/java/eu/simstadt/regionchooser/RegionChooserFX.java index c1ebd815771d16fcefbedc9bc00e57b6124f7df4..f3910e72303934b82cba278e7bf502bffbd0d344 100644 --- a/src/main/java/eu/simstadt/regionchooser/RegionChooserFX.java +++ b/src/main/java/eu/simstadt/regionchooser/RegionChooserFX.java @@ -1,26 +1,29 @@ package eu.simstadt.regionchooser; -import java.time.LocalDate; -import java.util.Objects; import javafx.application.Application; import javafx.scene.Scene; +import javafx.scene.image.Image; import javafx.scene.paint.Color; import javafx.stage.Stage; public class RegionChooserFX extends Application { + public static final Image APP_ICON_48 = new Image(RegionChooserFX.class.getResourceAsStream("simstadt_48.png")); + /** * Starting point of RegionChooser application. Either "Run as Java" from Eclipse or run * "RegionChooser.bat/.sh/.command" from deployed SimStadt folder. - * + * * This application is basically just a scene and a browser for the RegionChooser website (HTML + Javascript frontend * + Java backend). The Java part is written in RegionChooserBrowser. */ + @Override public void start(Stage stage) { - stage.setTitle("RegionChooser " + getApplicationVersion()); + stage.setTitle("RegionChooser " + RegionChooserUtils.getApplicationVersion()); Scene scene = new Scene(new RegionChooserBrowser(), 1024, 720, Color.web("#666970")); + stage.getIcons().addAll(APP_ICON_48); stage.setScene(scene); stage.show(); } @@ -28,16 +31,5 @@ public void start(Stage stage) { public static void main(String[] args) { launch(args); } - - /** - * Returns application version, if it has been written in the JAR file during deployment. - * - * e.g. "0.9.1-SNAPSHOT (rev. 73cbe48e, 2018-07-20)" - */ - private String getApplicationVersion() { - Package regionChooserJar = RegionChooserFX.class.getPackage(); - return Objects.toString(regionChooserJar.getImplementationVersion(), - String.format("development version (%s)", LocalDate.now())); - } } diff --git a/src/main/java/eu/simstadt/regionchooser/RegionChooserUtils.java b/src/main/java/eu/simstadt/regionchooser/RegionChooserUtils.java index 4bc38769e0ca51c39611085a31bc46645dca569c..b9e5f5cae20867adadbaa644be2dcd18da339775 100644 --- a/src/main/java/eu/simstadt/regionchooser/RegionChooserUtils.java +++ b/src/main/java/eu/simstadt/regionchooser/RegionChooserUtils.java @@ -3,6 +3,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.LocalDate; +import java.util.Objects; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -10,6 +12,9 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; +import org.locationtech.jts.io.WKTWriter; import org.osgeo.proj4j.BasicCoordinateTransform; import org.osgeo.proj4j.CRSFactory; import org.osgeo.proj4j.CoordinateReferenceSystem; @@ -23,6 +28,7 @@ private static final Pattern srsNamePattern = Pattern.compile("(?i)(?<=srsName=[\"'])[^\"']+(?=[\"'])"); private static final int CITYGML_HEADER_LENGTH = 50; private static final String EPSG = "EPSG:"; + private static final int BUFFER = 1024; private RegionChooserUtils() { // only static use @@ -32,18 +38,18 @@ private RegionChooserUtils() { * The srsName (The name by which this reference system is identified) inside the CityGML file can have multiple * formats. This method tries to parse the string and detect the corresponding reference system. If it is found, it * returns a proj4j.CoordinateReferenceSystem. It throws an IllegalArgumentException otherwise. - * + * * This method should be able to parse any EPSG id : e.g. "EPSG:1234". German Citygmls might also have "DE_DHDN_3GK3" * or "ETRS89_UTM32" as srsName, so those are also included. It isn't guaranteed that those formats are correctly * parsed, though. - * + * * The EPSG ids and parameters are defined in resources ('nad/epsg') inside proj4j-0.1.0.jar. Some EPSG ids are * missing though, e.g. 7415 - * + * * @param srsName * @return CoordinateReferenceSystem */ - private static CoordinateReferenceSystem crsFromSrsName(String srsName) { + public static CoordinateReferenceSystem crsFromSrsName(String srsName) { // EPSG:31467 Pattern pEPSG = Pattern.compile("^(EPSG:\\d+)$"); Matcher mEPSG = pEPSG.matcher(srsName); @@ -85,9 +91,9 @@ private static CoordinateReferenceSystem crsFromSrsName(String 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 @@ -107,12 +113,21 @@ public static Polygon changePolygonCRS(Polygon polygonInOriginalCRS, CoordinateR return geometryFactory.createPolygon(convexHullcoordinates); } + public static String wktPolygonToLocalCRS(String wktPolygonInWGS84, CoordinateReferenceSystem localCRS) + throws ParseException { + final WKTReader wktReader = new WKTReader(); + final WKTWriter wktWriter = new WKTWriter(); + // WKT coordinates are in WGS84, so should be first converted to srsName + Polygon wgs84Polygon = (Polygon) wktReader.read(wktPolygonInWGS84); + return wktWriter.write(changePolygonCRS(wgs84Polygon, WGS84, localCRS)); + } + /** - * + * * 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 @@ -132,7 +147,7 @@ public static CoordinateReferenceSystem crsFromCityGMLHeader(Path citygmlPath) t /** * Finds every CityGML in every .proj folder in a repository. - * + * * @param repository * @return a stream of CityGML Paths * @throws IOException @@ -146,4 +161,14 @@ public static Stream<Path> everyCityGML(Path repository) throws IOException { p.toString().toLowerCase().endsWith(".gml")); } + /** + * Returns application version, if it has been written in the JAR file during deployment. + * + * e.g. "0.9.1-SNAPSHOT (rev. 73cbe48e, 2018-07-20)" + */ + public static String getApplicationVersion() { + Package regionChooserJar = RegionChooserFX.class.getPackage(); + return Objects.toString(regionChooserJar.getImplementationVersion(), + String.format("development version (%s)", LocalDate.now())); + } } diff --git a/src/main/java/eu/simstadt/regionchooser/RegionExtractor.java b/src/main/java/eu/simstadt/regionchooser/RegionExtractor.java index e7889390f205cad30504e088edfb636cc8222621..316d378bd2c5c92365adeebd7f5d539122746a65 100644 --- a/src/main/java/eu/simstadt/regionchooser/RegionExtractor.java +++ b/src/main/java/eu/simstadt/regionchooser/RegionExtractor.java @@ -1,6 +1,7 @@ package eu.simstadt.regionchooser; import java.io.IOException; +import java.io.Writer; import java.nio.file.Path; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -21,10 +22,10 @@ public class RegionExtractor { - private static final WKTReader wktReader = new WKTReader(); + private static final WKTReader WKT_READER = new WKTReader(); private static final Logger LOGGER = Logger.getLogger(RegionExtractor.class.getName()); - private static final GeometryFactory gf = new GeometryFactory(); + private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); /** * Main method behind RegionChooser. Given CityGMLs (as Path[]) and a geometry (as Well-known text POLYGON, in the @@ -34,10 +35,12 @@ * * @param wktPolygon * @param srsName + * @param output * @param citygmlPaths * * - * @return a StringBuffer, full with the extracted Citygml, including header, buildings and footer. + * @writes the extracted Citygml, including header, buildings and footer to output + * @return counts of found building. * @throws ParseException * @throws IOException * @throws XPathEvalException @@ -45,13 +48,12 @@ * @throws XPathParseException * @throws NumberFormatException */ - static StringBuilder selectRegionDirectlyFromCityGML(String wktPolygon, String srsName, Path... citygmlPaths) - throws ParseException, XPathParseException, NavException, IOException { + static int selectRegionDirectlyFromCityGML(String wktPolygon, String srsName, Writer sb, + Path... citygmlPaths) throws ParseException, XPathParseException, NavException, IOException { int buildingsCount = 0; int foundBuildingsCount = 0; - StringBuilder sb = new StringBuilder(); - Geometry poly = wktReader.read(wktPolygon); + Geometry poly = WKT_READER.read(wktPolygon); CityGmlIterator citygml = null; for (int i = 0; i < citygmlPaths.length; i++) { @@ -65,7 +67,7 @@ static StringBuilder selectRegionDirectlyFromCityGML(String wktPolygon, String s buildingsCount += 1; if (buildingXmlNode.hasCoordinates()) { Coordinate coord = new Coordinate(buildingXmlNode.x, buildingXmlNode.y); - Point point = gf.createPoint(coord); + Point point = GEOMETRY_FACTORY.createPoint(coord); if (point.within(poly)) { foundBuildingsCount++; sb.append(buildingXmlNode.toString()); @@ -75,16 +77,19 @@ static StringBuilder selectRegionDirectlyFromCityGML(String wktPolygon, String s LOGGER.info("1000 buildings parsed"); } } - } if (citygml == null) { throw new IllegalArgumentException("There should be at least one citygml"); } + if (foundBuildingsCount == 0) { + LOGGER.warning("No building found in the selected region."); + } + LOGGER.info("Buildings found in selected region " + foundBuildingsCount); sb.append(citygml.getFooter()); - return sb; + return foundBuildingsCount; } /** diff --git a/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/BuildingXmlNode.java b/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/BuildingXmlNode.java index 8a752b099217df0f2d07910324f524679c2f33c5..1c2098534fe5b3d61387a006b25fbc75696607c2 100644 --- a/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/BuildingXmlNode.java +++ b/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/BuildingXmlNode.java @@ -28,7 +28,8 @@ public BuildingXmlNode(VTDNav navigator, int buildingOffset, int buildingLength) 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? + extractCoordinates(); + //TODO: Get Building ID too, in order to avoid duplicates? } public boolean hasCoordinates() { diff --git a/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/CityGmlIterator.java b/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/CityGmlIterator.java index b165ffac0bd9dd1e5dbd3121d243536c0b05a938..90af5396ef7a9763a5cfea3ea2ec9d496fe3234d 100644 --- a/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/CityGmlIterator.java +++ b/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/CityGmlIterator.java @@ -97,7 +97,7 @@ public String getHeader() throws NavException { * @return Citygml footer * @throws NavException */ - public Object getFooter() throws IOException, NavException { + public String getFooter() throws IOException, NavException { int footerOffset = buildingOffset + buildingLength; int footerLength = (int) (Files.size(citygmlPath) - footerOffset); return navigator.toRawString(footerOffset, footerLength); diff --git a/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/ConvexHullCalculator.java b/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/ConvexHullCalculator.java index 299110d86e487fb0b9607ae7694098de5b3d1ab6..4e9e6b99cc3c46978cf12c61f9500854449d42c7 100644 --- a/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/ConvexHullCalculator.java +++ b/src/main/java/eu/simstadt/regionchooser/fast_xml_parser/ConvexHullCalculator.java @@ -92,6 +92,7 @@ public static void extractHullsForEveryCityGML(Path repository, Consumer<String> try { Path kmlPath = getHullPath(repository, gmlPath); if (Files.exists(kmlPath)) { + //TODO: Check if size is the same as original. Recreate otherwise. LOGGER.fine("Using cache from " + repository.relativize(kmlPath)); return new String(Files.readAllBytes(kmlPath), StandardCharsets.UTF_8); } else { diff --git a/src/main/resources/eu/simstadt/regionchooser/simstadt_48.png b/src/main/resources/eu/simstadt/regionchooser/simstadt_48.png new file mode 100644 index 0000000000000000000000000000000000000000..b25a17ccbbc1ff25b2b39134193a3484f2d8fcef Binary files /dev/null and b/src/main/resources/eu/simstadt/regionchooser/simstadt_48.png differ diff --git a/src/main/resources/eu/simstadt/regionchooser/website/index.html b/src/main/resources/eu/simstadt/regionchooser/website/index.html index 6cfa47b6794999275492e980202aa2d520f6f9ae..141a41524beaf026e85be61ea9e093e2ee7f7729 100644 --- a/src/main/resources/eu/simstadt/regionchooser/website/index.html +++ b/src/main/resources/eu/simstadt/regionchooser/website/index.html @@ -5,9 +5,8 @@ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /> <meta name="viewport" content="initial-scale=1.0, user-scalable=no" /> <title>Simstadt Region Chooser</title> - <meta name="keywords" content="polygon,creator,google map,v3,draw,paint"/> - <meta name="description" - content="Google Map V3 Polygon Creator for Simstadt"/> + <meta name="keywords" content="region chooser,citygml,polygon,creator,extract,simstadt,buildings"/> + <meta name="description" content="RegionChooser for Simstadt"/> <link rel="stylesheet" type="text/css" href="style/style.css"/> <!-- Firebug for js console: @@ -24,7 +23,8 @@ <body> <div id="header"> <ul> - <li class="title">Simstadt Region Chooser</li> + <li class="title"><span id="repo_path">RegionChooser</span></li> + <li class="title" id="select_repository" style="visibility:hidden"><button onclick="regionChooser.selectRepository()">Select repository</button></li> </ul> </div> <div id="map" class="map" tabindex="0"></div> diff --git a/src/main/resources/eu/simstadt/regionchooser/website/script/simstadt_openlayers.js b/src/main/resources/eu/simstadt/regionchooser/website/script/simstadt_openlayers.js index 6cc760c02ffba35c93b482f229ffbd545d60f2e7..c06cf11ee740b22f80be4084d6c6b65eb9c7c4a4 100644 --- a/src/main/resources/eu/simstadt/regionchooser/website/script/simstadt_openlayers.js +++ b/src/main/resources/eu/simstadt/regionchooser/website/script/simstadt_openlayers.js @@ -1,12 +1,20 @@ var regionChooser = (function(){ + //TODO: Somehow split in classes. This file is getting too big and mixed var publicScope = {}; var fromJavaFX = navigator.userAgent.indexOf('JavaFX') !== -1; var dataPanel = $('#dataPanel'); var wgs84Sphere = new ol.Sphere(6378137); - var gmlId = 0; + var gmlId; + + publicScope.init = function(){ + //NOTE: Only called from JavaFX. At startup, or when Repo has been changed. + gmlId = 0; + kml_source.clear(); + document.getElementById("select_repository").style.visibility = "visible"; + } if (fromJavaFX){ - $("html").addClass("wait"); + document.documentElement.className = 'wait'; } var osm_layer = new ol.layer.Tile({ @@ -59,9 +67,9 @@ var regionChooser = (function(){ feature.setId(gmlId++); kml_source.addFeature(feature); dataPanel.append('.'); - srsName = feature.get("srsName") || "EPSG:31467"; + srsName = feature.get("srsName"); if (proj4.defs(srsName) === undefined){ - console.warning(srsName + " isn't defined by Proj4js!") + console.warn(srsName + " isn't defined by Proj4js!") } }; @@ -154,12 +162,8 @@ var regionChooser = (function(){ var sketch_percentage = Math.round(intersectionArea / polygonArea * 100); intersections.addFeature(intersection); var link = '<li>' - if (fromJavaFX) { - link += "<a href=\"#\" onclick=\"regionChooser.downloadRegionFrom" + feature["source"] + "(" + feature.getId() - + ");return false;\">" + feature["name"] + "</a>"; - } else { - link += feature['name']; - } + // TODO: If possible, highlight the corresponding polygon when hovering above a name. + link += '<input type="checkbox" id="citygml_' + feature.getId() + '" class="select_citygml" onclick="regionChooser.isDownloadPossible();"><label for="citygml_' + feature.getId() + '">' + feature['name'] + '</label>'; link += " (" + citygml_percentage + "%"; if (sketch_percentage == 100) { @@ -168,6 +172,11 @@ var regionChooser = (function(){ dataPanel.append(link + ")\n"); } + publicScope.isDownloadPossible = function(){ + var checkedBoxes = Array.from(document.querySelectorAll("input.select_citygml")).filter(c => c.checked); + document.getElementById("download_region_button").disabled = (checkedBoxes.length == 0); + } + function findIntersection(feature, polygon) { try { return turf.intersect(polygon, feature["geoJSON"]); @@ -181,19 +190,24 @@ var regionChooser = (function(){ var polygonArea = sketch.getGeometry().getArea(); var intersection_found = false; intersections.clear(); + //NOTE: getFeatures seems to not be sorted anymore. :-/ features_by_project = groupBy(kml_source.getFeatures(), "project"); - Object.keys(features_by_project).sort().forEach(function(project) { - features = features_by_project[project]; - features_and_intersections = features.map(f=> [f, findIntersection(f,polygon)]).filter(l => l[1] !== undefined); - if (features_and_intersections.length > 0){ - intersection_found = true; + Object.keys(features_by_project).forEach(function(project) { + features = features_by_project[project]; + features_and_intersections = features.map(f=> [f, findIntersection(f,polygon)]).filter(l => l[1] !== undefined); + if (features_and_intersections.length > 0){ + intersection_found = true; dataPanel.append("<h2 class='info'>" + project); - features_and_intersections.forEach(l => showLinkToDownload(l[0], l[1], polygonArea)); - } + features_and_intersections.forEach(l => showLinkToDownload(l[0], l[1], polygonArea)); + } }); - if (!intersection_found) { + if (intersection_found) { + document.getElementById("download_region").style.visibility = 'visible'; + } + else { + document.getElementById("download_region").style.visibility = 'hidden'; dataPanel.append("No intersection found with any CityGML file.<br/>\n"); } } @@ -202,28 +216,45 @@ var regionChooser = (function(){ dataPanel.append(text + "<br/>\n"); } - publicScope.downloadRegionFromCityGML = function(i) { - // TODO: Disable all links - // TODO: DRY - var feature = kml_source.getFeatureById(i); + publicScope.downloadRegionFromCityGMLs = function(checkbox_ids) { + var features = checkbox_ids.map(checkbox_id => { + var i = Number(checkbox_id.replace("citygml_", "")); + return kml_source.getFeatureById(i); + }) + + var project = features[0].get("project"); + var srsName = features[0].get("srsName"); + + if (!features.every( f => f.get("project") === project)){ + dataPanel.prepend("<h2 class='error'>Sorry, the CityGML files should all belong to the same project.</h2><br/>\n"); + return; + } + + if (!features.every( f => f.get("srsName") === srsName)){ + dataPanel.prepend("<h2 class='error'>Sorry, the CityGML files should all be written with the same coordinate system.</h2><br/>\n"); + } + + document.documentElement.className = 'wait'; + + var citygmlNames = features.map(f => f.get("name")); // Waiting 100ms in order to let the cursor change setTimeout(function() { var start = new Date().getTime(); - var srsName = feature.get("srsName") || "EPSG:31467"; if (proj4.defs(srsName)){ - $("html").addClass("wait"); console.log("Selected region is written in " + srsName + " coordinate system."); try { - fxapp.downloadRegionFromCityGML(sketchAsWKT(srsName), feature.get("project"), feature.get("name"), srsName); - dataPanel.append("<h2 class='ok'>Done!</h2><br/>\n"); + var count = fxapp.downloadRegionFromCityGMLs(sketchAsWKT(srsName), project, citygmlNames.join(";"), srsName); + dataPanel.prepend("<h2 class='ok'>Done! (" + count + " buildings found) </h2><br/>\n"); } catch (e) { - dataPanel.append("<h2 class='error'>Some problem occured!</h2><br/>\n"); + console.warn("ERROR : " + e); + dataPanel.prepend("<h2 class='error'>Some problem occured!</h2><br/>\n"); } var end = new Date().getTime(); var time = end - start; console.log('Download Execution time: ' + (time / 1000).toFixed(3) + 's'); setTimeout(function() { - $("html").removeClass("wait"); + document.getElementById("download_region_button").disabled = false; + document.documentElement.className = ''; // Stop waiting }, 100); } else { var msg = "ERROR : Unknown coordinate system : \"" + srsName + "\". Cannot extract any region"; @@ -235,24 +266,18 @@ var regionChooser = (function(){ function displayInfo() { dataPanel.empty(); - var geom = /** @type {ol.geom.Polygon} */ - (sketch.getGeometry().clone().transform(sourceProj, 'EPSG:4326')); + var geom = sketch.getGeometry().clone().transform(sourceProj, 'EPSG:4326'); var coordinates = geom.getLinearRing(0).getCoordinates(); var area = Math.abs(wgs84Sphere.geodesicArea(coordinates)); - var coords = geom.getLinearRing(0).getCoordinates(); - if (!fromJavaFX) { - var wgs84_coords = ""; - var n = coords.length; - for (var i = 0; i < n; i++) { - var wgs84_coord = coords[i]; - wgs84_coords += "(" + wgs84_coord[1] + "," + wgs84_coord[0] + ")<br/>"; - - } - dataPanel.append("WGS84 Coordinates<br/>"); - dataPanel.append(wgs84_coords + "<br/>\n"); - } + //NOTE: Could show m², ha or km² depending on magnitude dataPanel.append("<h3 class='clean'>Area : " + (area / 10000).toFixed(1) + " ha\n"); + dataPanel.append('<div style="visibility:hidden" id="download_region">' + + '<button type="button" onclick="regionChooser.downloadFromSelectedCityGMLs()" id="download_region_button" disabled>Download Region</button><br/>\n' + + '<a href="#" onclick="regionChooser.checkCityGMLS(true);">(Select All)</a>\n' + + '<a href="#" onclick="regionChooser.checkCityGMLS(false);">(Select None)</a>\n'+ + '</div>\n'); findIntersections(); + dataPanel.append('<button type="button" onclick="regionChooser.copyCoordinatesToClipboard()" id="get_wgs84">Copy coordinates</button><br/>\n') } draw.on('drawend', function() { @@ -274,7 +299,7 @@ var regionChooser = (function(){ draw.finishDrawing(); } finally { displayHelp(); - $("html").removeClass("wait"); + document.documentElement.className = ''; // Stop waiting draw.setActive(true); featureOverlay.getFeatures().clear(); intersections.clear(); @@ -283,7 +308,6 @@ var regionChooser = (function(){ } function sketchAsWKT(srsName) { - srsName = (typeof srsName === 'undefined') ? 'EPSG:4326' : srsName; var wktFormat = new ol.format.WKT(); return wktFormat.writeFeature(sketch, { dataProjection : ol.proj.get(srsName), @@ -292,7 +316,7 @@ var regionChooser = (function(){ } function focusOnMap() { - $('#map').focus(); + document.getElementById("map").focus(); } var fxapp = undefined; @@ -303,7 +327,7 @@ var regionChooser = (function(){ fxapp.log(message); } - console.warning = function(message){ + console.warn = function(message){ fxapp.warning(message); } } @@ -322,16 +346,83 @@ var regionChooser = (function(){ dataPanel.append("You can add a new point to an existing edge by clicking and dragging.<br>\n"); dataPanel.append("You can remove a point with SHIFT + clicking.<br>\n"); dataPanel.append("You can cancel drawing with ESC or DEL.<br><br>\n"); - dataPanel.append("After drawing a polygon which intersects with a GML file, you can download the corresponding part by clicking on the filename.<br>\n"); + dataPanel.append("After drawing a polygon which intersects with at least one GML file,<br>\n"); + dataPanel.append("you can download the corresponding part by checking the<br>\n"); + dataPanel.append("desired filenames and clicking on 'Download' button.<br>\n"); + dataPanel.append("<br>\n"); + dataPanel.append("More info is available in the "); + dataPanel.append("<a href='http://simstadt.hft-stuttgart.de/related-softwares/region-chooser/'>SimStadt documentation</a><br>\n"); } // Executed by JavaFX when whole page is loaded. publicScope.ready = function() { updateGMLPolygons(); displayHelp(); - $("html").removeClass("wait"); + document.documentElement.className = ''; // Stop waiting console.log("READY!"); } + + publicScope.downloadFromSelectedCityGMLs = function() { + document.getElementById("download_region_button").disabled = true; + var checkedBoxes = Array.from(document.querySelectorAll("input.select_citygml")).filter(c => c.checked); + // CheckBoxes isn't empty, because otherwise the button cannot be clicked. + publicScope.downloadRegionFromCityGMLs(checkedBoxes.map(c => c.id)); + } + + publicScope.checkCityGMLS = function(allOrNone) { + document.querySelectorAll("input.select_citygml").forEach(c => c.checked = allOrNone); + publicScope.isDownloadPossible(); + } + + publicScope.selectRepository = function() { + fxapp.selectRepository(); + } + + publicScope.copyCoordinatesToClipboard = function(){ + var geom = sketch.getGeometry().clone().transform(sourceProj, 'EPSG:4326'); + var wgs84Coords = geom.getLinearRing(0).getCoordinates(); + var wktPolygon = "POLYGON(("; + wktPolygon += wgs84Coords.map(lonLat => lonLat.join(" ")).join(", "); + publicScope.copyToClipboard(wktPolygon + "))"); + } + + // Copies a string to the clipboard. Must be called from within an + // event handler such as click. May return false if it failed, but + // this is not always possible. Browser support for Chrome 43+, + // Firefox 42+, Safari 10+, Edge and Internet Explorer 10+. + // Internet Explorer: The clipboard feature may be disabled by + // an administrator. By default a prompt is shown the first + // time the clipboard is used (per session). + // https://stackoverflow.com/a/33928558/6419007 + publicScope.copyToClipboard = function(text) { + if (window.clipboardData && window.clipboardData.setData) { + // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible. + return window.clipboardData.setData("Text", text); + } + else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { + var textarea = document.createElement("textarea"); + textarea.textContent = text; + textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge. + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand("copy"); // Security exception may be thrown by some browsers. + dataPanel.append("<h2 class='ok'>Coordinates copied to clipboard!</h2><br/>\n"); + return; + } + catch (ex) { + console.warn("Copy to clipboard failed.", ex); + return prompt("Copy to clipboard: Ctrl+C, Enter", text); + } + finally { + document.body.removeChild(textarea); + } + } +} + + publicScope.showRepositoryName = function(path) { + document.getElementById("repo_path").textContent = path; + } focusOnMap(); //var regionChooser = publicScope; //NOTE: In order to open closure. For debugging diff --git a/src/main/resources/eu/simstadt/regionchooser/website/style/style.css b/src/main/resources/eu/simstadt/regionchooser/website/style/style.css index d47cf34b8552df144cc5b559c6c25152aa4cd3b7..5e748cf692c8a1293d9464678319a0e8e2976ba0 100644 --- a/src/main/resources/eu/simstadt/regionchooser/website/style/style.css +++ b/src/main/resources/eu/simstadt/regionchooser/website/style/style.css @@ -130,10 +130,6 @@ div#dataPanel li { line-height: 1; } -div#side input{ - width:90%; -} - div#side input.navi{ font-size:18px; height:30px; diff --git a/src/test/java/eu/simstadt/regionchooser/RegionChooserCLITests.java b/src/test/java/eu/simstadt/regionchooser/RegionChooserCLITests.java new file mode 100644 index 0000000000000000000000000000000000000000..81722644717bcc3dfa219cb0cc1024019e849f10 --- /dev/null +++ b/src/test/java/eu/simstadt/regionchooser/RegionChooserCLITests.java @@ -0,0 +1,151 @@ +package eu.simstadt.regionchooser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.jupiter.api.AfterEach; // JUnit 5 +import org.junit.jupiter.api.BeforeEach; // JUnit 5 +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import picocli.CommandLine; + + +class RegionChooserCLITests +{ + final PrintStream originalOut = System.out; + final PrintStream originalErr = System.err; + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final ByteArrayOutputStream err = new ByteArrayOutputStream(); + + private static final Path TEST_REPOSITORY = Paths.get("src/test/resources/testdata/"); + + + @TempDir + Path tempDirectory; + Path outGML; + + @BeforeEach + public void setUp() { + out.reset(); + err.reset(); + System.setOut(new PrintStream(out)); + System.setErr(new PrintStream(err)); + outGML = tempDirectory.resolve("output.gml"); + } + + @AfterEach + public void restore() throws IOException { + System.setOut(originalOut); + System.setErr(originalErr); + Files.deleteIfExists(outGML); + } + + @Test + void testNoInput() { + new CommandLine(new RegionChooserCLI()).execute(""); + String expectedErr = "Missing required options: '--input=input.gml', '--output=output.gml', '--wkt=polygon.wkt'"; + assertTrue(err.toString().contains(expectedErr), err.toString() + " should contain " + expectedErr); + } + + @Test + void testExtractRegionFromTwoCitygmls() throws IOException { + String wktPolygon = "POLYGON((3512984.7003764412 5405148.310572891,3513038.6360455155 5405010.072163861,3513142.7277745553 5405004.02571992,3514204.1661769524 5405563.192081669,3514399.2818417274 5405720.905457244,3514291.6158155007 5405896.706492759,3512984.7003764412 5405148.310572891))"; + Path citygml1 = TEST_REPOSITORY.resolve("Stuttgart.proj/Stuttgart_LOD0_LOD1_small.gml"); + Path citygml2 = TEST_REPOSITORY.resolve("Stuttgart.proj/Stöckach_überarbeitete GML-NoBuildingPart.gml"); + Path inWKT = tempDirectory.resolve("polygon.wkt"); + try (BufferedWriter wkt = Files.newBufferedWriter(inWKT)) { + wkt.write(wktPolygon); + } + assertFalse(Files.exists(outGML)); + new CommandLine(new RegionChooserCLI()).execute("--input=" + citygml1 + "," + citygml2, + "--output=" + outGML, "--wkt=" + inWKT, "--epsg=31463", "--local"); + assertTrue(Files.exists(outGML)); + assertTrue(Files.size(outGML) > 600_000); + assertEquals(20, countBuildings(outGML)); + } + + + @Test + void testExtractRegionFromTwoCitygmlsInWGS84() throws IOException { + String wktPolygon = "POLYGON((9.18991539528286 48.79067456353269, 9.195580220722313 48.7873947720947, 9.172062612079735 48.78241812742368, 9.170946813129538 48.78569824412753, 9.18991539528286 48.79067456353269))"; + Path citygml1 = TEST_REPOSITORY.resolve("Stuttgart.proj/Stuttgart_LOD0_LOD1_small.gml"); + Path citygml2 = TEST_REPOSITORY.resolve("Stuttgart.proj/Stöckach_überarbeitete GML-NoBuildingPart.gml"); + Path inWKT = tempDirectory.resolve("polygon.wkt"); + try (BufferedWriter wkt = Files.newBufferedWriter(inWKT)) { + wkt.write(wktPolygon); + } + assertFalse(Files.exists(outGML)); + new CommandLine(new RegionChooserCLI()).execute("--input=" + citygml1 + "," + citygml2, + "--output=" + outGML, "--wkt=" + inWKT); + String expectedLog = "EPSG:31463"; + assertTrue(err.toString().contains(expectedLog), err.toString() + " should contain " + expectedLog); + assertTrue(Files.exists(outGML)); + assertTrue(Files.size(outGML) > 300_000); + assertEquals(22, countBuildings(outGML)); + } + + @Test + void testExtractRegionWithStandardInput() throws IOException { + String wktPolygon = "POLYGON((-73.9959209576448 40.73286384885367, -73.996317924579 40.732359794090684, -73.9947515145143 40.7315061442504, -73.99422580154739 40.73214841515045, -73.9959209576448 40.73286384885367))"; + Path citygml = TEST_REPOSITORY.resolve("NewYork.proj/ManhattanSmall.gml"); + InputStream stdin = new ByteArrayInputStream(wktPolygon.getBytes(StandardCharsets.UTF_8)); + System.setIn(stdin); + assertFalse(Files.exists(outGML)); + new CommandLine(new RegionChooserCLI()).execute("--input=" + citygml, "--output=" + outGML, + "--wkt=-"); + String expectedLog = "EPSG:32118"; + assertTrue(err.toString().contains(expectedLog), err.toString() + " should contain " + expectedLog); + assertTrue(Files.exists(outGML)); + assertEquals(2, countBuildings(outGML)); + } + + @Test + void testExtractRegionWithStandardInputAndStandardOutput() throws IOException { + String wktPolygon = "POLYGON((-73.99325421344473 40.730897087489666, -73.99359753619864 40.7304702545556, -73.99287870418264 40.7300800049056, -73.99244955074026 40.730592207101864, -73.99325421344473 40.730897087489666))"; + Path citygml = TEST_REPOSITORY.resolve("NewYork.proj/ManhattanSmall.gml"); + InputStream stdin = new ByteArrayInputStream(wktPolygon.getBytes(StandardCharsets.UTF_8)); + System.setIn(stdin); + Path noOutput = Paths.get("-"); + Files.deleteIfExists(noOutput); + assertFalse(Files.exists(noOutput)); + new CommandLine(new RegionChooserCLI()).execute("--input=" + citygml, "--output=-", "--wkt=-"); + String expectedLog = "EPSG:32118"; + assertTrue(err.toString().contains(expectedLog), err.toString() + " should contain " + expectedLog); + String expectedBuilding = "uuid_0547df65-ae80-459e-bb15-c839c1a2e566"; + assertTrue(out.toString().contains(expectedBuilding), out.toString() + " should contain " + expectedBuilding); + //TODO: Check if footer is here too + assertFalse(Files.exists(noOutput)); + } + + @Test + void testExtractRegionWithMissingInput() throws IOException { + String wktPolygon = "POLYGON((-73.9959209576448 40.73286384885367, -73.996317924579 40.732359794090684, -73.9947515145143 40.7315061442504, -73.99422580154739 40.73214841515045, -73.9959209576448 40.73286384885367))"; + Path citygml = TEST_REPOSITORY.resolve("NewYork.proj/ManhattanSmall.gml"); + Path inWKT = tempDirectory.resolve("polygon.wkt"); + try (BufferedWriter wkt = Files.newBufferedWriter(inWKT)) { + wkt.write(wktPolygon); + } + new CommandLine(new RegionChooserCLI()).execute("--input=" + citygml, "--output=" + outGML, + "--wkt=-"); + String expectedLog = "EPSG:32118"; + assertTrue(err.toString().contains(expectedLog), err.toString() + " should contain " + expectedLog); + expectedLog = "Please provide \"POLYGON((x1 y1, x2 y2, ...))\" to standard input."; + assertTrue(err.toString().contains(expectedLog), err.toString() + " should contain " + expectedLog); + assertFalse(Files.exists(outGML)); + } + + private long countBuildings(Path outGML) throws IOException { + return Files.readAllLines(outGML).stream().filter(line -> line.contains("bldg:Building gml:id=")).count(); + } +} + diff --git a/src/test/java/eu/simstadt/regionchooser/RegionExtractorTests.java b/src/test/java/eu/simstadt/regionchooser/RegionExtractorTests.java index bbac7f07ea45297ce215965f0d16d1e8ccaff5e9..ada5e84ca5d0ef745fa5251640ad0a9c4aa31cc5 100644 --- a/src/test/java/eu/simstadt/regionchooser/RegionExtractorTests.java +++ b/src/test/java/eu/simstadt/regionchooser/RegionExtractorTests.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.StringWriter; import java.nio.file.Path; import java.nio.file.Paths; import java.util.regex.Matcher; @@ -35,8 +36,10 @@ void testExtract3BuildingsFromGSK3Model() throws Throwable { //NOTE: Small region around Martinskirche in Grünbühl String wktPolygon = "POLYGON((3515848.896028535 5415823.108586172,3515848.9512289143 5415803.590347393,3515829.0815150724 5415803.338023346,3515830.9784850604 5415793.437034622,3515842.0946056456 5415793.272282251,3515843.3515515197 5415766.204935087,3515864.1064344468 5415766.557899496,3515876.489172751 5415805.433782301,3515876.343844858 5415822.009293416,3515848.896028535 5415823.108586172))"; Path citygmlPath = TEST_REPOSITORY.resolve("Gruenbuehl.proj/20140218_Gruenbuehl_LOD2.gml"); - String churchGMLString = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, "EPSG:31467", citygmlPath) - .toString(); + StringWriter gmlWriter = new StringWriter(); + int count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, "EPSG:31467", gmlWriter, citygmlPath); + String churchGMLString = gmlWriter.toString(); + assertEquals(3, count); assertEquals(3, countRegexMatches(churchGMLString, CITY_OBJECT_MEMBER_REGEX)); assertTrue(churchGMLString.contains("Donaustr")); assertTrue(churchGMLString.contains("DEBW_LOD2_203056")); @@ -53,30 +56,29 @@ void testExtract3BuildingsFromGSK3Model() throws Throwable { } @Test - void testExtractBuildingsWithoutCommentsInBetween() throws Throwable { - //NOTE: Small region around WashingtonSquare + void testExtractBuildingsWithCommentsInBetween() throws Throwable { String wktPolygon = "POLYGON((300259.78663489706 62835.835907766595,300230.33294975647 62792.0482567884,300213.5667431851 62770.83143720031,300183.6592861123 62730.20347659383,300252.9947486632 62676.938468840905,300273.3862256562 62701.767105345614,300257.5250407747 62715.760413539596,300308.2754543957 62805.14198211394,300259.78663489706 62835.835907766595))"; Path citygmlPath = TEST_REPOSITORY.resolve("NewYork.proj/ManhattanSmall.gml"); - String archGMLString = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, EPSG_32118, citygmlPath) - .toString(); - assertEquals(countRegexMatches(archGMLString, CITY_OBJECT_MEMBER_REGEX), 2); + StringWriter gmlWriter = new StringWriter(); + int count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, EPSG_32118, gmlWriter, citygmlPath); + String archGMLString = gmlWriter.toString(); + assertEquals(2, count); + assertEquals(2, countRegexMatches(archGMLString, CITY_OBJECT_MEMBER_REGEX)); assertTrue(archGMLString.contains("WASHINGTON SQUARE")); assertTrue(archGMLString.contains("uuid_c0980a6e-05ea-4d09-bc83-efab226945a1")); assertTrue(archGMLString.contains("uuid_0985cebb-922d-4b3e-95e5-15dc6089cd28")); assertTrue(archGMLString.contains(CITY_MODEL_HEADER)); assertTrue(archGMLString.contains(CITY_MODEL_FOOTER)); - assertFalse(archGMLString.contains("comment between buildings"), - "Comments between buildings shouldn't be extracted"); - assertFalse(archGMLString.contains("comment after last building"), - "Comments after buildings shouldn't be extracted"); } @Test void testExtractBuildingsAndChangeEnvelope() throws Throwable { String wktPolygon = "POLYGON((299761.8123557725 61122.68126771413,299721.46983062755 61058.11626595352,299780.84627343423 61021.99295737501,299823.9079725632 61083.3979344517,299761.8123557725 61122.68126771413))"; Path citygmlPath = TEST_REPOSITORY.resolve("NewYork.proj/FamilyCourt_LOD2_with_PLUTO_attributes.gml"); - String familyCourtBuilding = RegionExtractor - .selectRegionDirectlyFromCityGML(wktPolygon, EPSG_32118, citygmlPath).toString(); + StringWriter gmlWriter = new StringWriter(); + int count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, EPSG_32118, gmlWriter, citygmlPath); + String familyCourtBuilding = gmlWriter.toString(); + assertEquals(1, count); assertEquals(1, countRegexMatches(familyCourtBuilding, CITY_OBJECT_MEMBER_REGEX)); assertTrue(familyCourtBuilding.contains("Bldg_12210021066")); assertFalse( @@ -100,8 +102,10 @@ void testExtract0BuildingsWithWrongCoordinates() throws Throwable { //NOTE: Small region, far away from NYC String wktPolygon = "POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))"; Path citygmlPath = TEST_REPOSITORY.resolve("NewYork.proj/ManhattanSmall.gml"); - String emptyGMLString = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, EPSG_32118, citygmlPath) - .toString(); + StringWriter gmlWriter = new StringWriter(); + int count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, EPSG_32118, gmlWriter, citygmlPath); + String emptyGMLString = gmlWriter.toString(); + assertEquals(0, count); assertEquals(0, countRegexMatches(emptyGMLString, CITY_OBJECT_MEMBER_REGEX)); assertTrue(emptyGMLString.contains(CITY_MODEL_HEADER)); assertTrue(emptyGMLString.contains(CITY_MODEL_FOOTER)); @@ -112,8 +116,10 @@ void testExtract0BuildingsFromEmptyGML() throws Throwable { //NOTE: Small region, with too many spaces between coordinates String wktPolygon = "POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))"; Path citygmlPath = TEST_REPOSITORY.resolve("NewYork.proj/empty_model.gml"); - String emptyGMLString = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, EPSG_32118, citygmlPath) - .toString(); + StringWriter gmlWriter = new StringWriter(); + int count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, EPSG_32118, gmlWriter, citygmlPath); + String emptyGMLString = gmlWriter.toString(); + assertEquals(0, count); assertEquals(0, countRegexMatches(emptyGMLString, CITY_OBJECT_MEMBER_REGEX)); assertTrue(emptyGMLString.contains(CORE_CITY_MODEL_HEADER)); assertTrue(emptyGMLString.contains(CORE_CITY_MODEL_FOOTER)); @@ -124,8 +130,10 @@ void testExtract0BuildingsFromWeirdGML() throws Throwable { //NOTE: Small region, with too many spaces between coordinates String wktPolygon = "POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))"; Path citygmlPath = TEST_REPOSITORY.resolve("NewYork.proj/broken_nyc_lod2.gml"); - String emptyGMLString = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, EPSG_32118, citygmlPath) - .toString(); + StringWriter gmlWriter = new StringWriter(); + int count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, EPSG_32118, gmlWriter, citygmlPath); + String emptyGMLString = gmlWriter.toString(); + assertEquals(0, count); assertEquals(0, countRegexMatches(emptyGMLString, CITY_OBJECT_MEMBER_REGEX)); assertTrue(emptyGMLString.contains(CORE_CITY_MODEL_HEADER)); assertTrue(emptyGMLString.contains(CORE_CITY_MODEL_FOOTER)); @@ -135,9 +143,11 @@ void testExtract0BuildingsFromWeirdGML() throws Throwable { void testExtractBuildingsFromCitygmlWithoutZinEnvelope() throws Throwable { String wktPolygon = "POLYGON((3512683.1280912133 5404783.732132129,3512719.1608604863 5404714.627650777,3512831.40076119 5404768.344155442,3512790.239106708 5404838.614891164,3512683.1280912133 5404783.732132129))"; Path citygmlPath = TEST_REPOSITORY.resolve("Stuttgart.proj/Stuttgart_LOD0_LOD1_small.gml"); - String emptyGMLString = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, "EPSG:31463", citygmlPath) - .toString(); - assertEquals(2, countRegexMatches(emptyGMLString, "<bldg:Building gml:id")); + StringWriter gmlWriter = new StringWriter(); + int count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, "EPSG:31463", gmlWriter, citygmlPath); + String twoBuildings = gmlWriter.toString(); + assertEquals(2, count); + assertEquals(2, countRegexMatches(twoBuildings, "<bldg:Building gml:id")); } @Test @@ -146,9 +156,12 @@ void testExtractBuildingsFrom2Citygmls() throws Throwable { Path citygml1 = TEST_REPOSITORY.resolve("Stuttgart.proj/Stuttgart_LOD0_LOD1_small.gml"); Path citygml2 = TEST_REPOSITORY.resolve("Stuttgart.proj/Stöckach_überarbeitete GML-NoBuildingPart.gml"); - String emptyGMLString = RegionExtractor - .selectRegionDirectlyFromCityGML(wktPolygon, "EPSG:31463", citygml1, citygml2).toString(); - assertEquals(17 + 3, countRegexMatches(emptyGMLString, "<bldg:Building gml:id")); + StringWriter gmlWriter = new StringWriter(); + int count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, "EPSG:31463", gmlWriter, citygml1, + citygml2); + String gmlFromTwoGMLs = gmlWriter.toString(); + assertEquals(17 + 3, count); + assertEquals(17 + 3, countRegexMatches(gmlFromTwoGMLs, "<bldg:Building gml:id")); } diff --git a/src/test/java/eu/simstadt/regionchooser/RegionExtractorWithDifferentInputTests.java b/src/test/java/eu/simstadt/regionchooser/RegionExtractorWithDifferentInputTests.java index 47bd64717d5db996940dfda49901ff1abe8ba5d3..9f9b8c3eb6aa87d02b4701cad731f9f4a186107b 100644 --- a/src/test/java/eu/simstadt/regionchooser/RegionExtractorWithDifferentInputTests.java +++ b/src/test/java/eu/simstadt/regionchooser/RegionExtractorWithDifferentInputTests.java @@ -2,30 +2,25 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.io.StringWriter; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; -import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.io.ParseException; -import org.locationtech.jts.io.WKTReader; -import org.locationtech.jts.io.WKTWriter; import org.osgeo.proj4j.CoordinateReferenceSystem; import com.ximpleware.NavException; import com.ximpleware.XPathEvalException; import com.ximpleware.XPathParseException; -public class RegionExtractorWithDifferentInputTests +class RegionExtractorWithDifferentInputTests { - private static final WKTReader WKT_READER = new WKTReader(); - private static final WKTWriter WKT_WRITER = new WKTWriter(); private static final Path TEST_REPOSITORY = Paths.get("src/test/resources/testdata/"); - //NOTE: This test can be adapted to download a region which is too large for the server. Here with local coordinates @Test - public void testExtractRegionWithLocalCRS() + void testExtractRegionWithLocalCRS() throws IOException, XPathParseException, NavException, XPathEvalException, ParseException { String citygml = "DA13_DA14_3D_Buildings_Port_Morris.gml"; String projectName = "NewYork"; @@ -34,14 +29,14 @@ public void testExtractRegionWithLocalCRS() Path citygmlPath = project.resolve(citygml); CoordinateReferenceSystem localCRS = RegionChooserUtils.crsFromCityGMLHeader(citygmlPath); - StringBuilder sb = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, localCRS.getName(), citygmlPath); - assertTrue(sb.toString().contains("gml_ZVHMQQ6BZGRT0O3Q6RGXF12BDOV49QIZ58XB"), + StringWriter gmlWriter = new StringWriter(); + RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, localCRS.getName(), gmlWriter, citygmlPath); + assertTrue(gmlWriter.toString().contains("gml_ZVHMQQ6BZGRT0O3Q6RGXF12BDOV49QIZ58XB"), "One weird shaped roof should be inside the region"); } - //NOTE: This test can be adapted to download a region which is too large for the server. Here with WGS84 coordinates @Test - public void testExtractRegionWithWGS84() + void testExtractRegionWithWGS84() throws ParseException, IOException, XPathParseException, NavException, XPathEvalException { String wgs84WktPolygon = "POLYGON((-73.91140940026597 40.804246732157196,-73.91424181298568 40.80025100302325,-73.90934946374252 40.79755456207104,-73.90561582879378 40.80116062104605,-73.90960695580794 40.80340212653638,-73.91140940026597 40.804246732157196))"; String citygml = "DA13_DA14_3D_Buildings_Port_Morris.gml"; @@ -49,19 +44,16 @@ public void testExtractRegionWithWGS84() Path project = TEST_REPOSITORY.resolve(projectName + ".proj"); Path citygmlPath = project.resolve(citygml); - Polygon wgs84Polygon = (Polygon) WKT_READER.read(wgs84WktPolygon); CoordinateReferenceSystem localCRS = RegionChooserUtils.crsFromCityGMLHeader(citygmlPath); - String localWktPolygon = WKT_WRITER - .write(RegionChooserUtils.changePolygonCRS(wgs84Polygon, RegionChooserUtils.WGS84, localCRS)); - StringBuilder sb = RegionExtractor.selectRegionDirectlyFromCityGML(localWktPolygon, localCRS.getName(), - citygmlPath); - assertTrue(sb.toString().contains("gml_ZVHMQQ6BZGRT0O3Q6RGXF12BDOV49QIZ58XB"), + String localWktPolygon = RegionChooserUtils.wktPolygonToLocalCRS(wgs84WktPolygon, localCRS); + StringWriter gmlWriter = new StringWriter(); + RegionExtractor.selectRegionDirectlyFromCityGML(localWktPolygon, localCRS.getName(), gmlWriter, citygmlPath); + assertTrue(gmlWriter.toString().contains("gml_ZVHMQQ6BZGRT0O3Q6RGXF12BDOV49QIZ58XB"), "One weird shaped roof should be inside the region"); } - //NOTE: This test can be adapted to download a region which is too large for the server. Here with old coordinates from WebSimstadt @Test - public void testExtractRegionWithOldCoordinates() + void testExtractRegionWithOldCoordinates() throws ParseException, IOException, XPathParseException, NavException, XPathEvalException { String oldFormatPolygon = "(40.81173171854368,-73.93268437431763)\r\n" + "(40.81069231965162,-73.93068165999941)\r\n" + @@ -98,16 +90,12 @@ public void testExtractRegionWithOldCoordinates() Path project = TEST_REPOSITORY.resolve(projectName + ".proj"); Path citygmlPath = project.resolve(citygml); - Polygon wgs84Polygon = (Polygon) WKT_READER.read(wgs84WktPolygon); CoordinateReferenceSystem localCRS = RegionChooserUtils.crsFromCityGMLHeader(citygmlPath); - String localWktPolygon = WKT_WRITER - .write(RegionChooserUtils.changePolygonCRS(wgs84Polygon, RegionChooserUtils.WGS84, localCRS)); - StringBuilder sb = RegionExtractor.selectRegionDirectlyFromCityGML(localWktPolygon, localCRS.getName(), - citygmlPath); + String localWktPolygon = RegionChooserUtils.wktPolygonToLocalCRS(wgs84WktPolygon, localCRS); + StringWriter gmlWriter = new StringWriter(); + RegionExtractor.selectRegionDirectlyFromCityGML(localWktPolygon, localCRS.getName(), gmlWriter, citygmlPath); - assertTrue(sb.toString().contains("gml_ZVHMQQ6BZGRT0O3Q6RGXF12BDOV49QIZ58XB"), + assertTrue(gmlWriter.toString().contains("gml_ZVHMQQ6BZGRT0O3Q6RGXF12BDOV49QIZ58XB"), "One weird shaped roof should be inside the region"); } - - //TODO: Write a method to merge RegionChooser results from multiple gmls }