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