Subversion Repositories mkgmap

Rev

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

/**
 * Copyright (C) 2006 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
 * Date: 24-Dec-2006
 */

package uk.me.parabola.imgfmt.app.trergn;

import java.util.List;

import uk.me.parabola.imgfmt.app.BitWriter;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;

/**
 * This class holds all of the calculations needed to encode a line into
 * the garmin format.
 */

public class LinePreparer {
        private static final Logger log = Logger.getLogger(LinePreparer.class);

        // These are our inputs.
        private final Polyline polyline;

        private boolean extraBit;
        private final boolean extTypeLine;
        private boolean xSameSign;
        private boolean xSignNegative;     // Set if all negative

        private boolean ySameSign;
        private boolean ySignNegative;     // Set if all negative

        // The base number of bits
        private int xBase;
        private int yBase;

        // The delta changes between the points.
        private int[] deltas;
        private boolean[] nodes;

        LinePreparer(Polyline line) {
                if (line.isRoad() &&
                        line.getSubdiv().getZoom().getLevel() == 0 &&
                        line.roadHasInternalNodes()) {
                        // it might be safe to write the extra bits regardless,
                        // but who knows
                        extraBit = true;
                }

                extTypeLine = line.hasExtendedType();

                polyline = line;
                calcLatLong();
                calcDeltas();
        }

        /**
         * Write the bit stream to a BitWriter and return it.
         *
         * @return A class containing the written byte stream.
         */

        public BitWriter makeBitStream(int minPointsRequired) {

                assert xBase >= 0 && yBase >= 0;

                int xbits = 2;
                if (xBase < 10)
                        xbits += xBase;
                else
                        xbits += (2 * xBase) - 9;

                int ybits = 2;
                if (yBase < 10)
                        ybits += yBase;
                else
                        ybits += (2 * yBase) - 9;

                // Note no sign included.
                if (log.isDebugEnabled())
                        log.debug("xbits", xbits, ", y=", ybits);

                // Write the bitstream
                BitWriter bw = new BitWriter();

                // Pre bit stream info
                bw.putn(xBase, 4);
                bw.putn(yBase, 4);

                bw.put1(xSameSign);
                if (xSameSign)
                        bw.put1(xSignNegative);

                bw.put1(ySameSign);
                if (ySameSign)
                        bw.put1(ySignNegative);

                if (log.isDebugEnabled()) {
                        log.debug("x same is", xSameSign, "sign is", xSignNegative);
                        log.debug("y same is", ySameSign, "sign is", ySignNegative);
                }

                if(extTypeLine) {
                        bw.put1(false);         // no extra bits required
                }

                // first extra bit always appears to be false
                // refers to the start point?
                if (extraBit)
                        bw.put1(false);

                int numPointsEncoded = 1;
                for (int i = 0; i < deltas.length; i+=2) {
                        int dx = deltas[i];
                        int dy = deltas[i + 1];
                        if (dx == 0 && dy == 0){
                                if (extraBit && nodes[i/2+1] == false && i+2 != deltas.length) // don't skip CoordNode
                                        continue;
                        }
                        ++numPointsEncoded;

                        if (log.isDebugEnabled())
                                log.debug("x delta", dx, "~", xbits);
                        assert dx >> xbits == 0 || dx >> xbits == -1;
                        if (xSameSign) {
                                bw.putn(Math.abs(dx), xbits);
                        } else {
                                // catch inadvertent output of "magic" value that has
                                // sign bit set but other bits all 0
                                assert dx >= 0 || (dx & ((1 << xbits) - 1)) != 0;
                                bw.putn(dx, xbits);
                                bw.put1(dx < 0);
                        }

                        if (log.isDebugEnabled())
                                log.debug("y delta", dy, ybits);
                        assert dy >> ybits == 0 || dy >> ybits == -1;
                        if (ySameSign) {
                                bw.putn(Math.abs(dy), ybits);
                        } else {
                                // catch inadvertent output of "magic" value that has
                                // sign bit set but other bits all 0
                                assert dy >= 0 || (dy & ((1 << ybits) - 1)) != 0;
                                bw.putn(dy, ybits);
                                bw.put1(dy < 0);
                        }
                        if (extraBit)
                                bw.put1(nodes[i/2+1]);
                }

                if (log.isDebugEnabled())
                        log.debug(bw);

                if(numPointsEncoded < minPointsRequired)
                        return null;

                return bw;
        }

        /**
         * Calculate the correct lat and long points.  They must be shifted if
         * required by the zoom level.  The point that is taken to be the
         * location is just the first point in the line.
         */

        private void calcLatLong() {
                Coord co = polyline.getPoints().get(0);

                polyline.setLatitude(co.getLatitude());
                polyline.setLongitude(co.getLongitude());
        }

        /**
         * Calculate the deltas of one point to the other.  While we are doing
         * this we must save more information about the maximum sizes, if they
         * are all the same sign etc.  This must be done separately for both
         * the lat and long values.
         */

        private void calcDeltas() {
                Subdivision subdiv = polyline.getSubdiv();
                if(log.isDebugEnabled())
                        log.debug("label offset", polyline.getLabel().getOffset());
                int shift = subdiv.getShift();
                List<Coord> points = polyline.getPoints();

                // Space to hold the deltas
                int numPointsToUse = points.size();
                if (polyline instanceof Polygon){
                        if (points.get(0).equals(points.get(points.size()-1)))
                                --numPointsToUse; // no need to write the closing point
                }
                deltas = new int[2 * (numPointsToUse - 1)];

                if (extraBit)
                        nodes = new boolean[numPointsToUse];
                boolean first = true;

                // OK go through the points
                int lastLat = 0;
                int lastLong = 0;
                boolean xDiffSign = false; // The long values have different sign
                boolean yDiffSign = false; // The lat values have different sign
                int xSign = 0;  // If all the same sign, then this 1 or -1 depending on +ve or -ve
                int ySign = 0;  // As above for lat.
                int minDx = Integer.MAX_VALUE, maxDx = 0;
                int minDy = Integer.MAX_VALUE, maxDy = 0;
                // index of first point in a series of identical coords (after shift)
                int firstsame = 0;
                for (int i = 0; i < numPointsToUse; i++) {
                        Coord co = points.get(i);

                        int lat = subdiv.roundLatToLocalShifted(co.getLatitude());
                        int lon = subdiv.roundLonToLocalShifted(co.getLongitude());
                        if (log.isDebugEnabled())
                                log.debug("shifted pos", lat, lon);
                        if (first) {
                                lastLat = lat;
                                lastLong = lon;
                                first = false;
                                continue;
                        }

                        // compute normalized differences
                        //   -2^(shift-1) <= dx, dy < 2^(shift-1)
                        // XXX: relies on the fact that java integers are 32 bit signed
                        final int offset = 8+shift;
                        int dx = (lon - lastLong) << offset >> offset;
                        int dy = (lat - lastLat) << offset >> offset;
                        assert (dx == 0 && lon != lastLong) == false: ("delta lon too large: " +  (lon - lastLong));
                        assert (dy == 0 && lat != lastLat) == false: ("delta lat too large: " +  (lat - lastLat));
                        lastLong = lon;
                        lastLat = lat;

                        if (dx != 0 || dy != 0 || (extraBit && co.getId() != 0))
                                firstsame = i;

                        /*
                         * Current thought is that the node indicator is set when
                         * the point is a node. There's a separate first extra bit
                         * that always appears to be false. The last points' extra bit
                         * is set if the point is a node and this is not the last
                         * polyline making up the road.
                         * Todo: special case the last bit
                         */

                        if (extraBit) {
                                boolean extra = false;
                                if (co.getId() != 0) {
                                        if (i < nodes.length - 1)
                                                // inner node of polyline
                                                extra = true;
                                        else
                                                // end node of polyline: set if inner
                                                // node of road
                                                extra = !polyline.isLastSegment();
                                }

                                /*
                                 * Only the first among a range of equal points
                                 * is written, so set the bit if any of the points
                                 * is a node.
                                 * Since we only write extra bits at level 0 now,
                                 * this can only happen when points in the input
                                 * data round to the same point in map units, so
                                 * it may be better to handle this in the
                                 * reader.
                                 */

                                nodes[firstsame] = nodes[firstsame] || extra;
                        }

                        // See if they can all be the same sign.
                        if (!xDiffSign) {
                                int thisSign = (dx >= 0)? 1: -1;
                                if (xSign == 0) {
                                        xSign = thisSign;
                                } else if (thisSign != xSign) {
                                        // The signs are different
                                        xDiffSign = true;
                                }
                        }
                        if (!yDiffSign) {
                                int thisSign = (dy >= 0)? 1: -1;
                                if (ySign == 0) {
                                        ySign = thisSign;
                                } else if (thisSign != ySign) {
                                        // The signs are different
                                        yDiffSign = true;
                                }
                        }

                        // find largest delta values
                        if (dx < minDx)
                                minDx = dx;
                        if (dx > maxDx)
                                maxDx = dx;
                        if (dy < minDy)
                                minDy = dy;
                        if (dy > maxDy)
                                maxDy = dy;
                       
                        // Save the deltas
                        deltas[2*(i-1)] = dx;
                        deltas[2*(i-1) + 1] = dy;
                }
                // Find the maximum number of bits required to hold the delta values.
                int xBits = Math.max(bitsNeeded(minDx), bitsNeeded(maxDx));
                int yBits = Math.max(bitsNeeded(minDy), bitsNeeded(maxDy));

                // Now we need to know the 'base' number of bits used to represent
                // the value.  In decoding you start with that number and add various
                // adjustments to get the final value.  We need to try and work
                // backwards from this.
                //
                // I don't care about getting the smallest possible file size so
                // err on the side of caution.
                //
                // Note that the sign bit is already not included so there is
                // no adjustment needed for it.

                if (log.isDebugEnabled())
                        log.debug("initial xBits, yBits", xBits, yBits);

                if (xBits < 2)
                        xBits = 2;
                int tmp = xBits - 2;
                if (tmp > 10) {
                        if ((tmp & 0x1) == 0)
                                tmp++;
                        tmp = 9 + (tmp - 9) / 2;
                }
                this.xBase = tmp;

                if (yBits < 2)
                        yBits = 2;
                tmp = yBits - 2;
                if (tmp > 10) {
                        if ((tmp & 0x1) == 0)
                                tmp++;
                        tmp = 9 + (tmp - 9) / 2;
                }
                this.yBase = tmp;

                if (log.isDebugEnabled())
                        log.debug("initial xBase, yBase", xBase, yBase);

                // Set flags for same sign etc.
                this.xSameSign = !xDiffSign;
                this.ySameSign = !yDiffSign;
                this.xSignNegative = xSign < 0;
                this.ySignNegative = ySign < 0;
        }

        /**
         * The bits needed to hold a number without truncating it.
         *
         * @param val The number for bit counting.
         * @return The number of bits required.
         */

        public static int bitsNeeded(int val) {
                int n = Math.abs(val);

                int count = val < 0? 1: 0;
                while (n != 0) {
                        n >>>= 1;
                        count++;
                }
                return count;
        }

        public boolean isExtraBit() {
                return extraBit;
        }

}