/*
* 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: 03-Dec-2006
*/
package uk.me.parabola.imgfmt.app.lbl;
import java.util.HashMap;
import java.util.Map;
import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.BufferedImgFileWriter;
import uk.me.parabola.imgfmt.app.Exit;
import uk.me.parabola.imgfmt.app.ImgFile;
import uk.me.parabola.imgfmt.app.ImgFileWriter;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.labelenc.BaseEncoder;
import uk.me.parabola.imgfmt.app.labelenc.CharacterEncoder;
import uk.me.parabola.imgfmt.app.labelenc.CodeFunctions;
import uk.me.parabola.imgfmt.app.labelenc.EncodedText;
import uk.me.parabola.imgfmt.app.srt.Sort;
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.
*
* Would be quite simple, but there are a number of sections that hold country,
* region, city, etc. records.
*
* To begin with I shall only support regular labels.
*
* @author Steve Ratcliffe
*/
public class LBLFile
extends ImgFile
{
private static final Logger log =
Logger.
getLogger(LBLFile.
class);
private CharacterEncoder textEncoder = CodeFunctions.
getDefaultEncoder();
private final Map<EncodedText,
Label> labelCache =
new HashMap<>();
private final LBLHeader lblHeader =
new LBLHeader
();
private final PlacesFile places =
new PlacesFile
();
private Sort sort
;
/** Shift value for the label offset, we always use 1 here. */
private static final int OFFSET_MULTIPLIER =
1;
/** Garmin software doesn't display longer labels */
private static final int MAX_LABEL_LEN =
170;
public LBLFile
(ImgChannel chan, Sort sort
) {
this.
sort = sort
;
lblHeader.
setSort(sort
);
lblHeader.
setOffsetMultiplier(OFFSET_MULTIPLIER
);
setHeader
(lblHeader
);
setWriter
(new BufferedImgFileWriter
(chan,
"LBL"));
position
((long) LBLHeader.
HEADER_LEN + lblHeader.
getSortDescriptionLength());
// The zero offset is for no label.
getWriter
().
put1u(0);
alignForNext
();
places.
init(this, lblHeader.
getPlaceHeader());
places.
setSort(sort
);
labelCache.
put(BaseEncoder.
NO_TEXT,
Label.
NULL_OUT_LABEL);
}
public void write
() {
writeBody
();
}
public void writePost
() {
// Now that the body is written all the required offsets will be set up
// inside the header, so we can go back and write it.
ImgFileWriter writer = getWriter
();
getHeader
().
writeHeader(writer
);
// Text can be put between the header and the body of the file.
writer.
put(Utils.
toBytes(sort.
getDescription()));
writer.
put1u(0);
assert writer.
position() == LBLHeader.
HEADER_LEN + lblHeader.
getSortDescriptionLength();
}
private void writeBody
() {
// The label section has already been written, but we need to record
// its size before doing anything else.
lblHeader.
setLabelSize(getWriter
().
position() -
(LBLHeader.
HEADER_LEN + lblHeader.
getSortDescriptionLength()));
places.
write(getWriter
());
}
public void setCharacterType
(String cs,
boolean forceUpper
) {
log.
info("encoding type " + cs
);
CodeFunctions cfuncs = CodeFunctions.
createEncoderForLBL(cs
);
lblHeader.
setEncodingType(cfuncs.
getEncodingType());
textEncoder = cfuncs.
getEncoder();
if (forceUpper
&& textEncoder
instanceof BaseEncoder
) {
BaseEncoder baseEncoder =
(BaseEncoder
) textEncoder
;
baseEncoder.
setUpperCase(true);
}
}
public void setEncoder
(int encodingType,
int codepage
) {
CodeFunctions cfuncs = CodeFunctions.
createEncoderForLBL(encodingType, codepage
);
lblHeader.
setEncodingType(cfuncs.
getEncodingType());
textEncoder = cfuncs.
getEncoder();
}
/**
* Add a new label with the given text. Labels are shared, so that identical
* text is always represented by the same label.
*
* @param text The text of the label
* @return A reference to the created label
*/
public Label newLabel
(String text
) {
return newLabel
(text,
true);
}
/**
* Add a new label with the given text. Labels are shared, so that identical
* text is always represented by the same label.
*
* @param text The text of the label
* @param cutToMaxLen if true, cut labels to max length
* @return A reference to the created label
*/
public Label newLabel
(String text,
boolean cutToMaxLen
) {
if (text
!=
null && cutToMaxLen
&& text.
length() > MAX_LABEL_LEN
) {
int trimmedLen
;
try {
trimmedLen = text.
offsetByCodePoints(0, MAX_LABEL_LEN
);
} catch (IndexOutOfBoundsException e
) {
trimmedLen =
0; // short enough
}
if (trimmedLen
> 0) {
do { // logic elsewhere removes leading, trailing and multiple spaces, but now might have trailing; remove multiple anyway
--trimmedLen
;
if (text.
charAt(trimmedLen
) !=
' ')
break;
} while (trimmedLen
> 0);
text = text.
substring(0, trimmedLen +
1);
}
}
EncodedText encodedText = textEncoder.
encodeText(text
);
Label l = labelCache.
get(encodedText
);
if (l ==
null) {
l =
new Label(encodedText.
getChars());
labelCache.
put(encodedText, l
);
l.
setOffset(getNextLabelOffset
());
if (encodedText.
getLength() > 0)
getWriter
().
put(encodedText.
getCtext(),
0, encodedText.
getLength());
alignForNext
();
if (l.
getOffset() > 0x3fffff
)
throw new MapFailedException
("Overflow of LBL section");
}
return l
;
}
/**
* Align for the next label.
*
* Only has any effect when offsetMultiplier is not zero.
*/
private void alignForNext
() {
// Align ready for next label
while ((getCurrentLabelOffset
() & ((1 << OFFSET_MULTIPLIER
) -
1)) !=
0)
getWriter
().
put1u(0);
}
private int getNextLabelOffset
() {
return getCurrentLabelOffset
() >> OFFSET_MULTIPLIER
;
}
private int getCurrentLabelOffset
() {
return position
() -
(LBLHeader.
HEADER_LEN + lblHeader.
getSortDescriptionLength());
}
public POIRecord createPOI
(String name
) {
return places.
createPOI(name
);
}
public POIRecord createExitPOI
(String name, Exit exit
) {
return places.
createExitPOI(name, exit
);
}
public void createPOIIndex
(String name,
int poiIndex, Subdivision group,
int type
) {
places.
createPOIIndex(name, poiIndex, group, type
);
}
public Country createCountry
(String name,
String abbr
) {
return places.
createCountry(name, abbr
);
}
public Region createRegion
(Country country,
String region,
String abbr
) {
return places.
createRegion(country, region, abbr
);
}
public City createCity
(Region region,
String city,
boolean unique
) {
return places.
createCity(region, city, unique
);
}
public City createCity
(Country country,
String city,
boolean unique
) {
return places.
createCity(country, city, unique
);
}
public Zip createZip
(String code
) {
return places.
createZip(code
);
}
public Highway createHighway
(Region region,
String name
) {
return places.
createHighway(region, name
);
}
public ExitFacility createExitFacility
(int type,
char direction,
int facilities,
String description,
boolean last
) {
return places.
createExitFacility(type, direction, facilities, description, last
);
}
public void allPOIsDone
() {
places.
allPOIsDone();
}
public void setSort
(Sort sort
) {
this.
sort = sort
;
lblHeader.
setSort(sort
);
places.
setSort(sort
);
}
public int numCities
() {
return places.
numCities();
}
public int numZips
() {
return places.
numZips();
}
public int numHighways
() {
return places.
numHighways();
}
public int numExitFacilities
() {
return places.
numExitFacilities();
}
public int getCodePage
() {
return lblHeader.
getCodePage();
}
}