package me.osm.gazetter.striper.builders; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import me.osm.gazetter.addresses.AddressesUtils; import me.osm.gazetter.addresses.NamesMatcher; import me.osm.gazetter.addresses.impl.NamesMatcherImpl; import me.osm.gazetter.striper.BoundariesFallbacker; import me.osm.gazetter.striper.FeatureTypes; import me.osm.gazetter.striper.GeoJsonWriter; import me.osm.gazetter.striper.JSONFeature; import me.osm.gazetter.striper.Slicer; import me.osm.gazetter.striper.builders.handlers.BoundariesHandler; import me.osm.gazetter.striper.builders.handlers.PlacePointHandler; import me.osm.gazetter.striper.readers.PointsReader.Node; import me.osm.gazetter.striper.readers.RelationsReader.Relation; import me.osm.gazetter.striper.readers.WaysReader.Way; import org.apache.commons.lang3.StringUtils; import org.json.JSONArray; import org.json.JSONObject; import org.json.JSONString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.geom.util.GeometryEditor; import com.vividsolutions.jts.index.quadtree.Quadtree; import com.vividsolutions.jts.triangulate.VoronoiDiagramBuilder; import com.vividsolutions.jts.triangulate.quadedge.QuadEdgeSubdivision; import com.vividsolutions.jts.triangulate.quadedge.Vertex; public class PlaceBuilder extends BoundariesBuilder { private NamesMatcher namesMatcher = new NamesMatcherImpl(); private static final Logger log = LoggerFactory .getLogger(PlaceBuilder.class.getName()); private static GeometryFactory fatory = new GeometryFactory(); private PlacePointHandler handler; private Map<Coordinate, JSONObject> cityes = new HashMap<>(); private Map<Coordinate, JSONObject> neighbours = new HashMap<>(); private Quadtree cityesIndex = new Quadtree(); private final BBOX originalBBOX = new BBOX(); private final BBOX translatedBBOX = new BBOX(); // this tricky flag will mess everything around 180* private volatile boolean weAreInRussia = false; private final Set<String> files = new HashSet<>(); private static final Set<String> PLACE_CITY = new HashSet<String>( Arrays.asList(new String[] { "city", "town", "hamlet", "village", "isolated_dwelling" })); private static final Set<String> PLACE_NEIGHBOUR = new HashSet<String>( Arrays.asList(new String[] { "suburb", "neighbourhood", "quarter" })); private static final double DEGREE_OFFSET = -90.0; public static final double moveTo(double x) { if (x < DEGREE_OFFSET) { return x + 360 + DEGREE_OFFSET; } return x + DEGREE_OFFSET; } public static final double moveBack(double x) { if (x > -DEGREE_OFFSET) { return x - 360 - DEGREE_OFFSET; } return x - DEGREE_OFFSET; } public PlaceBuilder(PlacePointHandler slicer, BoundariesHandler handler, BoundariesFallbacker fallback) { super(handler, fallback); this.handler = slicer; } @Override protected boolean filterByTags(Map<String, String> tags) { return tags.containsKey("place") && (PLACE_CITY.contains(tags.get("place")) || PLACE_NEIGHBOUR.contains(tags.get("place"))); } @Override public void handle(Node node) { super.handle(node); if (filterByTags(node.tags)) { Coordinate coordinate = new Coordinate(node.lon, node.lat); Point pnt = fatory.createPoint(coordinate); JSONObject meta = new JSONObject(); meta.put("type", "node"); meta.put("id", node.id); handler.handlePlacePoint(node.tags, pnt, meta); originalBBOX.extend(node.lon, node.lat); translatedBBOX.extend(moveTo(node.lon), node.lat); String id = GeoJsonWriter.getId(FeatureTypes.PLACE_DELONEY_FTYPE, pnt, meta); JSONObject feature = GeoJsonWriter.createFeature(id, FeatureTypes.PLACE_DELONEY_FTYPE, node.tags, pnt, meta); Envelope envelope = pnt.getEnvelopeInternal(); if (PLACE_CITY.contains(node.tags.get("place"))) { cityesIndex.insert(envelope, feature); cityes.put(coordinate, feature); files.add(Slicer.getFilePrefix(node.lon)); } if (PLACE_NEIGHBOUR.contains(node.tags.get("place"))) { neighbours.put(coordinate, feature); files.add(Slicer.getFilePrefix(node.lon)); } } if (node.tags.containsKey("addr:housenumber")) { originalBBOX.extend(node.lon, node.lat); translatedBBOX.extend(moveTo(node.lon), node.lat); } } @Override public void secondRunDoneRelations() { buildVoronoyDiagrams(); //shutdown executor services super.secondRunDoneRelations(); this.handler.freeThreadPool(getThreadPoolUser()); } //single threaded private void buildVoronoyDiagrams() { // Possibly we processing Russia. // And we have wrong originalBBOX which covers whole planet. // So lets translate all coordinates for Vronoi diagramm and // move it back while writing if (originalBBOX.getDX() > translatedBBOX.getDX() + 0.0001) { weAreInRussia = true; log.trace("Wrap 180 degree line."); Map<Coordinate, JSONObject> russianCityes = new HashMap<>(); for (Entry<Coordinate, JSONObject> entry : cityes.entrySet()) { Coordinate c = entry.getKey(); c.x = moveTo(c.x); russianCityes.put(c, entry.getValue()); } cityes = russianCityes; Map<Coordinate, JSONObject> russianNeighbours = new HashMap<>(); for (Entry<Coordinate, JSONObject> entry : neighbours.entrySet()) { Coordinate c = entry.getKey(); c.x = moveTo(c.x); russianNeighbours.put(c, entry.getValue()); } neighbours = russianNeighbours; } VoronoiDiagramBuilder cvb = new VoronoiDiagramBuilder(); Quadtree neighboursQT = new Quadtree(); for (Entry<Coordinate, JSONObject> entry : neighbours.entrySet()) { neighboursQT.insert(new Envelope(entry.getKey()), entry.getValue()); } cvb.setSites(cityes.keySet()); BBOX bbox = weAreInRussia ? translatedBBOX : originalBBOX; cvb.setClipEnvelope(new Envelope(bbox.minX, bbox.maxX, bbox.minY, bbox.maxY)); QuadEdgeSubdivision subdivision = cvb.getSubdivision(); Map<JSONObject, Set<JSONObject>> nCities = new HashMap<JSONObject, Set<JSONObject>>(); @SuppressWarnings("unchecked") List<Vertex[]> triangleVertices = (List<Vertex[]>)subdivision.getTriangleVertices(false); for (Vertex[] vs : triangleVertices) { putNeighbours(vs[0], vs[1], nCities); putNeighbours(vs[0], vs[2], nCities); putNeighbours(vs[1], vs[2], nCities); } try { @SuppressWarnings("unchecked") Collection<Polygon> cityVoronoiPolygons = subdivision .getVoronoiCellPolygons(fatory); for (Polygon cityPolygon : cityVoronoiPolygons) { JSONObject cityJSON = cityes.get(cityPolygon .getUserData()); handleCityVoronoy(cityJSON, cityPolygon, neighboursQT, nCities.get(cityJSON)); } } catch (IllegalArgumentException e) { log.warn("Failed to build Voronoy cell"); } } private void putNeighbours(Vertex vertexA, Vertex vertexB, Map<JSONObject, Set<JSONObject>> nCities) { if(hasNaNCoordinates(vertexA) || hasNaNCoordinates(vertexB)) { log.warn("Skip neughbours, due to NaN coordinates."); return; } JSONObject ca = cityes.get(vertexA.getCoordinate()); JSONObject cb = cityes.get(vertexB.getCoordinate()); if(nCities.get(ca) == null){ nCities.put(ca, new HashSet<JSONObject>()); } if(nCities.get(cb) == null){ nCities.put(cb, new HashSet<JSONObject>()); } nCities.get(ca).add(cb); nCities.get(cb).add(ca); } private boolean hasNaNCoordinates(Vertex vertex) { return Double.isNaN(vertex.getX()) || Double.isNaN(vertex.getY()); } private static Coordinate getCoordinateFromGJSON(JSONObject gjson) { Object pc = gjson.getJSONObject(GeoJsonWriter.GEOMETRY).get( GeoJsonWriter.COORDINATES); if (pc instanceof JSONArray) { return new Coordinate(((JSONArray) pc).getDouble(0), ((JSONArray) pc).getDouble(1)); } else if (pc instanceof JSONString) { String[] split = StringUtils.split( ((JSONString) pc).toJSONString(), ",[]"); return new Coordinate(Double.parseDouble(split[0]), Double.parseDouble(split[1])); } return null; } /** * placeFeature - original coordinates cityPolygon - translated coordinates * neighboursQT - translated coordinates * @param nCities * */ private void handleCityVoronoy(JSONObject placeFeature, Polygon cityPolygon, Quadtree neighboursQT, Set<JSONObject> nCities) { Polygon originalCityPolygon = weAreInRussia ? movePolygonBack(cityPolygon) : cityPolygon; // original coords JSONObject rfeature = mergeDeloneyCenter(placeFeature, originalCityPolygon, FeatureTypes.PLACE_DELONEY_FTYPE); rfeature.put("neighbourCities", nCities); assert GeoJsonWriter.getId(rfeature.toString()).equals(rfeature.optString("id")) : "Failed getId for " + rfeature.toString(); assert GeoJsonWriter.getFtype(rfeature.toString()).equals(FeatureTypes.PLACE_DELONEY_FTYPE) : "Failed getFtype for " + rfeature.toString(); // original coordinates writePolygonToExistFiles(originalCityPolygon, rfeature); buildNighboursVoronoiPolygons(cityPolygon, neighboursQT, rfeature); } /** * cityPolygon - translated coordinates neighboursQT - translated * coordinates cityFeature - originalCoords * */ private void buildNighboursVoronoiPolygons(Polygon cityPolygon, Quadtree neighboursQT, JSONObject cityFeature) { // translated coords List<Coordinate> neighboursCoords = new ArrayList<>(); // translated coords Envelope cityPolygonEnv = cityPolygon.getEnvelopeInternal(); @SuppressWarnings("unchecked") List<JSONObject> neighbourCandidates = neighboursQT .query(cityPolygonEnv); for (JSONObject neighbour : neighbourCandidates) { // original coordinates Coordinate coordinate = getCoordinateFromGJSON(neighbour); if (weAreInRussia) { coordinate.x = moveTo(coordinate.x); } if (cityPolygon.contains(fatory.createPoint(coordinate))) { neighboursCoords.add(coordinate); } } if (!neighboursCoords.isEmpty()) { VoronoiDiagramBuilder nvb = new VoronoiDiagramBuilder(); nvb.setSites(neighboursCoords); nvb.setClipEnvelope(cityPolygonEnv); @SuppressWarnings({ "unchecked" }) List<Polygon> neighboursPolygons = nvb.getSubdivision() .getVoronoiCellPolygons(fatory); for (Polygon neighbourPolygon : neighboursPolygons) { Polygon intersection = (Polygon) neighbourPolygon .intersection(cityPolygon); if (!intersection.isEmpty()) { JSONObject neighbour = neighbours.get(neighbourPolygon .getUserData()); handleNeighbour(intersection, cityFeature, neighbour); } } } } /** * neighbourPolygon - translated coordinates cityFeature - original * coordinates neighbourFeature - original coordinates * */ private void handleNeighbour(Polygon neighbourPolygon, JSONObject cityFeature, JSONObject neighbourFeature) { if (weAreInRussia) { neighbourPolygon = movePolygonBack(neighbourPolygon); } JSONObject rfeature = mergeDeloneyCenter(neighbourFeature, neighbourPolygon, FeatureTypes.NEIGHBOUR_DELONEY_FTYPE); rfeature.put("cityID", cityFeature.getString("id")); assert GeoJsonWriter.getId(rfeature.toString()).equals(rfeature.optString("id")) : "Failed getId for " + rfeature.toString(); assert GeoJsonWriter.getFtype(rfeature.toString()).equals(FeatureTypes.NEIGHBOUR_DELONEY_FTYPE) : "Failed getFtype for " + rfeature.toString(); writePolygonToExistFiles(neighbourPolygon, rfeature); } private void writePolygonToExistFiles(Polygon polygon, JSONObject rfeature) { Envelope env = polygon.getEnvelopeInternal(); // TODO: handle 180* double minX = env.getMinX(); double maxX = env.getMaxX(); double dx = Math.abs(maxX - minX); double dxt = Math.abs(moveTo(maxX) - moveTo(minX)); Envelope envelope = polygon.getEnvelopeInternal(); JSONArray bbox = new JSONArray(); bbox.put(envelope.getMinX()); bbox.put(envelope.getMinY()); bbox.put(envelope.getMaxX()); bbox.put(envelope.getMaxY()); rfeature.getJSONObject(GeoJsonWriter.META).put( GeoJsonWriter.ORIGINAL_BBOX, bbox); GeoJsonWriter.addTimestamp(rfeature); String rstring = rfeature.toString(); if (dxt < dx) { int from = (new Double((-180.0 + 180.0) * 10.0).intValue()); int to = (new Double((minX + 180.0) * 10.0).intValue()); writeToExistFiles(rstring, from, to); from = (new Double((maxX + 180.0) * 10.0).intValue()); to = (new Double((180.0 + 180.0) * 10.0).intValue()); writeToExistFiles(rstring, from, to); } else { int from = (new Double((minX + 180.0) * 10.0).intValue()); int to = (new Double((maxX + 180.0) * 10.0).intValue()); writeToExistFiles(rstring, from, to); } } private void writeToExistFiles(String rstring, int from, int to) { for (int i = from; i <= to; i++) { String filePrefix = String.format("%04d", i); if (files.contains(filePrefix)) { handler.writeOut(rstring, filePrefix); } } } private Polygon movePolygonBack(Polygon translatedPolygon) { if (weAreInRussia) { return (Polygon) new GeometryEditor(fatory).edit(translatedPolygon, new GeometryEditor.CoordinateOperation() { @Override public Coordinate[] edit( Coordinate[] paramArrayOfCoordinate, Geometry paramGeometry) { Coordinate[] result = new Coordinate[paramArrayOfCoordinate.length]; int i = 0; for (Coordinate c : paramArrayOfCoordinate) { result[i++] = new Coordinate(moveBack(c.x), c.y); } return result; } }); } return translatedPolygon; } private JSONObject mergeDeloneyCenter(JSONObject centerFeature, Polygon polygon, String resultType) { String id = centerFeature.getString("id"); JSONObject meta = centerFeature.getJSONObject(GeoJsonWriter.META); meta.put( "sitePoint", centerFeature.getJSONObject(GeoJsonWriter.GEOMETRY).get( GeoJsonWriter.COORDINATES)); JSONObject rfeature = GeoJsonWriter.createFeature(id, resultType, new HashMap<String, String>(), polygon, meta); rfeature.put(GeoJsonWriter.PROPERTIES, centerFeature.getJSONObject(GeoJsonWriter.PROPERTIES)); return rfeature; } @Override protected void doneRelation(Relation rel, MultiPolygon geometry, JSONObject meta) { String fType = FeatureTypes.PLACE_BOUNDARY_FTYPE; Point originalCentroid = geometry.getEnvelope().getCentroid(); String id = GeoJsonWriter.getId(fType, originalCentroid, meta); JSONObject featureWithoutGeometry = GeoJsonWriter.createFeature(id, fType, rel.tags, null, meta); mergeWithCenter(featureWithoutGeometry, geometry); assert GeoJsonWriter.getId(featureWithoutGeometry.toString()).equals(id) : "Failed getId for " + featureWithoutGeometry.toString(); assert GeoJsonWriter.getFtype(featureWithoutGeometry.toString()).equals(FeatureTypes.PLACE_BOUNDARY_FTYPE) : "Failed getFtype " + featureWithoutGeometry.toString(); super.handler.handleBoundary(featureWithoutGeometry, geometry); } @Override protected void doneWay(Way line, MultiPolygon multiPolygon) { String fType = FeatureTypes.PLACE_BOUNDARY_FTYPE; Point originalCentroid = multiPolygon.getEnvelope().getCentroid(); JSONObject meta = getWayMeta(line); String id = GeoJsonWriter.getId(fType, originalCentroid, meta); JSONObject featureWithoutGeometry = GeoJsonWriter.createFeature(id, fType, line.tags, null, meta); mergeWithCenter(featureWithoutGeometry, multiPolygon); assert GeoJsonWriter.getId(featureWithoutGeometry.toString()).equals(id) : "Failed getId for " + featureWithoutGeometry.toString(); assert GeoJsonWriter.getFtype(featureWithoutGeometry.toString()).equals(FeatureTypes.PLACE_BOUNDARY_FTYPE) : "Failed getFtype for " + featureWithoutGeometry.toString(); super.handler.handleBoundary(featureWithoutGeometry, multiPolygon); } @Override public void firstRunDoneRelations() { super.firstRunDoneRelations(); this.handler.newThreadpoolUser(getThreadPoolUser()); } @SuppressWarnings("unchecked") private void mergeWithCenter(JSONObject featureWithoutGeometry, MultiPolygon geometry) { //we write places and neighbours, but merge only cities //so we need extra check if(PLACE_CITY.contains(featureWithoutGeometry.getJSONObject(GeoJsonWriter.PROPERTIES).optString("place"))) { HashSet<String> pbNamesSet = new HashSet<>( AddressesUtils.filterNameTags( featureWithoutGeometry.getJSONObject(GeoJsonWriter.PROPERTIES)).values()); for(int i = 0; i < geometry.getNumGeometries(); i++) { Polygon polygon = (Polygon) geometry.getGeometryN(i); if(!polygon.isEmpty() && polygon.isValid()) { for(JSONObject pp : (List<JSONObject>)cityesIndex.query(polygon.getEnvelopeInternal())) { Coordinate c = getCoordinateFromGJSON(pp); if(polygon.contains(fatory.createPoint(c))) { String placeName = pp.getJSONObject(GeoJsonWriter.PROPERTIES).optString("name"); if(namesMatcher.isPlaceNameMatch(placeName, pbNamesSet)) { handlePlaceMatch(featureWithoutGeometry, pp); return; } } } } } } } private void handlePlaceMatch(JSONObject boundary, JSONObject point) { //Keep all tagsa and coordinates from point for boundary JSONObject placePointRefer = JSONFeature.asRefer(point); placePointRefer.put("id", placePointRefer.getString("id") .replace(FeatureTypes.PLACE_DELONEY_FTYPE, FeatureTypes.PLACE_POINT_FTYPE)); placePointRefer.put(GeoJsonWriter.GEOMETRY, point.getJSONObject(GeoJsonWriter.GEOMETRY)); boundary.put("placePoint", placePointRefer); //I'm not sure, that it's usefull to keep boundary tags //for point, because later we will search for boundaries //duiring the point in polygon join phase. //But still there is not so many polygonal //place boundaries with corresponding place points. point.put("boundary", JSONFeature.asRefer(boundary)); } }