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