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