Subversion Repositories display

Rev

Rev 553 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2014 Steve Ratcliffe
 *
 * 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 test.check;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.net.NODHeader;

import test.display.check.Log;
import test.elements.Line;
import test.files.Nod2Record;
import test.files.NodeLocator;
import test.files.RoadData;
import test.files.RouteArc;
import test.files.RouteCenter;
import test.files.RouteNode;
import test.files.Segment;

import static test.files.RouteArc.directionFromDegrees;
import static test.files.RouteCenter.setHasExtraZeros;

/**
 * Check a NOD file for consistency and print out various things that might be useful in debugging.
 *
 * @author Steve Ratcliffe
 */

public class NodCheck extends CommonCheck {

    protected void print() {

                openLbl();

                openTre();

                openNet();
                openRgn();

            openNod();

        reader.position(0);

        List<RouteCenter> routeCenters = nod.readNodeCenters();

            initRoads();
            checkNod2();
            check(routeCenters);
                if (nod.hasClassSections())
                        checkBoundary4();

                System.out.printf("Distance %f\n", totMeters / totUnits);
        }

        private void checkNod2() {
                int end = ((NODHeader) nod.getHeader()).getRoadSection().getSize();

                int pos = 0;
                do {
                        Nod2Record nod2 = nod.getNod2(pos);

                        byte[] bitStream = nod2.getBitStream();
                        RoadData road = nod2.getRoadData();
                        if (road == null) {
                                if (nod2.getNode() == null)
                                        error("could not find road for nod2=%x, nor the node", pos);
                                else
                                        error("could not find road for nod2=%x node=%x", pos, nod2.getNode().getOffset());
                                pos = (int) nod2.getNext();
                                continue;
                        }

                        BitSet bset = BitSet.valueOf(bitStream);
                        int nInStream = nod2.getBitStreamLen();
                        info("Road %x: node=%x, %s", road.getOffset(), nod2.getNode().getOffset(),
                                        bitString(bset, nInStream));

                        matchRoadsNodes(road, nod2);
                        pos = (int) nod2.getNext();
                } while (pos < end && pos != 0);
        }

        private String bitString(BitSet bset, int size) {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < size; i++)
                        sb.append(bset.get(i) ? "1" : "0");

                return sb.toString();
        }

        /**
         * Match up routing nodes to points on the road.
         *
         * A road consists of one or more line segments.  Each line has a number of points.
         * Some of those points have the 'node-flag' set. Some of those flagged points are
         * routing nodes and some are only for house number ranges (and perhaps other reasons).
         *
         * The bitstream in nod 2 tells you which kind of node you have. The bit is set
         * to 1 for a routing node and zero otherwise.
         *
         * Note that the nod 2 bitstream also covers the first and last points of a road. Its best to
         * think of the first and last points in a road having the node-flag set implicitly.
         *
         * @param road The road to examine.
         * @param nod2 The NOD 2 record that the road refers to. A road can only point to one
         * NOD 2 record.
         */

        private void matchRoadsNodes(RoadData road, Nod2Record nod2) {
        BitSet bset = BitSet.valueOf(nod2.getBitStream());

                List<RouteNode> routeNodes = getRouteNodes(road, nod2.getNode());
                if (routeNodes.size() != bset.cardinality()) {
                        error("number of route nodes (%d) not equal to set bits %s", routeNodes.size(),
                                        bset.toString());

                        // At the moment, don't try to set the nodes up
                        return;
                }

                List<Segment> segments = road.getSegments();
        int nseg = segments.size();
        int rnod = 0;
        int rcount = 0;
        int npoint = 0;
                double length = 0;
                Coord lastCoord = null;

        for (int s = 0; s < nseg; s++) {
            Segment seg = segments.get(s);
            Line line = seg.getLine();
            List<Coord> coords = line.getCoords();

            int ncoord = coords.size();
            for (int c = 0; c < ncoord; c++) {
                Coord co = coords.get(c);

                    if (lastCoord != null)
                            length += lastCoord.distance(co);
                    lastCoord = co;

                    // This counts as a road node if it is the first or last in the road, or it has
                    // the node-flag set.
                    boolean last = c == ncoord - 1 && s == nseg - 1;
                    if (co.getId() > 0 || npoint == 0 || last) {
                        if (s > 0 && c == 0)
                                continue;

                    // See if this road node is also a routing node
                    if (bset.get(rnod)) {
                            RouteNode node = routeNodes.get(rcount);

                            if (!equalCoord(node.getCoord(), co))
                                    error("coords dont match n=%s, c=%s", node.getCoord(), co);

                            NodeLocator locator = new NodeLocator(node);

                            locator.setIndex(rcount);
                            locator.setSegmentNumber(s);
                            locator.setCoordNumber(c);
                                                locator.setCoordsInSegment(ncoord);
                                                locator.setLength((int) Math.round(length));

                            road.addNodeLocation(locator);
                            trace("  Node:%x %s", node.getOffset(), fmtCoord(co));
                                                node.updateDestClass(nod2.getRoadClass());
                                                node.addRoad(road);
                                                rcount++;
                    }
                    rnod++;
                }

                npoint++;
            }
        }

                if (rcount < routeNodes.size()) {
                        for (int i = rcount; i < routeNodes.size(); i++) {
                                RouteNode node = routeNodes.get(i);
                                StringBuilder sb = new StringBuilder();
                                for (Segment seg : road.getSegments()) {
                                        List<Coord> coords = seg.getLine().getCoords();
                                        sb.append(coords);
                                }
                                error("missing node %x %s %s", node.getOffset(), node.getCoord(),
                                                sb.toString());
                        }
                }
        }

        /**
         * Are coords equal to withing a margin of error.
         *
         * Often the node coords are one out compared to the corresponding coordinate. Possible
         * that we are missing something, as you might expect them to be the same.
         *
         * @param c1 First coordinate to compare.
         * @param c2 Second coordinate to compare.
         * @return True if they are equal to within one unit.
         */

        public boolean equalCoord(Coord c1, Coord c2) {
                if (Math.abs(c1.getLatitude() - c2.getLatitude()) < 2
                                && Math.abs(c1.getLongitude() - c2.getLongitude()) < 2)
                        return true;
                else
                        return false;
        }

        /**
         * Get a list of routing nodes for a road. We do this by walking along the forward arcs
         * for the road.
         *
         * We assume that the first arc in the forward direction is always a link to the next
         * node in this routine.  That is believed to be how it is meant to be.
         *
         * @param road The road to examine.
         * @param first The first (routing) node on the road as taken from nod 2.
         * @return A list of all the routing nodes found for the road. They are in order from
         * the first.
         */

        private List<RouteNode> getRouteNodes(RoadData road, RouteNode first) {
                List<RouteNode> nodes = new ArrayList<>();
                nodes.add(first);
                for (int i = 0; i < road.getNod2().getBitStreamLen(); i++) {
                        for (RouteArc arc : first.getArcs()) {
                                if (arc.isSign() && arc.getNetOffset() == road.getOffset()) {
                                        RouteNode next = nod.getNode(arc.getLink());
                                        nodes.add(next);

                                        first = next;
                                        break;
                                }
                        }
                }
                return nodes;
        }

        private void check(List<RouteCenter> routeCenters) {
        for (RouteCenter center : routeCenters) {
            info("Route center %d: table at %#x, loc(%s)", center.getIndex(), center.getOffset(),
                            fmtCoord(center.getCoord()));

            checkCenter(center);
        }
    }

    private void checkCenter(RouteCenter center) {
            // Check for repeated entries in Table A. A waste of space, rather than an error as such.
            Set<Integer> roadIds = new HashSet<>();
            for (RouteCenter.TableA aRecord : center.getTableA()) {
                    if (!roadIds.add(aRecord.getNetOffset()))
                            error("Center %x: Repeated table A entry %x", center.getOffset(), aRecord.getNetOffset());
            }

            // Check for repeated entries in Table B. A waste of space, rather than an error as such.
            Set<Integer> nodeIds = new HashSet<>();
            for (RouteCenter.TableB bRecord : center.getTableB()) {
                    if (!nodeIds.add(bRecord.getNodOffset()))
                            error("Center %x: Repeated table B node entry %x", center.getOffset(), bRecord.getNodOffset());
            }

            RouteNode last = null;
                double lastNodeSortPosition = Double.NEGATIVE_INFINITY;
            for (RouteNode node : center.nodes().values()) {
                checkNode(node, last);
                    last = node;

                        if (node.isBad())
                        continue;

                        double nodeSortPosition = node.getCoord().getLonDegrees();
                        if (nodeSortPosition < lastNodeSortPosition) {
                                error("Node %x: not sorted on longitude %f, %f", node.getOffset(),
                                                lastNodeSortPosition, nodeSortPosition);
                        }

                        lastNodeSortPosition = nodeSortPosition;
                        if (node.hasArcs()) {
                checkArcs(node);
                roadDiagrams(node);
                        }

                        int rc = predictGroupClass(node);

                        if (rc == node.getNodeClass())
                                trace("      Node:%x: node class %d, correct %d %s", node.getOffset(), node.getNodeClass(), rc, node.getClasses());
                        else
                                trace("      Node:%x: node class %d, predicted %d %s", node.getOffset(), node.getNodeClass(), rc, node.getClasses());

                        if (node.hasRestrictions())
                                checkRestrictions(node);
                }

                List<RouteCenter.Restriction> restrictions = center.getRestrictions();
                info("Restrictions for center %x", center.getOffset());
                for (RouteCenter.Restriction restr : restrictions) {
                        boolean valid = true;
                        List<RouteNode> nodes = new ArrayList<>();
                        for (int noff : restr.getNodeOffsets()) {
                                RouteNode node = nod.getNode(noff);
                                if (node == null) {
                                        valid = false;
                                        error("Center:%x:Restriction:%x: invalid node %x", center.getOffset(), restr.getOffset(), noff);
                                } else {
                                        trace("      Restriction:%x: node %x", restr.getOffset(), noff);
                                }

                                nodes.add(node);
                        }

                        List<Integer> roadOffsets = restr.getRoadOffsets();
                        for (int n = 0; n < roadOffsets.size(); n++) {
                                int roff = roadOffsets.get(n);
                                RoadData road = net.getRoad(roff);
                                if (road == null) {
                                        error("Restriction invalid road %x", roff);
                                        valid = false;
                                } else {
                                        trace("      Restriction:%x: road %x", restr.getOffset(), roff);
                                }

                                // If all nodes were not valid, no point checking further
                                if (!valid)
                                        continue;

                                RouteNode n1 = nodes.get(n);
                                NodeLocator nl1 = road.locateNode(n1);
                                RouteNode n2 = nodes.get(n + 1);
                                NodeLocator nl2 = road.locateNode(n2);
                                if (nl1 == null)
                                        error("Restriction: node %x not on road %x", n1.getOffset(), roff);

                                if (nl2 == null)
                                        error("Restriction: node %x not on road %x", n2.getOffset(), roff);
                        }
                }
        }

        private void checkRestrictions(RouteNode node) {
                List<RouteCenter.Restriction> restrictions = node.getRestrictions();
                for (RouteCenter.Restriction restr : restrictions) {
                        if (restr == null) {
                                error("Restriction not found");
                                continue;
                        }

                        //List<Integer> roadOffsets = restr.getRoadOffsets();
                        //for (int roff : roadOffsets) {
                        //      boolean found = false;
                        //      List<RoadData> roads = node.getRoads();
                        //      for (RoadData r : roads) {
                        //              if (r.getOffset() == roff) {
                        //                      found = true;
                        //                      break;
                        //              }
                        //      }
                        //      if (!found)
                        //              error("Node:%x:Restrict:%x: not found on road %x", node.getOffset(), restr.getOffset(), roff);
                        //}
                }
        }

        private static long getNodeSortPosition(RouteNode node) {
                Coord coord = node.getCoord();
                long longitude = (long) coord.getLongitude() + Utils.toMapUnit(180);
                long lat = (long) coord.getLatitude() + Utils.toMapUnit(180);
                return (longitude << 28) + lat;
        }

        private void roadDiagrams(RouteNode node) {
        RoadData lastRoad = null;
        List<RouteArc> roadArcs = new ArrayList<>();

                for (RouteArc arc : node.getArcs()) {
                        RoadData road = arc.getRoad();
                        if (road != lastRoad && lastRoad != null) {
                                roadDiagram(lastRoad, node, roadArcs);
                                predictRoadArcs(lastRoad, node, roadArcs);
                                roadArcs.clear();
            }
            roadArcs.add(arc);
            lastRoad = road;
        }
        roadDiagram(lastRoad, node, roadArcs);
                predictRoadArcs(lastRoad, node, roadArcs);
        }

        private int predictGroupClass(RouteNode node) {
                ClassFound found = new ClassFound();
                if (node.getArcs().isEmpty())
                        return 0;


                for (RoadData road : node.getRoads()) {
                        // In general the first and last nodes of a road are placed into a higher group.
                        // In 99.7% of cases that is all there is to it.
                        predictGroupFromEndPoints(node, road, found);
                }

                if (!found.isFound()) {
                        // If a node is never the start or the end of a road then it is usually in group 0.
                        // But if it is the crossing point between two high class roads, then we might want
                        // to bump the class up.
                        //test("setting to mid road cross");
                        int c = node.findFirstCross();
                        if (c > 0)
                                found.updateRoadClass(c);
                }

                return found.getNodeClass();
        }

        /**
         * First stage is to use the road end points to predict a group class. This tends to
         * predict 99.7% of the group classes.
         * @param node The road we are trying to predict the group class of.
         * @param road The road to examine.
         * @param found A structure to save the results to.
         */

        private void predictGroupFromEndPoints(RouteNode node, RoadData road,
                        ClassFound found) {
                List<NodeLocator> locations = road.getNodeLocations();
                int roadClass = road.getRoadClass();
                if (locations.get(0).getOffset() == node.getOffset()) {
                        //test("node:%x: is first in road %x %d", node.getOffset(), road.getOffset(), roadClass);
                        found.updateFromEnd(node, roadClass);
                } else if (locations.get(locations.size()-1).getOffset() == node.getOffset()) {
                        //test("node:%x: is last in road %x %d", node.getOffset(), road.getOffset(), roadClass);
                        found.updateFromEnd(node, roadClass);
                } else {
                        //test("node:%x: is nothing special in road %x", node.getOffset(), road.getOffset());
                        // Ignore for now, unless there are three different class at this node.
                        if (node.nClasses() > 2)
                                found.updateFromEnd(node, roadClass);
                }
        }

        /**
         * Predict or check the arcs that we expect with those that actually exist.
         *
         * @param road The road to examine.
         * @param node A node in that road; the source node.
         * @param roadArcs The actual road arcs. We compare what we predict with this array.
         */

        private void predictRoadArcs(RoadData road, RouteNode node, List<RouteArc> roadArcs) {
                if (!node.hasArcs() || !nod.hasClassSections())
                        return;

                List<NodeLocator> nodeLocations = road.getNodeLocations();

                // Determine starting position of the source node within the road.
                int start = -1;
                for (int i = 0; i < nodeLocations.size(); i++) {
                        NodeLocator loc = nodeLocations.get(i);
                        if (loc.getNode() == node) {
                                start = i;
                                break;
                        }
                }

                if (start < 0) {
                        error("Could not find start node within road");
                        return;
                }

                int currentClass = node.getNodeClass();
                int finalClass = road.getNod2().getRoadClass();

                List<RouteArc> predicted = new ArrayList<>();

                // Start with the backward direction. We assume this is always done first.
                int first = start - 1;
                for (int i = first; i >= 0; i--) {
                        RouteNode n = nodeLocations.get(i).getNode();
                        int nodeClass = n.getNodeClass();
                        if (nodeClass > currentClass || i == first) {
                                if (nodeClass > finalClass)
                                        nodeClass = finalClass;
                                currentClass = nodeClass;

                                addToPredicted(predicted, n, nodeClass);
                                if (nodeClass >= finalClass)
                                        break;
                        }
                }

                // Now the forward direction.
                currentClass = node.getNodeClass();
                finalClass = road.getNod2().getRoadClass();
                first = start + 1;
                for (int i = first; i < nodeLocations.size(); i++) {
                        RouteNode n = nodeLocations.get(i).getNode();
                        int nodeClass = n.getNodeClass();
                        if (nodeClass > currentClass || i == first) {
                                if (nodeClass > finalClass)
                                        nodeClass = finalClass;
                                currentClass = nodeClass;

                                addToPredicted(predicted, n, nodeClass);
                                if (nodeClass >= finalClass)
                                        break;
                        }
                }

                // Compare the arc details (node/class) that we predicted with the real thing.
                int size = Math.min(predicted.size(), roadArcs.size());
                for (int i = 0; i < size; i++) {
                        RouteArc parc = predicted.get(i);
                        RouteArc rarc = roadArcs.get(i);

                        if (parc.getTargetNode().getOffset() != rarc.getTargetNode().getOffset()) {
                                test("Predicted node %x, saw %x", parc.getTargetNode().getOffset(),
                                                rarc.getTargetNode().getOffset());
                        }

                        if (parc.getDestclass() != rarc.getDestclass()) {
                                test("Predicted arc class %d, saw %d. for node %x", parc.getDestclass(),
                                                rarc.getDestclass(), parc.getTargetNode().getOffset());
                        }
                }

                // Check that we had the same number too.
                if (predicted.size() != roadArcs.size()) {
                        test("mismatch between predicted number of arcs and actual %d/%d", predicted.size(),
                                        roadArcs.size());
                }
        }

        private void addToPredicted(List<RouteArc> predicted, RouteNode node, int nodeClass) {
                RouteArc arc = new RouteArc(null);
                arc.setAlt6(nodeClass);
                arc.setTargetNode(node);

                predicted.add(arc);
        }

        /**
     * Draw a diagram of the road and a given node with the arc destinations marked.
     *
     * Lists every node in the road. If the node is the source node it is marked with 'O', if
     * the node is an arc target it is marked with a '+', otherwise it is marked with a plain line '|'.
     *
     * The possible destinations at each node are printed, and if this is an actual arc destination
     * the actual arc-class is printed too.
     *
     * @param road A road to display.
     * @param node A node in that road; the source node.
     * @param roadArcs All the arcs from the given node that use this road.
     */

    private void roadDiagram(RoadData road, RouteNode node, List<RouteArc> roadArcs) {
        List<NodeLocator> nodeLocations = road.getNodeLocations();

        // Don't display if not interesting
        if (roadArcs.size() < 3)
            return;

        dtrace("   Road diagram %x class=%d/%d, narcs=%d t=%d %s\n", road.getOffset(),
                                road.getNod2().getRoadClass(), road.getNod2().getRoadClass(), roadArcs.size(),
                                road.getSegments().get(0).getLine().getType(), road.isOneway()?"oneway":"");

        for (NodeLocator loc : nodeLocations) {
            String mark = "|";
            if (loc.getOffset() == node.getOffset())
                mark = "O";


            RouteArc thisArc = null;
            for (RouteArc arc : roadArcs) {
                if (arc.getTargetNode().getOffset() == loc.getOffset()) {
                    mark = "+";
                    thisArc = arc;
                    break;
                }
            }
            RouteNode current = loc.getNode();
            dtrace("%6s %6x rc%4d[%d] %s", mark, current.getOffset(), current.getCenter().getIndex(),
                                        current.getNodeClass(), current.getClasses());
            if (thisArc != null) {
                dtrace(" arc%d dst=%d", thisArc.getIndex(), thisArc.getDestclass());
            }
            dtrace("\n");
        }
    }

        private void dtrace(String fmt, Object... args) {
                if (Log.isTrace()) {
                        System.out.format(fmt, args);
                }
        }

    private void checkNode(RouteNode node, RouteNode previous) {

                // Display a summary
                info(" %x:Node%s: (%s) flags:'%s', arcs=%d destclass=%d nodeclass=%d minclass=%d",
                                node.getOffset(), node.isProblem() ? "!" : "",
                                fmtCoord(node.getCoord()), node.flagString(), node.getArcs().size(),
                                node.getDestclass(), node.getNodeClass(), node.getMinDestclass());
                //
                // Check for gap or overlap with previous
                if (previous != null && node.getOffset() > previous.getNextStart()) {
                        error("Node:%x: gap %d from last (%x)", node.getOffset(),
                                        node.getOffset() - previous.getNextStart(),
                                        previous.getNextStart());
                } else if (previous != null && node.getOffset() < previous.getNextStart()) {
                        error("Node:%x: overlap with last (%x)", node.getOffset(), previous.getNextStart());
                }
        }

        /**
         * A node has zero or more arcs to other nodes.
         *
         * @param node The starting node.
         */

    private void checkArcs(RouteNode node) {
            int maxRoadclass = 0;
                int minRoadclass = 5;
        int count = 0;
            RouteArc last = null;
        for (RouteArc arc : node.getArcs()) {
            if (arc.isBad()) {
                info("   Arc %d[%x]: BAD", count, arc.getOffset());
                count++;
                continue;
            }

                if (arc.getRoadClass() > maxRoadclass)
                        maxRoadclass = arc.getRoadClass();
                        if (arc.getRoadClass() < minRoadclass)
                                minRoadclass = arc.getRoadClass();

                //findLinesForArc(node, arc);

                // Display info about the arc
                Integer netOffset = arc.getNetOffset();
                RoadData rd = net.getRoad(netOffset);
                assert rd != null : String.format("net %x", netOffset);
                arc.setRoad(rd);

                Label label = rd.getLabel(0);

                String roadInfo = String.format("rd=%x:%s:cls=%d/%d", arc.getNetOffset(),
                                Label.stripGarminCodes(label.getText()),
                                arc.getRoadClass(), arc.getSpeed() );

                info("   Arc %d[%x]: %s, [%s] ->%x%s, %dm %s%s",
                    count, arc.getOffset(), arc.alt6String(), roadInfo,
                    arc.getLink(), arc.isLongLink() ? "(LL)" : "",
                    nod.rawToMeters(arc.getDistance()), arc.hasDirection()? String.format("%.2fdeg", arc.getInitialDegrees()): "ND",
                    arc.isLast()? " LAST": "");

                // Check that the class/speed from the arc, matches the values from NET 2.
                Nod2Record nod2Record = rd.getNod2();
                if (arc.getRoadClass() != nod2Record.getRoadClass()) {
                        error("road class from arc %d, does not match from net2 %d",
                                        arc.getRoadClass(), nod2Record.getRoadClass());
                }

                if (arc.getSpeed() != nod2Record.getSpeed()) {
                        error("road speed from arc %d, does not match from net2 %d",
                                        arc.getSpeed(), nod2Record.getSpeed());
                }

                // Check the target node, make sure it exists and is valid
            int link = arc.getLink();
            RouteNode other = nod.getNode(link);
            if (other == null) {
                error("Node:%x/%d: node target not read %x", node.getOffset(), arc.getIndex(), link);
            } else if (other.isBad()) {
                error("Node:%x/%d: node target bad %x", node.getOffset(), arc.getIndex(), link);
            } else {
                    arc.setTargetNode(other);

                // Compare actual distance and direction with this other node from this arc
                compareActual(arc, node, other);

                    checkReverse(arc, other);

                    checkCurve(arc, other);

                    checkRoadClass(arc, other, last);

                    if (last != null)
                            checkWithLast(arc, last);
            }

                last = arc;
            count++;
        }

                if (maxRoadclass != node.getDestclass()) {
                    error("node:%x: class on node %d, max destclass of arcs %d", node.getOffset(),
                                    node.getDestclass(), maxRoadclass);
            }
    }

        private void checkWithLast(RouteArc arc, RouteArc last) {
                if (arc.getNetOffset().equals(last.getNetOffset())) {
                        // Same road, so we should have not have new net set
                        checkEqual(false, arc.newNet(),
                                        "Node:%x/%d: continuing road new-flag",
                                        arc.getNode().getOffset(), arc.getIndex());

                        // The target node should not be the same
                        if (arc.getLink() == last.getLink()) {
                                test("repeated arc");
                        }
                } else {
                        checkEqual(true, arc.newNet(), "Node:%x/%d: new road new-flag",
                                        arc.getNode().getOffset(), arc.getIndex());
                }

        }

        /**
         * Print out road class information and check it is correct.
         * Not sure about the road class understanding, often the arc dest class is zero
         * when you would not expect it to be.
         *
         * Investigating.
         *
         * @param arc The arc to check.
         * @param target The arc target node.
         * @param lastArc The previous arc.  If this is the first, then it is null.
         */

        private void checkRoadClass(RouteArc arc, RouteNode target, RouteArc lastArc) {
                RouteNode node = arc.getNode();

                trace("      road class: this=%d arcdst=%d source-node-dst=%d target-node-class=%d target-classes=%s",
                                arc.getRoadClass(), arc.getDestclass(), node.getDestclass(), target.getDestclass(), target.getClasses());

                int arcClass = arc.getDestclass();

                // The class on the arc is always less than or equal to that of the road it is on
                if (arcClass > arc.getRoadClass())
                        error("Node:%x/%d: arc class greater than road class", node.getOffset(), arc.getIndex());

                // Divide into next-node arcs and far-arcs. There is not really any difference so this distinction will
        // be removed once we have something better.
                if (lastArc == null || arc.newNet() || (!arc.newNet() && arc.isSign()!=lastArc.isSign())) {
                        // Next-node arcs

                        if (!target.connectsToClass(arcClass) && arcClass > 0)
                                error("Node:%x/%d: arc-class does not match any on the target node", node.getOffset(), arc.getIndex());

                } else {
                        // Far links
                        if (arcClass == 0)
                                error("Node:%x/%d: arc class is zero for a far-arc", node.getOffset(), arc.getIndex());

                        if (!target.connectsToClass(arcClass))
                                error("Node:%x/%d: arc-class does not match any on the target node %s", node.getOffset(), arc.getIndex(), node.getClasses());
                }
        }

        private void checkCurve(RouteArc arc, RouteNode other) {
                if (!arc.hasCurve())
                        return;
                Coord c1 = arc.getNode().getCoord();
                Coord c2 = other.getCoord();
                double actualStraighLine = c1.bearingTo(c2);
                int cdir = arc.getCurveDirection();
                int actualCDir = RouteArc.directionFromDegrees(actualStraighLine);
                trace("      curve A=%x[%x,%x->%x], B=%x direct=%.0fdeg[%x], dstc=%d, len=%dm", arc.getCurveA(), (arc.getCurveA() & 0xe0)>>5, arc.getCurveA() & 0x1f,
                                cdir,
                                arc.getCurveB(),
                                actualStraighLine, actualCDir,
                                arc.getDestclass(),
                                nod.rawToMeters(arc.getDistance()));

                int diff = Math.abs(cdir - actualCDir);
                if (diff > 128)
                        diff -= 256;
                if (diff > 8)
                        error("Node:%x/%d: curve direct bearing %x, expected %x", arc.getNode().getOffset(), arc.getIndex(),
                                        cdir, actualCDir);

                if (arc.hasCurve()) {
                        int direct= nod.metersToRaw(
                                        arc.getNode().getCoord().distance(other.getCoord()));
                        int path = arc.getDistance();
                        int calcRatio =  (int) Math.min(31, Math.round(32.0d * direct/ path));

                        int ratio = (arc.getCurveA() & 0xe0) >> 5;
                        int tolerance;
                        if (direct > 2 && path >= direct){
                                double hi = Math.min(31, Math.round(32.0d * (direct+1) / path));
                                double lo = Math.min(31, Math.floor(32.0d * (direct-1) / path));
                                tolerance = (int)(hi - lo);
                        } else
                                tolerance = 32; // ignore ratio errors, rounding errors are too big
                        if (ratio > 0) {
                                // Compressed/limited range of ratios, so adjust.
                                ratio = 2 * ratio + 17;
                                tolerance += 2;
                                //test("hi%x lo%x calc%x", hi, lo, calcRatio);
                        } else {
                                ratio = arc.getCurveA() & 0x1f;
                                if (ratio > 17){
                                        error("ratio > 17 found in curveA %d",ratio);
                                }
                                       
                        }

                        trace("      distance ratio: %d, calc=%d direct=%x,path=%x", ratio, calcRatio, direct, path);

                        if (Math.abs(ratio - calcRatio) > tolerance) {
                                error("Node:%x/%d: distance ratio %d, expected %d", arc.getNode().getOffset(),
                                                arc.getIndex(),
                                                ratio, calcRatio);
                        }
                }
        }

        /**
         * Check the reverse arc.
         * @param arc The forward arc, from the node under test.
         * @param other The target node that this arc leads towards.
         */

        private void checkReverse(RouteArc arc, RouteNode other) {
                // Find the arc from the other node that points back to us.
                // There is always a reverse arc unless the arc is really a link to a
                // higher road class.
                boolean found = false;
                for (RouteArc rarc : other.getArcs()) {
                    if (arc.isBad())
                        continue;
                    int rlink = rarc.getLink();

                    RouteNode rother = nod.getNode(rlink);
                    if (rother != null && arc.getNode() == rother && arc.getNetOffset().equals(rarc.getNetOffset())) {
                        // This is the arc that links back to this node - the 'reverse arc'
                        compareToReverseArc(arc, rarc);
                            found = true;
                        break;
                    }
                }

                if (!found && arc.newNet()) {
                        error("node:%x/%d: expected to find reverse arc",
                                        arc.getNode().getOffset(), arc.getIndex());
                }
        }

    private void compareToReverseArc(RouteArc arc, RouteArc rarc) {
        // It is believed that the signs should be different
        if (!(arc.isSign() ^ rarc.isSign()))
            error("Node:%x/%d: reverse arc has same sign", arc.getNode().getOffset(), arc.getIndex());

        // Distance should be similar. Perhaps not if there are two ways of getting between nodes - need to look into this.
        int d1 = arc.getDistance();
        int d2 = rarc.getDistance();
            int diff = Math.abs(d1 - d2);

            if (diff > 2)
            error("Node:%x/%d: distance along reverse arc different %d/%d", arc.getNode().getOffset(), arc.getIndex(), d1, d2);
    }

        private void compareActual(RouteArc arc, RouteNode node, RouteNode other) {
                Coord c1 = node.getCoord();
                Coord c2 = other.getCoord();
                compareActualDirection(arc, c1, c2);
                compareActualDistance(arc, node, other);
        }

    private void compareActualDirection(RouteArc arc, Coord c1, Coord c2) {
        double actualStraighLine = c1.bearingTo(c2);
        Double direction = arc.getInitialDegrees();
        Double actualInitial = arc.getActualInitialBearing();
        if (actualInitial == null) {
            error("Node:%x/%d: could not get initial bearing of road", arc.getNode().getOffset(), arc.getIndex());
            return;
        }

        trace("      direction %.0f (%x), actual: initial=%.0f(%02x), direct=%.0f(%02x)",
                direction, arc.getDirection(), actualInitial, actualInitial!=null ? directionFromDegrees( actualInitial) : 0,
                                actualStraighLine, directionFromDegrees(actualStraighLine));

            if (actualInitial == null)
                    actualInitial = actualStraighLine;
            int intInitial = directionFromDegrees(actualInitial);

            if (compareAngle(arc.getDirection(), intInitial))
            error("Node:%x/%d: direction %.0f(%02x), actual init=%.0f(%02x), direct=%.0f(%02x)",
                            arc.getNode().getOffset(), arc.getIndex(),
                            direction, arc.getDirection(),
                            actualInitial, intInitial,
                            actualStraighLine, directionFromDegrees(actualStraighLine));
    }

        private boolean compareAngle(int a1, int a2) {
                int allowed = 8;  // Allowed difference before we complain
                if ((a1 & 0x0f) == 0)
                        allowed += 0x10;

                int diff = Math.abs(a1 - a2);
                if (diff > 128)
                        diff -= 256;
                //System.out.printf("comp %d %d allowed=%x / diff=%x\n", a1, a2, allowed, diff);
                return diff > allowed;
        }

        private double totUnits;
        private double totMeters;
        /**
     * Compares the distance along the road with the value stored on the arc.
     *
         * @param arc The starting arc.
         * @param node The starting node.
         * @param other The target node that this arc leads towards.
         */

    private void compareActualDistance(RouteArc arc, RouteNode node, RouteNode other) {
            int units = arc.getDistance();
            int meters = nod.rawToMeters(units);

            RoadData road = arc.getRoad();
            NodeLocator loc1 = road.locateNode(node);
            NodeLocator loc2 = road.locateNode(other);
            if (loc2 == null) {
                    error("Node:%x/%d: Could not locate target node for arc", node.getOffset(), arc.getIndex());
                    return;
            }
            int pathDistance = loc2.getLength() - loc1.getLength();
            if (!arc.isSign())
                    pathDistance = -pathDistance;

                if (units > 200) {
                        totUnits += units;
                        totMeters += pathDistance;
                }
                int pathUnits = nod.metersToRaw(pathDistance);

            trace("      distance %dm[%x/%x], actual %dm[%x] %s", meters, arc.getDistance(), arc.getPartialDistance(),
                        pathDistance, pathUnits,
                        (units < pathUnits - 1)? "?short":"");

            // Check distance to within a percent.
        if (Math.abs(units - pathUnits) > units/100+2)
            error("Node:%x/%d: distance %dm[%x], actual %dm[%x]", arc.getNode().getOffset(), arc.getIndex(),
                    meters, units, pathDistance, pathUnits);
    }

        private void checkBoundary4() {
                info("Check NOD 4, boundary nodes with destclass > 0");

                List<RouteNode> allBoundaryNodes = nod.readBoundaryNodes();
                List<RouteNode> list = nod.readHighClassBoundaryNodes();
                long lastSort = 0;
                for (RouteNode node : list) {
                        info("ClassBoundaryNode:%x: c=%s", node.getOffset(), node.getClasses());

                        if (!node.isBoundary())
                                error("Node:%x: not a boundary node", node.getOffset());

                        boolean contained = allBoundaryNodes.remove(node);
                        if (!contained)
                                error("Node:%x: high class boundary not contained in full list", node.getOffset());

                        long sort = getNodeSortPosition(node);
                        if (sort <= lastSort)
                                error("class boundary node:%x: not sorted", node.getOffset());
                        lastSort = sort;
                }

                lastSort = 0;
                for (RouteNode node : allBoundaryNodes) {
                        if (node.getMaxDestclass() != 0) {
                                error("node %x: high class boundary node not included in NOD 4 %s",
                                                node.getOffset(), node.getClasses());
                                long sort = getNodeSortPosition(node);
                                if (sort <= lastSort)
                                        error("normal boundary node:%x: not sorted", node.getOffset());
                                lastSort = sort;
                        }
                }
        }

        public void extraArgs(Map<String, String> args) {
                String val = args.remove("tab-zero");
                if (val != null) {
                        boolean z = true;
                        if (val.equals("0"))
                                z = false;
                        setHasExtraZeros(z);
                }
                super.extraArgs(args);
        }

        public static void main(String[] args) {
                runMain(NodCheck.class, "NOD", args);
        }

        private class ClassFound {
                private boolean found;
                private int roadClass;

                public void set() {
                        found = true;
                }

                public boolean isFound() {
                        return found;
                }

                public int getNodeClass() {
                        return roadClass;
                }

                public void updateRoadClass(int roadClass) {
                        found = true;
                        this.roadClass = Math.max(this.roadClass, roadClass);
                }

                public void updateFromEnd(RouteNode node, int roadClass) {
                        int higher = node.higherConnectionsCount(roadClass);
                        if (node.nClasses() > 1 && node.connectionsAtClass(roadClass) <= 1 && higher == 0)
                                return;

                        found = true;
                        this.roadClass = Math.max(this.roadClass, roadClass);
                }
        }
}