/* * Copyright (C) 2008 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License 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. * * Create date: 07-Jul-2008 */ package uk.me.parabola.imgfmt.app.net; import it.unimi.dsi.fastutil.ints.IntArrayList; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; 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.log.Logger; /** * A routing node with its connections to other nodes via roads. * * @author Steve Ratcliffe */ public class RouteNode implements Comparable<RouteNode> { private static final Logger log = Logger.getLogger(RouteNode.class); /* * 1. instantiate * 2. setCoord, addArc * arcs, coords set * 3. write * node offsets set in all nodes * 4. writeSecond */ // Values for the first flag byte at offset 1 private static final int MAX_DEST_CLASS_MASK = 0x07; private static final int F_BOUNDARY = 0x08; private static final int F_RESTRICTIONS = 0x10; private static final int F_LARGE_OFFSETS = 0x20; private static final int F_ARCS = 0x40; // only used internally in mkgmap private static final int F_DISCARDED = 0x100; // node has been discarded private int offsetNod1 = -1; // arcs from this node private final List<RouteArc> arcs = new ArrayList<RouteArc>(4); // restrictions at (via) this node private final List<RouteRestriction> restrictions = new ArrayList<RouteRestriction>(); private int flags; private final CoordNode coord; private char latOff; private char lonOff; private List<RouteArc[]> throughRoutes; // contains the maximum of roads this node is on, written with the flags // field. It is also used for the calculation of the destination class on // arcs. private byte nodeClass; private byte nodeGroup = -1; public RouteNode(Coord coord) { this.coord = (CoordNode) coord; setBoundary(this.coord.getOnBoundary()); } private boolean haveLargeOffsets() { return (flags & F_LARGE_OFFSETS) != 0; } protected void setBoundary(boolean b) { if (b) flags |= F_BOUNDARY; else flags &= (~F_BOUNDARY) & 0xff; } public boolean isBoundary() { return (flags & F_BOUNDARY) != 0; } public void addArc(RouteArc arc) { arcs.add(arc); byte cl = (byte) arc.getRoadDef().getRoadClass(); if(log.isDebugEnabled()) log.debug("adding arc", arc.getRoadDef(), cl); if (cl > nodeClass) nodeClass = cl; flags |= F_ARCS; } public void addRestriction(RouteRestriction restr) { restrictions.add(restr); flags |= F_RESTRICTIONS; } /** * get all direct arcs to the given node and the given way id * @param otherNode * @param roadId * @return */ public List<RouteArc> getDirectArcsTo(RouteNode otherNode, long roadId) { List<RouteArc> result = new ArrayList<>(); for(RouteArc a : arcs){ if(a.isDirect() && a.getDest() == otherNode){ if(a.getRoadDef().getId() == roadId) result.add(a); } } return result; } /** * get all direct arcs on a given way id * @param roadId * @return */ public List<RouteArc> getDirectArcsOnWay(long roadId) { List<RouteArc> result = new ArrayList<>(); for(RouteArc a : arcs){ if(a.isDirect()){ if(a.getRoadDef().getId() == roadId) result.add(a); } } return result; } /** * Find arc to given node on given road. * @param otherNode * @param roadDef * @return */ public RouteArc getDirectArcTo(RouteNode otherNode, RoadDef roadDef) { for(RouteArc a : arcs){ if(a.isDirect() && a.getDest() == otherNode){ if(a.getRoadDef()== roadDef) return a; } } return null; } /** * Provide an upper bound to the size (in bytes) that * writing this node will take. * * Should be called only after arcs and restrictions * have been set. The size of arcs depends on whether * or not they are internal to the RoutingCenter. */ public int boundSize() { return 1 // table pointer + 1 // flags + 4 // assume large offsets required + arcsSize() + restrSize(); } private int arcsSize() { int s = 0; for (RouteArc arc : arcs) { s += arc.boundSize(); } return s; } private int restrSize() { return 2*restrictions.size(); } /** * Writes a nod1 entry. */ public void write(ImgFileWriter writer) { if(log.isDebugEnabled()) log.debug("writing node, first pass, nod1", coord.getId()); offsetNod1 = writer.position(); assert offsetNod1 < 0x1000000 : "node offset doesn't fit in 3 bytes"; assert (flags & F_DISCARDED) == 0 : "attempt to write discarded node"; writer.put((byte) 0); // will be overwritten later flags |= (nodeClass & MAX_DEST_CLASS_MASK); // max. road class of any outgoing road writer.put((byte) flags); if (haveLargeOffsets()) { writer.putInt((latOff << 16) | (lonOff & 0xffff)); } else { writer.put3((latOff << 12) | (lonOff & 0xfff)); } if (!arcs.isEmpty()) { boolean useCompactDirs = true; IntArrayList initialHeadings = new IntArrayList(arcs.size()+1); RouteArc lastArc = null; for (RouteArc arc: arcs){ if (lastArc == null || lastArc.getIndexA() != arc.getIndexA() || lastArc.isForward() != arc.isForward()){ int dir = RouteArc.directionFromDegrees(arc.getInitialHeading()); dir = dir & 0xf0; if (initialHeadings.contains(dir)){ useCompactDirs = false; break; } initialHeadings.add(dir); } else { // } lastArc = arc; } initialHeadings.add(0); // add dummy 0 so that we don't have to check for existence arcs.get(arcs.size() - 1).setLast(); lastArc = null; int index = 0; for (RouteArc arc: arcs){ Byte compactedDir = null; if (useCompactDirs){ if (lastArc == null || lastArc.getIndexA() != arc.getIndexA() || lastArc.isForward() != arc.isForward()){ if (index % 2 == 0) compactedDir = (byte) ((initialHeadings.get(index) >> 4) | initialHeadings.getInt(index+1)); index++; } } arc.write(writer, lastArc, useCompactDirs, compactedDir); lastArc = arc; } } if (!restrictions.isEmpty()) { restrictions.get(restrictions.size() - 1).setLast(); for (RouteRestriction restr : restrictions) restr.writeOffset(writer); } } /** * Writes a nod3 /nod4 entry. */ public void writeNod3OrNod4(ImgFileWriter writer) { assert isBoundary() : "trying to write nod3 for non-boundary node"; writer.put3(coord.getLongitude()); writer.put3(coord.getLatitude()); writer.put3(offsetNod1); } public void discard() { // mark the node as having been discarded flags |= F_DISCARDED; } public int getOffsetNod1() { if((flags & F_DISCARDED) != 0) { // return something so that the program can continue return 0; } assert offsetNod1 != -1: "failed for node " + coord.getId() + " at " + coord.toDegreeString(); return offsetNod1; } public void setOffsets(Coord centralPoint) { if(log.isDebugEnabled()) log.debug("center", centralPoint, ", coord", coord.toDegreeString()); setLatOff(coord.getLatitude() - centralPoint.getLatitude()); setLonOff(coord.getLongitude() - centralPoint.getLongitude()); } public Coord getCoord() { return coord; } private void checkOffSize(int off) { if (off > 0x7ff || off < -0x800) // does off fit in signed 12 bit quantity? flags |= F_LARGE_OFFSETS; // does off fit in signed 16 bit quantity? assert (off <= 0x7fff && off >= -0x8000); } private void setLatOff(int latOff) { if(log.isDebugEnabled()) log.debug("lat off", Integer.toHexString(latOff)); this.latOff = (char) latOff; checkOffSize(latOff); } private void setLonOff(int lonOff) { if(log.isDebugEnabled()) log.debug("long off", Integer.toHexString(lonOff)); this.lonOff = (char) lonOff; checkOffSize(lonOff); } /** * Second pass over the nodes. Fill in pointers and Table A indices. */ public void writeSecond(ImgFileWriter writer) { for (RouteArc arc : arcs) arc.writeSecond(writer); } /** * Return the node's class, which is the maximum of * classes of the roads it's on. */ public int getNodeClass() { return nodeClass; } public Iterable<? extends RouteArc> arcsIteration() { return new Iterable<RouteArc>() { public Iterator<RouteArc> iterator() { return arcs.iterator(); } }; } public List<RouteRestriction> getRestrictions() { return restrictions; } public String toString() { return String.valueOf(coord.getId()); } /* * For sorting node entries in NOD 3. */ public int compareTo(RouteNode otherNode) { return coord.compareTo(otherNode.getCoord()); } public void checkRoundabouts() { List<RouteArc> roundaboutArcs = new ArrayList<RouteArc>(); int countNonRoundaboutRoads = 0; int countNonRoundaboutOtherHighways = 0; RouteArc roundaboutArc = null; for(RouteArc a : arcs) { // ignore ways that have been synthesised by mkgmap RoadDef r = a.getRoadDef(); if (!r.isSynthesised() && a.isDirect()){ if(r.isRoundabout()) { roundaboutArcs.add(a); if (roundaboutArc == null) roundaboutArc = a; } else { // ignore footpaths and ways with no access byte access = r.getAccess(); if ((access & AccessTagsAndBits.CAR) != 0) countNonRoundaboutRoads++; else if ((access & (AccessTagsAndBits.BIKE | AccessTagsAndBits.BUS | AccessTagsAndBits.TAXI | AccessTagsAndBits.TRUCK)) != 0) countNonRoundaboutOtherHighways++; } } } if(arcs.size() > 1 && roundaboutArcs.size() == 1) log.warn("Roundabout",roundaboutArc.getRoadDef(),roundaboutArc.isForward() ? "starts at" : "ends at", coord.toOSMURL()); if (roundaboutArcs.size() > 0) { if (countNonRoundaboutRoads > 1) log.warn("Roundabout",roundaboutArc.getRoadDef(),"is connected to more than one road at",coord.toOSMURL()); else if ((countNonRoundaboutRoads == 1) && (countNonRoundaboutOtherHighways > 0)) log.warn("Roundabout",roundaboutArc.getRoadDef(),"is connected to a road and",countNonRoundaboutOtherHighways,"other highways at",coord.toOSMURL()); } if(roundaboutArcs.size() > 2) { for(RouteArc fa : arcs) { if(fa.isForward() && fa.isDirect()) { RoadDef rd = fa.getRoadDef(); for(RouteArc fb : arcs) { if(fb != fa && fb.isDirect() && fa.getPointsHash() == fb.getPointsHash() && ((fb.isForward() && fb.getDest() == fa.getDest()) || (!fb.isForward() && fb.getSource() == fa.getDest()))) { if(!rd.messagePreviouslyIssued("roundabout forks/overlaps")) { log.warn("Roundabout " + rd + " overlaps " + fb.getRoadDef() + " at " + coord.toOSMURL()); } } else if(fa != fb && fb.isForward()) { if(!rd.messagePreviouslyIssued("roundabout forks/overlaps")) { log.warn("Roundabout " + rd + " forks at " + coord.toOSMURL()); } } } } } } } // determine "distance" between two nodes on a roundabout private static int roundaboutSegmentLength(final RouteNode n1, final RouteNode n2) { List<RouteNode> seen = new ArrayList<RouteNode>(); int len = 0; RouteNode n = n1; boolean checkMoreLinks = true; while(checkMoreLinks && !seen.contains(n)) { checkMoreLinks = false; seen.add(n); for(RouteArc a : n.arcs) { if(a.isForward() && a.getRoadDef().isRoundabout() && !a.getRoadDef().isSynthesised()) { len += a.getLength(); n = a.getDest(); if(n == n2) return len; checkMoreLinks = true; break; } } } // didn't find n2 return Integer.MAX_VALUE; } // sanity check roundabout flare roads - the flare roads connect a // two-way road to a roundabout using short one-way segments so // the resulting sub-junction looks like a triangle with two // corners of the triangle being attached to the roundabout and // the last corner being connected to the two-way road public void checkRoundaboutFlares(int maxFlareLengthRatio) { for(RouteArc r : arcs) { // see if node has a forward arc that is part of a // roundabout if(!r.isForward() || !r.isDirect() || !r.getRoadDef().isRoundabout() || r.getRoadDef().isSynthesised()) continue; // follow the arc to find the first node that connects the // roundabout to a non-roundabout segment RouteNode nb = r.getDest(); List<RouteNode> seen = new ArrayList<RouteNode>(); seen.add(this); while (true) { if (seen.contains(nb)) { // looped - give up nb = null; break; } // remember we have seen this node seen.add(nb); boolean connectsToNonRoundaboutSegment = false; RouteArc nextRoundaboutArc = null; for (RouteArc nba : nb.arcs) { if (nba.isDirect() == false) continue; if (!nba.getRoadDef().isSynthesised()) { if (nba.getRoadDef().isRoundabout()) { if (nba.isForward()) nextRoundaboutArc = nba; } else connectsToNonRoundaboutSegment = true; } } if (connectsToNonRoundaboutSegment) { // great, that's what we're looking for break; } if (nextRoundaboutArc == null) { // not so good, the roundabout stops in mid air? nb = null; break; } nb = nextRoundaboutArc.getDest(); } if(nb == null) { // something is not right so give up continue; } // now try and find the two arcs that make up the // triangular "flare" connected to both ends of the // roundabout segment for(RouteArc fa : arcs) { if(!fa.isDirect() || !fa.getRoadDef().doFlareCheck()) continue; for(RouteArc fb : nb.arcs) { if(!fb.isDirect() || !fb.getRoadDef().doFlareCheck()) continue; if(fa.getDest() == fb.getDest()) { // found the 3rd point of the triangle that // should be connecting the two flare roads // first, special test required to cope with // roundabouts that have a single flare and no // other connections - only check the flare // for the shorter of the two roundabout // segments if(roundaboutSegmentLength(this, nb) >= roundaboutSegmentLength(nb, this)) continue; if(maxFlareLengthRatio > 0) { // if both of the flare roads are much // longer than the length of the // roundabout segment, they are probably // not flare roads at all but just two // roads that meet up - so ignore them final int maxFlareLength = roundaboutSegmentLength(this, nb) * maxFlareLengthRatio; if(maxFlareLength > 0 && fa.getLength() > maxFlareLength && fb.getLength() > maxFlareLength) { continue; } } // now check the flare roads for direction and // oneway // only issue one warning per flare if(!fa.isForward()) log.warn("Outgoing roundabout flare road " + fa.getRoadDef() + " points in wrong direction? " + fa.getSource().coord.toOSMURL()); else if(fb.isForward()) log.warn("Incoming roundabout flare road " + fb.getRoadDef() + " points in wrong direction? " + fb.getSource().coord.toOSMURL()); else if(!fa.getRoadDef().isOneway()) log.warn("Outgoing roundabout flare road " + fa.getRoadDef() + " is not oneway? " + fa.getSource().coord.toOSMURL()); else if(!fb.getRoadDef().isOneway()) log.warn("Incoming roundabout flare road " + fb.getRoadDef() + " is not oneway? " + fb.getDest().coord.toOSMURL()); else { // check that the flare road arcs are not // part of a longer way for(RouteArc a : fa.getDest().arcs) { if(a.isDirect() && a.getDest() != this && a.getDest() != nb) { if(a.getRoadDef() == fa.getRoadDef()) log.warn("Outgoing roundabout flare road " + fb.getRoadDef() + " does not finish at flare? " + fa.getDest().coord.toOSMURL()); else if(a.getRoadDef() == fb.getRoadDef()) log.warn("Incoming roundabout flare road " + fb.getRoadDef() + " does not start at flare? " + fb.getDest().coord.toOSMURL()); } } } } } } } } public void reportSimilarArcs() { for(int i = 0; i < arcs.size(); ++i) { RouteArc arci = arcs.get(i); if (arci.isDirect() == false) continue; for(int j = i + 1; j < arcs.size(); ++j) { RouteArc arcj = arcs.get(j); if (arcj.isDirect() == false) continue; if(arci.getDest() == arcj.getDest() && arci.getLength() == arcj.getLength() && arci.getPointsHash() == arcj.getPointsHash()) { log.warn("Similar arcs (" + arci.getRoadDef() + " and " + arcj.getRoadDef() + ") from " + coord.toOSMURL()); } } } } public void addThroughRoute(long roadIdA, long roadIdB) { if(throughRoutes == null) throughRoutes = new ArrayList<RouteArc[]>(); boolean success = false; for(RouteArc arc1 : arcs) { if(arc1.getRoadDef().getId() == roadIdA) { for(RouteArc arc2 : arcs) { if(arc2.getRoadDef().getId() == roadIdB) { throughRoutes.add(new RouteArc[] { arc1.getReverseArc(), arc2 }); success = true; break; } } } else if(arc1.getRoadDef().getId() == roadIdB) { for(RouteArc arc2 : arcs) { if(arc2.getRoadDef().getId() == roadIdA) { throughRoutes.add(new RouteArc[] { arc1.getReverseArc(), arc2 }); success = true; break; } } } } /* for(RouteArc arc1 : incomingArcs) { if(arc1.getRoadDef().getId() == roadIdA) { for(RouteArc arc2 : arcs) { if(arc2.getRoadDef().getId() == roadIdB) { throughRoutes.add(new RouteArc[] { arc1, arc2 }); success = true; break; } } } else if(arc1.getRoadDef().getId() == roadIdB) { for(RouteArc arc2 : arcs) { if(arc2.getRoadDef().getId() == roadIdA) { throughRoutes.add(new RouteArc[] { arc1, arc2 }); success = true; break; } } } } */ if(success) log.info("Added through route between ways " + roadIdA + " and " + roadIdB + " at " + coord.toOSMURL()); else log.warn("Failed to add through route between ways " + roadIdA + " and " + roadIdB + " at " + coord.toOSMURL() + " - perhaps they don't meet here?"); } /** * For each arc on the road, check if we can add indirect arcs to * other nodes of the same road. This is done if the other node * lies on a different road with a higher road class than the * highest other road of the target node of the arc. We do this * for both forward and reverse arcs. Multiple indirect arcs * may be added for each Node. An indirect arc will * always point to a higher road than the previous arc. * The length and direct bearing of the additional arc is measured * from the target node of the preceding arc to the new target node. * The initial bearing doesn't really matter as it is not written * for indirect arcs. * @param road * @param maxRoadClass */ public void addArcsToMajorRoads(RoadDef road){ assert road.getNode() == this; RouteNode current = this; // the nodes of this road List<RouteNode> nodes = new ArrayList<>(); // the forward arcs of this road List<RouteArc> forwardArcs = new ArrayList<>(); // will contain the highest other road of each node IntArrayList forwardArcPositions = new IntArrayList(); List<RouteArc> reverseArcs = new ArrayList<>(); IntArrayList reverseArcPositions = new IntArrayList(); // collect the nodes of the road and remember the arcs between them nodes.add(current); while (current != null){ RouteNode next = null; for (int i = 0; i < current.arcs.size(); i++){ RouteArc arc = current.arcs.get(i); if (arc.getRoadDef() == road){ if (arc.isDirect()){ if (arc.isForward()){ next = arc.getDest(); nodes.add(next); forwardArcs.add(arc); forwardArcPositions.add(i); } else { reverseArcPositions.add(i); reverseArcs.add(arc); } } } } current = next; } if (nodes.size() < 3) return; // System.out.println(road + " " + nodes.size() + " " + forwardArcs.size()); ArrayList<RouteArc> newArcs = new ArrayList<>(); IntArrayList arcPositions = forwardArcPositions; List<RouteArc> roadArcs = forwardArcs; for (int dir = 0; dir < 2; dir++){ // forward arcs first for (int i = 0; i + 2 < nodes.size(); i++){ RouteNode sourceNode = nodes.get(i); // original source node of direct arc RouteNode stepNode = nodes.get(i+1); RouteArc arcToStepNode = roadArcs.get(i); assert arcToStepNode.getDest() == stepNode; int currentClass = arcToStepNode.getArcDestClass(); int finalClass = road.getRoadClass(); if (finalClass <= currentClass) continue; newArcs.clear(); double partialArcLength = 0; double pathLength = arcToStepNode.getLengthInMeter(); for (int j = i+2; j < nodes.size(); j++){ RouteArc arcToDest = roadArcs.get(j-1); partialArcLength += arcToDest.getLengthInMeter(); pathLength += arcToDest.getLengthInMeter(); int cl = nodes.get(j).getGroup(); if (cl > currentClass){ if (cl > finalClass) cl = finalClass; currentClass = cl; // create indirect arc from node i+1 to node j RouteNode destNode = nodes.get(j); Coord c1 = sourceNode.getCoord(); Coord c2 = destNode.getCoord(); RouteArc newArc = new RouteArc(road, sourceNode, destNode, roadArcs.get(i).getInitialHeading(), // not used c1.bearingTo(c2), partialArcLength, // from stepNode to destNode on road pathLength, // from sourceNode to destNode on road c1.distance(c2), c1.hashCode() + c2.hashCode()); if (arcToStepNode.isDirect()) arcToStepNode.setMaxDestClass(0); else newArc.setMaxDestClass(cl); if (dir == 0) newArc.setForward(); newArc.setIndirect(); newArcs.add(newArc); arcToStepNode = newArc; stepNode = destNode; partialArcLength = 0; if (cl >= finalClass) break; } } if (newArcs.isEmpty() == false){ int directArcPos = arcPositions.getInt(i); assert nodes.get(i).arcs.get(directArcPos).isDirect(); assert nodes.get(i).arcs.get(directArcPos).getRoadDef() == newArcs.get(0).getRoadDef(); assert nodes.get(i).arcs.get(directArcPos).isForward() == newArcs.get(0).isForward(); nodes.get(i).arcs.addAll(directArcPos + 1, newArcs); if (dir == 0 && i > 0){ // check if the inserted arcs change the position of the direct reverse arc int reverseArcPos = reverseArcPositions.get(i-1); // i-1 because first node doesn't have reverse arc if (directArcPos < reverseArcPos) reverseArcPositions.set(i - 1, reverseArcPos + newArcs.size()); } } } if (dir > 0) break; // reverse the arrays for the other direction Collections.reverse(reverseArcs); Collections.reverse(reverseArcPositions); Collections.reverse(nodes); arcPositions = reverseArcPositions; roadArcs = reverseArcs; } } /** * Find the class group of the node. Rules: * 1. Find the highest class which is used more than once. * 2. Otherwise: use the class if the only one, or else the n-1 class. * (eg: if [1,] then use 1, if [1,2,] then use 1, if [1,2,3,] then use 2. * * @return the class group */ public int getGroup() { if (nodeGroup < 0){ HashSet<RoadDef> roads = new HashSet<>(); for (RouteArc arc: arcs){ roads.add(arc.getRoadDef()); } int[] classes = new int[5]; int numClasses = 0; // find highest class that is used more than once for (RoadDef road: roads){ int cl = road.getRoadClass(); int n = ++classes[cl]; if (n == 1) numClasses++; else if (n > 1 && cl > nodeGroup) nodeGroup = (byte) cl; } if (nodeGroup >= 0) return nodeGroup; if (numClasses == 1) nodeGroup = nodeClass; // only one class else { // find n-1 class int n = 0; for (int cl = 4; cl >= 0; cl--){ if (classes[cl] > 0){ if (n == 1){ nodeGroup = (byte) cl; break; } n++; } } } } return nodeGroup; } public List<RouteArc> getArcs() { return arcs; } public int hashCode(){ return getCoord().getId(); } public List<RouteArc> getDirectArcsBetween(RouteNode otherNode) { List<RouteArc> result = new ArrayList<>(); for(RouteArc a : arcs){ if(a.isDirect() && a.getDest() == otherNode){ result.add(a); } } return result; } }