Subversion Repositories display

Rev

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

/*
 * Copyright (C) 2007,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 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.
 *
 *
 * Author: Steve Ratcliffe
 * Create date: Dec 16, 2007
 */

package test.display;

import java.util.BitSet;
import java.util.HashMap;
import java.util.Map;

import uk.me.parabola.imgfmt.Utils;

import static test.util.AccessBits.*;
import static test.util.ArcFlags.*;
import static test.util.NodeFlags.*;
import static test.util.RestrictionBits.*;

/**
 * Standalone program to display the NOD file as it is worked out.  Will start
 * out with what is in imgdecode by John Mechalas.
 *
 * Credits:
 * Alexander Atanasov and his libgarmin (http://libgarmin.sourceforge.net/) project.
 * Robert Vollmert
 */

public class NodDisplay extends CommonDisplay {

        private int nodesStart;
        private int nodesLen;

        private int roadDataStart;
        private int roadDataLen;

        private int boundriesStart;
        private int boundriesLen;
        private int boundriesRecsize;

        private final Map<Integer, Offset> nod1recs = new HashMap<>();

        private NetDisplay net;

        private byte align;
        private int aMask;
        private int tableARecordLen;

        // Initialise this here if the automatic detection does not work.
        private Boolean hasExtraZeros;
        private byte mult;

        protected void print() {
                readCommonHeader();

                printHeader();

                printRoadData();
                printBoundryNodes();

                printNodes();
        }

        private long calcTablePosition(long start, int low) {
                long pos = start - nodesStart;
                pos += aMask + 1;
                pos += low * ((long) 1 << align);
                pos &= ~aMask;
                return nodesStart + pos;
        }

        private void printNodes() {
                Displayer d = new Displayer(reader);
                d.setTitle("NOD 1 (nodes)");
                d.setSectStart(nodesStart);

                reader.position(nodesStart);
                while (reader.position() < nodesStart + nodesLen) {
                        while (mult != 0 && ((reader.position() - nodesStart) & ((1 << mult) - 1)) != 0)
                                d.byteValue("pad");

                        long start = reader.position();
                        int low = (d.byteValue("low byte %x, pointer to")) & 0xff;

                        long end = calcTablePosition(start, low);
                        d.item().addText("tables at %x", end);

                        // determine size of Table C offsets
                        reader.position(end);
                        // read Table C format
                        int fmt = reader.get();
                        int tabCOffsetSize = 0;
                        if((fmt & 2) != 0) {
                                // Table C size is 16 bits, so offset must be too
                                tabCOffsetSize = 2;
                        }
                        else if((fmt & 1) != 0) {
                                // this is tricky because the size could be more than
                                // 128 in which case the offset needs to be 16 bits so
                                // we need to actually read the size to find out
                                reader.position(end + 7);
                                int tabASize = reader.get() & 0xff;
                                int tabBSize = reader.get() & 0xff;
                                reader.position(end + 9 + tabASize * 5 + tabBSize * 3);
                                if((reader.get() & 0xff) >= 0x80)
                                        tabCOffsetSize = 2;
                                else
                                        tabCOffsetSize = 1;
                        }
                        reader.position(start); // step back to low byte

                        if(tabCOffsetSize == 1)
                                d.item().addText("Table C offsets are 8 bits");
                        else if(tabCOffsetSize == 2)
                                d.item().addText("Table C offsets are 16 bits");

                        printNode(d, end, tabCOffsetSize);

                        reader.position(end);
                        d.print(outStream);
                        printTables(d, 0);
                }

                d.print(outStream);
        }

        private void printNode(Displayer d, long end, int tabCOffsetSize) {
                long groupStart = reader.position();
                TableHeader tableHeader = readTableHeader((int) end);
                while (reader.position() < end) {

                        d.gap();
                        d.item().addText("New node");

                        long nodeOff = reader.position();
                        DisplayItem item = d.byteItem();
            int low = item.getValue();

            item.addText("low %x", low & 0xff);

                        if (calcTablePosition(nodeOff, low) != end && low > 0) {

                                d.item().addText("error lost sync calc-end=%x, should be=%x",
                                                                 calcTablePosition(nodeOff, low), end);

                low = d.byteValue("retry low %x") & 0xff;
                if (calcTablePosition(nodeOff + 1, low) != end) {
                    d.rawValue((int) (end - reader.position()), "remaining bytes");
                    break;
                }
                nodeOff += 1;
                        }

                        // byte2 looks like flags, only a small number of values
                        int flags = d.byteValue("Flags %x");

                        if (low == 0 && flags == 0) {
                                item.addText("End of nodes");
                                d.rawValue((int) (end - reader.position()), "perhaps more nodes");
                                continue;
                        }

                        boolean arcs = (flags & NODE_ARCS) == 0x40; // has arcs
                        boolean bigoff = (flags & NODE_LARGE_OFFSET) == 0x20;           // 2 bytes each for lat/lon offsets
                        boolean restr = (flags & NODE_RESTRICTIONS) == 0x10;            // restrictions present at this node
                        boolean boundary = (flags & NODE_BOUNDARY) == 0x08;     // this is a boundary node
                        int destclass = flags & NODE_DESTCLASS_MASK;                 // Highest road class this node leads to
                        boolean unkFlags = (flags & NODE_UNKNOWN_MASK) != 0;        // has unknown flags

                        StringBuilder sb = new StringBuilder(": ");
                        if (restr) sb.append("restrictions ");
                        if (bigoff) sb.append("large-offsets ");
                        if (boundary) sb.append("boundary-node ");
                        if (arcs) sb.append("arcs ");
                        sb.append("destclass=");
                        sb.append(destclass);
                        sb.append(" ");
                        if (unkFlags) sb.append("UNKNOWN ");
                        d.item().addText(sb.toString());

                        // 1. even spread
                        // 2. biased toward 00 f0 0f ff etc
                        // 3. even
                        // 4. uneven, peaks at 0x20, 40, 60
                        // 5. uneven again
                        // 6. fairly even but big peak at 3e,3f
                        // 7. fairly even
                        // 8. number decreases with value

                        positionOffsets(d, tableHeader, bigoff);

                        if(arcs)
                                pointerPart(d, tableHeader, nodeOff, groupStart, end);

                        if(restr) {
                            boolean done = false;
                            while(!done) {
                                        if(tabCOffsetSize == 1) {
                                                int off = d.byteValue("Restriction offset 0x%x");
                                                done = (off & 0x80) == 0x80;
                                        }
                                        else if(tabCOffsetSize == 2) {
                                                int off = d.charValue("Restriction offset 0x%x");
                                                done = (off & 0x8000) == 0x8000;
                                        }
                                        else {
                                                done = true;
                                        }
                            }
                        }

                        while (mult != 0 && ((reader.position() - nodesStart) & ((1 << mult) - 1)) != 0)
                                d.byteValue("pad");
                }
        }

        private TableHeader readTableHeader(int offset) {
        return new TableHeader(reader, offset);
        }

        private void positionOffsets(Displayer d, TableHeader currentTableHeader, boolean bigoff) {

                DisplayItem item;
                if (bigoff) {
                        item = d.item();
                        short longoff = (short) reader.getChar();
                        item.setBytes((char) longoff);
                        item.addText("longitude %.5f %+d", Utils.toDegrees(longoff + currentTableHeader.getLon()), longoff);

                        item = d.item();
                        short latoff = (short) reader.getChar();
                        item.setBytes((char) latoff);
                        item.addText("latitude %.5f %+d", Utils.toDegrees(latoff + currentTableHeader.getLat()), latoff);

                } else {
                        item = d.item();
                        int latlon = reader.get3();

                        short latoff = (short) (latlon >> 12);
                        if ((latoff & 0x800) != 0)
                                latoff |= 0xf000;
                        short longoff = (short) (latlon & 0xfff);
                        if ((longoff & 0x800) != 0)
                                longoff |= 0xf000;

                        item.setBytes3(latlon);
                        item.addText("longitude %.5f %+d", Utils.toDegrees(longoff + currentTableHeader.getLon()), longoff);
                        item.addText("latitude %.5f %+d", Utils.toDegrees(latoff + currentTableHeader.getLat()), latoff);
                }
        }

        private void pointerPart(Displayer d, TableHeader tableHeader, long offset, long min, long max) {
                // looks like there are 2b before the low1 pointer and 4 after.
                boolean end = false;
                boolean first = true;
                boolean lastSign = false;

                do {
                        // Start with alt6 byte
                        // bit 0x20 seems to determine whether there's an extra byte at the end
                        int alt6 = d.byteValue("alt6 byte %02x");

                        // this is not the class of the segment, but the max of classes of the dest node
                        int classmask = 0x07;
                        boolean newRoad = (alt6 & ARC_HASNET) == 0x80;
                        int destclass = alt6 & classmask;
                        boolean sign = (alt6 & ARC_SIGN) != 0;
                        boolean curve = (alt6 & ARC_CURVE) != 0;
                        d.item().addText("net=%d sign=%d curve=%x len=%02x destclass=%d",
                                        newRoad?1:0, sign?1:0, curve?1:0, alt6 & ARC_LEN, destclass);

                        // Continue with two byte values.  The first one has the top
                        // bit set if this is the last pointer in the node record.
                        DisplayItem item = d.item();
                        int intro1 = item.setBytes(reader.get());

                        // Note that this is the last if it is.
                        if ((intro1 & 0x80) == 0x80) {
                                end = true;
                                item.addText("last one");
                        }

                        // The second highest bit, means inter-section pointer
                        boolean longlink = (intro1 & 0x40) == 0x40;
                        if (longlink) {
                                int idx = intro1 & 0x3f;
                                if (idx == 0x3f) {
                                        idx = item.setBytes(reader.get()) & 0xff;
                                }
                int linkoff = tableHeader.getLink(idx);
                if (linkoff >= 0)
                    item.addText("inter-section link, index %d -> %x", idx, linkoff);
                else
                    item.addText("error: inter-section link, index %d -> BAD INDEX", idx);
            } else {
                                // in-section relative node pointer
                                int intro2 = item.setBytes(reader.get()) & 0xff;
                                short nodeoff = (short) ((intro1 << 8 | intro2) & 0x3fff);

                                // Construct a pointer to another node, signed 16 bit quantity
                                if ((nodeoff & 0x2000) != 0)
                                        nodeoff |= 0xc000;
                                nodeoff <<= mult;

                                item.addText("pointer to another node %x", nodeoff);
                                long otherNode = offset + nodeoff - nodesStart;
                                item.addText(" -> node %x", otherNode);

                                checkValidNode(item, max, otherNode);

                                if (min - nodesStart > otherNode || otherNode >= max - nodesStart) {
                                        item.addText("error INVALID node: pointer out of range!");
                                }
                        }

                        if (first || newRoad) {
                                d.byteValue("pointer to local net index %d");
                        } else {
                                d.item().addText("no segment pointer");
                        }

                        int len;
                        if ((alt6 & ARC_EXTRA) == ARC_EXTRA) {
                                item = d.byteItem();
                                int len1 = item.getValue() & 0xff;

                                if ((len1 & 0x80) == 0x80) {
                                        if ((len1 & 0x40) == 0x40) {
                                                item.addText("22 bit length + curve");
                                                int len2 = item.setBytes(reader.getChar());
                                                len = (len1 & 0x3f) | (len2 << 6); // 6+16 bits
                                                curve = true;
                                        } else {
                                                int len2 = item.setBytes(reader.get()) & 0xff;
                                                item.addText("14 bit length %02x %02x", len1 & 0x3f, len2);
                                                len = (len1 & 0x3f) | (len2 << 6); // 6+8 bits
                                                curve = false;
                                        }
                                } else {
                                        item.addText("15 bit length + curve");
                                        int len2 = item.setBytes(reader.get()) & 0xff;
                                        len = (len1 & 0x7f) | (len2 << 7); // 7+8 bits
                                        curve = true;
                                }
                        }
                        else {
                                curve = (alt6 & ARC_CURVE) == ARC_CURVE;
                                if(curve)
                                        d.item().addText("9 bit length + curve");
                                else
                                        d.item().addText("9 bit length");
                                len = (alt6 & ARC_LEN) << 5;
                                item = d.item();
                                len |= item.setBytes(reader.get()) & 0xff; // 2+8 bits
                        }
                        // length should be in units of 16 feet
                        item.addText("length %d (%dm)", len, (int)(len * 16 / 3.25));

                        if (first) {
                                haveDir = false;
                                largeDir = !newRoad;
                        }

                        if (first || newRoad || (sign != lastSign)) {
                                fetchDirection(d);
                        }

                        if (curve) {
                                item = d.item();
                                int curvea = item.setBytes(reader.get()) & 0xff;
                                if ((curvea & 0xe0) == 0) {
                                        int curveb = item.setBytes(reader.get()) & 0xff;
                                        item.addText("curve[0] %02x curve[1] %02x (two bytes)", curvea, curveb);
                                } else {
                                        int angle = 360 * (byte)(((curvea & 0x1f) << 3) |
                                                                                         ((curvea & 0xe0) >> 5)) / 256;
                                        item.addText("curve[0] %02x (%d deg?)", curvea, angle);
                                }
                        }
                        d.line();

                        first = false;
                        lastSign = sign;
                } while (!end);
        }

        private byte savedDir;
        private boolean haveDir;
        private boolean largeDir;

        /**
         * Fetch the direction.  See the {@link test.files.DirectionReader} class for more details.
         * It could be used here but we want a byte by byte breakdown printed out.
         */

        private int fetchDirection(Displayer d) {
                if (largeDir) {
                        DisplayItem item = d.byteItem();
                        int dir = item.getValue();
                        item.addText("large direction %d (%.0fdeg)", dir, (dir * 360.0) / 256);
                        return dir;
                }

                if (haveDir) {
                        haveDir = false;
                        int dir = (savedDir & 0xf0);
                        d.item().addText("saved direction %x (%.0fdeg)", dir, (dir * 360.0) / 256);
                        return dir;
                } else {
                        DisplayItem item = d.byteItem();
                        savedDir = (byte) item.getValue();
                        haveDir = true;
                        int dir = savedDir & 0xf;
                        dir <<= 4;
                        item.addText("read direction %x (%.0fdeg)", dir, (dir * 360.0) / 256);
                        return dir;
                }
        }

        private void checkValidNode(DisplayItem item, long tablePosition, long nodeStart) {
                // Work out if this is actually a node to help in debugging.  False positives
                // are possible, but not false negatives.
                if (nodeStart < 0) {
                        item.addText("error INVALID node, start=%x", nodeStart);
                        return;
                }

                long savpos = reader.position();

                reader.position(nodeStart + nodesStart);
                int low = reader.get() & 0xff;
                long tabpos = calcTablePosition(nodeStart + nodesStart, low);
                if (tabpos != tablePosition) {
                        item.addText("error INVALID node, low=%x, tabpos=%x, max=%x", low, tabpos, tablePosition);
                }

                // restore position
                reader.position(savpos);
        }

        private void printTables(Displayer d, int len) {
                d.setTitle("Tables");
                int remain = len;

                // Get the header
                TableHeader tableHeader = new TableHeader();
                tableHeader.setPosition(reader.position());
                int restrformat = d.byteValue("Table C format");
                remain -= 1;

                int l = latLongField(d, "longitude");
                tableHeader.setLong(l);
                remain -= 3;

                l = latLongField(d, "latitude");
                tableHeader.setLat(l);
                remain -= 3;

                int n = d.byteValue("%d records in Table A") & 0xff;
                remain -= 1;
                int m = d.byteValue("%d records in Table B") & 0xff;
                remain -= 1;
                d.print(outStream);

                // Now do 'Table A' (segments)
                d.setTitle("Table A (segments)");
                for (int i = 0; i < n; i++) {
                        d.item().addText("net pointer index %d %02x", i, i);

                        DisplayItem item = d.item();
                        int off = item.setBytes3(reader.get3());
                        // top 2 bits of net pointer are access bits
                        int access = (off & 0xc00000) >> 8;
                        off &= 0x3fffff;

                        item.addText("pointer to net %06x (%s)", off, getNameFromNetOff(off));

                        item = d.item();

                        int paramA = item.setBytes(reader.get());
                        int paramB = 0;
                        if (tableARecordLen >= 5)
                                paramB = item.setBytes(reader.get());
                        String par = "class %d, speed %d";
                        if ((paramA & TOLL) == TOLL)
                                par += ", toll";
                        if ((paramA & ONEWAY) == ONEWAY)
                                par += ", oneway";
                        if ((paramB & NO_THROUGH_ROUTING) == NO_THROUGH_ROUTING)
                                par += ", no through routing";
                        item.addText(par, (paramA & CLASS) >> 4, paramA & SPEED);

                        access |= paramB & 0xff;
                        par = "";
                        par += (access & NOEMERGENCY) == 0 ? "emergency, " : "no emergency, ";
                        par += (access & NODELIVERY) == 0 ? "delivery, " : "no delivery, ";
                        par += (access & NOCAR) == 0 ? "car, " : "no car, ";
                        par += (access & NOBUS) == 0 ? "bus, " : "no bus, ";
                        par += (access & NOTAXI) == 0 ? "taxi, " : "no taxi, ";
                        par += (access & NOCARPOOL) == 0 ? "carpool, " : "no carpool, ";
                        par += (access & NOFOOT) == 0 ? "foot, " : "no foot, ";
                        par += (access & NOBIKE) == 0 ? "bike, " : "no bike, ";
                        par += (access & NOTRUCK) == 0 ? "truck, " : "no truck, ";

                        item.addText("access: %s", par);
                        if((paramB & 0x08) != 0)
                                item.addText("Unknown access bits: %02x", paramB & 0x08);
                        remain -= 5;
                }
                d.print(outStream);

                // 'Table B' (inter-section pointers)
                d.setTitle("Table B (inter-section pointers)");
                for (int i = 0; i < m; i++) {
                        d.item().addText("node pointer index %d %02x", i, i);
                        d.int3Value("offset into Nod1 %x");
                        remain -= 3;
                }
                d.print(outStream);

                // 'Table C' (restrictions)
                d.setTitle("Table C (restrictions)");
                int size=0;
                if ((restrformat & 1) != 0) {
                        size = d.byteValue("table c size") & 0xff;
                        remain--;
                } else if ((restrformat & 2) != 0) {
                        size = d.charValue("table c size") & 0xffff;
                        remain -= 2;
                }

                if (size % 11 == 0) {
                        // assume these are fixed length records of size 11
                        remain -= size;
                        for (; size > 0; size -= 11) {
                                // turn restriction at second node from first node (via first segment)
                                // to third node (via second segment)
                                d.rawValue(3, "restriction header");
                                for (int i = 0; i < 3; i++) {
                                        // top bit always set
                                        DisplayItem item = d.item();
                                        int off = item.setBytes(reader.getChar());
                                        item.addText("node pointer %06x",
                                                        tableHeader.getPosition() - (off & 0x7fff));
                                }
                                d.byteValue("pointer to local net index %d");
                                d.byteValue("pointer to local net index %d");
                        }
                } else {
                        d.rawValue(size, "restrictions table (in unknown format");
                        remain -= size;
                }

                if ((restrformat & 4) != 0) {
                        d.byteValue("??? restrformat4");
                        remain--;
                }

                if ((restrformat & 8) != 0) {
                        d.byteValue("%d unpaved roads");
                        remain--;
                }

                if((restrformat & 0x10) != 0) {
                        d.byteValue("%d ferry routes");
                        remain--;
                }

                if((restrformat & 0x03) == 0) {
                        // Sometimes there is an extra zero here. Eg mkgmap currently adds one.
                        // If there is no zero then we know that that isn't the case for this file.
                        // If it is however, the it could be the start of a node with a very short section.
                        // So we can't really tell, but if we found a node at this position from NOD 2,
                        // then we know that there definitely is not.
                        long pos = reader.position();
                        byte b = reader.get();
                        reader.position(pos);

                        // Is there a node at pos? If so then there are not extra bytes.
                        Offset offset = nod1recs.get((int) pos - nodesStart);

                        // Since the file either has this or not, then remember this the first time. It will
                        // then always be right or always wrong.  Initialise hasExtraZeros at the top if
                        // looking at a file where this heuristic does not work.
                        if (hasExtraZeros == null) {
                                if (b == 0 && offset == null) {
                                        hasExtraZeros = true;
                                } else {
                                        hasExtraZeros = false;
                                }
                        }
                        if (hasExtraZeros) {
                                d.byteValue("Skip zero value %d");
                                remain--;
                        }
                }

                d.print(outStream);

                // if there's something left, we probably missed the first
                // node of the next section
                if (remain > 0) {
                        d.setTitle("left over");
                        d.rawValue(remain, "extra data remaining");
                }

                d.print(outStream);
                d.setTitle("");
        }

        private int latLongField(Displayer d, String label) {
                DisplayItem item1 = d.item();
                int l = item1.setBytes3(reader.get3());
                if ((l & 0x800000) != 0)
                        l |= 0xff000000;
                item1.addText("%s %.5f %x", label, Utils.toDegrees(l), l);
                return l;
        }

        private String getNameFromNetOff(int off) {
                if (net != null)
                        return net.getNameFromNetOff(off);
                else
                        return "";
        }

        private void printRoadData() {
                Displayer d = new Displayer(reader);
                d.setTitle("NOD 2 (road data)");
                d.setSectStart(roadDataStart);
                d.print(outStream);

                reader.position(roadDataStart);
                int end = roadDataStart + roadDataLen;
                int nrecords = 0;
                while (reader.position() < end) {
                        nrecords++;
                        d = new Displayer(reader);
                        d.setSectStart(roadDataStart);

                        int recstart = (int) reader.position();

                        // XXX looks like a set of flags but ?
                        // agree with speed & class in RouteParam (plus one bit)
                        DisplayItem item = d.item();
                        int flags = item.setBytes(reader.get());
                        item.addText("Road classification speed=%d, class=%d",
                                                 (flags & 0xf) >> 1, (flags & 0x70) >> 4);
                        if((flags & 1) == 0)
                                item.addText("bit 0 is zero");
                        int sectoff = d.int3Value("offset into NOD 1 %06x");
                        String origin = String.format("nod 2 %06x", recstart);
                        Offset offset = nod1recs.get(sectoff);
                        if (offset != null) {
                                offset.appendOrigin(origin);
                        } else {
                                Offset offval = new Offset(sectoff, origin);
                                nod1recs.put(sectoff, offval);
                        }

                        // Ok this is the number of bits in the following.
                        int nbits = d.charValue("Bit stream len %d");

                        // The number of set bits appears to be the number of nodes in the road.
                        // Usually th lowest nbits appear to be set, and I've seen the lowest
                        // missing when an end of the road is not a node. --Rob

                        // A number of bits follows.  Work out how many bytes are needed to
                        // hold that number of bits.
                        int nstream = (nbits+7)/8;
                        byte[] bs = d.rawValue(nstream, "Bit stream");
                        String bsStr = bitStreamAsString(bs, nbits);
                        d.item().addText("BIT STREAM %s", bsStr);

                        if((flags & 0x80) != 0) {
                                int extraFormat = d.byteValue("extra data format");
                                if(extraFormat >= 0x01 && extraFormat <= 0x0b) {
                                        if((extraFormat & 0x01) != 0) {
                                                int len = d.byteValue("len") & 0xff;
                                                d.rawValue(len >> 1, "extra data 1");
                                        }
                                        if((extraFormat & 0x02) != 0) {
                                                int len = d.byteValue("len") & 0xff;
                                                d.rawValue(len >> 1, "extra data 2");
                                        }
                                        if((extraFormat & 0x04) != 0) {
                                                d.byteValue("extra data 4");
                                        }
                                        if((extraFormat & 0x08) != 0) {
                                                d.charValue("extra data 8");
                                        }
                                }
                                else if(extraFormat == 0x0c) {
                                        int len = d.byteValue("len") & 0xff;
                                        d.rawValue(len >> 1, "extra data c");
                                }
                                else if(extraFormat == 0x0d) {
                                        int len = d.byteValue("len") & 0xff;
                                        d.rawValue(len >> 1, "extra data da");
                                        len = d.byteValue("len");
                                        d.rawValue(len >> 1, "extra data db");
                                }
                                else if(extraFormat == 0x0e) {
                                        int len = d.byteValue("len") & 0xff;
                                        d.rawValue(len >> 1, "extra data ea");
                                        len = d.byteValue("len") & 0xff;
                                        d.rawValue(len >> 1, "extra data eb");
                                }
                                else if(extraFormat == 0x0f) {
                                        int len = d.byteValue("len") & 0xff;
                                        d.rawValue(len >> 1, "extra data fa");
                                        len = d.byteValue("len") & 0xff;
                                        d.rawValue(len >> 1, "extra data fb");
                                        len = d.byteValue("len") & 0xff;
                                        d.rawValue(len >> 1, "extra data fc");
                                }
                                else
                                        d.item().addText("Unknown format");
                        }

                        d.gap();
                        d.print(outStream);
                }

                d.item().addText("Number of records %d", nrecords);
                d.print(outStream);
        }

        private String bitStreamAsString(byte[] bs, int nbits) {
                BitSet bset = BitSet.valueOf(bs);
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < nbits; i++)
                        sb.append(bset.get(i) ? "1" : "0");
                return sb.toString();
        }

        /**
         * This is a set of fixed length records, so is a good one to start with.
         */

        private void printBoundryNodes() {
                Displayer d = new Displayer(reader);
                d.setSectStart(nodesStart);
                d.setTitle("NOD 3 (boundary nodes)");

                reader.position(boundriesStart);
                for (int pos = boundriesStart; pos < boundriesStart + boundriesLen; pos += boundriesRecsize) {
                        latLongField(d, "longitude");
                        latLongField(d, "latitude");

                        // limited range
                        d.int3Value("offset into NOD 1 %06x");

                        d.gap();
                }

                d.print(outStream);
        }

        private void printHeader() {
                Displayer d = new Displayer(reader);
                d.setTitle("NOD header");

                reader.position();

                nodesStart = d.intValue("NOD 1 (nodes) at offset %#08x");
                nodesLen = d.intValue("NOD 1 length %d");
                d.item().addText("End of section %08x, len %#08x", nodesStart + nodesLen, nodesLen);

                int flags = d.charValue("flags"); // usually 0x0025 or 0x0027
                if ((flags & 0x02) != 0)
                        d.item().addText("have restrictions");
                d.charValue("???");
                //d.charValue("???");
                align = d.byteValue("node align %x");
                mult = d.byteValue("ptr multiplier %d");
                aMask = (1<<align) - 1;
                tableARecordLen = d.charValue("Table A record len %d");

                roadDataStart = d.intValue("NOD 2 (road data) at offset %#08x");
                roadDataLen = d.intValue("NOD 2 length %d");
                d.item().addText("End of section %08x, len %#08x", roadDataStart + roadDataLen, roadDataLen);

                d.intValue("???");

                boundriesStart = d.intValue("NOD 3 (boundary nodes) at offset %#08x");
                boundriesLen = d.intValue("NOD 3 length %d");
                boundriesRecsize = d.charValue("NOD 3 record size %d");
                d.item().addText("End of section %08x, len %#08x", boundriesStart + boundriesLen, boundriesLen)
                                .addText("Number of records %d", boundriesLen / boundriesRecsize);

        d.rawValue((int) (getHeaderLen() - reader.position()));
                d.print(outStream);
        }


        public static void main(String[] args) {
                if (args.length < 1) {
                        System.err.println("Usage: noddisplay <filename>");
                        System.exit(1);
                }

                String name = args[0];

                NodDisplay nd = new NodDisplay();
                nd.display(name, "NOD");
        }

        public void setNet(NetDisplay net) {
                this.net = net;
        }
}