package eu.simstadt.regionchooser; import java.io.BufferedWriter; 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; 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 RegionChooserUtils { 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 static final int CITYGML_HEADER_LENGTH = 50; private static final String EPSG = "EPSG:"; private static final int BUFFER = 1024; private RegionChooserUtils() { // only static use } /** * 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); if (mURN.find()) { String shortSrsName = mURN.group(1); // Gauss Krueger: if (shortSrsName.startsWith("DE_DHDN_3GK")) { int gaussKruegerBaseEPSG = 31464; int gaussKruegerId = Integer.parseInt(shortSrsName.substring(11)); return CRS_FACTORY.createFromName(EPSG + (gaussKruegerBaseEPSG + gaussKruegerId)); } // UTM North: if (shortSrsName.startsWith("ETRS89_UTM")) { int utmBaseEPSG = 25800; int utmId = Integer.parseInt(shortSrsName.substring(10)); return CRS_FACTORY.createFromName(EPSG + (utmBaseEPSG + utmId)); } } 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 { try (Stream lines = Files.lines(citygmlPath).limit(CITYGML_HEADER_LENGTH)) { Optional line = lines.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); } } } /** * Finds every CityGML in every .proj folder in a repository. * * @param repository * @return a stream of CityGML Paths * @throws IOException */ public static Stream 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")); } public static void writeStringBuilderToFile(StringBuilder sb, Path outputFile) throws IOException { if (outputFile != null) { try (BufferedWriter writer = Files.newBufferedWriter(outputFile)) { char[] chars = new char[BUFFER]; for (int aPosStart = 0; aPosStart < sb.length(); aPosStart += BUFFER) { int chunk = Math.min(BUFFER, sb.length() - aPosStart); sb.getChars(aPosStart, aPosStart + chunk, chars, 0); writer.write(chars, 0, chunk); } } } } /** * 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())); } }