Subversion Repositories mkgmap

Rev

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

/*
 * 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 uk.me.parabola.imgfmt.app.ImgFileWriter;
import uk.me.parabola.log.Logger;

/**
 * An arc joins two nodes within a {@link RouteCenter}.  This may be renamed
 * to a Segment.
 * The arc also references the road that it is a part of.
 *
 * There are also links between nodes in different centers.
 *
 * @author Steve Ratcliffe
 */

public class RouteArc {
        private static final Logger log = Logger.getLogger(RouteArc.class);
       
        // Flags A
        private static final int FLAG_HASNET = 0x80;
        private static final int FLAG_FORWARD = 0x40;
        private static final int MASK_DESTCLASS = 0x7;
        public static final int MASK_CURVE_LEN = 0x38;

        // Flags B
        private static final int FLAG_LAST_LINK = 0x80;
        private static final int FLAG_EXTERNAL = 0x40;

        private int offset;

        // heading / bearing:
        private float initialHeading; // degrees (A-> B in an arc ABCD)
        private final float directHeading; // degrees (A-> D in an arc ABCD)

        private final RoadDef roadDef;

        // The nodes that this arc comes from and goes to
        private final RouteNode source;
        private final RouteNode dest;

        // The index in Table A describing this arc.
        private byte indexA;
        // The index in Table B that this arc goes via, if external.
        private byte indexB;
       
        private byte flagA;
        private byte flagB;

        private final boolean haveCurve;
        private final int length;
        private final byte lengthRatio;
        private final int pointsHash;
        private boolean isForward;
        private final float lengthInMeter;

        private boolean isDirect;

        // this field may limit the arcs destination class
        private byte maxDestClass = -1;
       
        // pointer to the reverse arc if this is a direct arc
        private RouteArc reverseArc;

        /**
         * Create a new arc. An arc can contain multiple points (eg. A->B->C->D->E)
         * @param roadDef The road that this arc segment is part of.
         * @param source The source node. (A)
         * @param dest The destination node (E).
         * @param initialBearing The initial heading (signed degrees) (A->B)
         * @param directBearing The direct heading (signed degrees) (A->E)
         * @param arcLength the length of the arc in meter (A->B->C->D->E)
         * @param pathLength the length of the arc in meter (summed length for additional arcs)
         * @param directLength the length of the arc in meter (A-E)
         * @param pointsHash
         */

        public RouteArc(RoadDef roadDef,
                                        RouteNode source, RouteNode dest,
                                        double initialBearing, double directBearing,
                                        double arcLength,
                                        double pathLength,
                                        double directLength,
                                        int pointsHash) {
                this.isDirect = true; // unless changed
                this.roadDef = roadDef;
                this.source = source;
                this.dest = dest;
                this.initialHeading = (float) initialBearing;
                this.directHeading = (directBearing < 180) ? (float) directBearing : -180.0f;
                int len = NODHeader.metersToRaw(arcLength);
                if (len >= (1 << 22)) {
                        log.error("Way " + roadDef.getName() + " (id " + roadDef.getId() + ") contains an arc whose length (" + len + " units) is too big to be encoded, using length",((1 << 22) - 1));
                        len = (1 << 22) - 1;
                }
                this.length = len;
                this.lengthInMeter = (float) arcLength;
                this.pointsHash = pointsHash;
                // calculate length ratio between way length on road and direct distance between end points
                int ratio = 0;
                int pathEncoded = NODHeader.metersToRaw(pathLength);
                if (pathEncoded >= 2){
                        int directEncoded = NODHeader.metersToRaw(directLength);
                        if (pathEncoded > directEncoded){
                                ratio = (int) Math.min(31, Math.round(32.0d * directLength/ pathLength));
                        }
                }
                if (ratio == 0 && len >= (1 << 14))
                        ratio = 0x1f; // force encoding of curve info for very long arcs
                lengthRatio = (byte)ratio;
                haveCurve = lengthRatio > 0;
        }

        public float getInitialHeading() {
                return initialHeading;
        }

        public float getDirectHeading() {
                return directHeading;
        }

        public void setInitialHeading(float ih) {
                initialHeading = ih;
        }

        public float getFinalHeading() {
                float fh = 0;
                if (lengthInMeter != 0){
                        fh = getReverseArc().getInitialHeading();
                        fh = (fh <= 0) ? 180.0f + fh : -(180.0f - fh) % 180.0f;
                }
                return fh;
        }

        public RouteNode getSource() {
                return source;
        }

        public RouteNode getDest() {
                return dest;
        }

        public int getLength() {
                return length;
        }

        public int getPointsHash() {
                return pointsHash;
        }

        /**
         * Provide an upper bound for the written size in bytes.
         */

        public int boundSize() {

                int[] lendat = encodeLength();
                //TODO: take into account that initialHeading and indexA are not always written
                // 1 (flagA) + 1-2 (offset) + 1 (indexA) + 1 (initialHeading)
                int size = 5 + lendat.length;
                if(haveCurve)
                        size += encodeCurve().length;
                return size;
        }

        /**
         * Is this an arc within the RouteCenter?
         */

        public boolean isInternal() {
                // we might check that setInternal has been called before
                return (flagB & FLAG_EXTERNAL) == 0;
        }

        public void setInternal(boolean internal) {
                if (internal)
                        flagB &= ~FLAG_EXTERNAL;
                else
                        flagB |= FLAG_EXTERNAL;
        }


        /**
         * Set this arc's index into Table A.
         */

        public void setIndexA(byte indexA) {
                this.indexA = indexA;
        }

        /**
         * Get this arc's index into Table A.
         *
         * Required for writing restrictions (Table C).
         */

        public byte getIndexA() {
                return indexA;
        }

        /**
         * Set this arc's index into Table B. Applies to external arcs only.
         */

        public void setIndexB(byte indexB) {
                assert !isInternal() : "Trying to set index on internal arc.";
                this.indexB = indexB;
        }

        /**
         * Get this arc's index into Table B.
         *
         * Required for writing restrictions (Table C).
         */

        public int getIndexB() {
                return indexB & 0xff;
        }
         
        public int getArcDestClass(){
                return Math.min(getRoadDef().getRoadClass(), dest.getGroup());
        }
       
        public float getLengthInMeter(){
                return lengthInMeter;
        }
       
        public static byte directionFromDegrees(float dir) {
                return (byte) Math.round(dir * 256.0 / 360) ;
        }

        public void write(ImgFileWriter writer, RouteArc lastArc, boolean useCompactDirs, Byte compactedDir) {
                boolean first = lastArc == null;
                if (first){
                        if (useCompactDirs)
                                flagA |= FLAG_HASNET; // first arc: the 1 tells us that initial directions are in compacted format
                } else {
                        if (lastArc.getRoadDef() != this.getRoadDef())
                                flagA |= FLAG_HASNET; // not first arc: the 1 tells us that we have to read table A
                       
                }

                if (isForward)
                        flagA |= FLAG_FORWARD;
               
                offset = writer.position();
                if(log.isDebugEnabled())
                        log.debug("writing arc at", offset, ", flagA=", Integer.toHexString(flagA));

                // fetch destination class -- will have been set correctly by now
                setDestinationClass(getArcDestClass());

                // determine how to write length and curve bit
                int[] lendat = encodeLength();

                writer.put(flagA);
                if (isInternal()) {
                        // space for 14 bit node offset, written in writeSecond.
                        writer.put(flagB);
                        writer.put((byte) 0);
                } else {
                        if(indexB < 0 || indexB >= 0x3f) {
                                writer.put((byte) (flagB | 0x3f));
                                writer.put(indexB);
                        }
                        else
                                writer.put((byte) (flagB | indexB));
                }
               
                 // only write out the local net index if it is the first arc or else if newDir is set.
                if (first || lastArc.indexA != this.indexA)
                        writer.put(indexA);

                if(log.isDebugEnabled())
                        log.debug("writing length", length);
                for (int aLendat : lendat)
                        writer.put((byte) aLendat);

                if (first || lastArc.indexA != this.indexA || lastArc.isForward != isForward){
                        if (useCompactDirs){
                                // determine if we have to write direction info
                                if (compactedDir != null)
                                        writer.put(compactedDir);
                        } else
                                writer.put(directionFromDegrees(initialHeading));
                } else {
//                      System.out.println("skipped writing of initial dir");
                }
                if (haveCurve) {
                        int[] curvedat = encodeCurve();
                        for (int aCurvedat : curvedat)
                                writer.put((byte) aCurvedat);
                }
        }

        /**
         * Second pass over the nodes in this RouteCenter.
         * Node offsets are now all known, so we can write the pointers
         * for internal arcs.
         */

        public void writeSecond(ImgFileWriter writer) {
                if (!isInternal())
                        return;

                writer.position(offset + 1);
                char val = (char) (flagB << 8);
                int diff = dest.getOffsetNod1() - source.getOffsetNod1();
                assert diff < 0x2000 && diff >= -0x2000
                        : "relative pointer too large for 14 bits (source offset = " + source.getOffsetNod1() + ", dest offset = " + dest.getOffsetNod1() + ")";
                val |= diff & 0x3fff;

                // We write this big endian
                if(log.isDebugEnabled())
                        log.debug("val is", Integer.toHexString((int)val));
                writer.put((byte) (val >> 8));
                writer.put((byte) val);
        }

        /*
         * length and curve flag are stored in a variety of ways, involving
         * 1. flagA & 0x38 (3 bits)
         * 2. 1-3 bytes following the possible Table A index
         *
         * There's even more different encodings supposedly.
         */

        private int[] encodeLength() {
                int[] lendat;
                if(length < 0x300 || (length < 0x400 && haveCurve == false)) {
                        // 10 bit length optional curve
                        // clear bits
                        flagA &= ~0x38;
                       
                        if(haveCurve)
                                flagA |= 0x20;
                        flagA |= (length >> 5) & 0x18; // top two bits of length (at least one must be zero)
                        lendat = new int[1];               // one byte of data
                        lendat[0] = length;                        // bottom 8 bits of length
                        assert (flagA & 0x38) != 0x38;
                } else {
                        flagA |= 0x38;           // all three bits set
                        if(length >= (1 << 14)) {
                                // 22 bit length with curve
                                lendat = new int[3];               // three bytes of data
                                lendat[0] = 0xC0 | (length & 0x3f); // 0x80 set, 0x40 set, 6 low bits of length
                                lendat[1] = (length >> 6) & 0xff; // 8 more bits of length
                                lendat[2] = (length >> 14) & 0xff; // 8 more bits of length
                        }
                        else if(haveCurve) {
                                // 15 bit length with curve
                                lendat = new int[2];             // two bytes of data
                                lendat[0] = (length & 0x7f); // 0x80 not set, 7 low bits of length
                                lendat[1] = (length >> 7) & 0xff; // 8 more bits of length
                        }
                        else {
                                // 14 bit length no curve
                                lendat = new int[2]; // two bytes of data
                                lendat[0] = 0x80 | (length & 0x3f); // 0x80 set, 0x40 not set, 6 low bits of length
                                lendat[1] = (length >> 6) & 0xff; // 8 more bits of length
                        }
                }
                return lendat;
        }

        /**
         * Encode the curve data into a sequence of bytes.
         * Curve data contains a ratio between arc length and direct distance
         * and the direct bearing. This is typically encode in one byte,
         * for extreme ratios two bytes are used.
         */

        private int[] encodeCurve() {
                assert lengthRatio != 0;
                int[] curveData;
                int dh = directionFromDegrees(directHeading);
                if (lengthRatio >= 1 && lengthRatio <= 17) {
                        // two byte curve data neeeded
                        curveData = new int[2];
                        curveData[0] = lengthRatio;
                        curveData[1] = dh;
                       
                } else {
                        // use compacted form
                        int compactedRatio = lengthRatio / 2 - 8;
                        assert compactedRatio > 0 && compactedRatio < 8;
                        curveData = new int[1];
                        curveData[0] = (compactedRatio << 5) | ((dh >> 3) & 0x1f);
                       
                        /* check math:
                        int dhx = curveData[0] & 0x1f;
                        int decodedDirectHeading = (dhx <16) ?  dhx << 3 : -(256 - (dhx<<3));
                        if ((byte) (dh & 0xfffffff8) != (byte) decodedDirectHeading)
                                log.error("failed to encode direct heading", directHeading, dh, decodedDirectHeading);
                        int ratio = (curveData[0] & 0xe0) >> 5;
                        if (ratio != compactedRatio)
                                log.error("failed to encode length ratio", lengthRatio, compactedRatio, ratio);
                         */

                }
                return curveData;
        }

        public RoadDef getRoadDef() {
                return roadDef;
        }

        public void setForward() {
                isForward = true;
        }

        public boolean isForward() {
                return isForward;
        }

        public void setLast() {
                flagB |= FLAG_LAST_LINK;
        }

        protected void setDestinationClass(int destinationClass) {
                if(log.isDebugEnabled())
                        log.debug("setting destination class", destinationClass);
                flagA |= (destinationClass & MASK_DESTCLASS);
        }

        public void setIndirect() {
                this.isDirect = false;
               
        }

        public boolean isDirect() {
                return isDirect;
        }

        public void setMaxDestClass(int destClass) {
                if (this.maxDestClass < 0)
                        this.maxDestClass = (byte) destClass;
                else if (destClass < maxDestClass)
                        maxDestClass = (byte)destClass;
        }

        @Override
        public String toString() {
                return "RouteArc [" + (isForward ? "->":"<-") + (isDirect ? "direct":"indirect")
                                + roadDef +  " " + dest + "]";
        }

        public RouteArc getReverseArc() {
                return reverseArc;
        }

        public void setReverseArc(RouteArc reverseArc) {
                this.reverseArc = reverseArc;
        }
}