Rev 3346 | 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 Ratcliffe
* Create date: 07-Dec-2006
*/
package uk.me.parabola.imgfmt.app.trergn;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.ImgFileReader;
import uk.me.parabola.imgfmt.app.ImgFileWriter;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.lbl.LBLFile;
import uk.me.parabola.log.Logger;
/**
* The map is divided into areas, depending on the zoom level. These are
* known as subdivisions.
*
* A subdivision 'belongs' to a zoom level and cannot be interpreted correctly
* without knowing the <i>bitsPerCoord</i> of the associated zoom level.
*
* Subdivisions also form a tree as subdivisions are further divided at
* lower levels. The subdivisions need to know their child divisions
* because this information is represented in the map.
*
* @author Steve Ratcliffe
*/
public class Subdivision {
private static final Logger log = Logger.getLogger(Subdivision.class);
private static final int MAP_POINT = 0;
private static final int MAP_INDEXED_POINT = 1;
private static final int MAP_LINE = 2;
private static final int MAP_SHAPE = 3;
private final LBLFile lblFile;
private final RGNFile rgnFile;
// The start pointer is set for read and write. The end pointer is only
// set for subdivisions that are read from a file.
private int startRgnPointer;
private int endRgnPointer;
private int lastMapElement;
// The zoom level contains the number of bits per coordinate which is
// critical for scaling quantities by.
private final Zoom zoomLevel;
private boolean hasPoints;
private boolean hasIndPoints;
private boolean hasPolylines;
private boolean hasPolygons;
private int numPolylines;
// The location of the central point, not scaled AFAIK
private final int longitude;
private final int latitude;
// The width and the height in map units scaled by the bits-per-coordinate
// that applies at the map level.
private final int width;
private final int height;
private int number;
// Set if this is the last one.
private boolean last;
private final List<Subdivision> divisions = new ArrayList<>();
private int extTypeAreasOffset;
private int extTypeLinesOffset;
private int extTypePointsOffset;
private int extTypeAreasSize;
private int extTypeLinesSize;
private int extTypePointsSize;
/**
* Subdivisions can not be created directly, use either the
* {@link #topLevelSubdivision} or {@link #createSubdivision} factory
* methods.
*
* @param ifiles The internal files.
* @param area The area this subdivision should cover.
* @param z The zoom level.
*/
private Subdivision(InternalFiles ifiles, Area area, Zoom z) {
this.lblFile = ifiles.getLblFile();
this.rgnFile = ifiles.getRgnFile();
this.zoomLevel = z;
int shift = getShift();
int mask = getMask();
// Calculate the center, move it right and up so that it lies on a point
// which is divisible by 2 ^shift
this.latitude = Utils.roundUp((area.getMinLat() + area.getMaxLat())/2, shift);
this.longitude = Utils.roundUp((area.getMinLong() + area.getMaxLong())/2, shift);
int w = 2 * (longitude - area.getMinLong());
int h = 2 * (latitude - area.getMinLat());
// encode the values for the img format
w = ((w + 1)/2 + mask) >> shift;
h = ((h + 1)/2 + mask) >> shift;
if (w > 0x7fff) {
log.warn("Subdivision width is " + w + " at " + getCenter());
w = 0x7fff;
}
if (h > 0xffff) {
log.warn("Subdivision height is " + h + " at " + getCenter());
h = 0xffff;
}
this.width = w;
this.height = h;
}
private Subdivision(Zoom z, SubdivData data) {
lblFile = null;
rgnFile = null;
zoomLevel = z;
latitude = data.getLat();
longitude = data.getLon();
this.width = data.getWidth();
this.height = data.getHeight();
startRgnPointer = data.getRgnPointer();
endRgnPointer = data.getEndRgnOffset();
int elem = data.getFlags();
if ((elem & 0x10) != 0)
setHasPoints(true);
if ((elem & 0x20) != 0)
setHasIndPoints(true);
if ((elem & 0x40) != 0)
setHasPolylines(true);
if ((elem & 0x80) != 0)
setHasPolygons(true);
}
/**
* Create a subdivision at a given zoom level.
*
* @param ifiles The RGN and LBL ifiles.
* @param area The (unshifted) area that the subdivision covers.
* @param zoom The zoom level that this division occupies.
*
* @return A new subdivision.
*/
public Subdivision createSubdivision(InternalFiles ifiles,
Area area, Zoom zoom)
{
Subdivision div = new Subdivision(ifiles, area, zoom);
zoom.addSubdivision(div);
addSubdivision(div);
return div;
}
/**
* This should be called only once per map to create the top level
* subdivision. The top level subdivision covers the whole map and it
* must be empty.
*
* @param ifiles The LBL and RGN ifiles.
* @param area The area bounded by the map.
* @param zoom The zoom level which must be the highest (least detailed)
* zoom in the map.
*
* @return The new subdivision.
*/
public static Subdivision topLevelSubdivision(InternalFiles ifiles,
Area area, Zoom zoom)
{
Subdivision div = new Subdivision(ifiles, area, zoom);
zoom.addSubdivision(div);
return div;
}
/**
* Create a subdivision that only contains the number. This is only
* used when reading cities and similar such usages that do not really
* require the full subdivision to be present.
* @param number The subdivision number.
* @return An empty subdivision. Any operation other than getting the
* subdiv number is likely to fail.
*/
public static Subdivision createEmptySubdivision(int number) {
Subdivision sd = new Subdivision(null, new SubdivData(0,0,0,0,0,0,0));
sd.setNumber(number);
return sd;
}
public static Subdivision readSubdivision(Zoom zoom, SubdivData subdivData) {
return new Subdivision(zoom, subdivData);
}
public Zoom getZoom() {
return zoomLevel;
}
/**
* Get the shift value, that is the number of bits to left shift by for
* values that need to be saved shifted in the file. Related to the
* resolution.
*
* @return The shift value. It is 24 minus the number of bits per coord.
* @see #getResolution()
*/
public final int getShift() {
return 24 - zoomLevel.getResolution();
}
/**
* Get the shift mask. The bits that will be lost due to the resolution
* shift level.
*
* @return A bit mask with the lower <i>shift</i> bits set.
*/
protected int getMask() {
return (1 << getShift()) - 1;
}
/**
* Get the resolution of this division. Resolution goes from 1 to 24
* and the higher the number the more detail there is.
*
* @return The resolution.
*/
public final int getResolution() {
return zoomLevel.getResolution();
}
/**
* Format this record to the file.
*
* @param file The file to write to.
*/
public void write(ImgFileWriter file) {
log.debug("write subdiv", latitude, longitude);
file.put3(startRgnPointer);
file.put(getType());
file.put3(longitude);
file.put3(latitude);
assert width <= 0x7fff;
assert height <= 0xffff;
file.putChar((char) (width | ((last) ? 0x8000 : 0)));
file.putChar((char) height);
if (!divisions.isEmpty()) {
file.putChar((char) getNextLevel());
}
}
public Point createPoint(String name) {
Point p = new Point(this);
Label label = lblFile.newLabel(name);
p.setLabel(label);
return p;
}
public Polyline createLine(String[] labels) {
// don't be tempted to "trim()" the name as it zaps the highway shields
Label label = lblFile.newLabel(labels[0]);
String nameSansGC = Label.stripGarminCodes(labels[0]);
Polyline pl = new Polyline(this);
pl.setLabel(label);
if(labels[1] != null) {
// ref may contain multiple ids separated by ";"
int maxSetIdx = 3;
if (labels[3] == null) {
if (labels[2] == null) {
maxSetIdx = 1;
} else {
maxSetIdx = 2;
}
}
String[] refs = Arrays.copyOfRange(labels, 1, maxSetIdx+1);
if(refs.length == 1) {
// don't bother to add a single ref that looks the
// same as the name (sans shield) because it doesn't
// change the routing directions
String tr = refs[0].trim();
String trSansGC = Label.stripGarminCodes(tr);
if(trSansGC.length() > 0 &&
!trSansGC.equalsIgnoreCase(nameSansGC)) {
pl.addRefLabel(lblFile.newLabel(tr));
}
}
else if (refs.length > 1){
// multiple refs, always add the first so that it will
// be used in routing instructions when the name has a
// shield prefix
pl.addRefLabel(lblFile.newLabel(refs[0].trim()));
// only add the remaining refs if they differ from the
// name (sans shield)
for(int i = 1; i < refs.length; ++i) {
String tr = refs[i].trim();
String trSansGC = Label.stripGarminCodes(tr);
if(trSansGC.length() > 0 &&
!trSansGC.equalsIgnoreCase(nameSansGC)) {
pl.addRefLabel(lblFile.newLabel(tr));
}
}
}
}
return pl;
}
public void setPolylineNumber(Polyline pl) {
pl.setNumber(++numPolylines);
}
public Polygon createPolygon(String name) {
Label label = lblFile.newLabel(name);
Polygon pg = new Polygon(this);
pg.setLabel(label);
return pg;
}
public void setNumber(int n) {
number = n;
}
public void setLast(boolean last) {
this.last = last;
}
public void setStartRgnPointer(int startRgnPointer) {
this.startRgnPointer = startRgnPointer;
}
public int getStartRgnPointer() {
return startRgnPointer;
}
public int getEndRgnPointer() {
return endRgnPointer;
}
public int getLongitude() {
return longitude;
}
public int getLatitude() {
return latitude;
}
public void setHasPoints(boolean hasPoints) {
this.hasPoints = hasPoints;
}
public void setHasIndPoints(boolean hasIndPoints) {
this.hasIndPoints = hasIndPoints;
}
public void setHasPolylines(boolean hasPolylines) {
this.hasPolylines = hasPolylines;
}
public void setHasPolygons(boolean hasPolygons) {
this.hasPolygons = hasPolygons;
}
public boolean hasPoints() {
return hasPoints;
}
public boolean hasIndPoints() {
return hasIndPoints;
}
public boolean hasPolylines() {
return hasPolylines;
}
public boolean hasPolygons() {
return hasPolygons;
}
/**
* Needed if it exists and is not first, ie there is a points
* section.
* @return true if pointer needed
*/
public boolean needsIndPointPtr() {
return hasIndPoints && hasPoints;
}
/**
* Needed if it exists and is not first, ie there is a points or
* indexed points section.
* @return true if pointer needed.
*/
public boolean needsPolylinePtr() {
return hasPolylines && (hasPoints || hasIndPoints);
}
/**
* As this is last in the list it is needed if it exists and there
* is another section.
* @return true if pointer needed.
*/
public boolean needsPolygonPtr() {
return hasPolygons && (hasPoints || hasIndPoints || hasPolylines);
}
public String toString() {
return "Sub" + zoomLevel + '(' + getCenter().toOSMURL() + ')';
}
/**
* Get a type that shows if this area has lines, points etc.
*
* @return A code showing what kinds of element are in this subdivision.
*/
private byte getType() {
byte b = 0;
if (hasPoints)
b |= 0x10;
if (hasIndPoints)
b |= 0x20;
if (hasPolylines)
b |= 0x40;
if (hasPolygons)
b |= 0x80;
return b;
}
/**
* Get the number of the first subdivision at the next level.
* @return The first subdivision at the next level.
*/
private int getNextLevel() {
return divisions.get(0).getNumber();
}
public boolean hasNextLevel() {
return !divisions.isEmpty();
}
public int getExtTypeAreasOffset() {
return extTypeAreasOffset;
}
public int getExtTypeLinesOffset() {
return extTypeLinesOffset;
}
public int getExtTypePointsOffset() {
return extTypePointsOffset;
}
public int getExtTypeAreasSize() {
return extTypeAreasSize;
}
public int getExtTypeLinesSize() {
return extTypeLinesSize;
}
public int getExtTypePointsSize() {
return extTypePointsSize;
}
public void startDivision() {
rgnFile.startDivision(this);
extTypeAreasOffset = rgnFile.getExtTypeAreasSize();
extTypeLinesOffset = rgnFile.getExtTypeLinesSize();
extTypePointsOffset = rgnFile.getExtTypePointsSize();
}
public void endDivision() {
extTypeAreasSize = rgnFile.getExtTypeAreasSize() - extTypeAreasOffset;
extTypeLinesSize = rgnFile.getExtTypeLinesSize() - extTypeLinesOffset;
extTypePointsSize = rgnFile.getExtTypePointsSize() - extTypePointsOffset;
}
public void writeExtTypeOffsetsRecord(ImgFileWriter file) {
file.putInt(extTypeAreasOffset);
file.putInt(extTypeLinesOffset);
file.putInt(extTypePointsOffset);
int kinds = 0;
if(extTypeAreasSize != 0)
++kinds;
if(extTypeLinesSize != 0)
++kinds;
if(extTypePointsSize != 0)
++kinds;
file.put((byte)kinds);
}
public void writeLastExtTypeOffsetsRecord(ImgFileWriter file) {
file.putInt(rgnFile.getExtTypeAreasSize());
file.putInt(rgnFile.getExtTypeLinesSize());
file.putInt(rgnFile.getExtTypePointsSize());
file.put((byte)0);
}
/**
* Read offsets for extended type data and set sizes for predecessor sub-div.
* Corresponds to {@link #writeExtTypeOffsetsRecord(ImgFileWriter)}
* @param reader the reader
* @param sdPrev the pred. sub-div or null
*/
public void readExtTypeOffsetsRecord(ImgFileReader reader,
Subdivision sdPrev) {
extTypeAreasOffset = reader.getInt();
extTypeLinesOffset = reader.getInt();
extTypePointsOffset = reader.getInt();
reader.get();
if (sdPrev != null){
sdPrev.extTypeAreasSize = extTypeAreasOffset - sdPrev.extTypeAreasOffset;
sdPrev.extTypeLinesSize = extTypeLinesOffset - sdPrev.extTypeLinesOffset;
sdPrev.extTypePointsSize = extTypePointsOffset - sdPrev.extTypePointsOffset;
}
}
/**
* Set the sizes for the extended type data. See {@link #writeLastExtTypeOffsetsRecord(ImgFileWriter)}
*/
public void readLastExtTypeOffsetsRecord(ImgFileReader reader) {
extTypeAreasSize = reader.getInt() - extTypeAreasOffset;
extTypeLinesSize = reader.getInt() - extTypeLinesOffset;
extTypePointsSize = reader.getInt() - extTypePointsOffset;
byte test = reader.get();
assert test == 0;
}
/**
* Add this subdivision as our child at the next level. Each subdivision
* can be further divided into smaller divisions. They form a tree like
* arrangement.
*
* @param sd One of our subdivisions.
*/
private void addSubdivision(Subdivision sd) {
divisions.add(sd);
}
public int getNumber() {
return number;
}
/**
* We are starting to draw the points. These must be done first.
*/
public void startPoints() {
if (lastMapElement > MAP_POINT)
throw new IllegalStateException("Points must be drawn first");
lastMapElement = MAP_POINT;
}
/**
* We are starting to draw the lines. These must be done before
* polygons.
*/
public void startIndPoints() {
if (lastMapElement > MAP_INDEXED_POINT)
throw new IllegalStateException("Indexed points must be done before lines and polygons");
lastMapElement = MAP_INDEXED_POINT;
rgnFile.setIndPointPtr();
}
/**
* We are starting to draw the lines. These must be done before
* polygons.
*/
public void startLines() {
if (lastMapElement > MAP_LINE)
throw new IllegalStateException("Lines must be done before polygons");
lastMapElement = MAP_LINE;
rgnFile.setPolylinePtr();
}
/**
* We are starting to draw the shapes. This is done last.
*/
public void startShapes() {
lastMapElement = MAP_SHAPE;
rgnFile.setPolygonPtr();
}
/**
* Convert an absolute Lat to a local, shifted value
*/
public int roundLatToLocalShifted(int absval) {
int shift = getShift();
int val = absval - getLatitude();
val += ((1 << shift) / 2);
return (val >> shift);
}
/**
* Convert an absolute Lon to a local, shifted value
*/
public int roundLonToLocalShifted(int absval) {
int shift = getShift();
int val = absval - getLongitude();
val += ((1 << shift) / 2);
return (val >> shift);
}
public Coord getCenter(){
return new Coord(getLatitude(),getLongitude());
}
/**
* Get the unshifted width of the subdivision.
* @return The true (unshifted) width.
*/
public int getWidth() {
return width << getShift();
}
/**
* Get the unshifted height of the subdivision.
* @return The true (unshifted) height.
*/
public int getHeight() {
return height << getShift();
}
}