v3 - now based on r1265. Added --adjust-turn-headings option to enable
this functionality. Reports now downgraded to info (from warn).

------------------------

v2 - various changes - should now work with junctions that have more
than 1 side road - should be able to treat exits from roundabouts in
the same way as side roads.

-------------------------

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 bf7a587..e31a747 100644
--- a/resources/help/en/options
+++ b/resources/help/en/options
@@ -211,6 +211,13 @@ Miscellaneous options:
 	than that length will be removed. If a length is not
 	specified, only zero-length arcs will be removed.
 
+--adjust-turn-headings
+	Where possible, ensure that turns off to side roads change
+	heading sufficiently so that the GPS believes that a turn is
+	required rather than a fork. This also avoids spurious
+	instructions to "keep right/left" when the road doesn't
+	actually fork.
+
 --road-name-pois[=GarminCode]
 	Generate a POI for each named road. By default, the POIs'
 	Garmin type code is 0x640a. If desired, a different type code
diff --git a/src/uk/me/parabola/imgfmt/app/net/RoadDef.java b/src/uk/me/parabola/imgfmt/app/net/RoadDef.java
index c50d591..0b431cf 100644
--- a/src/uk/me/parabola/imgfmt/app/net/RoadDef.java
+++ b/src/uk/me/parabola/imgfmt/app/net/RoadDef.java
@@ -69,7 +69,7 @@ public class RoadDef implements Comparable {
 
 	// for diagnostic purposes
 	public String toString() {
-		return "RoadDef(" + name + ", " + id + ")";
+		return "(" + name + ", " + id + ")";
 	}
 
 	public String getName() {
@@ -105,6 +105,8 @@ public class RoadDef implements Comparable {
 
 	private City city;
 	private Zip zip;
+	private boolean roundabout;
+	private boolean linkRoad;
 
 	/**
 	 * This is for writing to NET1.
@@ -537,6 +539,10 @@ public class RoadDef implements Comparable {
 		netFlags |= NET_FLAG_ONEWAY;
 	}
 
+	public boolean isOneway() {
+		return (netFlags & NET_FLAG_ONEWAY) != 0;
+	}
+
 	public void setCity(City city) {
 		this.city = city;
 		netFlags |= NET_FLAG_ADDRINFO;
@@ -561,4 +567,20 @@ public class RoadDef implements Comparable {
 	public City getCity() {
 		return city;
 	}
+
+	public void setRoundabout(boolean r) {
+		roundabout = r;
+	}
+
+	public boolean isRoundabout() {
+		return roundabout;
+	}
+
+	public void setLinkRoad(boolean lr) {
+		linkRoad = lr;
+	}
+
+	public boolean isLinkRoad() {
+		return linkRoad;
+	}
 }
diff --git a/src/uk/me/parabola/imgfmt/app/net/RouteArc.java b/src/uk/me/parabola/imgfmt/app/net/RouteArc.java
index 57f3f50..50ace24 100644
--- a/src/uk/me/parabola/imgfmt/app/net/RouteArc.java
+++ b/src/uk/me/parabola/imgfmt/app/net/RouteArc.java
@@ -43,8 +43,8 @@ public class RouteArc {
 
 	private int offset;
 
-	private final byte initialHeading;
-	private byte endDirection;
+	private int initialHeading; // degrees
+	private int finalHeading; // degrees
 
 	private final RoadDef roadDef;
 
@@ -69,10 +69,12 @@ 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) {
+	public RouteArc(RoadDef roadDef,
+					RouteNode source, RouteNode dest,
+					int initialHeading, int finalHeading,
+					double length) {
 		this.roadDef = roadDef;
 		this.source = source;
 		this.dest = dest;
@@ -80,25 +82,28 @@ 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;
+		this.finalHeading = finalHeading;
 		// too early: dest.nodeClass may still increase
 		//setDestinationClass(dest.getNodeClass());
 	}
 
+	public int getInitialHeading() {
+		return initialHeading;
+	}
+
+	public void setInitialHeading(int ih) {
+		initialHeading = ih;
+	}
+
+	public int getFinalHeading() {
+		return finalHeading;
+	}
+
+	public void setFinalHeading(int fh) {
+		finalHeading = fh;
+	}
+
 	public RouteNode getSource() {
 		return source;
 	}
@@ -167,23 +172,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());
-
-		double angle = source.getCoord().bearingTo(end);
-
-		if(log.isDebugEnabled())
-			log.debug("angle is ", angle);
-
-		byte b = (byte) (256 * angle / 360);
-		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?
@@ -222,7 +210,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();
@@ -314,13 +302,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..8086fda 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
+	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,169 @@ public class RouteNode implements Comparable<RouteNode> {
 	public int compareTo(RouteNode otherNode) {
 		return coord.compareTo(otherNode.getCoord());
 	}
+
+	private static boolean possiblySameRoad(RouteArc raa, RouteArc rab) {
+
+		if(raa == rab) {
+			// the arcs are the same object
+			return true;
+		}
+
+		RoadDef rda = raa.getRoadDef();
+		RoadDef rdb = rab.getRoadDef();
+		if(rda == rdb) {
+			// the arcs share the same RoadDef
+			return true;
+		}
+
+		boolean bothArcsNamed = false;
+		for(Label laba : rda.getLabels()) {
+			if(laba != null && laba.getOffset() != 0) {
+				for(Label labb : rdb.getLabels()) {
+					if(labb != null && labb.getOffset() != 0) {
+						bothArcsNamed = true;
+						if(laba.equals(labb)) {
+							// the roads have the same name
+							if(rda.isLinkRoad() == rdb.isLinkRoad()) {
+								// if both are a link road or both are
+								// not a link road, consider them the
+								// same road
+								return true;
+							}
+							// one's a link road and the other isn't
+							// so consider them different roads
+							return false;
+						}
+					}
+				}
+			}
+		}
+
+		if(bothArcsNamed) {
+			// both roads have names and they don't match
+			return false;
+		}
+
+		// at least one road is unnamed
+		if(rda.isRoundabout() && rdb.isRoundabout()) {
+			// hopefully, segments of the same (unnamed) roundabout
+			return true;
+		}
+
+		return false;
+	}
+
+	public void tweezeArcs() {
+		if(arcs.size() >= 3) {
+			final int maxMainRoadHeadingChange = 120;
+			final int minSideRoadHeadingChange = 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 maxMainRoadHeadingChange, ensure
+			// that the side road heading differs from the continuing
+			// heading by at least minSideRoadHeadingChange
+
+			for(RouteArc inArc : incomingArcs) {
+
+				if(!inArc.isForward() && inArc.getRoadDef().isOneway()) {
+					// don't bother with reverse arcs if the road is
+					// oneway
+					continue;
+				}
+
+				int inHeading = inArc.getFinalHeading();
+				// determine the outgoing (continuing) arc that is
+				// likely to be the same road as the incoming arc
+				RouteArc continuingArc = null;
+				for(RouteArc ca : arcs) {
+					if(ca.getDest() != inArc.getSource()) {
+						// this arc is not going to the same node as
+						// inArc came from
+						if(possiblySameRoad(inArc, ca)) {
+							continuingArc = ca;
+							break;
+						}
+					}
+				}
+				// if we did not find the continuing arc, give up with
+				// this incoming arc
+				if(continuingArc == null) {
+					//log.info("Can't continue road " + inArc.getRoadDef() + " at " + coord.toOSMURL());
+					continue;
+				}
+				int continuingHeading = continuingArc.getInitialHeading();
+				int mainHeadingDelta = continuingHeading - inHeading;
+				while(mainHeadingDelta > 180)
+					mainHeadingDelta -= 360;
+				while(mainHeadingDelta < -180)
+					mainHeadingDelta += 360;
+				//log.info(inArc.getRoadDef() + " continues to " + continuingArc.getRoadDef() + " with a heading change of " + mainHeadingDelta + " at " + coord.toOSMURL());
+
+				if(Math.abs(mainHeadingDelta) > maxMainRoadHeadingChange) {
+					// if the continuation road heading change is
+					// greater than maxMainRoadHeadingChange don't
+					// adjust anything
+					continue;
+				}
+
+				for(RouteArc otherArc : arcs) {
+
+					// for each other arc leaving this node, tweeze
+					// its heading if its heading change from the
+					// continuation heading is less than
+					// minSideRoadHeadingChange
+
+					if(otherArc.getDest() == inArc.getSource() ||
+					   otherArc == continuingArc) {
+						// we're looking at the incoming or continuing
+						// arc, ignore it
+						continue;
+					}
+
+
+					if(possiblySameRoad(inArc, otherArc) ||
+					   possiblySameRoad(continuingArc, otherArc)) {
+						// not obviously a different road so give up
+						continue;
+					}
+
+					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() + " to " + otherArc.getRoadDef() + " at " + coord.toOSMURL());
+					if(outHeadingDelta > 0 &&
+					   outHeadingDelta < minSideRoadHeadingChange) {
+						int newHeading = continuingHeading + minSideRoadHeadingChange;
+						if(newHeading > 180)
+							newHeading -= 360;
+						otherArc.setInitialHeading(newHeading);
+						log.info("Adjusting turn heading from " + outHeading + " to " + newHeading + " at junction of " + inArc.getRoadDef() + " and " + otherArc.getRoadDef() + " at " + coord.toOSMURL());
+					}
+					else if(outHeadingDelta < 0 &&
+							outHeadingDelta > -minSideRoadHeadingChange) {
+						int newHeading = continuingHeading - minSideRoadHeadingChange;
+
+						if(newHeading < -180)
+							newHeading += 360;
+						otherArc.setInitialHeading(newHeading);
+						log.info("Adjusting turn heading from " + outHeading + " to " + newHeading + " at junction of " + inArc.getRoadDef() + " and " + otherArc.getRoadDef() + " at " + coord.toOSMURL());
+					}
+				}
+			}
+		}
+	}
 }
diff --git a/src/uk/me/parabola/mkgmap/general/MapDetails.java b/src/uk/me/parabola/mkgmap/general/MapDetails.java
index 71a54fe..22b94d4 100644
--- a/src/uk/me/parabola/mkgmap/general/MapDetails.java
+++ b/src/uk/me/parabola/mkgmap/general/MapDetails.java
@@ -28,6 +28,7 @@ import uk.me.parabola.imgfmt.app.trergn.Overview;
 import uk.me.parabola.imgfmt.app.trergn.PointOverview;
 import uk.me.parabola.imgfmt.app.trergn.PolygonOverview;
 import uk.me.parabola.imgfmt.app.trergn.PolylineOverview;
+import uk.me.parabola.util.EnhancedProperties;
 
 /**
  * The map features that we are going to map are collected here.
@@ -51,6 +52,10 @@ public class MapDetails implements MapCollector, MapDataSource {
 
 	private final RoadNetwork roadNetwork = new RoadNetwork();
 
+	public void config(EnhancedProperties props) {
+		roadNetwork.config(props);
+	}
+
 	/**
 	 * Add a point to the map.
 	 *
diff --git a/src/uk/me/parabola/mkgmap/general/MapRoad.java b/src/uk/me/parabola/mkgmap/general/MapRoad.java
index 2b0a96a..720c2bf 100644
--- a/src/uk/me/parabola/mkgmap/general/MapRoad.java
+++ b/src/uk/me/parabola/mkgmap/general/MapRoad.java
@@ -108,4 +108,12 @@ public class MapRoad extends MapLine {
 	public void setRoadZip(Zip z) {
 		this.roadDef.setZip(z);
 	}
+
+	public void setRoundabout(boolean r) {
+		this.roadDef.setRoundabout(r);
+	}
+
+	public void setLinkRoad(boolean lr) {
+		this.roadDef.setLinkRoad(lr);
+	}
 }
diff --git a/src/uk/me/parabola/mkgmap/general/RoadNetwork.java b/src/uk/me/parabola/mkgmap/general/RoadNetwork.java
index 8506381..a6bfcfe 100644
--- a/src/uk/me/parabola/mkgmap/general/RoadNetwork.java
+++ b/src/uk/me/parabola/mkgmap/general/RoadNetwork.java
@@ -30,6 +30,7 @@ import uk.me.parabola.imgfmt.app.net.RouteCenter;
 import uk.me.parabola.imgfmt.app.net.RouteNode;
 import uk.me.parabola.imgfmt.app.net.RouteRestriction;
 import uk.me.parabola.log.Logger;
+import uk.me.parabola.util.EnhancedProperties;
 
 /**
  * This holds the road network.  That is all the roads and the nodes
@@ -60,6 +61,11 @@ public class RoadNetwork {
 
 	private final List<RoadDef> roadDefs = new ArrayList<RoadDef>();
 	private List<RouteCenter> centers = new ArrayList<RouteCenter>();
+	private boolean adjustTurnHeadings;
+
+	public void config(EnhancedProperties props) {
+		adjustTurnHeadings = props.getProperty("adjust-turn-headings", false);
+	}
 
 	public void addRoad(MapRoad road) {
 		//mapRoads.add(road);
@@ -110,18 +116,53 @@ public class RoadNetwork {
 					log.error("  " + co.toOSMURL());
 				}
 
+				Coord bearingPoint = coordList.get(lastIndex + 1);
+				if(lastCoord.equals(bearingPoint)) {
+					// 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))) {
+							bearingPoint = coordList.get(bi);
+							break;
+						}
+					}
+				}
+				int forwardBearing = (int)lastCoord.bearingTo(bearingPoint);
+				int inverseForwardBearing = (int)bearingPoint.bearingTo(lastCoord);
+
+				bearingPoint = coordList.get(index - 1);
+				if(co.equals(bearingPoint)) {
+					// 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))) {
+							bearingPoint = coordList.get(bi);
+							break;
+						}
+					}
+				}
+				int reverseBearing = (int)co.bearingTo(bearingPoint);
+				int inverseReverseBearing = (int)bearingPoint.bearingTo(co);
+
 				// Create forward arc from node1 to node2
-				Coord bearing = coordList.get(lastIndex + 1);
-				RouteArc arc = new RouteArc(road.getRoadDef(), node1, node2, bearing, arcLength);
+				RouteArc arc = new RouteArc(road.getRoadDef(),
+											node1,
+											node2,
+											forwardBearing,
+											inverseReverseBearing,
+											arcLength);
 				arc.setForward();
 				node1.addArc(arc);
+				node2.addIncomingArc(arc);
 
 				// Create the reverse arc
-				bearing = coordList.get(index - 1);
-				RouteArc arc2 = new RouteArc(road.getRoadDef(),
-							node2, node1,
-							bearing, arcLength);
-				node2.addArc(arc2);
+				arc = new RouteArc(road.getRoadDef(),
+								   node2, node1,
+								   reverseBearing,
+								   inverseForwardBearing,
+								   arcLength);
+				node2.addArc(arc);
+				node1.addIncomingArc(arc);
 			} else {
 				// This is the first node in the road
 				road.getRoadDef().setNode(getNode(id, co));
@@ -162,8 +203,11 @@ public class RoadNetwork {
 
 		NOD1Part nod1 = new NOD1Part();
 
-		for (RouteNode node : nodes.values())
+		for (RouteNode node : nodes.values()) {
+			if(adjustTurnHeadings)
+				node.tweezeArcs();
 			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 f05829d..0e0e4fe 100644
--- a/src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java
+++ b/src/uk/me/parabola/mkgmap/osmstyle/StyledConverter.java
@@ -975,6 +975,11 @@ public class StyledConverter implements OsmConverter {
 
 		MapRoad road = new MapRoad(way.getId(), line);
 
+		if("roundabout".equals(way.getTag("junction")))
+			road.setRoundabout(true);
+
+		road.setLinkRoad(gt.getType() == 0x08 || gt.getType() == 0x09);
+
 		// set road parameters.
 		road.setRoadClass(gt.getRoadClass());
 		if (way.isBoolTag("oneway")) {
diff --git a/src/uk/me/parabola/mkgmap/reader/MapperBasedMapDataSource.java b/src/uk/me/parabola/mkgmap/reader/MapperBasedMapDataSource.java
index a61b37b..9ae86d0 100644
--- a/src/uk/me/parabola/mkgmap/reader/MapperBasedMapDataSource.java
+++ b/src/uk/me/parabola/mkgmap/reader/MapperBasedMapDataSource.java
@@ -114,6 +114,7 @@ public abstract class MapperBasedMapDataSource implements MapDataSource, Configu
 
 	public void config(EnhancedProperties props) {
 		configProps = props;
+		mapper.config(props);
 	}
 
 	protected EnhancedProperties getConfig() {
_______________________________________________
mkgmap-dev mailing list
[email protected]
http://www.mkgmap.org.uk/mailman/listinfo/mkgmap-dev

Reply via email to