diff --git a/.classpath b/.classpath
index 23303aedd9d49ecab8fd18c2cde12cea9b6c3e49..c765133e2a10bdce9a6ddca81c16523a48995267 100644
--- a/.classpath
+++ b/.classpath
@@ -5,6 +5,9 @@
 	<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.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"/>
 </classpath>
diff --git a/src/eu/simstadt/geo/GeoUtils.java b/src/eu/simstadt/geo/GeoUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..0eedfb99941cf8978f7bd0ccdb3c9c1cace87d34
--- /dev/null
+++ b/src/eu/simstadt/geo/GeoUtils.java
@@ -0,0 +1,189 @@
+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"));
+	}
+
+}
diff --git a/src/eu/simstadt/geo/fast_xml_parser/BuildingXmlNode.java b/src/eu/simstadt/geo/fast_xml_parser/BuildingXmlNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e4a6ae3deec34c5ebdc590d84f5d9b0eb74e8b1
--- /dev/null
+++ b/src/eu/simstadt/geo/fast_xml_parser/BuildingXmlNode.java
@@ -0,0 +1,91 @@
+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 "";
+		}
+	}
+}
diff --git a/src/eu/simstadt/geo/fast_xml_parser/CityGmlIterator.java b/src/eu/simstadt/geo/fast_xml_parser/CityGmlIterator.java
new file mode 100644
index 0000000000000000000000000000000000000000..547d44830072dd9be08225f8e35d751eadffb9d6
--- /dev/null
+++ b/src/eu/simstadt/geo/fast_xml_parser/CityGmlIterator.java
@@ -0,0 +1,105 @@
+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);
+	}
+
+}
diff --git a/src/eu/simstadt/geo/fast_xml_parser/ConvexHullCalculator.java b/src/eu/simstadt/geo/fast_xml_parser/ConvexHullCalculator.java
new file mode 100644
index 0000000000000000000000000000000000000000..8cf54e7c236c56d4dc0ddbf338b10504519dc8ec
--- /dev/null
+++ b/src/eu/simstadt/geo/fast_xml_parser/ConvexHullCalculator.java
@@ -0,0 +1,209 @@
+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);
+	}
+}
diff --git a/test/eu/simstadt/geo/fast_xml_parser/CitygmlParserTests.java b/test/eu/simstadt/geo/fast_xml_parser/CitygmlParserTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..7e926ee650b1dd14b70960910dd14ead48a41295
--- /dev/null
+++ b/test/eu/simstadt/geo/fast_xml_parser/CitygmlParserTests.java
@@ -0,0 +1,78 @@
+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);
+	}
+}
diff --git a/test/eu/simstadt/geo/fast_xml_parser/ConvexHullCalculatorTests.java b/test/eu/simstadt/geo/fast_xml_parser/ConvexHullCalculatorTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..16422d46d59c088f122048947468ec62b1820d45
--- /dev/null
+++ b/test/eu/simstadt/geo/fast_xml_parser/ConvexHullCalculatorTests.java
@@ -0,0 +1,66 @@
+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);
+	}
+}