ConvexHullCalculator.java 9.34 KB
Newer Older
1
package eu.simstadt.regionchooser.fast_xml_parser;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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;
21
import eu.simstadt.regionchooser.RegionChooserUtils;
22
23
24
25
26
27


public class ConvexHullCalculator
{
	private static final Logger LOGGER = Logger.getLogger(ConvexHullCalculator.class.getName());

28
29
30
31
	private ConvexHullCalculator() {
		throw new IllegalStateException("Utility class");
	}

32
33
34
35
36
	/**
	 * 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.
37
	 *
38
	 * The convex hull is needed for RegionChooser. No SimstadtModel or CityDoctor model is needed, thanks to
39
40
	 * eu.simstadt.regionchooser.fast_xml_parser.CityGmlIterator.
	 *
41
42
43
44
45
46
	 * 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))
47
	 *
48
	 * for "20140218_Gruenbuehl_LOD2.gml"
49
	 *
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
	 * @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();

74
		CoordinateReferenceSystem originalCRS = RegionChooserUtils.crsFromCityGMLHeader(citygmlPath);
75
76
		Polygon convexHull = RegionChooserUtils.changePolygonCRS(originalConvexHull, originalCRS,
				RegionChooserUtils.WGS84);
77
78
79
80
81
82
83
		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.
84
	 *
85
	 * If hull has been extracted, get it from cache, calculate it otherwise.
86
	 *
87
88
89
90
91
	 * @param repository
	 * @param callback
	 * @throws IOException
	 */
	public static void extractHullsForEveryCityGML(Path repository, Consumer<String> callback) throws IOException {
92
93
94
95
		if (!Files.exists(repository)) {
			LOGGER.warning(repository + " does not appear to exist.");
			return;
		}
96
		LOGGER.info("Parsing " + repository);
97
		RegionChooserUtils.everyCityGML(repository)
98
99
100
101
102
103
104
105
106
107
108
109
110
				.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();
111
						return null;
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
					}
				})
				.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.
128
	 *
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
	 * @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.
199
	 *
200
201
202
	 * The original CityGML filesize is written in KML basename, in order to update the hull if the CityGML file is
	 * modified, e.g. LoD2_564_5512_2_BY_11946489.kml
	 *
203
204
	 * @param repository
	 * @param citygmlPath
205
	 *
206
207
208
209
	 * @return KML Path
	 * @throws IOException
	 */
	private static Path getHullPath(Path repository, Path citygmlPath) throws IOException {
210
211
		String kmlFilename = citygmlPath.getFileName().toString().replaceAll("(?i)\\.gml$",
				"_" + Files.size(citygmlPath) + ".kml");
212
213
214
215
216
217
218
219
220
221
222
		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);
	}
}