/*
* 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.lbl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import uk.me.parabola.imgfmt.app.BufferedImgFileReader;
import uk.me.parabola.imgfmt.app.ImgFile;
import uk.me.parabola.imgfmt.app.ImgFileReader;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.labelenc.CharacterDecoder;
import uk.me.parabola.imgfmt.app.labelenc.CodeFunctions;
import uk.me.parabola.imgfmt.app.labelenc.DecodedText;
import uk.me.parabola.imgfmt.app.trergn.Subdivision;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import static uk.me.parabola.imgfmt.app.Label.NULL_LABEL;
/**
* The file that holds all the labels for the map.
*
* There are also a number of sections that hold country,
* region, city, etc. records.
*
* 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 LBLFileReader extends ImgFile {
private CharacterDecoder textDecoder = CodeFunctions.getDefaultDecoder();
private final LBLHeader header = new LBLHeader();
private final Int2ObjectOpenHashMap<Label> labels = new Int2ObjectOpenHashMap<>();
private final Int2ObjectOpenHashMap<POIRecord> pois = new Int2ObjectOpenHashMap<>();
private final List<Country> countries = new ArrayList<>();
private final List<Region> regions = new ArrayList<>();
private final Int2ObjectOpenHashMap<Zip> zips = new Int2ObjectOpenHashMap<>();
private final List<City> cities = new ArrayList<>();
public LBLFileReader(ImgChannel chan) {
setHeader(header);
setReader(new BufferedImgFileReader(chan));
header.readHeader(getReader());
int offsetMultiplier = header.getOffsetMultiplier();
CodeFunctions funcs = CodeFunctions.createEncoderForLBL(
header.getEncodingType(), header.getCodePage());
textDecoder = funcs.getDecoder();
readLables(offsetMultiplier);
readCountries();
readRegions();
readCities();
readZips();
readPoiInfo();
}
/**
* Get a label by its offset in the label area.
* @param offset The offset in the label section. The offset 0 always
* is an empty string.
* @return The label including its text.
*/
public Label fetchLabel(int offset) {
Label label = labels.get(offset);
if (label == null) {
assert offset == 0 : "Invalid label offset found " + offset;
return NULL_LABEL;
}
return label;
}
/**
* Get a list of cites. This is not cached here.
* @return A list of City objects.
*/
public List<City> getCities() {
return cities;
}
public List<Country> getCountries() {
return Collections.unmodifiableList(countries);
}
public List<Region> getRegions() {
return Collections.unmodifiableList(regions);
}
public List<Zip> getZips() {
return new ArrayList<>(zips.values());
}
/**
* Return POI information.
* @param offset The offset of the poi information in the header.
* @return Returns a poi record at the given offset. Returns null if
* there isn't one at that offset (probably a bug if that does happen though...).
*/
public POIRecord fetchPoi(int offset) {
return pois.get(offset);
}
/**
* Read a cache the countries. These are used when reading cities.
*/
private void readCountries() {
ImgFileReader reader = getReader();
PlacesHeader placeHeader = header.getPlaceHeader();
countries.add(null); // 1 based indexes
int start = placeHeader.getCountriesStart();
int end = placeHeader.getCountriesEnd();
reader.position(start);
int index = 1;
while (reader.position() < end) {
int offset = reader.getu3();
Label label = fetchLabel(offset);
if (label != null) {
Country country = new Country(index);
country.setLabel(label);
countries.add(country);
}
index++;
}
}
/**
* Read an cache the regions. These are used when reading cities.
*/
private void readRegions() {
ImgFileReader reader = getReader();
PlacesHeader placeHeader = header.getPlaceHeader();
int start = placeHeader.getRegionsStart();
int end = placeHeader.getRegionsEnd();
regions.add(null);
reader.position(start);
int index = 1;
while (reader.position() < end) {
int country = reader.getChar();
int offset = reader.getu3();
Label label = fetchLabel(offset);
if (label != null) {
Region region = new Region(countries.get(country));
region.setIndex(index);
region.setLabel(label);
regions.add(region);
}
index++;
}
}
/**
* Read in the city section and cache the results here. They are needed
* to read in the POI properties section.
*/
private void readCities() {
PlacesHeader placeHeader = header.getPlaceHeader();
int start = placeHeader.getCitiesStart();
int end = placeHeader.getCitiesEnd();
ImgFileReader reader = getReader();
// Since cities are indexed starting from 1, we add a null one at index 0
reader.position(start);
int index = 1;
while (reader.position() < end) {
// First is either a label offset or a point/subdiv combo, we
// don't know until we have read further
int label = reader.getu3();
int info = reader.getChar();
City city;
if ((info & 0x4000) == 0) {
Region region = regions.get(info & 0x3fff);
city = new City(region);
} else {
Country country = countries.get(info & 0x3fff);
city = new City(country);
}
city.setIndex(index);
if ((info & 0x8000) == 0) {
city.setSubdivision(Subdivision.createEmptySubdivision(1));
Label label1 = labels.get(label & 0x3fffff);
city.setLabel(label1);
} else {
// Has subdiv/point index
int pointIndex = label & 0xff;
int subdiv = (label >> 8) & 0xffff;
city.setPointIndex((byte) pointIndex);
city.setSubdivision(Subdivision.createEmptySubdivision(subdiv));
}
cities.add(city);
index++;
}
}
/**
* Read and cache all the labels.
*
* Note: It is pretty pointless saving the whole label rather than just
* the text, except that other objects take a Label. Perhaps this can
* be changed.
*/
private void readLables(int mult) {
ImgFileReader reader = getReader();
labels.put(0, NULL_LABEL);
int start = header.getLabelStart();
int size = header.getLabelSize();
reader.position(start + mult);
int labelOffset = mult;
for (int off = mult; off <= size; off++) {
byte b = reader.get();
if (textDecoder.addByte(b)) {
labelOffset = saveLabel(labelOffset, off, mult);
// If there is an offset multiplier greater than one then padding will be used to
// ensure that the labels are on suitable boundaries. We must skip over any such padding.
while ((labelOffset & (mult - 1)) != 0) {
textDecoder.reset();
if (labelOffset <= off) {
// In the 6bit decoder, we may have already read the (first) padding byte and so
// we increment the label offset without reading anything more.
labelOffset++;
} else {
reader.get();
//noinspection AssignmentToForLoopParameter
off++;
labelOffset++;
}
}
}
}
}
/**
* We have a label and we need to save it.
*
* @param labelOffset The offset of the label we are about to save.
* @param currentOffset The current offset that last read from.
* @param multiplier The label offset multiplier.
* @return The offset of the next label.
*/
private int saveLabel(int labelOffset, int currentOffset, int multiplier) {
DecodedText encText = textDecoder.getText();
String text = encText.getText();
Label label = new Label(text);
assert (labelOffset & (multiplier - 1)) == 0;
int adustedOffset = labelOffset / multiplier;
label.setOffset(adustedOffset);
labels.put(adustedOffset, label);
// Calculate the offset of the next label. This is not always
// the current offset + 1 because there may be bytes left
// inside the decoder.
return currentOffset + 1 + encText.getOffsetAdjustment();
}
/**
* Reads the zips.
*/
private void readZips() {
ImgFileReader reader = getReader();
PlacesHeader placeHeader = header.getPlaceHeader();
int start = placeHeader.getZipsStart();
int end = placeHeader.getZipsEnd();
reader.position(start);
int zipIndex = 1;
while (reader.position() < end) {
int lblOffset = reader.get3();
Zip zip = new Zip();
zip.setLabel(fetchLabel(lblOffset));
zip.setIndex(zipIndex);
zips.put(zip.getIndex(), zip);
zipIndex++;
}
}
/**
* Read all the POI information.
* This will create a POIRecord, but we just get the name at the minute.
*
* TODO: not finished
*/
private void readPoiInfo() {
ImgFileReader reader = getReader();
PlacesHeader placeHeader = header.getPlaceHeader();
int poiGlobalFlags = placeHeader.getPOIGlobalFlags();
int start = placeHeader.getPoiPropertiesStart();
int end = placeHeader.getPoiPropertiesEnd();
reader.position(start);
PoiMasks localMask = makeLocalMask(placeHeader);
while (reader.position() < end) {
int poiOffset = position() - start;
int val = reader.getu3();
int labelOffset = val & 0x3fffff;
boolean override = (val & 0x800000) != 0;
POIRecord poi = new POIRecord();
poi.setLabel(fetchLabel(labelOffset));
// We have what we want, but now have to find the start of the
// next record as they are not fixed length.
int flags;
boolean hasStreet;
boolean hasStreetNum;
boolean hasCity;
boolean hasZip;
boolean hasPhone;
boolean hasHighwayExit;
boolean hasTides;
if (override) {
flags = reader.get();
hasStreetNum = (flags & localMask.streetNumMask) != 0;
hasStreet = (flags & localMask.streetMask) != 0;
hasCity = (flags & localMask.cityMask) != 0;
hasZip = (flags & localMask.zipMask) != 0;
hasPhone = (flags & localMask.phoneMask) != 0;
hasHighwayExit = (flags & localMask.highwayExitMask) != 0;
hasTides = (flags & localMask.tidesMask) != 0;
} else {
flags = poiGlobalFlags;
hasStreetNum = (flags & POIRecord.HAS_STREET_NUM) != 0;
hasStreet = (flags & POIRecord.HAS_STREET) != 0;
hasCity = (flags & POIRecord.HAS_CITY) != 0;
hasZip = (flags & POIRecord.HAS_ZIP) != 0;
hasPhone = (flags & POIRecord.HAS_PHONE) != 0;
hasHighwayExit = (flags & POIRecord.HAS_EXIT) != 0;
hasTides = (flags & POIRecord.HAS_TIDE_PREDICTION) != 0;
}
if (hasStreetNum) {
byte b = reader.get();
if ((b & 0x80) == 0) {
int mpoffset = (b << 16) & 0xff0000;
mpoffset |= reader.getChar() & 0xffff;
poi.setComplexStreetNumber(fetchLabel(mpoffset));
} else {
poi.setSimpleStreetNumber(reader.getBase11str(b, '-'));
}
}
if (hasStreet) {
int streetNameOffset = reader.getu3();// label for street
Label label = fetchLabel(streetNameOffset);
poi.setStreetName(label);
}
if (hasCity) {
int cityIndex;
if (placeHeader.getNumCities() > 0xFF)
cityIndex = reader.getChar();
else
cityIndex = reader.get() & 0xff;
poi.setCity(cities.get(cityIndex-1));
}
if (hasZip) {
int zipIndex;
if (placeHeader.getNumZips() > 0xff)
zipIndex = reader.getChar();
else
zipIndex = reader.get() & 0xff;
poi.setZip(zips.get(zipIndex-1));
}
if (hasPhone) {
byte b = reader.get();
if ((b & 0x80) == 0) {
// Yes this is a bit strange it is a byte followed by a char
int mpoffset = (b << 16) & 0xff0000;
mpoffset |= reader.getChar() & 0xffff;
poi.setComplexPhoneNumber(fetchLabel(mpoffset));
} else {
poi.setSimplePhoneNumber(reader.getBase11str(b, '-'));
}
}
if (hasHighwayExit) {
int lblinfo = reader.getu3();
int highwayLabelOffset = lblinfo & 0x3FFFF;
boolean indexed = (lblinfo & 0x800000) != 0;
boolean overnightParking = (lblinfo & 0x400000) != 0;
int highwayIndex = (placeHeader.getNumHighways() > 255)
? reader.getChar() : reader.get();
if (indexed) {
int eidx = (placeHeader.getNumExits() > 255) ?
reader.getChar() :
reader.get();
}
}
if (hasTides) {
System.out.println("Map has tide prediction, please implement!");
}
pois.put(poiOffset, poi);
}
}
/**
* The meaning of the bits in the local flags depends on which bits
* are set in the global flags. Hence we have to calculate the
* masks to use. These are held in an instance of PoiMasks
* @param placeHeader The label header.
* @return The masks as modified by the global flags.
*/
private PoiMasks makeLocalMask(PlacesHeader placeHeader) {
int globalPoi = placeHeader.getPOIGlobalFlags();
char mask= 0x1;
boolean hasStreetNum = (globalPoi & POIRecord.HAS_STREET_NUM) != 0;
boolean hasStreet = (globalPoi & POIRecord.HAS_STREET) != 0;
boolean hasCity = (globalPoi & POIRecord.HAS_CITY) != 0;
boolean hasZip = (globalPoi & POIRecord.HAS_ZIP) != 0;
boolean hasPhone = (globalPoi & POIRecord.HAS_PHONE) != 0;
boolean hasHighwayExit = (globalPoi & POIRecord.HAS_EXIT) != 0;
boolean hasTides = (globalPoi & POIRecord.HAS_TIDE_PREDICTION) != 0;
PoiMasks localMask = new PoiMasks();
if (hasStreetNum) {
localMask.streetNumMask = mask;
mask <<= 1;
}
if (hasStreet) {
localMask.streetMask = mask;
mask <<= 1;
}
if (hasCity) {
localMask.cityMask = mask;
mask <<= 1;
}
if (hasZip) {
localMask.zipMask = mask;
mask <<= 1;
}
if (hasPhone) {
localMask.phoneMask = mask;
mask <<= 1;
}
if (hasHighwayExit) {
localMask.highwayExitMask = mask;
mask <<= 1;
}
if (hasTides) {
localMask.tidesMask = mask;
mask <<= 1;
}
return localMask;
}
public Map<Integer, String> getLabels() {
Map<Integer, String> m = new HashMap<>();
for (Map.Entry<Integer, Label> ent : labels.entrySet()) {
m.put(ent.getKey(), ent.getValue().getText());
}
return m;
}
public int getCodePage() {
return header.getCodePage();
}
public int getSortOrderId() {
return header.getSortOrderId();
}
private class PoiMasks {
private char streetNumMask;
private char streetMask;
private char cityMask;
private char zipMask;
private char phoneMask;
private char highwayExitMask;
private char tidesMask;
}
public int getEncodingType() {
return header.getEncodingType();
}
}