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;
}
}