Subversion Repositories mkgmap

Rev

Rev 3683 | 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;

        private boolean ignoreNumberOnlyNodes;

        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;
                }
                if (!line.hasHouseNumbers())
                        ignoreNumberOnlyNodes = true;

                extTypeLine = line.hasExtendedType();

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

        /**
         * Write the bit stream to a BitWriter and return it.
         * Try different values for xBase and yBase to find the one
         * that results in the shortest bit stream.
         *
         * @return A class containing the written byte stream.
         */

        public BitWriter makeShortestBitStream(int minPointsRequired) {
                BitWriter bsSimple = makeBitStream(minPointsRequired, xBase, yBase);
                if (bsSimple == null)
                        return bsSimple;
                BitWriter bsBest = bsSimple;
                int xBestBase = xBase;
                int yBestBase = yBase;
                if (xBase > 0 ||  yBase > 0){
                        if (log.isDebugEnabled())
                                log.debug("start opt:", xBase, yBase, xSameSign, xSignNegative, ySameSign, ySignNegative);
                }
                if (xBase > 0){
                        int notBetter = 0;
                        boolean xSameSignBak = xSameSign;
                        xSameSign = false;
                        for (int xTestBase = xBase-1; xTestBase >= 0; xTestBase--){
                                BitWriter bstest = makeBitStream(minPointsRequired, xTestBase, yBase);
//                              System.out.println(xBase + " "  + xTestBase + " -> " + bsBest.getBitPosition() + " " + bstest.getBitPosition());
                                if (bstest.getBitPosition() >= bsBest.getBitPosition() ){
                                        if (++notBetter >= 2)
                                                break; // give up
                                } else {
                                        xBestBase = xTestBase;
                                        bsBest = bstest;
                                        xSameSignBak = false;
                                }
                        }
                        xSameSign = xSameSignBak;
                }
                if (yBase > 0){
                        int notBetter = 0;
                        boolean ySameSignBak = ySameSign;
                        ySameSign = false;
                        for (int yTestBase = yBase-1; yTestBase >= 0; yTestBase--){
                                BitWriter bstest = makeBitStream(minPointsRequired, xBestBase, yTestBase);
//                              System.out.println(yBase + " "  + yTestBase + " -> " + bsBest.getBitPosition() + " " + bstest.getBitPosition());
                                if (bstest.getBitPosition() >= bsBest.getBitPosition()){
                                        if (++notBetter >= 2)
                                                break; // give up
                                } else {
                                        yBestBase = yTestBase;
                                        bsBest = bstest;
                                        ySameSignBak = false;
                                }
                        }
                        ySameSign = ySameSignBak;
                }
                if (xBase != xBestBase || yBestBase != yBase){
                        if (log.isInfoEnabled()){
                                if (bsSimple.getLength() > bsBest.getLength())
                                        log.info("optimizer reduced bit stream byte length from",bsSimple.getLength(),"->",bsBest.getLength(),"(" + (bsSimple.getLength()-bsBest.getLength()), " byte(s)) for",polyline.getClass().getSimpleName(),"with",polyline.getPoints().size(),"points");
                                else
                                        log.info("optimizer only reduced bit stream bit length from",bsSimple.getBitPosition(),"->",bsBest.getBitPosition(),"bits for",polyline.getClass().getSimpleName(),"with",polyline.getPoints().size(),"points, using original bit stream");
                        }
                }
                if (bsSimple.getLength() == bsBest.getLength()){
                        // if the (byte) length was not improved,
                        // prefer the bit stream that doesn't need the special "trick"
                        // to encode large values, it is assumed that this can safe a  
                        // few CPU cycles when reading the map
                        return bsSimple;
                }
                return bsBest;
        }
        /**
         * Write the bit stream to a BitWriter and return it.
         *
         * @return A class containing the written byte stream.
         */

        public BitWriter makeBitStream(int minPointsRequired, int xb, int yb) {
                assert xb >= 0 && yb >= 0;
               
                int xbits = base2Bits(xb);
                if (!xSameSign)
                        xbits++;
                int ybits = base2Bits(yb);
                if (!ySameSign)
                        ybits++;

                       
                // 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(xb, 4);
                bw.putn(yb, 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);
                        if (xSameSign) {
                                bw.putn(Math.abs(dx), xbits);
                        } else {
                                bw.sputn(dx, xbits);
                        }

                        if (log.isDebugEnabled())
                                log.debug("y delta", dy, ybits);
                        if (ySameSign) {
                                bw.putn(Math.abs(dy), ybits);
                        } else {
                                bw.sputn(dy, ybits);
                        }
                        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());
                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;
                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;
                        }

                        int dx = lon - lastLong;
                        int dy = lat - lastLat;
                        lastLong = lon;
                        lastLat = lat;
                        boolean isSpecialNode = false;
                        if (co.getId() > 0 || (co.isNumberNode() && ignoreNumberOnlyNodes == false))
                                isSpecialNode = true;
                        if (dx != 0 || dy != 0 || extraBit && isSpecialNode)
                                firstsame = i;

                        /*
                         * Current thought is that the node indicator is set when
                         * the point is a routing node or a house number 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.
                         */

                        if (extraBit) {
                                boolean extra = false;
                                if (isSpecialNode) {
                                        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;
                        }

                        // 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.
                //
                // 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);

                this.xBase = bits2Base(xBits);
                this.yBase = bits2Base(yBits);

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

                // Set flags for same sign etc.
                this.xSameSign = !(minDx < 0 && maxDx > 0);
                this.ySameSign = !(minDy < 0 && maxDy > 0);
                if (this.xSameSign)
                        this.xSignNegative = minDx < 0;
                if (this.ySameSign)
                        this.ySignNegative = minDy < 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 = 0;
                while (n != 0) {
                        n >>>= 1;
                        count++;
                }
                return count;
//              count should be equal to Integer.SIZE - Integer.numberOfLeadingZeros(Math.abs(val));

        }

        public boolean isExtraBit() {
                return extraBit;
        }

        private static int base2Bits(int base){
                int bits = 2;
                if (base < 10)
                        return bits + base;
                else
                        return bits + (2 * base) - 9;
        }
       
        private static int bits2Base(int bits){
                int base = Math.max(0, bits - 2);
                if (base > 10) {
                        if ((base & 0x1) == 0)
                                base++;
                        base = 9 + (base - 9) / 2;
                }
                return base;
        }
}