The attached patch (which also includes the drive-on-left patch because
it's based on that) attempts to improve the quality of routing
instructions so that when a turn off a road that doesn't involve a big
heading change is required, the GPS should say "turn left/right" rather
than "keep left/right". It can also reduce the number of "keep
left/right" instructions you get when you are not leaving the main road
but passing a junction that has a shallow turn (e.g. sometimes happens
on motorways when it tells you to keep left/right even though you are
not leaving the motorway).

I have tested it a little and it appears to work. I expect there are
situations where it does the wrong thing so it would be great if people
could give it a go and see if it behaves OK.

There isn't any option to enable the feature at the moment. If you use
the patch, it will be enabled.

It's currently issuing a warning for each change it makes but later that
can be downgraded to info.

Cheers,

Mark
diff --git a/resources/help/en/options b/resources/help/en/options
index a608abc..bf7a587 100644
--- a/resources/help/en/options
+++ b/resources/help/en/options
@@ -178,6 +178,19 @@ Miscellaneous options:
 	Experimental: Create maps that support routing.  This implies --net
 	(so that --net need not be given if --route is given).
 
+--drive-on-left
+--drive-on-right
+	Explicitly specify which side of the road vehicles are
+	expected to drive on. If neither of these options are
+	specified, it is assumed that vehicles drive on the right
+	unless --check-roundabouts is specified and the first
+	roundabout processed is clockwise.
+
+--check-roundabouts
+	Check that roundabouts have the expected direction (clockwise
+	when vehicles drive on the left). Roundabouts that are complete
+	loops and have the wrong direction are reversed.
+
 --ignore-maxspeeds
 	When reading OSM files, ignore any "maxspeed" tags.
 
diff --git a/src/uk/me/parabola/imgfmt/app/Coord.java b/src/uk/me/parabola/imgfmt/app/Coord.java
index 5462602..335284d 100644
--- a/src/uk/me/parabola/imgfmt/app/Coord.java
+++ b/src/uk/me/parabola/imgfmt/app/Coord.java
@@ -174,6 +174,22 @@ public class Coord implements Comparable<Coord> {
 						 (int)(longitude + (other.longitude - longitude) * fraction));
 	}
 
+
+	// returns bearing (in degrees) from current point to another point
+	public double bearingTo(Coord point) {
+		double lat1 = Utils.toRadians(latitude);
+		double lat2 = Utils.toRadians(point.latitude);
+		double lon1 = Utils.toRadians(longitude);
+		double lon2 = Utils.toRadians(point.longitude);
+
+		double dlon = lon2 - lon1;
+
+		double y = Math.sin(dlon) * Math.cos(lat2);
+		double x = Math.cos(lat1)*Math.sin(lat2) -
+			Math.sin(lat1)*Math.cos(lat2)*Math.cos(dlon);
+		return Math.atan2(y, x) * 180 / Math.PI;
+	}
+
 	/**
 	 * Sort lexicographically by longitude, then latitude.
 	 *
diff --git a/src/uk/me/parabola/imgfmt/app/net/NODHeader.java b/src/uk/me/parabola/imgfmt/app/net/NODHeader.java
index 1623fa7..2a8a709 100644
--- a/src/uk/me/parabola/imgfmt/app/net/NODHeader.java
+++ b/src/uk/me/parabola/imgfmt/app/net/NODHeader.java
@@ -37,6 +37,8 @@ public class NODHeader extends CommonHeader {
 
 	private final char align = DEF_ALIGN;
 
+	private static boolean driveOnLeft;
+
 	public NODHeader() {
 		super(HEADER_LEN, "GARMIN NOD");
 	}
@@ -62,7 +64,10 @@ public class NODHeader extends CommonHeader {
 		nodes.writeSectionInfo(writer);
 
 		// now sets 0x02 (enable turn restrictions?)
-		writer.putInt(0x27);
+		int val = 0x27;
+		if(driveOnLeft)
+			val |= 0x0300;
+		writer.putInt(val);
 
 		writer.putChar(align);
 		writer.putChar((char) (align - 1));
@@ -100,4 +105,8 @@ public class NODHeader extends CommonHeader {
 	public Section getBoundarySection() {
 		return boundary;
 	}
+
+	public static void setDriveOnLeft(boolean dol) {
+		driveOnLeft = dol;
+	}
 }
diff --git a/src/uk/me/parabola/imgfmt/app/net/RouteArc.java b/src/uk/me/parabola/imgfmt/app/net/RouteArc.java
index f349248..0b80259 100644
--- a/src/uk/me/parabola/imgfmt/app/net/RouteArc.java
+++ b/src/uk/me/parabola/imgfmt/app/net/RouteArc.java
@@ -43,7 +43,7 @@ public class RouteArc {
 
 	private int offset;
 
-	private final byte initialHeading;
+	private int initialHeading; // degrees
 	private byte endDirection;
 
 	private final RoadDef roadDef;
@@ -69,10 +69,10 @@ public class RouteArc {
 	 * @param roadDef The road that this arc segment is part of.
 	 * @param source The source node.
 	 * @param dest The destination node.
-	 * @param nextCoord The heading coordinate.
+	 * @param initialHeading The initial heading (signed degrees)
 	 */
 	public RouteArc(RoadDef roadDef, RouteNode source, RouteNode dest,
-				Coord nextCoord, double length) {
+					int initialHeading, double length) {
 		this.roadDef = roadDef;
 		this.source = source;
 		this.dest = dest;
@@ -80,23 +80,20 @@ public class RouteArc {
 		this.length = convertMeters(length);
 		if(log.isDebugEnabled())
 			log.debug("set length", this.length);
-		// if nextCoord is so close to the source node that the
-		// coordinates are equal, it won't be possible to determine
-		// the heading angle so use the dest node instead
-		if(source.getCoord().equals(nextCoord)) {
-			if(source.getCoord().equals(dest.getCoord())) {
-				log.warn("Can't determine arc heading (using 0) at " + source.getCoord().toOSMURL());
-				this.initialHeading = (byte)0;
-			}
-			else {
-				log.info("Using destination node to determine arc heading at " + source.getCoord().toOSMURL());
-				this.initialHeading = calcAngle(dest.getCoord());
-			}
-		}
-		else
-			this.initialHeading = calcAngle(nextCoord);
+		this.initialHeading = initialHeading;
 		// too early: dest.nodeClass may still increase
 		//setDestinationClass(dest.getNodeClass());
+
+		source.addArc(this);
+		dest.addIncomingArc(this);
+	}
+
+	public int getInitialHeading() {
+		return initialHeading;
+	}
+
+	public void setInitialHeading(int ih) {
+		initialHeading = ih;
 	}
 
 	public RouteNode getSource() {
@@ -167,36 +164,6 @@ public class RouteArc {
 		return indexB;
 	}
 	 
-	private byte calcAngle(Coord end) {
-		Coord start = source.getCoord();
-
-		if(log.isDebugEnabled())
-			log.debug("start", start.toDegreeString(), ", end", end.toDegreeString());
-
-		// Quite possibly too slow...  TODO 
-		double lat1 = Utils.toRadians(start.getLatitude());
-		double lat2 = Utils.toRadians(end.getLatitude());
-		double lon1 = Utils.toRadians(start.getLongitude());
-		double lon2 = Utils.toRadians(end.getLongitude());
-
-		//double dlat = lat2 - lat1;
-		double dlon = lon2 - lon1;
-
-		double y = Math.sin(dlon) * Math.cos(lat2);
-		double x = Math.cos(lat1)*Math.sin(lat2) -
-				Math.sin(lat1)*Math.cos(lat2)*Math.cos(dlon);
-		double angle = Math.atan2(y, x);
-
-		// angle is in radians
-		if(log.isDebugEnabled())
-			log.debug("angle is ", angle, ", deg", angle*57.29);
-
-		byte b = (byte) (256 * (angle / (2 * Math.PI)));
-		if(log.isDebugEnabled())
-			log.debug("deg from ret val", (360 * b) / 256);
-
-		return b;
-	}
 
 	private static int convertMeters(double l) {
 		// XXX: really a constant factor?
@@ -235,7 +202,7 @@ public class RouteArc {
 		for (int aLendat : lendat)
 			writer.put((byte) aLendat);
 
-		writer.put(initialHeading);
+		writer.put((byte)(256 * initialHeading / 360));
 
 		if (curve) {
 			int[] curvedat = encodeCurve();
@@ -327,13 +294,4 @@ public class RouteArc {
 			log.debug("setting destination class", destinationClass);
 		flagA |= (destinationClass & MASK_DESTCLASS);
 	}
-
-	public void setEndDirection(double ang) {
-		endDirection = angleToByte(ang);
-		curve = true;
-	}
-
-	private static byte angleToByte(double ang) {
-		return (byte) (255 * ang / 360);
-	}
 }
diff --git a/src/uk/me/parabola/imgfmt/app/net/RouteNode.java b/src/uk/me/parabola/imgfmt/app/net/RouteNode.java
index 90a46cd..27fbfce 100644
--- a/src/uk/me/parabola/imgfmt/app/net/RouteNode.java
+++ b/src/uk/me/parabola/imgfmt/app/net/RouteNode.java
@@ -21,6 +21,7 @@ import java.util.List;
 import uk.me.parabola.imgfmt.app.Coord;
 import uk.me.parabola.imgfmt.app.CoordNode;
 import uk.me.parabola.imgfmt.app.ImgFileWriter;
+import uk.me.parabola.imgfmt.app.Label;
 import uk.me.parabola.log.Logger;
 
 /**
@@ -58,7 +59,9 @@ public class RouteNode implements Comparable<RouteNode> {
 	private final List<RouteArc> arcs = new ArrayList<RouteArc>();
 	// restrictions at (via) this node
 	private final List<RouteRestriction> restrictions = new ArrayList<RouteRestriction>();
-	
+	// arcs to this node - order must be the same as arcs
+	private final List<RouteArc> incomingArcs = new ArrayList<RouteArc>();
+
 	private int flags = F_UNK_NEEDED;
 
 	private final CoordNode coord;
@@ -107,6 +110,10 @@ public class RouteNode implements Comparable<RouteNode> {
 		flags |= F_ARCS;
 	}
 
+	public void addIncomingArc(RouteArc arc) {
+		incomingArcs.add(arc);
+	}
+
 	public void addRestriction(RouteRestriction restr) {
 		restrictions.add(restr);
 		flags |= F_RESTRICTIONS;
@@ -285,4 +292,142 @@ public class RouteNode implements Comparable<RouteNode> {
 	public int compareTo(RouteNode otherNode) {
 		return coord.compareTo(otherNode.getCoord());
 	}
+
+	private boolean possiblySameRoad(RouteArc raa, RouteArc rab) {
+		if(raa == rab)
+			return true;
+		RoadDef rda = raa.getRoadDef();
+		RoadDef rdb = rab.getRoadDef();
+		if(rda == rdb)
+			return true;
+		Label[] laba = rda.getLabels();
+		Label[] labb = rdb.getLabels();
+		for(int i = 0; i < rda.getNumLabels(); ++i)
+			for(int j = 0; j < rdb.getNumLabels(); ++j)
+				if(laba[i].equals(labb[j]))
+					return true;
+
+		return false;
+	}
+
+	public void tweakArcs() {
+		if(arcs.size() == 3) {
+			final int minHeadingChangeWanted = 45; // min change to be a "turn"
+
+			// detect the "shallow turn" scenario where at a junction
+			// on some road, the side road leaves the original road at
+			// a very shallow angle and the GPS says "keep right/left"
+			// when it would be better if it said "turn right/left"
+
+			// the code tries to detect a pair of arcs (the "incoming"
+			// arc and the "continuing" arc) that are the "main road"
+			// and the remaining arc (called the "other" arc) which is
+			// the "side road"
+
+			// having worked out the roles for the arcs, the heuristic
+			// applied is that if the main road doesn't change its
+			// heading by more than minHeadingChangeWanted, ensure
+			// that the side road heading differs from the continuing
+			// heading by at least minHeadingChangeWanted
+
+			for(int ia = 0; ia < incomingArcs.size(); ++ia) {
+				RouteArc inArc = incomingArcs.get(ia);
+				// determine heading of incoming arc by reversing
+				// heading of matching outgoing arc
+				int inHeading = arcs.get(ia).getInitialHeading() + 180;
+				while(inHeading > 180)
+					inHeading -= 360;
+				while(inHeading < -180)
+					inHeading += 360;
+				// determine the outgoing (continuing) arc that is
+				// likely to be the same road as the incoming arc
+				RouteArc continuingArc = null;
+				int ca = 0;
+				while(ca < arcs.size()) {
+					if(ca != ia) {
+						if(possiblySameRoad(inArc, arcs.get(ca))) {	
+							continuingArc = arcs.get(ca);
+							break;
+						}
+					}
+					++ca;
+				}
+				// if we did not find the continuing arc, give up with
+				// this incoming arc
+				if(continuingArc == null) {
+					//					log.warn("Can't continue road " + inArc.getRoadDef().getName() + " at " + coord.toOSMURL());
+					continue;
+				}
+				int continuingHeading = continuingArc.getInitialHeading();
+				int mainHeadingDelta = continuingHeading - inHeading;
+				while(mainHeadingDelta > 180)
+					mainHeadingDelta -= 360;
+				while(mainHeadingDelta < -180)
+					mainHeadingDelta += 360;
+				//				log.warn(inArc.getRoadDef().getName() + " continues to " + continuingArc.getRoadDef().getName() + " with a heading change of " + mainHeadingDelta + " at " + coord.toOSMURL());
+
+				// if the continuation road heading change is less
+				// than minHeadingChangeWanted, force the other road's
+				// heading change to be at least minHeadingChangeWanted
+				// from the continuation heading
+				if(Math.abs(mainHeadingDelta) < minHeadingChangeWanted) {
+					int oa = 3 - ia - ca;
+					RouteArc otherArc = arcs.get(oa);
+					if(!possiblySameRoad(inArc, otherArc)) {
+						int outHeading = otherArc.getInitialHeading();
+						int outHeadingDelta = outHeading - continuingHeading;
+						while(outHeadingDelta > 180)
+							outHeadingDelta -= 360;
+						while(outHeadingDelta < -180)
+							outHeadingDelta += 360;
+						//						log.warn("Found turn ("+ outHeadingDelta + " deg) from " + inArc.getRoadDef().getName() + " to " + otherArc.getRoadDef().getName() + " at " + coord.toOSMURL());
+						final int veryShallowTurnDelta = 1;
+						if(outHeadingDelta > veryShallowTurnDelta &&
+						   outHeadingDelta < minHeadingChangeWanted) {
+							log.warn("Found shallow turn (" + outHeadingDelta + " deg) from " + inArc.getRoadDef().getName() + " to " + otherArc.getRoadDef().getName() + " at " + coord.toOSMURL());
+							int newHeading = continuingHeading + minHeadingChangeWanted;
+							if(newHeading > 180)
+								newHeading -= 360;
+							otherArc.setInitialHeading(newHeading);
+						}
+						/*
+						else if(outHeadingDelta > 0 &&
+								outHeadingDelta <= veryShallowTurnDelta) {
+							log.warn("Found very shallow turn (" + outHeadingDelta + " deg) from " + inArc.getRoadDef().getName() + " to " + otherArc.getRoadDef().getName() + " at " + coord.toOSMURL());
+							int newHeading = continuingHeading + minHeadingChangeWanted/2;
+							if(newHeading > 180)
+								newHeading -= 360;
+							otherArc.setInitialHeading(newHeading);
+						}
+						*/
+						else if(outHeadingDelta < -veryShallowTurnDelta &&
+								outHeadingDelta > -minHeadingChangeWanted) {
+							log.warn("Found shallow turn (" + outHeadingDelta + " deg) from " + inArc.getRoadDef().getName() + " to " + otherArc.getRoadDef().getName() + " at " + coord.toOSMURL());
+							int newHeading = continuingHeading - minHeadingChangeWanted;
+							if(newHeading < -180)
+								newHeading += 360;
+							otherArc.setInitialHeading(newHeading);
+						}
+						/*
+						else if(outHeadingDelta < 0 &&
+								outHeadingDelta >= -veryShallowTurnDelta) {
+							log.warn("Found very shallow turn (" + outHeadingDelta + " deg) from " + inArc.getRoadDef().getName() + " to " + otherArc.getRoadDef().getName() + " at " + coord.toOSMURL());
+							int newHeading = continuingHeading - minHeadingChangeWanted/2;
+							if(newHeading < -180)
+								newHeading += 360;
+							otherArc.setInitialHeading(newHeading);
+						}
+						*/
+					}
+				}
+			}
+		}
+		/*
+		else {
+			log.warn("Node has " + arcs.size() + " arcs at " + coord.toOSMURL());
+			for(RouteArc a : arcs)
+				log.warn("  arc " + a.getRoadDef().getName());
+		}
+		*/
+	}
 }
diff --git a/src/uk/me/parabola/mkgmap/general/RoadNetwork.java b/src/uk/me/parabola/mkgmap/general/RoadNetwork.java
index 8506381..d885612 100644
--- a/src/uk/me/parabola/mkgmap/general/RoadNetwork.java
+++ b/src/uk/me/parabola/mkgmap/general/RoadNetwork.java
@@ -112,16 +112,39 @@ public class RoadNetwork {
 
 				// Create forward arc from node1 to node2
 				Coord bearing = coordList.get(lastIndex + 1);
-				RouteArc arc = new RouteArc(road.getRoadDef(), node1, node2, bearing, arcLength);
+				if(lastCoord.equals(bearing)) {
+					// bearing point is too close to last node to be
+					// useful - try some more points
+					for(int bi = lastIndex + 2; bi <= index; ++bi) {
+						if(!lastCoord.equals(coordList.get(bi))) {
+							bearing = coordList.get(bi);
+							break;
+						}
+					}
+				}
+				RouteArc arc = new RouteArc(road.getRoadDef(),
+											node1,
+											node2,
+											(int)lastCoord.bearingTo(bearing),
+											arcLength);
 				arc.setForward();
-				node1.addArc(arc);
 
 				// Create the reverse arc
 				bearing = coordList.get(index - 1);
+				if(co.equals(bearing)) {
+					// bearing point is too close to this node to be
+					// useful - try some more points
+					for(int bi = index - 2; bi > lastIndex; --bi) {
+						if(!co.equals(coordList.get(bi))) {
+							bearing = coordList.get(bi);
+							break;
+						}
+					}
+				}
 				RouteArc arc2 = new RouteArc(road.getRoadDef(),
-							node2, node1,
-							bearing, arcLength);
-				node2.addArc(arc2);
+											 node2, node1,
+											 (int)co.bearingTo(bearing),
+											 arcLength);
 			} else {
 				// This is the first node in the road
 				road.getRoadDef().setNode(getNode(id, co));
@@ -162,8 +185,10 @@ public class RoadNetwork {
 
 		NOD1Part nod1 = new NOD1Part();
 
-		for (RouteNode node : nodes.values())
+		for (RouteNode node : nodes.values()) {
+			node.tweakArcs();
 			nod1.addNode(node);
+		}
 		centers = nod1.subdivide();
 	}
 
diff --git a/src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java b/src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java
index 2e124ea..f05829d 100644
--- a/src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java
+++ b/src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java
@@ -28,6 +28,7 @@ import uk.me.parabola.imgfmt.app.Area;
 import uk.me.parabola.imgfmt.app.Coord;
 import uk.me.parabola.imgfmt.app.CoordNode;
 import uk.me.parabola.imgfmt.app.Exit;
+import uk.me.parabola.imgfmt.app.net.NODHeader;
 import uk.me.parabola.imgfmt.app.trergn.ExtTypeAttributes;
 import uk.me.parabola.log.Logger;
 import uk.me.parabola.mkgmap.general.AreaClipper;
@@ -98,6 +99,9 @@ public class StyledConverter implements OsmConverter {
 	private final Rule relationRules;
 
 	private boolean ignoreMaxspeeds;
+	private boolean driveOnLeft;
+	private boolean driveOnRight;
+	private boolean checkRoundabouts;
 
 	class AccessMapping {
 		private final String type;
@@ -139,6 +143,10 @@ public class StyledConverter implements OsmConverter {
 		nodeRules = style.getNodeRules();
 		relationRules = style.getRelationRules();
 		ignoreMaxspeeds = props.getProperty("ignore-maxspeeds") != null;
+		driveOnLeft = props.getProperty("drive-on-left") != null;
+		NODHeader.setDriveOnLeft(driveOnLeft);
+		driveOnRight = props.getProperty("drive-on-right") != null;
+		checkRoundabouts = props.getProperty("check-roundabouts") != null;
 
 		LineAdder overlayAdder = style.getOverlays(lineAdder);
 		if (overlayAdder != null)
@@ -523,6 +531,69 @@ public class StyledConverter implements OsmConverter {
 		}
 
 		if("roundabout".equals(way.getTag("junction"))) {
+			List<Coord> points = way.getPoints();
+			// if roundabout checking is enabled and roundabout has at
+			// least 3 points and it has not been marked as "don't
+			// check", check its direction
+			if(checkRoundabouts &&
+			   way.getPoints().size() > 3 &&
+			   !way.isBoolTag("mkgmap:no-dir-check")) {
+				Coord centre = way.getCofG();
+				int dir = 0;
+				// check every third segment
+				for(int i = 0; (i + 1) < points.size(); i += 3) {
+					Coord pi = points.get(i);
+					Coord pi1 = points.get(i + 1);
+					// don't check segments that are very short
+					if(pi.quickDistance(centre) > 2.5 &&
+					   pi.quickDistance(pi1) > 2.5) {
+						// determine bearing from segment that starts with
+						// point i to centre of roundabout
+						double a = pi.bearingTo(pi1);
+						double b = pi.bearingTo(centre) - a;
+						while(b > 180)
+							b -= 360;
+						while(b < -180)
+							b += 360;
+						// if bearing to centre is between 15 and 165
+						// degrees consider it trustworthy
+						if(b >= 15 && b < 165)
+							++dir;
+						else if(b <= -15 && b > -165)
+							--dir;
+					}
+				}
+				if(dir != 0) {
+					boolean clockwise = dir > 0;
+					if(points.get(0) == points.get(points.size() - 1)) {
+						// roundabout is a loop
+						if(!driveOnLeft && !driveOnRight) {
+							if(clockwise) {
+								log.info("Roundabout " + way.getId() + " is clockwise so assuming vehicles should drive on left side of road (" + centre.toOSMURL() + ")");
+								driveOnLeft = true;
+								NODHeader.setDriveOnLeft(true);
+							}
+							else {
+								log.info("Roundabout " + way.getId() + " is anti-clockwise so assuming vehicles should drive on right side of road (" + centre.toOSMURL() + ")");
+								driveOnRight = true;
+							}
+						}
+						if(driveOnLeft && !clockwise ||
+						   driveOnRight && clockwise) {
+							log.warn("Roundabout " + way.getId() + " direction is wrong - reversing it (see " + centre.toOSMURL() + ")");
+							way.reverse();
+						}
+					}
+					else if(driveOnLeft && !clockwise ||
+							driveOnRight && clockwise) {
+						// roundabout is a line
+						log.warn("Roundabout segment " + way.getId() + " direction looks wrong (see " + points.get(0).toOSMURL() + ")");
+					}
+				}
+				else
+					log.info("Roundabout segment " + way.getId() + " direction unknown (see " + points.get(0).toOSMURL() + ")");
+			}
+
 			String frigFactorTag = way.getTag("mkgmap:frig_roundabout");
 			if(frigFactorTag != null) {
 				// do special roundabout frigging to make gps
diff --git a/src/uk/me/parabola/mkgmap/reader/osm/Way.java b/src/uk/me/parabola/mkgmap/reader/osm/Way.java
index 1f2242b..1c152ef 100644
--- a/src/uk/me/parabola/mkgmap/reader/osm/Way.java
+++ b/src/uk/me/parabola/mkgmap/reader/osm/Way.java
@@ -101,4 +101,18 @@ public class Way extends Element {
 		sb.append(toTagString());
 		return sb.toString();
 	}
+
+	public Coord getCofG() {
+		int lat = 0;
+		int lon = 0;
+		int numPoints = points.size();
+		if(numPoints < 1)
+			return null;
+		for(Coord p : points) {
+			lat += p.getLatitude();
+			lon += p.getLongitude();
+		}
+		return new Coord((lat + numPoints / 2) / numPoints,
+						 (lon + numPoints / 2) / numPoints);
+	}
 }
_______________________________________________
mkgmap-dev mailing list
[email protected]
http://www.mkgmap.org.uk/mailman/listinfo/mkgmap-dev

Reply via email to