package se.kodapan.osm.sweden.ext.se.posten.postnummer.local; import com.vividsolutions.jts.geom.*; import com.vividsolutions.jts.geom.impl.CoordinateArraySequence; import se.kodapan.osm.domain.*; import se.kodapan.osm.util.distance.ArcDistance; import se.kodapan.osm.jts.LineInterpolation; import se.kodapan.osm.parser.xml.instantiated.InstantiatedOsmXmlParser; import se.kodapan.osm.domain.root.Root; import se.kodapan.osm.services.overpass.Overpass; import se.kodapan.osm.services.overpass.OverpassUtils; import se.kodapan.osm.sweden.util.Scored; import se.kodapan.osm.jts.AdjacentClassVoronoiClusterer; import se.kodapan.osm.xml.OsmXmlWriter; import java.io.*; import java.text.DecimalFormat; import java.util.*; /** * @author kalle * @since 2013-08-31 6:29 PM */ public class PostnummerClassifier { private GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(PrecisionModel.FLOATING)); private se.kodapan.osm.sweden.ext.se.posten.postnummer.local.Root localPosten; private Overpass overpass; private OverpassUtils overpassUtils; public PostnummerClassifier(se.kodapan.osm.sweden.ext.se.posten.postnummer.local.Root localPosten, Overpass overpass) { this.localPosten = localPosten; this.overpass = overpass; this.overpassUtils = new OverpassUtils(overpass); } public static class Response { private List<Scored<Postnummer>> classification; private List<Scored<Postort>> postortScores; private List<Scored<Postort>> postortVoronoiDistances; private Root voronoiRoot; private String voronoiOsmXml; private Map<Postnummer, List<Polygon>> geometryPerPostnummer; public List<Scored<Postort>> getPostortVoronoiDistances() { return postortVoronoiDistances; } public List<Scored<Postort>> getPostortScores() { return postortScores; } public Map<Postnummer, List<Polygon>> getGeometryPerPostnummer() { return geometryPerPostnummer; } public String getVoronoiOsmXml() throws IOException { if (voronoiOsmXml == null) { StringWriter osmXmlStringWriter = new StringWriter(49152); OsmXmlWriter osmXmlWriter = new OsmXmlWriter(osmXmlStringWriter); osmXmlWriter.write(voronoiRoot); osmXmlWriter.close(); voronoiOsmXml = osmXmlStringWriter.toString(); } return voronoiOsmXml; } public List<Scored<Postnummer>> getClassification() { return classification; } public Root getVoronoiRoot() { return voronoiRoot; } } private Map<Postort, List<Polygon>> geometriesPerPostort; public Response classify(double longitude, double latitude) throws Exception { return classify(null, longitude, latitude); } public Response classify(Postort postort, double longitude, double latitude) throws Exception { return classify(postort, longitude, latitude, 200, 1.7d, 1000, 1d); } public Response classify( Postort inputPostort, final double longitude, final double latitude, int diagonalMetersSearchArea, double diagonalMetersSearchAreaIncrementFactor, int diagonalMetersSearchAreaMaximum, double metersWayInterpolation ) throws Exception { if (geometriesPerPostort == null) { init(); } Response response = new Response(); List<Scored<Postnummer>> distanceToPostnummer = new ArrayList<Scored<Postnummer>>(100); Coordinate coordinate = new Coordinate(longitude, latitude); CoordinateSequence coordinates = new CoordinateArraySequence(new Coordinate[]{coordinate}); Point point = new Point(coordinates, geometryFactory); // Download streets and house numbers from OSM in an envelope around this point. double s = latitude; double n = latitude; double w = longitude; double e = longitude; { double diagonalKiloMetersSearchArea = 0.001 * (double) diagonalMetersSearchArea; ArcDistance distance = new ArcDistance(); double increment = 0.00001; while (distance.calculate(s, longitude, n, longitude) < diagonalKiloMetersSearchArea) { s -= increment; n += increment; } while (distance.calculate(latitude, w, latitude, e) < diagonalKiloMetersSearchArea) { w -= increment; e += increment; } } final InstantiatedOsmXmlParser parser = new InstantiatedOsmXmlParser(); parser.setRoot(new Root()); // parser.parse(new StringReader( // overpass.execute("<osm-script>\n" + // " <bbox-query s=\"" + df.format(s) + "\" n=\"" + df.format(n) + "\" w=\"" + df.format(w) + "\" e=\"" + df.format(e) + "\"/>\n" + // "<recurse type=\"node-way\"/>\n" + // "<print/>\n" + // "</osm-script>"))); overpassUtils.loadEnvelope(parser, s, w, n, e); Set<Way> streetWays = new HashSet<Way>(); Set<Way> houseNumberWays = new HashSet<Way>(); Set<Node> houseNumberNodes = new HashSet<Node>(); for (Way way : parser.getRoot().getWays().values()) { if (way.getTag("highway") != null && way.getTag("name") != null) { streetWays.add(way); } else if (way.getTag("addr:housenumber") != null && way.getTag("addr:street") != null) { houseNumberWays.add(way); } } for (Node node : parser.getRoot().getNodes().values()) { if (node.getTag("addr:housenumber") != null && node.getTag("addr:street") != null) { houseNumberNodes.add(node); } } // gather street names Set<String> streetNames = new HashSet<String>(); for (OsmObject object : streetWays) { streetNames.add(object.getTag("name")); } for (OsmObject object : houseNumberWays) { streetNames.add(object.getTag("addr:street")); } for (OsmObject object : houseNumberNodes) { streetNames.add(object.getTag("addr:street")); } Map<String, Set<PostnummerSegment>> segmentsPerStreetName = new HashMap<String, Set<PostnummerSegment>>(); Map<Postnummer, Set<PostnummerSegment>> segmentsPerPostnummer = new HashMap<Postnummer, Set<PostnummerSegment>>(); Map<Postort, Set<PostnummerSegment>> segmentsPerPostort = new HashMap<Postort, Set<PostnummerSegment>>(); for (String streetName : streetNames) { Set<PostnummerSegment> segments = localPosten.getSegmentsByName().get(streetName); if (segments != null) { segmentsPerStreetName.put(streetName, segments); for (PostnummerSegment segment : segments) { Set<PostnummerSegment> postnummerSegments = segmentsPerPostnummer.get(segment.getPostnummer()); if (postnummerSegments == null) { postnummerSegments = new HashSet<PostnummerSegment>(); segmentsPerPostnummer.put(segment.getPostnummer(), postnummerSegments); } postnummerSegments.add(segment); } for (PostnummerSegment segment : segments) { Set<PostnummerSegment> postortSegments = segmentsPerPostort.get(segment.getPostnummer().getPostort()); if (postortSegments == null) { postortSegments = new HashSet<PostnummerSegment>(); segmentsPerPostort.put(segment.getPostnummer().getPostort(), postortSegments); } postortSegments.add(segment); } } } Postort selectedPostort = inputPostort; if (selectedPostort == null) { // guess postort // attempt using the one containing the greatest number of matching street names { List<Scored<Postort>> postortScores = new ArrayList<Scored<Postort>>(); for (Map.Entry<Postort, Set<PostnummerSegment>> entry : segmentsPerPostort.entrySet()) { Set<String> segmentNames = new HashSet<String>(); for (PostnummerSegment segment : entry.getValue()) { segmentNames.add(segment.getGatunamn()); } postortScores.add(new Scored<Postort>(segmentNames.size(), entry.getKey())); } Collections.sort(postortScores, Scored.topScoreFirstComparator); if (postortScores.size() > 1 && postortScores.get(0).getScore() > 5) { if (postortScores.get(1).getScore() < postortScores.get(0).getScore() / 2d) { selectedPostort = postortScores.get(0).getObject(); } } response.postortScores = postortScores; } if (selectedPostort == null) { // attempt using voronoi postort polygons. not as safe but better than nothing. List<Scored<Postort>> postortVoronoiDistances = new ArrayList<Scored<Postort>>(); for (Map.Entry<Postort, List<Polygon>> entry : geometriesPerPostort.entrySet()) { for (Geometry geometry : entry.getValue()) { if (geometry.contains(point)) { // todo 0=centroid of geometry. add distance the closer to border it is. and the same also in postnummer voronoi classifier code! postortVoronoiDistances.add(new Scored<Postort>(0d, entry.getKey())); } else { postortVoronoiDistances.add(new Scored<Postort>(geometry.distance(point), entry.getKey())); } } } Collections.sort(postortVoronoiDistances, Scored.lowScoreFirstComparator); selectedPostort = postortVoronoiDistances.get(0).getObject(); System.currentTimeMillis(); response.postortVoronoiDistances = postortVoronoiDistances; } System.currentTimeMillis(); } // construct local postnummer map AdjacentClassVoronoiClusterer<Postnummer> postnummerVoronoiFactory = new AdjacentClassVoronoiClusterer<Postnummer>(geometryFactory); postnummerVoronoiFactory.setBounds(s, w, n, e); // get streets in this postort that only exist in one postnummer Set<PostnummerSegment> postortSegments = segmentsPerPostort.get(selectedPostort); if (postortSegments != null) { Map<String, Set<Postnummer>> postnummerBySegmentName = new HashMap<String, Set<Postnummer>>(streetNames.size()); for (PostnummerSegment segment : postortSegments) { Set<Postnummer> postnummer = postnummerBySegmentName.get(segment.getGatunamn()); if (postnummer == null) { postnummer = new HashSet<Postnummer>(); postnummerBySegmentName.put(segment.getGatunamn(), postnummer); } postnummer.add(segment.getPostnummer()); } PostnummerSegmentIndex singlePostnummerSegments = new PostnummerSegmentIndex(); PostnummerSegmentIndex multiPostnummerSegments = new PostnummerSegmentIndex(); for (Map.Entry<String, Set<Postnummer>> segmentNameAndPostnummer : postnummerBySegmentName.entrySet()) { for (PostnummerSegment segment : postortSegments) { if (segment.getGatunamn().equals(segmentNameAndPostnummer.getKey())) { if (segmentNameAndPostnummer.getValue().size() == 1) { singlePostnummerSegments.add(segment); } else { multiPostnummerSegments.add(segment); } } } } LineInterpolation lineInterpolation = new LineInterpolation(); Set<Postnummer> needsSinglePostnummerStreetNames = new HashSet<Postnummer>(segmentsPerPostnummer.keySet()); for (Way houseNumberWay : houseNumberWays) { int houseNumber; try { houseNumber = Integer.valueOf(houseNumberWay.getTag("addr:housenumber").replaceAll("[^0-9]+", "")); } catch (NumberFormatException nfe) { continue; } Set<PostnummerSegment> segments = segmentsPerPostort.get(selectedPostort); for (PostnummerSegment segment : segments) { if (segment.getGatunamn().equalsIgnoreCase(houseNumberWay.getTag("addr:street")) && segment.containsHusnummer(houseNumber)) { for (Node node : houseNumberWay.getNodes()) { if (node.isLoaded()) { // ensure coordinates set postnummerVoronoiFactory.addCoordinate(segment.getPostnummer(), node); // needsSinglePostnummerStreetNames.remove(segment.getPostnummer()); } } } } } for (Node houseNumberNode : houseNumberNodes) { int houseNumber; try { houseNumber = Integer.valueOf(houseNumberNode.getTag("addr:housenumber").replaceAll("[^0-9]+", "")); } catch (NumberFormatException nfe) { continue; } Set<PostnummerSegment> segments = segmentsPerPostort.get(selectedPostort); for (PostnummerSegment segment : segments) { if (segment.getGatunamn().equalsIgnoreCase(houseNumberNode.getTag("addr:street")) && segment.containsHusnummer(houseNumber)) { if (houseNumberNode.isLoaded()) { // ensure coordinates set postnummerVoronoiFactory.addCoordinate(segment.getPostnummer(), houseNumberNode); needsSinglePostnummerStreetNames.remove(segment.getPostnummer()); } } } } for (Way streetWay : streetWays) { Set<PostnummerSegment> segments = singlePostnummerSegments.get(streetWay.getTag("name").toLowerCase()); if (segments != null) { Postnummer postnummer = segments.iterator().next().getPostnummer(); if (needsSinglePostnummerStreetNames.contains(postnummer)) { Coordinate previousCoordinate = null; for (Node node : streetWay.getNodes()) { if (node.isLoaded()) { // ensure coordinates set Coordinate nodeCoordinate = new Coordinate(node.getLongitude(), node.getLatitude()); if (previousCoordinate != null) { for (Coordinate interpolated : lineInterpolation.interpolate(metersWayInterpolation * 0.001, nodeCoordinate, previousCoordinate)) { postnummerVoronoiFactory.addCoordinate(postnummer, interpolated); } } postnummerVoronoiFactory.addCoordinate(postnummer, nodeCoordinate); previousCoordinate = nodeCoordinate; } } } } } } if (postnummerVoronoiFactory.getCoordinatesByClass().isEmpty()) { if (diagonalMetersSearchArea < diagonalMetersSearchAreaMaximum) { int m = (int)(diagonalMetersSearchArea * diagonalMetersSearchAreaIncrementFactor); if (m > diagonalMetersSearchAreaMaximum) { m = diagonalMetersSearchAreaMaximum; } return classify(inputPostort, longitude, latitude, m, diagonalMetersSearchAreaIncrementFactor, diagonalMetersSearchAreaMaximum, metersWayInterpolation); } throw new RuntimeException("Närområdet i OSM saknar punkter<br/>som kan associeras med postnummer<br/>i vad som troligen är postorten " + selectedPostort.getDisplayName() + ",<br/>trots att sökområdet utökats till " + diagonalMetersSearchArea + " meter."); } Map<Postnummer, List<Polygon>> geometryPerPostnummer = postnummerVoronoiFactory.build(); response.geometryPerPostnummer = geometryPerPostnummer; response.voronoiRoot = new AdjacentClassVoronoiClusterer.OsmRootFactory<Postnummer>() { @Override public void setNode(Node node, Postnummer postnummer, List<Polygon> geometries, Polygon geometry, Coordinate coordinate) { setDefaultTags(node, postnummer); } @Override public void setWay(Way way, Postnummer postnummer, List<Polygon> geometries, Polygon geometry) { setDefaultTags(way, postnummer); } @Override public void setClassTypeInstanceMultiPolygon(Relation relation, Postnummer postnummer, List<Polygon> geometries) { setDefaultTags(relation, postnummer); relation.setTag("name", postnummer.getIdentity() + ", " + postnummer.getPostort()); } @Override public void setGeometryMultiPolygon(Relation relation, Postnummer postnummer, List<Polygon> geometries, Polygon geometry) { setDefaultTags(relation, postnummer); } private void setDefaultTags(OsmObject osmObject, Postnummer postnummer) { osmObject.setTag("note", "ref:se:pts:postnummer boundary, might be inaccurate."); osmObject.setTag("source", "osm.kodapan.se postnummer classifier"); osmObject.setTag("ref:se:pts:postnummer", postnummer.getIdentity()); osmObject.setTag("ref:se:pts:postort", postnummer.getPostort().getIdentity()); } }.factory(geometryPerPostnummer); for (Map.Entry<Postnummer, List<Polygon>> postnummerAndGeometries : geometryPerPostnummer.entrySet()) { Scored<Postnummer> result = new Scored<Postnummer>(); double closestDistance = Double.MAX_VALUE; result.setScore(Double.MAX_VALUE); result.setObject(postnummerAndGeometries.getKey()); for (Geometry geometry : postnummerAndGeometries.getValue()) { if (geometry.contains(point)) { result.setScore(0); break; } else { double distance = geometry.distance(point); if (closestDistance > distance) { closestDistance = distance; } } } if (Double.MAX_VALUE == result.getScore()) { result.setScore(closestDistance); } distanceToPostnummer.add(result); } Collections.sort(distanceToPostnummer, Scored.lowScoreFirstComparator); response.classification = distanceToPostnummer; return response; } private void init() throws Exception { final InstantiatedOsmXmlParser parser = new InstantiatedOsmXmlParser(); final AdjacentClassVoronoiClusterer<Postort> postortsVoronoiClusterer = new AdjacentClassVoronoiClusterer<Postort>(geometryFactory); for (final Postort postort1 : localPosten.getPostortByIdentity().values()) { for (OsmObject osmObject : postort1.getReferencedObjects()) { osmObject.accept(new OsmObjectVisitor<Void>() { @Override public Void visit(Node node) { if (!node.isLoaded()) { try { overpassUtils.loadNode(parser, node.getId()); } catch (Exception e1) { throw new RuntimeException(e1); } } postortsVoronoiClusterer.addCoordinate(postort1, node); return null; } @Override public Void visit(Way way) { // todo implement when such data exist osm! add log warn now! return null; } @Override public Void visit(Relation relation) { // todo implement when such data exist osm! add log warn now! return null; } }); } } Map<Postort, List<Polygon>> geometriesPerPostort = postortsVoronoiClusterer.build(); OsmXmlWriter osmxml = new OsmXmlWriter(new OutputStreamWriter(new FileOutputStream(new File("postorter.osm.xml")), "utf8")); AdjacentClassVoronoiClusterer.OsmRootFactory<Postort> factory = new AdjacentClassVoronoiClusterer.OsmRootFactory<Postort>() { private String source = "Voronoi of points with ref:se:pts:postort set. osm@kodapan.se"; @Override public void setNode(Node node, Postort postort, List<Polygon> geometries, Polygon geometry, Coordinate coordinate) { node.setTag("note", "Border point of Swedish postort (postal town). Might be inaccurate."); node.setTag("source", source); } @Override public void setWay(Way way, Postort postort, List<Polygon> geometries, Polygon geometry) { way.setTag("note", "Border of Swedish postort (postal town). Might be inaccurate."); way.setTag("source", source); } @Override public void setClassTypeInstanceMultiPolygon(Relation relation, Postort postort, List<Polygon> geometries) { relation.setTag("note", "All polygons describing the Swedish postal town " + postort.getIdentity()); relation.setTag("source", source); } @Override public void setGeometryMultiPolygon(Relation relation, Postort postort, List<Polygon> geometries, Polygon geometry) { relation.setTag("note", "A complex polygon describing part of the Swedish postal town " + postort.getIdentity()); relation.setTag("source", source); } }; osmxml.write(factory.factory(geometriesPerPostort)); osmxml.close(); this.geometriesPerPostort = geometriesPerPostort; } }