/** * 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; } }