/*
* Copyright (C) 2009.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 or
* 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.
*/
package uk.me.parabola.imgfmt.app.trergn;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.BitReader;
import uk.me.parabola.imgfmt.app.BufferedImgFileReader;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.CoordNode;
import uk.me.parabola.imgfmt.app.ImgFileReader;
import uk.me.parabola.imgfmt.app.ImgReader;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.lbl.LBLFileReader;
import uk.me.parabola.imgfmt.app.lbl.POIRecord;
import uk.me.parabola.imgfmt.app.net.NETFileReader;
import uk.me.parabola.imgfmt.app.net.RoadDef;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.log.Logger;
import uk.me.parabola.util.EnhancedProperties;
/**
* The region file. Holds actual details of points and lines etc.
*
* This is the view of the file when it is being read. Use {@link RGNFile}
* for writing the file.
*
* The main focus of mkgmap is creating files, there are plenty of applications
* that read and display the data, reading is implemented only to the
* extent required to support creating the various auxiliary files etc.
*
* @author Steve Ratcliffe
*/
public class RGNFileReader extends ImgReader {
private static final Logger log = Logger.getLogger(RGNFileReader.class);
private final RGNHeader rgnHeader;
private LBLFileReader lblFile;
private NETFileReader netFile;
public RGNFileReader(ImgChannel chan) {
rgnHeader = new RGNHeader();
setHeader(rgnHeader);
setReader(new BufferedImgFileReader(chan));
rgnHeader.readHeader(getReader());
}
public void config(EnhancedProperties props) {
//config = props;
}
/**
* Get a list of all points for the given subdivision. This includes
* both the indexed points section and the points section.
*
* The numbering of the points carries through the sections.
* @param sd The subdivision that we are interested in.
* @return A list of all points for the subdiv.
*/
public List<Point> pointsForSubdiv(Subdivision sd, boolean withExtType) {
ArrayList<Point> list = new ArrayList<Point>();
if (sd.hasIndPoints() || sd.hasPoints()){
RgnOffsets rgnOffsets = getOffsets(sd);
// Even though the indexed points are after the points, the numbering
// starts with 1 for the first indexed point and carries on into the
// points section.
fetchPointsCommon(sd, rgnOffsets.getIndPointStart(), rgnOffsets.getIndPointEnd(), list);
fetchPointsCommon(sd, rgnOffsets.getPointStart(), rgnOffsets.getPointEnd(), list);
}
if (withExtType && sd.getExtTypePointsSize() > 0)
fetchPointsCommonExtType(sd, rgnHeader.getExtTypePointsOffset() + sd.getExtTypePointsOffset(), sd.getExtTypePointsSize(), list);
return list;
}
/**
* The indexed points and the points sections are both read just the same.
*/
private void fetchPointsCommon(Subdivision sd, long start, long end, List<Point> points) {
position(start);
ImgFileReader reader = getReader();
int number = points.size() + 1;
while (position() < end) {
Point p = new Point(sd);
byte t = reader.get();
int val = reader.getu3();
boolean hasSubtype = false;
if ((val & 0x800000) != 0)
hasSubtype = true;
boolean hasPoi = false;
if ((val & 0x400000) != 0)
hasPoi = true;
Label l;
int labelOffset = val & 0x3fffff;
if (hasPoi) {
POIRecord record = lblFile.fetchPoi(labelOffset);
if (record != null) {
l = record.getNameLabel();
p.setPOIRecord(record);
} else
l = lblFile.fetchLabel(0);
} else {
l = lblFile.fetchLabel(labelOffset);
}
p.setLabel(l);
p.setDeltaLong((short)reader.getChar());
p.setDeltaLat((short)reader.getChar());
if (hasSubtype) {
byte st = reader.get();
p.setType(((t & 0xff) << 8) | (st & 0xff));
//p.setHasSubtype(true);
} else {
p.setType(t & 0xff);
}
p.setNumber(number++);
points.add(p);
}
}
/**
* The indexed points and the points sections are both read just the same.
*/
private void fetchPointsCommonExtType(Subdivision sd, long start, long end, List<Point> points) {
position(start);
ImgFileReader reader = getReader();
int number = points.size() + 1;
while (position() < end) {
Point p = new Point(sd);
int type = reader.get() << 8;
byte b = reader.get();
type |= 0x10000 + (b & 0x1f);
p.setType(type);
p.setDeltaLong((short)reader.getChar());
p.setDeltaLat((short)reader.getChar());
Label l;
if ((b & 0x20) != 0 ){
int labelOffset = reader.getu3();
boolean hasPoi = (labelOffset & 0x400000) != 0;
if (hasPoi) {
POIRecord record = lblFile.fetchPoi(labelOffset);
if (record != null) {
l = record.getNameLabel();
p.setPOIRecord(record);
} else
l = lblFile.fetchLabel(0);
} else {
l = lblFile.fetchLabel(labelOffset);
}
p.setLabel(l);
}
if ((b & 0x80) != 0){
extractExtraBytes(reader, p);
}
p.setNumber(number++);
points.add(p);
}
}
/**
* Get all the lines for a given subdivision.
* @param div The subdivision we want the line from.
* @return A list of lines.
*/
public List<Polyline> linesForSubdiv(Subdivision div) {
ArrayList<Polyline> list = new ArrayList<Polyline>();
if (div.hasPolylines()){
RgnOffsets rgnOffsets = getOffsets(div);
int start = rgnOffsets.getLineStart();
int end = rgnOffsets.getLineEnd();
position(start);
while (position() < end) {
Polyline line = new Polyline(div);
readLineCommon(getReader(), div, line);
list.add(line);
}
}
if (div.getExtTypeLinesSize() > 0){
int start = rgnHeader.getExtTypeLinesOffset() + div.getExtTypeLinesOffset();
int end = start + div.getExtTypeLinesSize();
position(start);
while (position() < end) {
Polyline line = new Polyline(div);
readLineCommonExtType(getReader(), div, line);
list.add(line);
}
}
return list;
}
/**
* Get all the polygons for a given subdivision.
*/
public List<Polygon> shapesForSubdiv(Subdivision div) {
ArrayList<Polygon> list = new ArrayList<Polygon>();
if (div.hasPolygons()){
RgnOffsets rgnOffsets = getOffsets(div);
int start = rgnOffsets.getPolygonStart();
int end = rgnOffsets.getPolygonEnd();
position(start);
while (position() < end) {
Polygon line = new Polygon(div);
readLineCommon(getReader(), div, line);
list.add(line);
}
}
if (div.getExtTypeAreasSize() > 0){
int start = rgnHeader.getExtTypeAreasOffset() + div.getExtTypeAreasOffset();
int end = start + div.getExtTypeAreasSize();
position(start);
while (position() < end) {
Polygon line = new Polygon(div);
readLineCommonExtType(getReader(), div, line);
list.add(line);
}
}
return list;
}
/**
* Since polygons are pretty much like polylines in the img format the
* reading code can be shared.
*
* @param reader The reader for the img file.
* @param div The subdivision.
* @param line The line or shape that is to be populated.
*/
private void readLineCommon(ImgFileReader reader, Subdivision div, Polyline line) {
byte type = reader.get();
if (line instanceof Polygon)
line.setType(type & 0x7f);
else {
line.setType(type & 0x3f);
line.setDirection((type & 0x40) != 0);
}
int labelOffset = reader.getu3();
// Extra bit (for bit stream)
boolean extra = (labelOffset & 0x400000) != 0;
Label label;
if ((labelOffset & 0x800000) == 0) {
label = lblFile.fetchLabel(labelOffset & 0x7fffff);
} else {
int netoff = labelOffset & 0x3fffff;
labelOffset = netFile.getLabelOffset(netoff);
label = lblFile.fetchLabel(labelOffset);
RoadDef roadDef = new RoadDef(0, netoff, label.getText());
line.setRoadDef(roadDef);
}
line.setLabel(label);
line.setDeltaLong((short)reader.getChar());
line.setDeltaLat((short)reader.getChar());
int len;
if ((type & 0x80) == 0)
len = reader.get() & 0xff;
else
len = reader.getChar();
int base = reader.get();
byte[] bitstream = reader.get(len);
BitReader br = new BitReader(bitstream);
// This reads the bit stream and adds all the points found
readBitStream(br, div, line, extra, len, base);
}
/**
* Common code to read extended type lines or polygons for a given sub division.
* @param reader The reader for the img file.
* @param div The subdivision.
* @param line The line or shape that is to be populated.
*/
private void readLineCommonExtType(ImgFileReader reader, Subdivision div, Polyline line) {
int type = reader.get();
type = (type & 0xff) << 8;
byte b1 = reader.get();
boolean hasExtraBytes = (b1 & 0x80) != 0;
boolean hasLabel = (b1 & 0x20) != 0;
type |= 0x10000 + (b1 & 0x1f);
line.setType(type);
line.setDeltaLong((short)reader.getChar());
line.setDeltaLat((short)reader.getChar());
b1 = reader.get();
int len;
// one byte or two byte length field?
if ((b1 & 0x01) != 0){
len = (b1 >> 1) & 0x7f;
assert len < 0x7f;
}
else {
byte b2 = reader.get();
len = (((b2 & 0xff) << 8) + (b1 & 0xff)) >> 2;
assert len >= 0x7f;
}
--len; // the encoded value includes the base field
assert len > 0;
int base = reader.get();
byte[] bitstream = reader.get(len);
BitReader br = new BitReader(bitstream);
// This reads the bit stream and adds all the points found,
readBitStream(br, div, line, false, len, base);
if (hasLabel){
int labelOffset = reader.getu3();
Label label;
if ((labelOffset & 0x800000) == 0) {
label = lblFile.fetchLabel(labelOffset & 0x7fffff);
} else {
int netoff = labelOffset & 0x3fffff;
labelOffset = netFile.getLabelOffset(netoff);
label = lblFile.fetchLabel(labelOffset);
RoadDef roadDef = new RoadDef(0, netoff, label.getText());
line.setRoadDef(roadDef);
}
line.setLabel(label);
}
if (hasExtraBytes){
extractExtraBytes(reader, line);
}
}
/**
* Extract extra bytes.
* @param reader
*/
void extractExtraBytes(ImgFileReader reader, MapObject o){
long pos = reader.position();
StringBuilder sb = new StringBuilder();
ArrayList<Byte> bytes = new ArrayList<Byte>();
byte b1 = reader.get();
bytes.add(b1);
if ((b1 & 0xe0) != 0){
// varying length, search for 0x01 as this seems to be the terminator
do{
b1 = reader.get();
bytes.add(b1);
} while (b1 != 0x01);
} else if ((b1 & 0xa0) != 0){
bytes.add(reader.get());
bytes.add(reader.get());
}else if ((b1 & 0x80) != 0){
bytes.add(reader.get());
}
for (Byte b: bytes){
sb.append(String.format("%x", b));
}
ExtTypeAttributes eta = new ExtTypeAttributes(Collections.singletonMap("extra-bytes", sb.toString()), "data from img pos " + pos);
o.setExtTypeAttributes(eta);
}
/**
* Read the bit stream for a single line in the file.
* @param br The bit stream reader.
* @param div The subdivision that the line is in.
* @param line The line itself.
* @param extra True if there is an 'extra' bit in the stream. Used for nodes.
* @param len The length of the stream.
* @param base The base size of the deltas.
*/
private void readBitStream(BitReader br, Subdivision div, Polyline line, boolean extra, int len, int base) {
int currLat = line.getLat();
int currLon = line.getLong();
log.debug(String.format("Start point %.5f,%.5f",
Utils.toDegrees(currLat),
Utils.toDegrees(currLon)));
if (extra)
line.addCoord(new CoordNode(currLat, currLon, 0/* XXX */, false));
else
line.addCoord(new Coord(currLat, currLon));
int xbase = 2;
int n = base & 0xf;
if (n <= 9)
xbase += n;
else
xbase += (2 * n) - 9;
n = (base >>> 4) & 0xf;
int ybase = 2;
if (n <= 9)
ybase += n;
else
ybase += (2 * n) - 9;
if (len == 0)
return;
boolean xneg = false;
boolean xsame = br.get1();
if (xsame) {
xneg = br.get1();
} else
xbase++;
boolean ysame = br.get1();
boolean yneg = false;
if (ysame) {
yneg = br.get1();
} else
ybase++;
if(line.hasExtendedType()) {
br.get1();
}
if (extra) {
boolean firstextra = br.get1();
log.debug("the first extra bit is", firstextra);
}
// All is now prepared, read the actual deltas and decode them into
// proper lat/long coords.
while (br.getBitPosition() <= 8* len - ((extra ? 1:0) + xbase + ybase)) {
br.getBitPosition();
int dx;
if (xsame) {
dx = br.get(xbase);
if (xneg)
dx = -dx;
} else {
dx = br.sget2(xbase);
}
int dy;
if (ysame) {
dy = br.get(ybase);
if (yneg)
dy = -dy;
} else {
dy = br.sget2(ybase);
}
boolean isnode = false;
if (extra)
isnode = br.get1();
currLat += dy << (24 - div.getResolution());
currLon += dx << (24 - div.getResolution());
Coord coord;
if (isnode)
coord = new CoordNode(currLat, currLon, 0/* XXX */, false);
else
coord = new Coord(currLat, currLon);
line.addCoord(coord);
}
if (line instanceof Polygon){
// make sure that polygon is closed
line.addCoord(line.getPoints().get(0));
}
}
/**
* Get the offsets to the points, lines etc in RGN for the given subdiv.
* @param sd The subdivision is needed to work out the starting points.
* @return An Offsets class that allows you to obtain the offsets.
*/
private RgnOffsets getOffsets(Subdivision sd) {
int off = sd.getStartRgnPointer();
position(rgnHeader.getDataOffset() + off);
return new RgnOffsets(sd);
}
public void setLblFile(LBLFileReader lblFile) {
this.lblFile = lblFile;
}
public void setNetFile(NETFileReader netFile) {
this.netFile = netFile;
}
/**
* Class to hold the start and end points of point, lines etc within
* the area for a given subdivision in the RGN data.
*/
private class RgnOffsets {
private final int pointOffset;
private int pointEnd;
private int indPointOffset;
private int indPointEnd;
private int lineOffset;
private int lineEnd;
private int polygonOffset;
private int polygonEnd;
private final int start;
private int headerLen;
/**
* Calculate the offsets for the given subdivision.
* After this is called the position will be set after any pointers that
* exist at the beginning of the area.
*
* @param sd The subdivision.
*/
private RgnOffsets(Subdivision sd) {
ImgFileReader reader = getReader();
start = (int) position();
pointOffset = 0;
if (sd.needsIndPointPtr()) {
indPointOffset = reader.getChar();
headerLen += 2;
}
if (sd.needsPolylinePtr()) {
lineOffset = reader.getChar();
headerLen += 2;
}
if (sd.needsPolygonPtr()) {
polygonOffset = reader.getChar();
headerLen += 2;
}
if (sd.hasPoints()) {
if (sd.hasIndPoints())
pointEnd = indPointOffset;
else if (sd.hasPolylines())
pointEnd = lineOffset;
else if (sd.hasPolygons())
pointEnd = polygonOffset;
else
pointEnd = sd.getEndRgnPointer() - sd.getStartRgnPointer();
}
if (sd.hasIndPoints()) {
if (sd.hasPolylines())
indPointEnd = lineOffset;
else if (sd.hasPolygons())
indPointEnd = polygonOffset;
else
indPointEnd = sd.getEndRgnPointer() - sd.getStartRgnPointer();
}
if (sd.hasPolylines()) {
if (sd.hasPolygons())
lineEnd = polygonOffset;
else
lineEnd = sd.getEndRgnPointer() - sd.getStartRgnPointer();
}
if (sd.hasPolygons()) {
polygonEnd = sd.getEndRgnPointer() - sd.getStartRgnPointer();
}
}
public String toString() {
return String.format("rgn div offsets: %x-%x/%x-%x/%x-%x/%x-%x",
pointOffset, pointEnd, indPointOffset, indPointEnd,
lineOffset, lineEnd, polygonOffset, polygonEnd);
}
public long getPointStart() {
return pointOffset == 0 ? start + headerLen : start + pointOffset;
}
public long getPointEnd() {
return start + pointEnd;
}
public long getIndPointStart() {
return indPointOffset == 0 ? start + headerLen : start + indPointOffset;
}
public long getIndPointEnd() {
return start + indPointEnd;
}
public int getLineStart() {
return lineOffset == 0? start + headerLen: start + lineOffset;
}
public int getLineEnd() {
return start + lineEnd;
}
public int getPolygonStart() {
return polygonOffset == 0? start + headerLen: start + polygonOffset;
}
public int getPolygonEnd() {
return start + polygonEnd;
}
}
}