Commit 5afd8f90 authored by Eric Duminil's avatar Eric Duminil
Browse files

Save OSM files in cache

parent 2c31e4f1
...@@ -2,17 +2,16 @@ package de.hft.stuttgart.citygml.green.alkis; ...@@ -2,17 +2,16 @@ package de.hft.stuttgart.citygml.green.alkis;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.citygml4j.core.model.core.CityModel; import org.citygml4j.core.model.core.CityModel;
import org.citygml4j.xml.CityGMLContextException; import org.citygml4j.xml.CityGMLContextException;
import org.citygml4j.xml.reader.CityGMLReadException; import org.citygml4j.xml.reader.CityGMLReadException;
...@@ -26,7 +25,6 @@ import org.locationtech.jts.geom.MultiPolygon; ...@@ -26,7 +25,6 @@ import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.geom.Polygon;
import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.simple.SimpleFeatureType;
import de.hft.stuttgart.citygml.green.osm.GreenArea; import de.hft.stuttgart.citygml.green.osm.GreenArea;
import de.hft.stuttgart.citygml.green.osm.GreenEnricher; import de.hft.stuttgart.citygml.green.osm.GreenEnricher;
import de.hft.stuttgart.citygml.green.osm.LandUseArea; import de.hft.stuttgart.citygml.green.osm.LandUseArea;
...@@ -35,25 +33,29 @@ import de.hft.stuttgart.citygml.green.osm.RoadArea; ...@@ -35,25 +33,29 @@ import de.hft.stuttgart.citygml.green.osm.RoadArea;
import de.hft.stuttgart.citygml.green.osm.TreeUtils; import de.hft.stuttgart.citygml.green.osm.TreeUtils;
import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.JAXBException;
public class AlkisGreenEnricher {
public class AlkisGreenEnricher
{
private static Set<String> greenAreaTypes = new HashSet<>(); private static Set<String> greenAreaTypes = new HashSet<>();
private static Set<String> roadAreaTypes = new HashSet<>(); private static Set<String> roadAreaTypes = new HashSet<>();
static { static {
greenAreaTypes.add("Wald"); greenAreaTypes.add("Wald");
greenAreaTypes.add("Landwirtschaft"); greenAreaTypes.add("Landwirtschaft");
greenAreaTypes.add("Friedhof"); greenAreaTypes.add("Friedhof");
greenAreaTypes.add("Gehölz"); greenAreaTypes.add("Gehölz");
greenAreaTypes.add("Sport-, Freizeit- und Erholungsfläche"); greenAreaTypes.add("Sport-, Freizeit- und Erholungsfläche");
roadAreaTypes.add("Weg"); roadAreaTypes.add("Weg");
roadAreaTypes.add("Straßenverkehr"); roadAreaTypes.add("Straßenverkehr");
roadAreaTypes.add("Bahnverkehr"); roadAreaTypes.add("Bahnverkehr");
} }
public static void main(String[] args) throws IOException, CityGMLContextException, CityGMLReadException, JAXBException, CityGMLWriteException { public static void main(String[] args)
throws IOException, InterruptedException, CityGMLContextException, CityGMLReadException, JAXBException,
CityGMLWriteException {
System.out.println("Reading CityGML file"); System.out.println("Reading CityGML file");
Path inFile = Paths.get(args[0]); Path inFile = Paths.get(args[0]);
CityModel cityModel = GreenEnricher.readCityGml(inFile); CityModel cityModel = GreenEnricher.readCityGml(inFile);
...@@ -62,33 +64,37 @@ public class AlkisGreenEnricher { ...@@ -62,33 +64,37 @@ public class AlkisGreenEnricher {
OsmData osmData = new OsmData(); OsmData osmData = new OsmData();
String boundingBoxString = GreenEnricher.extractAndConvertBoundingBox(cityModel, osmData); String boundingBoxString = GreenEnricher.extractAndConvertBoundingBox(cityModel, osmData);
// HttpResponse<String> response = getOsmData(boundingBoxString); String boundingBoxBasename = boundingBoxString.replace(",", "__").replace('.', '_');
// Files.write(Path.of("osm_response.xml"), response.body().getBytes(StandardCharsets.UTF_8)); Path osmCache = Paths.get("data", "cache", "osm_response_" + boundingBoxBasename + ".xml");
// String osmResponse = response.body(); if (!Files.exists(osmCache)) {
String osmResponse = Files.readString(Paths.get("data", "osm_response.xml")); System.out.println("Downloading OSM data for " + boundingBoxString);
HttpResponse<String> response = GreenEnricher.getOsmData(boundingBoxString);
Files.write(osmCache, response.body().getBytes(StandardCharsets.UTF_8));
}
String osmResponse = Files.readString(osmCache);
System.out.println("Parsing OSM response"); System.out.println("Parsing OSM response");
GreenEnricher.parseOsmResponse(osmResponse, osmData); GreenEnricher.parseOsmResponse(osmResponse, osmData);
// ignore green areas from osm // ignore green areas from osm
osmData.getGreenAreas().clear(); osmData.getGreenAreas().clear();
parseAlkisData(osmData); parseAlkisData(osmData);
System.out.println("Fit data in bounding box"); System.out.println("Fit data in bounding box");
GreenEnricher.fitToBoundingBox(osmData); GreenEnricher.fitToBoundingBox(osmData);
GreenEnricher.convertGreenAreasToCityGML(cityModel, osmData.getGreenAreas()); GreenEnricher.convertGreenAreasToCityGML(cityModel, osmData.getGreenAreas());
GreenEnricher.convertWaterAreasToCityGML(cityModel, osmData); GreenEnricher.convertWaterAreasToCityGML(cityModel, osmData);
GreenEnricher.convertRoadAreasToCityGML(cityModel, osmData); GreenEnricher.convertRoadAreasToCityGML(cityModel, osmData);
GreenEnricher.converLandUseAreasToCityGML(cityModel, osmData); GreenEnricher.converLandUseAreasToCityGML(cityModel, osmData);
TreeUtils.insertTrees(cityModel, osmData); TreeUtils.insertTrees(cityModel, osmData);
GreenEnricher.clampToGround(cityModel); GreenEnricher.clampToGround(cityModel);
String inputString = inFile.getFileName().toString(); String inputString = inFile.getFileName().toString();
String inputPathWithoutFileEnding = inputString.substring(0, inputString.lastIndexOf('.')); String inputPathWithoutFileEnding = inputString.substring(0, inputString.lastIndexOf('.'));
Path outputPath = Paths.get("data", inputPathWithoutFileEnding + "_with_alkis_greens_realistic.gml"); Path outputPath = Paths.get("data", inputPathWithoutFileEnding + "_with_alkis_greens_realistic.gml");
...@@ -101,7 +107,7 @@ public class AlkisGreenEnricher { ...@@ -101,7 +107,7 @@ public class AlkisGreenEnricher {
private static void parseAlkisData(OsmData osmData) throws MalformedURLException, IOException { private static void parseAlkisData(OsmData osmData) throws MalformedURLException, IOException {
Path alkisDataPath = Paths.get("data", "tn_09663", "Nutzung.shp"); Path alkisDataPath = Paths.get("data", "tn_09663", "Nutzung.shp");
Map<String, Object> readParameters = new HashMap<>(); Map<String, Object> readParameters = new HashMap<>();
readParameters.put("url", alkisDataPath.toUri().toURL()); readParameters.put("url", alkisDataPath.toUri().toURL());
readParameters.put("charset", StandardCharsets.UTF_8); readParameters.put("charset", StandardCharsets.UTF_8);
...@@ -111,15 +117,15 @@ public class AlkisGreenEnricher { ...@@ -111,15 +117,15 @@ public class AlkisGreenEnricher {
FeatureSource<SimpleFeatureType, SimpleFeature> source = dataStore.getFeatureSource(typeName); FeatureSource<SimpleFeatureType, SimpleFeature> source = dataStore.getFeatureSource(typeName);
FeatureCollection<SimpleFeatureType, SimpleFeature> collection = source.getFeatures(); FeatureCollection<SimpleFeatureType, SimpleFeature> collection = source.getFeatures();
List<GreenArea> greenAreas = osmData.getGreenAreas(); List<GreenArea> greenAreas = osmData.getGreenAreas();
List<RoadArea> roadAreas = osmData.getRoadAreas(); List<RoadArea> roadAreas = osmData.getRoadAreas();
List<LandUseArea> landUseAreas = osmData.getLandUseAreas(); List<LandUseArea> landUseAreas = osmData.getLandUseAreas();
try (FeatureIterator<SimpleFeature> features = collection.features()) { try (FeatureIterator<SimpleFeature> features = collection.features()) {
while (features.hasNext()) { while (features.hasNext()) {
SimpleFeature feature = features.next(); SimpleFeature feature = features.next();
MultiPolygon geometry = (MultiPolygon) feature.getAttribute("the_geom"); MultiPolygon geometry = (MultiPolygon) feature.getAttribute("the_geom");
String nutzart = feature.getAttribute("nutzart").toString(); String nutzart = feature.getAttribute("nutzart").toString();
if (geometry.getNumGeometries() > 1) { if (geometry.getNumGeometries() > 1) {
......
...@@ -18,9 +18,7 @@ import java.util.Set; ...@@ -18,9 +18,7 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import org.citygml4j.core.model.CityGMLVersion; import org.citygml4j.core.model.CityGMLVersion;
import org.citygml4j.core.model.building.Building; import org.citygml4j.core.model.building.Building;
import org.citygml4j.core.model.core.AbstractCityObject; import org.citygml4j.core.model.core.AbstractCityObject;
...@@ -62,7 +60,6 @@ import org.xmlobjects.gml.model.geometry.aggregates.MultiSurfaceProperty; ...@@ -62,7 +60,6 @@ import org.xmlobjects.gml.model.geometry.aggregates.MultiSurfaceProperty;
import org.xmlobjects.gml.model.geometry.primitives.AbstractRingProperty; import org.xmlobjects.gml.model.geometry.primitives.AbstractRingProperty;
import org.xmlobjects.gml.model.geometry.primitives.LinearRing; import org.xmlobjects.gml.model.geometry.primitives.LinearRing;
import org.xmlobjects.gml.model.geometry.primitives.SurfaceProperty; import org.xmlobjects.gml.model.geometry.primitives.SurfaceProperty;
import de.hft.stuttgart.citygml.green.osm.jaxb.OSM; import de.hft.stuttgart.citygml.green.osm.jaxb.OSM;
import de.hft.stuttgart.citygml.green.osm.jaxb.OsmMember; import de.hft.stuttgart.citygml.green.osm.jaxb.OsmMember;
import de.hft.stuttgart.citygml.green.osm.jaxb.OsmNode; import de.hft.stuttgart.citygml.green.osm.jaxb.OsmNode;
...@@ -73,7 +70,9 @@ import de.hft.stuttgart.citygml.green.osm.jaxb.WayNode; ...@@ -73,7 +70,9 @@ import de.hft.stuttgart.citygml.green.osm.jaxb.WayNode;
import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.JAXBException;
public class GreenEnricher {
public class GreenEnricher
{
private static final int BOUNDING_BOX_INCREASE_IN_M = 100; private static final int BOUNDING_BOX_INCREASE_IN_M = 100;
...@@ -123,9 +122,9 @@ public class GreenEnricher { ...@@ -123,9 +122,9 @@ public class GreenEnricher {
OsmData osmData = new OsmData(); OsmData osmData = new OsmData();
String boundingBoxString = extractAndConvertBoundingBox(cityModel, osmData); String boundingBoxString = extractAndConvertBoundingBox(cityModel, osmData);
// HttpResponse<String> response = getOsmData(boundingBoxString); // HttpResponse<String> response = getOsmData(boundingBoxString);
// Files.write(Path.of("osm_response.xml"), response.body().getBytes(StandardCharsets.UTF_8)); // Files.write(Path.of("osm_response.xml"), response.body().getBytes(StandardCharsets.UTF_8));
// String osmResponse = response.body(); // String osmResponse = response.body();
String osmResponse = Files.readString(Paths.get("data", "osm_response.xml")); String osmResponse = Files.readString(Paths.get("data", "osm_response.xml"));
System.out.println("Parsing OSM response"); System.out.println("Parsing OSM response");
...@@ -143,7 +142,7 @@ public class GreenEnricher { ...@@ -143,7 +142,7 @@ public class GreenEnricher {
// trees // trees
TreeUtils.insertTrees(cityModel, osmData); TreeUtils.insertTrees(cityModel, osmData);
clampToGround(cityModel); clampToGround(cityModel);
String inputString = inFile.getFileName().toString(); String inputString = inFile.getFileName().toString();
...@@ -229,7 +228,7 @@ public class GreenEnricher { ...@@ -229,7 +228,7 @@ public class GreenEnricher {
landUseArea.setArea((Polygon) intersection); landUseArea.setArea((Polygon) intersection);
} }
} }
landUseAreas.addAll(newLandUseAreas); landUseAreas.addAll(newLandUseAreas);
} }
...@@ -318,6 +317,7 @@ public class GreenEnricher { ...@@ -318,6 +317,7 @@ public class GreenEnricher {
} }
} }
} }
public static void clampToGround(CityModel cityModel) { public static void clampToGround(CityModel cityModel) {
for (AbstractCityObjectProperty afp : cityModel.getCityObjectMembers()) { for (AbstractCityObjectProperty afp : cityModel.getCityObjectMembers()) {
AbstractCityObject af = afp.getObject(); AbstractCityObject af = afp.getObject();
...@@ -376,7 +376,7 @@ public class GreenEnricher { ...@@ -376,7 +376,7 @@ public class GreenEnricher {
return result; return result;
} }
private static HttpResponse<String> getOsmData(String boundingBoxString) throws IOException, InterruptedException { public static HttpResponse<String> getOsmData(String boundingBoxString) throws IOException, InterruptedException {
String data = OSM_STRING.replace("{{bbox}}", boundingBoxString); String data = OSM_STRING.replace("{{bbox}}", boundingBoxString);
String body = URLEncoder.encode("data", StandardCharsets.UTF_8) + "=" String body = URLEncoder.encode("data", StandardCharsets.UTF_8) + "="
+ URLEncoder.encode(data, StandardCharsets.UTF_8); + URLEncoder.encode(data, StandardCharsets.UTF_8);
...@@ -436,7 +436,7 @@ public class GreenEnricher { ...@@ -436,7 +436,7 @@ public class GreenEnricher {
} }
} }
} }
if (!coordinates.isEmpty() && coordinates.get(0).equals2D(coordinates.get(coordinates.size() - 1))) { if (!coordinates.isEmpty() && coordinates.get(0).equals2D(coordinates.get(coordinates.size() - 1))) {
// one huge polygon apparently // one huge polygon apparently
Polygon polygon = GEOM_FACTORY.createPolygon(coordinates.toArray(new Coordinate[coordinates.size()])); Polygon polygon = GEOM_FACTORY.createPolygon(coordinates.toArray(new Coordinate[coordinates.size()]));
...@@ -445,13 +445,13 @@ public class GreenEnricher { ...@@ -445,13 +445,13 @@ public class GreenEnricher {
} else { } else {
// line // line
} }
for (OsmTag tag : rel.getTags()) { for (OsmTag tag : rel.getTags()) {
if ("water".equals(tag.getValue()) || "waterway".equals(tag.getKey())) { if ("water".equals(tag.getValue()) || "waterway".equals(tag.getKey())) {
isWater = true; isWater = true;
} }
} }
if (isWater) { if (isWater) {
for (Polygon p : polygons) { for (Polygon p : polygons) {
data.getWaterAreas().add(new WaterArea(p)); data.getWaterAreas().add(new WaterArea(p));
...@@ -587,28 +587,28 @@ public class GreenEnricher { ...@@ -587,28 +587,28 @@ public class GreenEnricher {
data.getWaterAreas().add(new WaterArea(polygon)); data.getWaterAreas().add(new WaterArea(polygon));
} }
// validateRing(outerRing); // validateRing(outerRing);
// create the outer ring // create the outer ring
// org.locationtech.jts.geom.LinearRing outerLinearRing = geomFactory // org.locationtech.jts.geom.LinearRing outerLinearRing = geomFactory
// .createLinearRing(outerRing.toArray(new Coordinate[outerRing.size()])); // .createLinearRing(outerRing.toArray(new Coordinate[outerRing.size()]));
// //
// // create the inner rings // // create the inner rings
// List<org.locationtech.jts.geom.LinearRing> innerLinearRings = new ArrayList<>(); // List<org.locationtech.jts.geom.LinearRing> innerLinearRings = new ArrayList<>();
// for (List<Coordinate> innerRing : innerRings) { // for (List<Coordinate> innerRing : innerRings) {
// org.locationtech.jts.geom.LinearRing innerLinearRing = geomFactory // org.locationtech.jts.geom.LinearRing innerLinearRing = geomFactory
// .createLinearRing(innerRing.toArray(new Coordinate[innerRing.size()])); // .createLinearRing(innerRing.toArray(new Coordinate[innerRing.size()]));
// innerLinearRings.add(innerLinearRing); // innerLinearRings.add(innerLinearRing);
// } // }
// //
// if (outerRing.isEmpty()) { // if (outerRing.isEmpty()) {
// return false; // return false;
// } // }
// //
// // create the polygon // // create the polygon
// Polygon polygon = geomFactory.createPolygon(outerLinearRing, // Polygon polygon = geomFactory.createPolygon(outerLinearRing,
// innerLinearRings.toArray(new org.locationtech.jts.geom.LinearRing[innerLinearRings.size()])); // innerLinearRings.toArray(new org.locationtech.jts.geom.LinearRing[innerLinearRings.size()]));
// //
// data.getGreenAreas().add(new GreenArea(polygon)); // data.getGreenAreas().add(new GreenArea(polygon));
return true; return true;
} }
...@@ -637,9 +637,9 @@ public class GreenEnricher { ...@@ -637,9 +637,9 @@ public class GreenEnricher {
ProjCoordinate converted = convertCoordinatesFrom84(lon, lat); ProjCoordinate converted = convertCoordinatesFrom84(lon, lat);
coordinates.add(new Coordinate(converted.x, converted.y)); coordinates.add(new Coordinate(converted.x, converted.y));
} else if ("tag".equals(child.getNodeName())) { } else if ("tag".equals(child.getNodeName())) {
// if (existTagWithValue(child, "k", "natural") && existTagWithValue(child, "v", "tree_row")) { // if (existTagWithValue(child, "k", "natural") && existTagWithValue(child, "v", "tree_row")) {
// line = true; // line = true;
// } // }
if ((existTagWithValue(child, "k", "natural") && existTagWithValue(child, "v", "water")) if ((existTagWithValue(child, "k", "natural") && existTagWithValue(child, "v", "water"))
|| existTagWithValue(child, "k", "waterway")) { || existTagWithValue(child, "k", "waterway")) {
water = true; water = true;
......
...@@ -4,7 +4,6 @@ import java.io.IOException; ...@@ -4,7 +4,6 @@ import java.io.IOException;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Iterator; import java.util.Iterator;
import java.util.UUID; import java.util.UUID;
import org.citygml4j.core.model.core.AbstractCityObjectProperty; import org.citygml4j.core.model.core.AbstractCityObjectProperty;
import org.citygml4j.core.model.core.CityModel; import org.citygml4j.core.model.core.CityModel;
import org.citygml4j.core.model.vegetation.SolitaryVegetationObject; import org.citygml4j.core.model.vegetation.SolitaryVegetationObject;
...@@ -13,23 +12,26 @@ import org.xmlobjects.gml.model.basictypes.Code; ...@@ -13,23 +12,26 @@ import org.xmlobjects.gml.model.basictypes.Code;
import org.xmlobjects.gml.model.geometry.aggregates.MultiSurface; import org.xmlobjects.gml.model.geometry.aggregates.MultiSurface;
import org.xmlobjects.gml.model.geometry.aggregates.MultiSurfaceProperty; import org.xmlobjects.gml.model.geometry.aggregates.MultiSurfaceProperty;
public class TreeUtils {
public class TreeUtils
{
public static void insertTrees(CityModel cityModel, OsmData osmData) throws IOException { public static void insertTrees(CityModel cityModel, OsmData osmData) throws IOException {
TreeKatasterData katasterData = TreeKatasterData.parseTreeKatasterData(Paths.get("data", "Trees_realisticScenario_20240201.shp")); TreeKatasterData katasterData = TreeKatasterData
.parseTreeKatasterData(Paths.get("data", "Trees", "Trees_realisticScenario_20240201.shp"));
generateTreesFromKataster(cityModel, katasterData); generateTreesFromKataster(cityModel, katasterData);
// TreeKatasterData katasterData2 = TreeKatasterData.parseTreeKatasterData(Paths.get("data", "Trees_realistic_20240201.shp")); // TreeKatasterData katasterData2 = TreeKatasterData.parseTreeKatasterData(Paths.get("data", "Trees_realistic_20240201.shp"));
// generateTreesFromKataster(cityModel, katasterData2); // generateTreesFromKataster(cityModel, katasterData2);
// All kataster trees are taken, osm trees are removed // All kataster trees are taken, osm trees are removed
filterDuplicateTreesFromOSM(osmData, katasterData); filterDuplicateTreesFromOSM(osmData, katasterData);
// filterDuplicateTreesFromOSM(osmData, katasterData2); // filterDuplicateTreesFromOSM(osmData, katasterData2);
generateTreesFromOSM(cityModel, osmData); generateTreesFromOSM(cityModel, osmData);
} }
private static void filterDuplicateTreesFromOSM(OsmData osmData, TreeKatasterData katasterData) { private static void filterDuplicateTreesFromOSM(OsmData osmData, TreeKatasterData katasterData) {
for (Iterator<TreePoint> iterator = osmData.getTreePoints().iterator(); iterator.hasNext();) { for (Iterator<TreePoint> iterator = osmData.getTreePoints().iterator(); iterator.hasNext();) {
TreePoint tp = iterator.next(); TreePoint tp = iterator.next();
...@@ -41,7 +43,7 @@ public class TreeUtils { ...@@ -41,7 +43,7 @@ public class TreeUtils {
} }
} }
} }
private static void generateTreesFromOSM(CityModel cityModel, OsmData osmData) { private static void generateTreesFromOSM(CityModel cityModel, OsmData osmData) {
for (TreePoint tp : osmData.getTreePoints()) { for (TreePoint tp : osmData.getTreePoints()) {
// standard tree // standard tree
......
package de.hft.stuttgart.citygml.green.alkis; package de.hft.stuttgart.citygml.green.alkis;
import java.io.IOException;
import org.citygml4j.xml.CityGMLContextException;
import org.citygml4j.xml.reader.CityGMLReadException;
import org.citygml4j.xml.writer.CityGMLWriteException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import jakarta.xml.bind.JAXBException;
class AlkisGreenEnricherTest { class AlkisGreenEnricherTest
{
@Test @Test
void testAlkisGreen() throws IOException, CityGMLContextException, CityGMLReadException, JAXBException, CityGMLWriteException { void testAlkisGreen() throws Exception {
String[] args = new String[] {"data/Grombühl_v4_case_study.gml"}; String[] args = new String[] { "data/Grombühl_v4_case_study.gml" };
AlkisGreenEnricher.main(args); AlkisGreenEnricher.main(args);
} }
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment