/* * Copyright (C) 2013-2014. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 or * version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. */ package uk.me.parabola.mkgmap.osmstyle; //import java.io.File; //import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.Area; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.reader.osm.CoordPOI; import uk.me.parabola.mkgmap.reader.osm.Node; import uk.me.parabola.mkgmap.reader.osm.RestrictionRelation; import uk.me.parabola.mkgmap.reader.osm.Way; //import uk.me.parabola.splitter.O5mMapWriter; import uk.me.parabola.util.GpxCreator; /** * We are rounding coordinates with double precision to map units with a * precision of < 2m. This hasn't a big visible effect for single points, * but wherever points are connected with lines the lines may show * heavy zig-zagging while the original lines were almost straight. * This happens when one of the points was rounded to one direction * and the next point was rounded to the opposite direction. * The effect is esp. visible with parallel roads, rails, and buildings, * but also in small roundabouts. * The methods in this class try to fix these wrong bearings by * moving or removing points. * * @author GerdP * */ public class WrongAngleFixer { private static final Logger log = Logger.getLogger(WrongAngleFixer.class); static private final double MAX_BEARING_ERROR = 15; static private final double MAX_BEARING_ERROR_HALF = MAX_BEARING_ERROR / 2; static private final double MAX_DIFF_ANGLE_STRAIGHT_LINE = 3; private final Area bbox; private final String gpxPath = null; static final int MODE_ROADS = 0; static final int MODE_LINES = 1; private int mode = MODE_ROADS; public WrongAngleFixer(Area bbox) { this.bbox = bbox; if (gpxPath != null && bbox != null){ if (bbox.getWidth() * bbox.getHeight() < 100000){ List<Coord> grid = new ArrayList<>(); for (int lat = bbox.getMinLat(); lat < bbox.getMaxLat(); lat++){ for (int lon = bbox.getMinLong(); lon < bbox.getMaxLong(); lon++){ grid.add(new Coord(lat,lon)); } } GpxCreator.createGpx("e:/ld/grid", bbox.toCoords(), grid); } } } /** * Find wrong angles caused by rounding to map units. Try to fix them by * moving, removing or merging points. * When done, remove obsolete points. * @param roads list of roads, elements might be set to null by this method * @param lines list of non-routable ways * @param modifiedRoads Will be enlarged by all roads modified in this method * @param deletedRoads Will be enlarged by all roads in roads that were set to null by this method * @param restrictions Map with restriction relations */ public void optimizeWays(List<ConvertedWay> roads, List<ConvertedWay> lines, HashMap<Long, ConvertedWay> modifiedRoads, HashSet<Long> deletedRoads, List<RestrictionRelation> restrictions ) { printBadAngles("bad_angles_start", roads); writeOSM("roads_orig", roads); writeOSM("lines_orig", lines); removeWrongAngles(roads, lines, modifiedRoads, deletedRoads, restrictions); writeOSM("roads_post_rem_wrong_angles", roads); removeObsoletePoints(roads, modifiedRoads); writeOSM("roads_post_rem_obsolete_points", roads); printBadAngles("bad_angles_finish", roads); this.mode = MODE_LINES; writeOSM("lines_after_roads", lines); removeWrongAngles(null, lines, modifiedRoads, null, restrictions); writeOSM("lines_post_rem_wrong_angles", lines); removeObsoletePoints(lines, modifiedRoads); writeOSM("lines_final", lines); } private void replaceCoord(Coord toRepl, Coord replacement, Map<Coord, Coord> replacements) { assert toRepl != replacement; if (toRepl.getOnBoundary()){ if (replacement.equals(toRepl) == false){ log.error("boundary node is replaced by node with non-equal coordinates at", toRepl.toOSMURL()); assert false : "boundary node is replaced" ; } replacement.setOnBoundary(true); } toRepl.setReplaced(true); if (toRepl instanceof CoordPOI) { CoordPOI cp = (CoordPOI) toRepl; if (cp.isUsed()){ replacement = new CoordPOI(replacement); ((CoordPOI) replacement).setNode(cp.getNode()); ((CoordPOI) replacement).setUsed(true); ((CoordPOI) replacement).setConvertToViaInRouteRestriction(cp.getConvertToViaInRouteRestriction()); if (replacement.highPrecEquals(cp.getNode().getLocation()) == false){ log.error("CoordPOI node is replaced with non-equal coordinates at", toRepl.toOSMURL()); } } } if (toRepl.isViaNodeOfRestriction()) replacement.setViaNodeOfRestriction(true); replacements.put(toRepl, replacement); while (toRepl.getHighwayCount() > replacement.getHighwayCount()) replacement.incHighwayCount(); if (mode == MODE_LINES && toRepl.isEndOfWay() ){ replacement.setEndOfWay(true); } } /** * Common code to handle replacements of points in ways. Checks for special * cases regarding CoordPOI. * * @param p point to replace * @param way way that contains p * @param replacements the Map containing the replaced points * @return the replacement */ private static Coord getReplacement(Coord p, Way way, Map<Coord, Coord> replacements) { // check if this point is to be replaced because // it was previously merged into another point if (p.isReplaced()) { Coord replacement = null; Coord r = p; while ((r = replacements.get(r)) != null) { replacement = r; } if (replacement != null) { if (p instanceof CoordPOI) { CoordPOI cp = (CoordPOI) p; Node node = cp.getNode(); if (cp.isUsed() && way != null && way.getId() != 0) { String wayPOI = way.getTag(StyledConverter.WAY_POI_NODE_IDS); if (wayPOI != null && wayPOI.contains("["+node.getId()+"]")){ if (replacement instanceof CoordPOI) { Node rNode = ((CoordPOI) replacement).getNode(); if (rNode.getId() != node.getId()) { if (wayPOI.contains("["+ rNode.getId() + "]")){ log.warn("CoordPOI", node.getId(), "replaced by CoordPOI", rNode.getId(), "in way", way.toBrowseURL()); } else log.warn("CoordPOI", node.getId(), "replaced by ignored CoordPOI", rNode.getId(), "in way", way.toBrowseURL()); } } else log.warn("CoordPOI", node.getId(), "replaced by simple coord in way", way.toBrowseURL()); } } } return replacement; } log.error("replacement not found for point " + p.toOSMURL()); } return p; } /** * Find wrong angles caused by rounding to map units. Try to fix them by * moving, removing or merging points. * @param roads list with routable ways or null, if lines should be optimized * @param lines list with non-routable ways * @param modifiedRoads map of modified routable ways (modified by this routine) * @param deletedRoads set of ids of deleted routable ways (modified by this routine) * @param restrictions Map with restriction relations. The restriction relations may be modified by this routine */ private void removeWrongAngles(List<ConvertedWay> roads, List<ConvertedWay> lines, HashMap<Long, ConvertedWay> modifiedRoads, HashSet<Long> deletedRoads, List<RestrictionRelation> restrictions) { // replacements maps those nodes that have been replaced to // the node that replaces them Map<Coord, Coord> replacements = new IdentityHashMap<>(); final HashSet<Coord> changedPlaces = new HashSet<>(); int numNodesMerged = 0; HashSet<Way> waysWithBearingErrors = new HashSet<>(); HashSet<Long> waysThatMapToOnePoint = new HashSet<>(); int pass = 0; Way lastWay = null; List<ConvertedWay> convertedWays = (roads != null) ? roads: lines; boolean anotherPassRequired = true; while (anotherPassRequired && pass < 20) { anotherPassRequired = false; log.info("Removing wrong angles - PASS " + ++pass); writeOSM(((mode==MODE_LINES) ? "lines_pass_" + pass:"roads_pass_" + pass), convertedWays); // Step 1: detect points which are parts of line segments with wrong bearings lastWay = null; for (ConvertedWay cw : convertedWays) { if (!cw.isValid() || cw.isOverlay()) continue; Way way = cw.getWay(); if (way.equals(lastWay)) continue; if (pass != 1 && waysWithBearingErrors.contains(way) == false) continue; lastWay = way; List<Coord> points = way.getPoints(); // scan through the way's points looking for line segments with big // bearing errors Coord prev = null; if (points.get(0) == points.get(points.size()-1) && points.size() >= 2) prev = points.get(points.size()-2); boolean hasNonEqualPoints = false; for (int i = 0; i < points.size(); ++i) { Coord p = points.get(i); if (pass == 1) p.setRemove(false); p = getReplacement(p, way, replacements); if (i == 0 || i == points.size()-1){ p.setEndOfWay(true); } if (prev != null) { if (pass == 1 && p.equals(prev) == false) hasNonEqualPoints = true; double err = calcBearingError(p,prev); if (err >= MAX_BEARING_ERROR){ // bearing error is big p.setPartOfBadAngle(true); prev.setPartOfBadAngle(true); } } prev = p; } if (pass == 1 && hasNonEqualPoints == false){ waysThatMapToOnePoint.add(way.getId()); log.info("all points of way",way.toBrowseURL(),"are rounded to equal map units" ); } } // Step 2: collect the line segments that are connected to critical points IdentityHashMap<Coord, CenterOfAngle> centerMap = new IdentityHashMap<>(); List<CenterOfAngle> centers = new ArrayList<>(); // needed for ordered processing int centerId = 0; lastWay = null; for (ConvertedWay cw : convertedWays) { if (!cw.isValid() || cw.isOverlay()) continue; Way way = cw.getWay(); if (way.equals(lastWay)) continue; if (pass != 1 && waysWithBearingErrors.contains(way) == false) continue; lastWay = way; boolean wayHasSpecialPoints = false; List<Coord> points = way.getPoints(); // scan through the way's points looking for line segments with big // bearing errors Coord prev = null; if (points.get(0) == points.get(points.size()-1) && points.size() >= 2) prev = points.get(points.size()-2); for (int i = 0; i < points.size(); ++i) { Coord p = points.get(i); if (prev != null) { if (p == prev){ points.remove(i); --i; if (mode == MODE_ROADS) modifiedRoads.put(way.getId(), cw); continue; } if (p.isPartOfBadAngle() || prev.isPartOfBadAngle()) { wayHasSpecialPoints = true; // save both points with their neighbour Coord p1 = prev; Coord p2 = p; CenterOfAngle coa1 = centerMap.get(p); if (coa1 == null) { coa1 = new CenterOfAngle(p, centerId++); centerMap.put(p, coa1); centers.add(coa1); } CenterOfAngle coa2 = centerMap.get(prev); if (coa2 == null) { coa2 = new CenterOfAngle(prev, centerId++); centerMap.put(prev, coa2); centers.add(coa2); } coa1.addNeighbour(coa2); coa2.addNeighbour(coa1); if (points.size() == 2) { // way has only two points, don't merge them coa1.addBadMergeCandidate(coa2); } if (mode == MODE_ROADS){ if (p1.getHighwayCount() >= 2 && p2.getHighwayCount() >= 2){ if (cw.isRoundabout()) { // avoid to merge exits of roundabouts coa1.addBadMergeCandidate(coa2); } } } } } prev = p; } if (pass == 1 && wayHasSpecialPoints) waysWithBearingErrors.add(way); } // Step 3: Update list of ways with bearing errors or points next to them lastWay = null; for (ConvertedWay cw : convertedWays) { if (!cw.isValid() || cw.isOverlay()) continue; Way way = cw.getWay(); if (way.equals(lastWay)) continue; lastWay = way; if (waysWithBearingErrors.contains(way)) continue; List<Coord> points = way.getPoints(); // scan through the way's points looking for line segments with big // bearing errors for (Coord p: points) { if (p.getHighwayCount() < 2) continue; if (centerMap.containsKey(p)){ waysWithBearingErrors.add(way); break; } } } log.info("pass " + pass + ": analysing " + centers.size() + " points with bearing problems."); centerMap = null; // Return to GC // Step 4: try to correct the errors List<CenterOfAngle> checkAgainList = null; boolean tryMerge = false; while (true){ checkAgainList = new ArrayList<>(); for (CenterOfAngle coa : centers) { coa.center.setPartOfBadAngle(false); // reset flag for next pass if (coa.getCurrentLocation(replacements) == null) continue; // removed center if (coa.isOK(replacements) == false) { boolean changed = coa.tryChange(replacements, tryMerge); if (changed){ if (gpxPath != null) changedPlaces.add(coa.center); continue; } checkAgainList.add(coa); } } if (tryMerge) break; // leave when 2nd pass finished tryMerge = true; centers = checkAgainList; } // Step 5: apply the calculated corrections to the ways lastWay = null; boolean lastWayModified = false; ConvertedWay lastConvertedWay = null; for (ConvertedWay cw : convertedWays) { if (!cw.isValid() || cw.isOverlay()) continue; Way way = cw.getWay(); if (waysWithBearingErrors.contains(way) == false) continue; List<Coord> points = way.getPoints(); if (way.equals(lastWay)) { if (lastWayModified){ points.clear(); points.addAll(lastWay.getPoints()); if (cw.isReversed() != lastConvertedWay.isReversed()) Collections.reverse(points); } continue; } lastWay = way; lastConvertedWay = cw; lastWayModified = false; // loop backwards because we may delete points for (int i = points.size() - 1; i >= 0; i--) { Coord p = points.get(i); if (p.isToRemove()) { points.remove(i); anotherPassRequired = true; lastWayModified = true; if (i > 0 && i < points.size()) { // special case: handle micro loop if (points.get(i - 1) == points.get(i)) points.remove(i); } continue; } // check if this point is to be replaced because // it was previously moved Coord replacement = getReplacement(p, way, replacements); if (p == replacement) continue; if (p.isViaNodeOfRestriction()){ // make sure that we find the restriction with the new coord instance replacement.setViaNodeOfRestriction(true); p.setViaNodeOfRestriction(false); } p = replacement; // replace point in way points.set(i, p); if (p.getHighwayCount() >= 2) numNodesMerged++; lastWayModified = true; if (i + 1 < points.size() && points.get(i + 1) == p) { points.remove(i); anotherPassRequired = true; } if (i -1 >= 0 && points.get(i-1) == p){ points.remove(i); anotherPassRequired = true; } } if (lastWayModified && mode == MODE_ROADS){ modifiedRoads.put(way.getId(), cw); } } } // finish: remove remaining duplicate points int numWaysDeleted = 0; lastWay = null; boolean lastWayModified = false; ConvertedWay lastConvertedWay = null; for (ConvertedWay cw : convertedWays) { if (cw.isOverlay()) continue; Way way = cw.getWay(); List<Coord> points = way.getPoints(); if (points.size() < 2) { if (log.isInfoEnabled()) log.info(" Way " + way.getTag("name") + " (" + way.toBrowseURL() + ") has less than 2 points - deleting it"); if (mode == MODE_LINES && waysThatMapToOnePoint.contains(way.getId()) == false) log.warn("non-routable way " ,way.getId(),"was removed"); if (mode == MODE_ROADS) deletedRoads.add(way.getId()); ++numWaysDeleted; continue; } if (way.equals(lastWay)) { if (lastWayModified){ points.clear(); points.addAll(lastWay.getPoints()); if (cw.isReversed() != lastConvertedWay.isReversed()) Collections.reverse(points); } continue; } lastWay = way; lastConvertedWay = cw; lastWayModified = false; Coord prev = points.get(points.size() - 1); // loop backwards because we may delete points for (int i = points.size() - 2; i >= 0; i--) { Coord p = points.get(i); if (p == prev){ points.remove(i); lastWayModified = true; } // if (p.equals(prev) && (p.getHighwayCount() < 2 || prev.getHighwayCount() < 2)){ // // not an error, but should not happen // log.warn("way " + way.getId() + " still has consecutive equal points at " + p.toOSMURL()); // } prev = p; } } if (mode == MODE_ROADS){ // treat special case: non-routable ways may be connected to moved // points in roads for (ConvertedWay cw : lines) { if (!cw.isValid() || cw.isOverlay()) continue; Way way = cw.getWay(); List<Coord> points = way.getPoints(); int n = points.size(); boolean hasReplacedPoints = false; for (int i = 0; i < n; i++) { Coord p = points.get(i); if (p.isReplaced()) { hasReplacedPoints = true; points.set(i, getReplacement(p, null, replacements)); } } if (hasReplacedPoints && gpxPath != null) { GpxCreator.createGpx(gpxPath + way.getId() + "_mod_non_routable", points); } } for (RestrictionRelation rr: restrictions){ for (Coord p: rr.getViaCoords()){ Coord replacement = getReplacement(p, null, replacements); if (p != replacement){ rr.replaceViaCoord(p, replacement); } } } } if (gpxPath != null) { GpxCreator.createGpx(gpxPath + "solved_badAngles", bbox.toCoords(), new ArrayList<>(changedPlaces)); } if (anotherPassRequired) log.error("Removing wrong angles - didn't finish in " + pass + " passes, giving up!"); else log.info("Removing wrong angles - finished in", pass, "passes (", numNodesMerged, "nodes merged,", numWaysDeleted, "ways deleted)"); } /** * remove obsolete points in ways. Obsolete are points which are * very close to 180 degrees angles in the real line or wrong points. * Wrong points are those that produce wrong angles, so that * removing them reduces the error. * @param convertedWays * @param modifiedRoads */ private void removeObsoletePoints(List<ConvertedWay> convertedWays, HashMap<Long, ConvertedWay> modifiedRoads){ ConvertedWay lastConvertedWay = null; int numPointsRemoved = 0; boolean lastWasModified = false; List<Coord> removedInWay = new ArrayList<>(); List<Coord> obsoletePoints = new ArrayList<>(); List<Coord> modifiedPoints = new ArrayList<>(); for (ConvertedWay cw : convertedWays) { if (!cw.isValid() || cw.isOverlay()) continue; Way way = cw.getWay(); if (lastConvertedWay != null && way.equals(lastConvertedWay.getWay())) { if (lastWasModified){ List<Coord> points = way.getPoints(); points.clear(); points.addAll(lastConvertedWay.getPoints()); if (cw.isReversed() != lastConvertedWay.isReversed()) Collections.reverse(points); } continue; } lastConvertedWay = cw; lastWasModified = false; List<Coord> points = way.getPoints(); modifiedPoints.clear(); double maxErrorDistance = calcMaxErrorDistance(points.get(0)); boolean draw = false; removedInWay.clear(); modifiedPoints.add(points.get(0)); // scan through the way's points looking for points which are // on almost straight line and therefore obsolete for (int i = 1; i+1 < points.size(); i++) { Coord cm = points.get(i); if (allowedToRemove(cm) == false){ modifiedPoints.add(cm); continue; } Coord c1 = modifiedPoints.get(modifiedPoints.size()-1); Coord c2 = points.get(i+1); if (c1 == c2){ // loop, handled by split routine modifiedPoints.add(cm); continue; } boolean keepThis = true; double realAngle = Utils.getAngle(c1, cm, c2); if (Math.abs(realAngle) < MAX_DIFF_ANGLE_STRAIGHT_LINE){ double distance = cm.distToLineSegment(c1, c2); if (distance >= maxErrorDistance){ modifiedPoints.add(cm); continue; } keepThis = false; } else { double displayedAngle = Utils.getDisplayedAngle(c1, cm, c2); if (displayedAngle < 0 && realAngle > 0 || displayedAngle > 0 && realAngle < 0){ // straight line is closer to real angle keepThis = false; } else if (Math.abs(displayedAngle) < 1){ // displayed line is nearly straight if (c1.getHighwayCount() < 2 && c2.getHighwayCount() < 2){ // we can remove the point keepThis = false; } } else if (Math.abs(realAngle-displayedAngle) > 2 * Math.abs(realAngle) && Math.abs(realAngle) < MAX_BEARING_ERROR_HALF){ // displayed angle is much sharper than wanted, straight line is closer to real angle keepThis = false; } } if (keepThis){ modifiedPoints.add(cm); continue; } if (log.isDebugEnabled()) log.debug("removing obsolete point on almost straight segment in way ",way.toBrowseURL(),"at",cm.toOSMURL()); if (gpxPath != null){ obsoletePoints.add(cm); removedInWay.add(cm); } numPointsRemoved++; lastWasModified = true; } if (lastWasModified){ modifiedPoints.add(points.get(points.size()-1)); points.clear(); points.addAll(modifiedPoints); if (mode == MODE_ROADS) modifiedRoads.put(way.getId(), cw); if (gpxPath != null){ if (draw || cw.isRoundabout()) { GpxCreator.createGpx(gpxPath+way.getId()+"_dpmod", points,removedInWay); } } } } if (gpxPath != null){ GpxCreator.createGpx(gpxPath + "obsolete", bbox.toCoords(), new ArrayList<>(obsoletePoints)); } log.info("Removed", numPointsRemoved, "obsolete points in lines"); } /** * debug code * @param roads */ private void printBadAngles(String name, List<ConvertedWay> roads){ if (gpxPath == null) return; List<ConvertedWay> badWays = new ArrayList<>(); Way lastWay = null; List<Coord> badAngles = new ArrayList<>(); for (int w = 0; w < roads.size(); w++) { ConvertedWay cw = roads.get(w); if (!cw.isValid()) continue; Way way = cw.getWay(); if (way.equals(lastWay)) { continue; } boolean hasBadAngles = false; lastWay = way; List<Coord> points = way.getPoints(); // scan through the way's points looking for points which are // on almost straight line and therefore obsolete for (int i = points.size() - 2; i >= 1; --i) { Coord cm = points.get(i); Coord c1 = points.get(i-1); Coord c2 = points.get(i+1); if (c1 == c2){ // loop, handled by split routine continue; } double realAngle = Utils.getAngle(c1, cm, c2); double displayedAngle = Utils.getDisplayedAngle(c1, cm, c2); if (Math.abs(displayedAngle-realAngle) > 30){ badAngles.add(cm); hasBadAngles = true; // badAngles.addAll(cm.getAlternativePositions()); } } if (points.size() > 2){ Coord p0 = points.get(0); Coord plast = points.get(points.size()-1); if (p0 == plast){ Coord cm = points.get(0); Coord c1 = points.get(points.size()-2); Coord c2 = points.get(1); if (c1 == c2){ // loop, handled by split routine continue; } double realAngle = Utils.getAngle(c1, cm, c2); double displayedAngle = Utils.getDisplayedAngle(c1, cm, c2); if (Math.abs(displayedAngle-realAngle) > 30){ badAngles.add(cm); hasBadAngles = true; // badAngles.addAll(cm.getAlternativePositions()); } } } if (hasBadAngles) badWays.add(cw); } GpxCreator.createGpx(gpxPath + name, bbox.toCoords(), new ArrayList<>(badAngles)); writeOSM(name, badWays); } /** * Check if the point can safely be removed from a road. * @param p * @return true if remove is okay */ private boolean allowedToRemove(Coord p){ if (p.getOnBoundary()) return false; if (mode == MODE_LINES && p.isEndOfWay()) return false; if (p instanceof CoordPOI){ if (((CoordPOI) p).isUsed()){ return false; } } if (p.getHighwayCount() >= 2 || p.isViaNodeOfRestriction()) { return false; } return true; } /** * helper class */ private class CenterOfAngle { final Coord center; final List<CenterOfAngle> neighbours; final int id; // debugging aid boolean wasMerged; List<CenterOfAngle> badMergeCandidates; public CenterOfAngle(Coord center, int id) { this.center = center; assert center.isReplaced() == false; this.id = id; neighbours = new ArrayList<>(); } @Override public String toString() { return "CenterOfAngle [id=" + id + ", wasMerged=" + wasMerged + ", num Neighbours="+neighbours.size()+"]"; } @Override public int hashCode() { return center.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; return center == ((CenterOfAngle) obj).center; } /** * returns current center position or null if removed * @param replacements * @return */ public Coord getCurrentLocation(Map<Coord, Coord> replacements){ Coord c = getReplacement(center, null, replacements); if (c.isToRemove()) return null; return c; } /** * Add neighbour which should not be merged * @param other */ public void addBadMergeCandidate(CenterOfAngle other) { if (badMergeCandidates == null) badMergeCandidates = new ArrayList<>(); badMergeCandidates.add(other); } public void addNeighbour(CenterOfAngle other) { if (this == other){ log.error("neighbour is equal" ); } boolean isNew = true; // we want only different Coord instances here for (CenterOfAngle neighbour : neighbours) { if (neighbour == other) { isNew = false; break; } } if (isNew) neighbours.add(other); } /** * * @param replacements * @return false if this needs changes */ public boolean isOK(Map<Coord, Coord> replacements) { Coord c = getCurrentLocation (replacements); if (c == null) return true; // removed center: nothing to do for (CenterOfAngle neighbour : neighbours) { Coord n = neighbour.getCurrentLocation(replacements); if (n == null) continue; // skip removed neighbours double err = calcBearingError(c, n); if (err >= MAX_BEARING_ERROR) return false; } return true; } /** * Try whether a move or remove or merge of this centre * fixes bearing problems. * @param replacements * @param tryAlsoMerge true means merge is allowed * @return true if something was changed */ public boolean tryChange(Map<Coord, Coord> replacements, boolean tryAlsoMerge) { if (wasMerged ) { return false; } Coord currentCenter = getCurrentLocation(replacements); if (currentCenter == null) return false; // cannot modify removed centre CenterOfAngle worstNeighbour = null; Coord worstNP = null; double initialMaxError = 0; double initialSumErr = 0; for (CenterOfAngle neighbour : neighbours) { Coord n = neighbour.getCurrentLocation(replacements); if (n == null) return false; // neighbour was removed if (currentCenter.highPrecEquals(n)){ if (currentCenter == n){ log.error(id + ": bad neighbour " + neighbour.id + " zero distance"); } if (badMergeCandidates != null && badMergeCandidates.contains(neighbour ) || neighbour.badMergeCandidates != null && neighbour.badMergeCandidates.contains(this)) { //not allowed to merge } else { replaceCoord(currentCenter, n, replacements); neighbour.wasMerged = wasMerged = true; return true; } } double err = calcBearingError(currentCenter, n); if (err != Double.MAX_VALUE) initialSumErr += err; if (err > initialMaxError){ initialMaxError = err; worstNeighbour = neighbour; worstNP = n; } } if (initialMaxError < MAX_BEARING_ERROR) return false; double removeErr = calcRemoveError(replacements); if (removeErr == 0){ // createGPX(gpxPath+id+"_rem_0", replacements); currentCenter.setRemove(true); return true; } if (initialMaxError == Double.MAX_VALUE) initialSumErr = initialMaxError; double bestReplErr = initialMaxError; Coord bestCenterReplacement = null; List<Coord> altPositions = currentCenter.getAlternativePositions(); for (Coord altCenter : altPositions){ double err = calcBearingError(altCenter, worstNP); if (err >= bestReplErr) continue; // alt. position is improvement, check all neighbours double errMax = calcMaxError(replacements, currentCenter, altCenter); if (errMax >= initialMaxError) continue; bestReplErr = err; bestCenterReplacement = altCenter; } Coord bestNeighbourReplacement = null; if (worstNP.hasAlternativePos()){ for (Coord altCenter : altPositions){ replaceCoord(currentCenter, altCenter, replacements); for (Coord altN: worstNP.getAlternativePositions()){ double err = calcBearingError(altCenter, altN); if (err >= bestReplErr) continue; double errNeighbour = worstNeighbour.calcMaxError(replacements, worstNP, altN); if (errNeighbour >= bestReplErr) continue; bestReplErr = err; bestCenterReplacement = altCenter; bestNeighbourReplacement = altN; } replacements.remove(currentCenter); currentCenter.setReplaced(false); } } if (bestReplErr < MAX_BEARING_ERROR){ // String msg = "_good"; if (removeErr < bestReplErr && initialMaxError - removeErr >= MAX_BEARING_ERROR_HALF && removeErr < MAX_BEARING_ERROR_HALF){ bestCenterReplacement = null; // createGPX(gpxPath+id+"_rem_pref", replacements); } else if (initialMaxError - bestReplErr < MAX_BEARING_ERROR_HALF || bestReplErr > MAX_BEARING_ERROR_HALF){ // msg = "_rather_good"; } if (bestCenterReplacement != null){ replaceCoord(currentCenter, bestCenterReplacement, replacements); if (bestNeighbourReplacement != null) replaceCoord(worstNP, bestNeighbourReplacement, replacements); double modifiedSumErr = calcSumOfErrors(replacements); if (modifiedSumErr < initialSumErr){ // if ("_good".equals(msg) == false) // createGPX(gpxPath+id+msg, replacements); if (bestNeighbourReplacement != null){ // worstNeighbour.createGPX(gpxPath+worstNeighbour.id+msg+"_n", replacements); } return true; } // revert changes // System.out.println("ignoring possible improvement at center " + id + " " + initialMaxError + " -> " + bestReplErr + " " + initialSumErr + " --> " + modifiedSumErr); // createGPX(gpxPath+id+"_reverted_"+msg, replacements); replacements.remove(currentCenter); currentCenter.setReplaced(false); replacements.remove(worstNP); worstNP.setReplaced(false); // createGPX(gpxPath+id+"_as_is", replacements); bestCenterReplacement = null; } } if (removeErr < MAX_BEARING_ERROR){ // createGPX(gpxPath+id+"_rem", replacements); currentCenter.setRemove(true); return true; } if (!tryAlsoMerge) return false; double dist = currentCenter.distance(worstNP); double maxDist = calcMaxErrorDistance(currentCenter) * 2; boolean forceMerge = dist < maxDist || currentCenter.equals(worstNP); if (forceMerge || this.neighbours.size() == 3 && worstNeighbour.neighbours.size() == 3) return tryMerge(forceMerge, initialMaxError, worstNeighbour, replacements); if (bestCenterReplacement != null){ double replImprovement = initialMaxError - bestReplErr; if (replImprovement < MAX_BEARING_ERROR) return false; replaceCoord(currentCenter, bestCenterReplacement, replacements); if (bestNeighbourReplacement != null){ replaceCoord(worstNP, bestNeighbourReplacement, replacements); } double modifiedSumErr = calcSumOfErrors(replacements); if (modifiedSumErr < initialSumErr){ // System.out.println("ignoring possible improvement at center " + id + " " + initialMaxError + " -> " + bestReplErr + " " + initialSumErr + " --> " + modifiedSumErr); // createGPX(gpxPath+id+"_possible", replacements); } replacements.remove(currentCenter); currentCenter.setReplaced(false); if (bestNeighbourReplacement != null){ replacements.remove(worstNP); worstNP.setReplaced(false); } if (modifiedSumErr < initialSumErr){ // createGPX(gpxPath+id+"_as_is", replacements); } } return false; } /** * Calculate error when two centres are merged. If they are not equal * and the error is too big, nothing is changed and false is returned. * * @param forceMerge true: skip straight line check * @param initialMaxError max. bearing error of this centre * @param neighbour neighbour to merge * @param replacements * @return true if merge is okay */ private boolean tryMerge(boolean forceMerge, double initialMaxError, CenterOfAngle neighbour, Map<Coord, Coord> replacements) { if (badMergeCandidates != null && badMergeCandidates.contains(neighbour ) || neighbour.badMergeCandidates != null && neighbour.badMergeCandidates.contains(this)) { return false; // not allowed to merge } Coord c = getCurrentLocation(replacements); Coord n = neighbour.getCurrentLocation(replacements); // check special cases: don't merge if // 1) both points are via nodes // 2) both nodes are boundary nodes with non-equal coords // 3) on point is via node and the other is a boundary node, the result could be that the restriction is ignored if (c.getOnBoundary()){ if (n.isViaNodeOfRestriction() || n.getOnBoundary() && c.equals(n) == false) return false; } if (c.isViaNodeOfRestriction() && (n.isViaNodeOfRestriction() || n.getOnBoundary())) return false; if (c instanceof CoordPOI && (n instanceof CoordPOI || n.getOnBoundary())) return false; if (n instanceof CoordPOI && (c instanceof CoordPOI || c.getOnBoundary())) return false; Coord mergePoint; if (c.getOnBoundary() || c instanceof CoordPOI) mergePoint = c; else if (n.getOnBoundary() || n instanceof CoordPOI) mergePoint = n; else if (c.equals(n)) mergePoint = c; else mergePoint = c.makeBetweenPoint(n, 0.5); double err = 0; if (c.equals(n) == false){ err = calcMergeErr(neighbour, mergePoint, replacements); if (err == Double.MAX_VALUE && initialMaxError == Double.MAX_VALUE){ log.warn("still equal neighbour after merge",c.toOSMURL()); } else { if (err >= MAX_BEARING_ERROR) return false; if (initialMaxError - err < MAX_BEARING_ERROR_HALF && err > MAX_BEARING_ERROR_HALF){ return false; // improvement too small } } } if (!forceMerge){ // merge only if the merged line is part of a (nearly) straight line going through both centres, if (!checkNearlyStraight(c.bearingTo(n), neighbour, replacements) || !neighbour.checkNearlyStraight(n.bearingTo(c), this, replacements)) { // createGPX(gpxPath + "no_more_merge_" + id, replacements); // neighbour.createGPX(gpxPath + "no_more_merge_" + neighbour.id, replacements); // System.out.println("no_merge at " + mergePoint.toDegreeString() + " " + mergePoint.toOSMURL() + " at " + id); return false; } } int hwc = c.getHighwayCount() + n.getHighwayCount() - 1; for (int i = 0; i < hwc; i++) mergePoint.incHighwayCount(); if (c != mergePoint) replaceCoord(c, mergePoint, replacements); if (n != mergePoint){ replaceCoord(n, mergePoint, replacements); } // createGPX(gpxPath+id+"_merged", replacements); // neighbour.createGPX(gpxPath+neighbour.id+"_merged_w_"+id, replacements); neighbour.wasMerged = wasMerged = true; return true; } /** * Try to find a line that builds a nearly straight line * with the connection to an other centre. * @param bearing bearing of the connection to the other centre * @param other the other centre * @param replacements * @return true if a nearly straight line exists */ private boolean checkNearlyStraight(double bearing, CenterOfAngle other, Map<Coord, Coord> replacements) { Coord c = getCurrentLocation(replacements); for (CenterOfAngle neighbour : neighbours) { if (neighbour == other) continue; Coord n = neighbour.getCurrentLocation(replacements); if (n == null) continue; double bearing2 = c.bearingTo(n); double angle = bearing2 - (bearing - 180); while(angle > 180) angle -= 360; while(angle < -180) angle += 360; if (Math.abs(angle) < 10) // tolerate small angle return true; } return false; } /** * Calculate max. error of this merged with other centres. * @param other the other centre * @param mergePoint the point which should be used as a new centre for both * @param replacements * @return the error */ private double calcMergeErr(CenterOfAngle other, Coord mergePoint, Map<Coord, Coord> replacements) { double maxErr = 0; for (CenterOfAngle neighbour : neighbours) { if (neighbour == other) continue; Coord n = neighbour.getCurrentLocation(replacements); if (n != null){ double err = calcBearingError(mergePoint, n); if (err > maxErr) maxErr = err; } } for (CenterOfAngle othersNeighbour : other.neighbours) { if (othersNeighbour == this) continue; Coord n = othersNeighbour.getCurrentLocation(replacements); if (n != null){ double err = calcBearingError(mergePoint, n); if (err > maxErr) maxErr = err; } } return maxErr; } /** * Calculate max. bearing error of centre point to all neighbours. * @param replacements * @param toRepl if centre or a neighbour center is identical to this, use replacement instead * @param replacement see toRepl * @return error [0..180] or Double.MAX_VALUE in case of equal points */ private double calcMaxError(Map<Coord, Coord> replacements, Coord toRepl, Coord replacement) { double maxErr = 0; Coord c = getCurrentLocation(replacements); for (CenterOfAngle neighbour : neighbours) { Coord n = neighbour.getCurrentLocation(replacements); if (n == null) continue; // neighbour was removed double err; if (c == toRepl) err = calcBearingError(replacement, n); else if (n == toRepl) err = calcBearingError(c, replacement); else err = calcBearingError(c, n); if (err == Double.MAX_VALUE) return err; if (err > maxErr) maxErr = err; } return maxErr; } /** * Calculate sum of errors for a centre. * @param replacements * @return */ private double calcSumOfErrors(Map<Coord, Coord> replacements) { double SumErr = 0; Coord c = getCurrentLocation(replacements); for (CenterOfAngle neighbour : neighbours) { Coord n = neighbour.getCurrentLocation(replacements); if (n == null) continue; // skip removed neighbour double err = calcBearingError(c, n); if (err == Double.MAX_VALUE) return err; SumErr += err; } return SumErr; } /** * Calculate error for a removed centre. * @param replacements * @return Double.MAX_VALUE if centre must not be deleted, else [0..180] */ private double calcRemoveError(Map<Coord, Coord> replacements) { if (allowedToRemove(center) == false) return Double.MAX_VALUE; Coord c = getCurrentLocation(replacements); if (neighbours.size() > 2) return Double.MAX_VALUE; Coord[] outerPoints = new Coord[neighbours.size()]; for (int i = 0; i < neighbours.size(); i++) { CenterOfAngle neighbour = neighbours.get(i); Coord n = neighbour.getCurrentLocation(replacements); if (n == null) return Double.MAX_VALUE; if (c.equals(n)){ if (c.getDistToDisplayedPoint() < n.getDistToDisplayedPoint()) return 0; } outerPoints[i] = n; } if (neighbours.size() < 2) return Double.MAX_VALUE; if (c.getDistToDisplayedPoint() < Math.max(outerPoints[0].getDistToDisplayedPoint(), outerPoints[1].getDistToDisplayedPoint())) return Double.MAX_VALUE; double dsplAngle = Utils.getDisplayedAngle(outerPoints[0], c, outerPoints[1]); if (Math.abs( dsplAngle ) < 3) return Double.MAX_VALUE; double realAngle = Utils.getAngle(outerPoints[0], c, outerPoints[1]); double err = Math.abs(realAngle) / 2; return err; } // TODO: remove this debugging aid @SuppressWarnings("unused") private void createGPX(String gpxName, Map<Coord, Coord> replacements) { if (gpxName == null || gpxPath == null) return; if (gpxName.isEmpty()) gpxName = gpxPath + id + "_no_info"; // print lines after change Coord c = getReplacement(center, null, replacements); List<Coord> alternatives = c.getAlternativePositions(); for (int i = 0; i < neighbours.size(); i++) { CenterOfAngle n = neighbours.get(i); Coord nc = getReplacement(n.center, null, replacements); if (nc == null) continue; // skip removed neighbour if (i == 0 && alternatives.isEmpty() == false) { GpxCreator.createGpx(gpxName + "_" + i, Arrays.asList(c, nc), alternatives); } else GpxCreator.createGpx(gpxName + "_" + i, Arrays.asList(c, nc)); } if (neighbours.isEmpty()) GpxCreator.createGpx(gpxName + "_empty", Arrays.asList(c, c), alternatives); } } private void writeOSM(String name, List<ConvertedWay> convertedWays){ //TODO: comment or remove /* if (gpxPath == null) return; File outDir = new File(gpxPath + "/."); if (outDir.getParentFile() != null) { outDir.getParentFile().mkdirs(); } Map<String,byte[]> dummyMap = new HashMap<>(); for (int pass = 1; pass <= 2; pass ++){ IdentityHashMap<Coord, Integer> allPoints = new IdentityHashMap<>(); uk.me.parabola.splitter.Area bounds = new uk.me.parabola.splitter.Area( bbox.getMinLat(),bbox.getMinLong(),bbox.getMaxLat(),bbox.getMaxLong()); O5mMapWriter writer = new O5mMapWriter(bounds, outDir, 0, 0, dummyMap, dummyMap); writer.initForWrite(); Integer nodeId; try { for (ConvertedWay cw: convertedWays){ if (cw == null) continue; for (Coord p: cw.getPoints()){ nodeId = allPoints.get(p); if (nodeId == null){ nodeId = allPoints.size(); allPoints.put(p, nodeId); uk.me.parabola.splitter.Node nodeOut = new uk.me.parabola.splitter.Node(); if (pass == 1) nodeOut.set(nodeId+1000000000L, p.getLatDegrees(), p.getLonDegrees()); // high prec else nodeOut.set(nodeId+1000000000L, Utils.toDegrees(p.getLatitude()), Utils.toDegrees(p.getLongitude())); if (p instanceof CoordPOI){ for (Map.Entry<String, String> tagEntry : ((CoordPOI) p).getNode().getTagEntryIterator()) { nodeOut.addTag(tagEntry.getKey(), tagEntry.getValue()); } } writer.write(nodeOut); } } } for (int w = 0; w < convertedWays.size(); w++){ ConvertedWay cw = convertedWays.get(w); if (cw == null) continue; Way way = cw.getWay(); uk.me.parabola.splitter.Way wayOut = new uk.me.parabola.splitter.Way(); for (Coord p: way.getPoints()){ nodeId = allPoints.get(p); assert nodeId != null; wayOut.addRef(nodeId+1000000000L); } for (Map.Entry<String, String> tagEntry : way.getTagEntryIterator()) { wayOut.addTag(tagEntry.getKey(), tagEntry.getValue()); } wayOut.setId(way.getId()); writer.write(wayOut); } } catch (IOException e) { e.printStackTrace(); } writer.finishWrite(); File f = new File(outDir.getAbsoluteFile() , "00000000.o5m"); File ren = new File(outDir.getAbsoluteFile() , name+((pass==1) ? "_hp":"_mu") + ".o5m"); if (ren.exists()) ren.delete(); f.renameTo(ren); } */ } private static double calcBearingError(Coord p1, Coord p2){ if (p1.equals(p2) || p1.highPrecEquals(p2)) { return Double.MAX_VALUE; } double realBearing = p1.bearingTo(p2); double displayedBearing = p1.getDisplayedCoord().bearingTo(p2.getDisplayedCoord()); double err = displayedBearing - realBearing; while(err > 180) err -= 360; while(err < -180) err += 360; return Math.abs(err); } /** * Calculate the rounding error tolerance for a given point. * The latitude error may be max higher. Maybe this should be * @param p0 * @return */ private static double calcMaxErrorDistance(Coord p0){ Coord test = new Coord(p0.getLatitude(),p0.getLongitude()+1); double lonErr = p0.getDisplayedCoord().distance(test) / 2; return lonErr; } /** * Remove obsolete points on straight lines and spikes * and some wrong angles caused by rounding errors. * TODO: optimise by moving * @param points list of coordinates that form a shape * @return reduced list */ public static List<Coord> fixAnglesInShape(List<Coord> points) { List<Coord> modifiedPoints = new ArrayList<>(points.size()); double maxErrorDistance = calcMaxErrorDistance(points.get(0)); int n = points.size(); // scan through the way's points looking for points which are // on almost straight line and therefore obsolete for (int i = 0; i+1 < points.size(); i++) { Coord c1; if (modifiedPoints.size() > 0) c1 = modifiedPoints.get(modifiedPoints.size()-1); else { c1 = (i > 0) ? points.get(i-1):points.get(n-2); } Coord cm = points.get(i); if (cm.highPrecEquals(c1)){ if (modifiedPoints.size() > 1){ modifiedPoints.remove(modifiedPoints.size()-1); c1 = modifiedPoints.get(modifiedPoints.size()-1); // might be part of spike } else { continue; } } Coord c2 = points.get(i+1); int straightTest = Utils.isHighPrecStraight(c1, cm, c2); if (straightTest == Utils.STRICTLY_STRAIGHT || straightTest == Utils.STRAIGHT_SPIKE){ continue; } double realAngle = Utils.getAngle(c1, cm, c2); if (Math.abs(realAngle) < MAX_DIFF_ANGLE_STRAIGHT_LINE){ double distance = cm.distToLineSegment(c1, c2); if (distance < maxErrorDistance) continue; } modifiedPoints.add(cm); } if (modifiedPoints.size() > 1 && modifiedPoints.get(0) != modifiedPoints.get(modifiedPoints.size()-1)) modifiedPoints.add(modifiedPoints.get(0)); return modifiedPoints; } }