package eu.simstadt.regionchooser.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.regionchooser.RegionChooserUtils; 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.regionchooser.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 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 = RegionChooserUtils.crsFromCityGMLHeader(citygmlPath); Polygon convexHull = RegionChooserUtils.changePolygonCRS(originalConvexHull, originalCRS, RegionChooserUtils.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 callback) throws IOException { LOGGER.info("Parsing " + repository); RegionChooserUtils.everyCityGML(repository) .map(gmlPath -> { LOGGER.fine("Importing " + repository.relativize(gmlPath)); 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 { 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("")) { 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 don't seem to be needed. They're easier to concatenate without. StringBuilder kmlPlacemark = new StringBuilder(); kmlPlacemark.append(" \n"); kmlPlacemark.append(" "); kmlPlacemark.append(gmlPath.getFileName()); kmlPlacemark.append("\n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" "); kmlPlacemark.append(gmlPath.getParent().getFileName().toString().replace(".proj", "")); kmlPlacemark.append("\n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" "); kmlPlacemark.append(convexHull.getUserData()); kmlPlacemark.append("\n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" 1\n"); kmlPlacemark.append(" "); for (Coordinate coordinate : convexHull.getCoordinates()) { kmlPlacemark.append(coordinate.x); kmlPlacemark.append(","); kmlPlacemark.append(coordinate.y); kmlPlacemark.append(",0 "); } kmlPlacemark.append("\n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" \n"); kmlPlacemark.append(" \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 = "\n\n"; } 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); } }