/*
* 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 static uk.me.parabola.imgfmt.app.Label.NULL_LABEL;
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.Utils;
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.Section;
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 uk.me.parabola.log.Logger;
/**
* 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
) {
this(chan,
true,
0);
}
public LBLFileReader
(ImgChannel chan,
boolean fullData,
int gmpOffset
) {
setHeader
(header
);
setReader
(new BufferedImgFileReader
(chan, gmpOffset
));
header.
readHeader(getReader
());
if (!fullData
)
return;
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
Section s = placeHeader.
getCountrySection();
int start = s.
getPosition();
int end = s.
getEndPos();
int recSize = s.
getItemSize();
int unkSize = recSize -
3; // new maps may have extra data
reader.
position(start
);
int index =
1;
while (reader.
position() < end
) {
int offset = reader.
get3u();
Label label = fetchLabel
(offset
& 0x3fffff
);
if (unkSize
> 0)
reader.
get(unkSize
);
if (label
!=
null) {
Country country =
new Country
(index, 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();
Section s = placeHeader.
getRegionSection();
int start = s.
getPosition();
int end = s.
getEndPos();
int recSize = s.
getItemSize();
int unkSize = recSize -
5; // new maps may have extra data
regions.
add(null);
reader.
position(start
);
int index =
1;
while (reader.
position() < end
) {
int country = reader.
get2u();
int offset = reader.
get3u();
if (unkSize
> 0)
reader.
get(unkSize
);
Label label = fetchLabel
(offset
& 0x3fffff
);
if (label
!=
null) {
Region region =
new Region(countries.
get(country
), label
);
region.
setIndex(index
);
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();
Section s = placeHeader.
getCitySection();
int start = s.
getPosition();
int end = s.
getEndPos();
int recSize = s.
getItemSize();
int unkSize = recSize -
5; // new maps may have extra data
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.
get3u();
int info = reader.
get2u();
if (unkSize
> 0)
reader.
get(unkSize
);
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 = fetchLabel
(label
& 0x3fffff
);
city.
setLabel(label1
);
} else {
// Has subdiv/point index
int pointIndex = label
& 0xff
;
int subdiv =
(label
>> 8) & 0xffff
;
city.
setPointIndex(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((long) 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();
Section s = placeHeader.
getZipSection();
int start = s.
getPosition();
int end = s.
getEndPos();
int recSize = s.
getItemSize();
int unkSize = recSize -
3; // new maps may have extra data
reader.
position(start
);
int zipIndex =
1;
while (reader.
position() < end
) {
int lblOffset = reader.
get3u();
if (unkSize
> 0)
reader.
get(unkSize
);
Zip zip =
new Zip
(fetchLabel
(lblOffset
& 0x3fffff
));
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.
get3u();
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.
get1u();
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.
get2u();
poi.
setComplexStreetNumber(fetchLabel
(mpoffset
));
} else {
poi.
setSimpleStreetNumber(reader.
getBase11str(b,
'-'));
}
}
if (hasStreet
) {
int streetNameOffset = reader.
get3u();// label for street
Label label = fetchLabel
(streetNameOffset
);
poi.
setStreetName(label
);
}
if (hasCity
) {
int cityIndex = reader.
getNu(Utils.
numberToPointerSize(placeHeader.
getNumCities()));
poi.
setCity(cities.
get(cityIndex-
1));
}
if (hasZip
) {
int zipIndex = reader.
getNu(Utils.
numberToPointerSize(placeHeader.
getNumZips()));
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.
get2u();
poi.
setComplexPhoneNumber(fetchLabel
(mpoffset
));
} else {
poi.
setSimplePhoneNumber(reader.
getBase11str(b,
'-'));
}
}
if (hasHighwayExit
) {
int lblinfo = reader.
get3u();
int highwayLabelOffset = lblinfo
& 0x3FFFF
;
boolean indexed =
(lblinfo
& 0x800000
) !=
0;
boolean overnightParking =
(lblinfo
& 0x400000
) !=
0;
int highwayIndex = reader.
getNu(Utils.
numberToPointerSize(placeHeader.
getNumHighways()));
if (indexed
) {
int eidx = reader.
getNu(Utils.
numberToPointerSize(placeHeader.
getNumExits()));
}
}
if (hasTides
) {
Logger.
defaultLogger.
warn("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();
int 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 int streetNumMask
;
private int streetMask
;
private int cityMask
;
private int zipMask
;
private int phoneMask
;
private int highwayExitMask
;
private int tidesMask
;
}
public int getEncodingType
() {
return header.
getEncodingType();
}
}