/*
* Copyright (C) 2011.
*
* 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.mkgmap.typ;
import java.io.StringReader;
import java.util.HashSet;
import java.util.Set;
import uk.me.parabola.imgfmt.app.typ.AlphaAdder;
import uk.me.parabola.imgfmt.app.typ.BitmapImage;
import uk.me.parabola.imgfmt.app.typ.ColourInfo;
import uk.me.parabola.imgfmt.app.typ.Image;
import uk.me.parabola.imgfmt.app.typ.Rgb;
import uk.me.parabola.imgfmt.app.typ.TrueImage;
import uk.me.parabola.imgfmt.app.typ.TypData;
import uk.me.parabola.imgfmt.app.typ.TypElement;
import uk.me.parabola.imgfmt.app.typ.Xpm;
import uk.me.parabola.mkgmap.scan.SyntaxException;
import uk.me.parabola.mkgmap.scan.Token;
import uk.me.parabola.mkgmap.scan.TokenScanner;
/**
* Much of the processing between lines and polygons is the same, these routines
* are shared.
*
* @author Steve Ratcliffe
*/
public class CommonSection {
private static final Set<String> seen = new HashSet<String>();
protected final TypData data;
private boolean hasXpm;
protected CommonSection(TypData data) {
this.data = data;
}
/**
* Deal with all the keys that are common to the different element types.
* Most tags are in fact the same for every element.
*
* @return True if this routine has processed the tag.
*/
protected boolean commonKey(TokenScanner scanner, TypElement current, String name, String value) {
if (name.equalsIgnoreCase("Type")) {
try {
int ival = Integer.decode(value);
if (ival >= 0x100) {
current.setType(ival >>> 8);
current.setSubType(ival & 0xff);
} else {
current.setType(ival & 0xff);
}
} catch (NumberFormatException e) {
throw new SyntaxException(scanner, "Bad number " + value);
}
} else if (name.equalsIgnoreCase("SubType")) {
try {
int ival = Integer.decode(value);
current.setSubType(ival);
} catch (NumberFormatException e) {
throw new SyntaxException(scanner, "Bad number for sub type " + value);
}
} else if (name.toLowerCase().startsWith("string")) {
try {
current.addLabel(value);
} catch (NumberFormatException e) {
throw new SyntaxException(scanner, "Bad number in " + value);
}
} else if (name.equalsIgnoreCase("Xpm")) {
Xpm xpm = readXpm(scanner, value, current.simpleBitmap());
current.setXpm(xpm);
} else if (name.equalsIgnoreCase("FontStyle")) {
int font = decodeFontStyle(value);
current.setFontStyle(font);
} else if (name.equalsIgnoreCase("CustomColor") || name.equals("ExtendedLabels")) {
// These are just noise, the appropriate flag is set if any feature is used.
} else if (name.equalsIgnoreCase("DaycustomColor")) {
current.setDayFontColor(value);
} else if (name.equalsIgnoreCase("NightcustomColor")) {
current.setNightCustomColor(value);
} else if (name.equalsIgnoreCase("Comment")) {
// a comment that is ignored.
} else {
return false;
}
return true;
}
protected int decodeFontStyle(String value) {
if (value.startsWith("NoLabel") || value.equalsIgnoreCase("nolabel")) {
return 1;
} else if (value.equalsIgnoreCase("SmallFont") || value.equalsIgnoreCase("Small")) {
return 2;
} else if (value.equalsIgnoreCase("NormalFont") || value.equalsIgnoreCase("Normal")) {
return 3;
} else if (value.equalsIgnoreCase("LargeFont") || value.equalsIgnoreCase("Large")) {
return 4;
} else if (value.equalsIgnoreCase("Default")) {
return 0;
} else {
warnUnknown("font value " + value);
return 0;
}
}
/**
* Parse the XPM header in a typ file.
*
* There are extensions compared to a regular XPM file.
*
* @param scanner Only for reporting syntax errors.
* @param info Information read from the string is stored here.
* @param header The string containing the xpm header and other extended data provided on the
* same line.
*/
private void parseXpmHeader(TokenScanner scanner, ColourInfo info, String header) {
TokenScanner s2 = new TokenScanner("string", new StringReader(header));
if (s2.checkToken("\""))
s2.nextToken();
try {
info.setWidth(s2.nextInt());
info.setHeight(s2.nextInt());
info.setNumberOfColours(s2.nextInt());
info.setCharsPerPixel(s2.nextInt());
} catch (NumberFormatException e) {
throw new SyntaxException(scanner, "Bad number in XPM header " + header);
}
}
/**
* Read the colour lines from the XPM format image.
*/
protected ColourInfo readColourInfo(TokenScanner scanner, String header) {
ColourInfo colourInfo = new ColourInfo();
parseXpmHeader(scanner, colourInfo, header);
for (int i = 0; i < colourInfo.getNumberOfColours(); i++) {
scanner.validateNext("\"");
int cpp = colourInfo.getCharsPerPixel();
Token token = scanner.nextRawToken();
String colourTag = token.getValue();
while (colourTag.length() < cpp)
colourTag += scanner.nextRawToken().getValue();
colourTag = colourTag.substring(0, cpp);
scanner.validateNext("c");
String colour = scanner.nextValue();
if (colour.charAt(0) == '#') {
colour = scanner.nextValue();
colourInfo.addColour(colourTag, new Rgb(colour));
} else if (colour.equalsIgnoreCase("none")) {
colourInfo.addTransparent(colourTag);
} else {
throw new SyntaxException(scanner, "Unrecognised colour: " + colour);
}
scanner.validateNext("\"");
readExtraColourInfo(scanner, colourInfo);
}
return colourInfo;
}
/**
* Get any keywords that are on the end of the colour line. Must not step
* over the new line boundary.
*/
private void readExtraColourInfo(TokenScanner scanner, AlphaAdder colour) {
while (!scanner.isEndOfFile()) {
Token tok = scanner.nextRawToken();
if (tok.isEol())
break;
String word = tok.getValue();
// TypWiz uses alpha, TypViewer uses "canalalpha"
if (word.endsWith("alpha")) {
scanner.validateNext("=");
String aval = scanner.nextValue();
try {
// Convert to rgba format
int alpha = Integer.decode(aval);
alpha = 255 - ((alpha<<4) + alpha);
colour.addAlpha(alpha);
} catch (NumberFormatException e) {
throw new SyntaxException(scanner, "Bad number for alpha value " + aval);
}
} // ignore everything we don't recognise.
}
}
/**
* Read the bitmap part of a XPM image.
*
* In the TYP file, XPM is used when there is not really an image, so this is not
* always called.
*
* Almost all of this routine is checking that the strings are valid. They have the
* correct length, there are quotes at the beginning and end at that each pixel tag
* is listed in the colours section.
*/
protected BitmapImage readImage(TokenScanner scanner, ColourInfo colourInfo) {
StringBuffer sb = new StringBuffer();
int width = colourInfo.getWidth();
int height = colourInfo.getHeight();
int cpp = colourInfo.getCharsPerPixel();
for (int i = 0; i < height; i++) {
String line = scanner.readLine();
if (line.isEmpty())
throw new SyntaxException(scanner, "Invalid blank line in bitmap.");
if (line.charAt(0) != '"')
throw new SyntaxException(scanner, "xpm bitmap line must start with a quote: " + line);
if (line.length() < 1 + width * cpp)
throw new SyntaxException(scanner, "short image line: " + line);
line = line.substring(1, 1+width*cpp);
sb.append(line);
// Do the syntax check, to avoid an error later when we don't have the line number any more
for (int cidx = 0; cidx < width * cpp; cidx += cpp) {
String tag = line.substring(cidx, cidx + cpp);
try {
colourInfo.getIndex(tag);
} catch (Exception e) {
throw new SyntaxException(scanner,
String.format("Tag '%s' is not one of the defined colour pixels", tag));
}
}
}
if (sb.length() != width * height * cpp) {
throw new SyntaxException(scanner, "Got " + sb.length() + " of image data, " +
"expected " + width * height * cpp);
}
return new BitmapImage(colourInfo, sb.toString());
}
/**
* The true image format is represented by one colour value for each pixel in the
* image.
*
* The colours are on several lines surrounded by double quotes.
* <pre>
* "#ff9900 #ffaa11 #feab10 #feab10"
* "#f79900 #f7aa11 #feab10 #feab20"
* ...
* </pre>
* There can be any number of colours on the same line, and the spaces are not needed.
*
* Transparency is represented by using RGBA values "#ffeeff00" or by appending alpha=N
* to the end of the colour line. If using the 'alpha=N' method, then there can be only one
* colour per line (well it is only the last colour value that is affected if more than one).
*
* <pre>
* "#ff8801" alpha=2
* </pre>
*
* The alpha values go from 0 to 15 where 0 is opaque and 15 transparent.
*/
private Image readTrueImage(TokenScanner scanner, ColourInfo colourInfo) {
int width = colourInfo.getWidth();
int height = colourInfo.getHeight();
final int[] image = new int[width * height];
int nPixels = width * height;
int count = 0;
while (count < nPixels) {
scanner.validateNext("\"");
count = readTrueImageLine(scanner, image, count);
}
if (scanner.checkToken("\"")) {
// An extra colour, so this is probably meant to be a mode=16 image.
// Remove the first pixel and shuffle the rest down, unset the alpha
// on all the transparent pixels.
int transPixel = image[0];
for (int i = 1; i < nPixels; i++) {
int pix = image[i];
if (pix == transPixel)
pix &= ~0xff;
image[i-1] = pix;
}
// Add the final pixel
scanner.validateNext("\"");
readTrueImageLine(scanner, image, nPixels-1);
}
return new TrueImage(colourInfo, image);
}
/**
* Read a single line of pixel colours.
*
* There can be one or more colours on the line and the colours are surrounded
* by quotes. The can be trailing attribute that sets the opacity of
* the final pixel.
*/
private int readTrueImageLine(TokenScanner scanner, final int[] image, int count) {
do {
scanner.validateNext("#");
String col = scanner.nextValue();
try {
int val = (int) Long.parseLong(col, 16);
if (col.length() <= 6)
val = (val << 8) + 0xff;
image[count++] = val;
} catch (NumberFormatException e) {
throw new SyntaxException(scanner, "Not a valid colour value ");
}
} while (scanner.checkToken("#"));
scanner.validateNext("\"");
// Look for any trailing alpha=N stuff.
final int lastColourIndex = count - 1;
readExtraColourInfo(scanner, new AlphaAdder() {
/**
* Add the alpha value to the last colour that was read in.
*
* @param alpha A true alpha value ie 0 is transparent, 255 opaque.
*/
public void addAlpha(int alpha) {
image[lastColourIndex] = (image[lastColourIndex] & ~0xff) | (alpha & 0xff);
}
});
return count;
}
/**
* Read an XMP image from the input scanner.
*
* Note that this is sometimes used just for colours so need to deal with
* different cases.
*/
protected Xpm readXpm(TokenScanner scanner, String header, boolean simple) {
ColourInfo colourInfo = readColourInfo(scanner, header);
String msg = colourInfo.analyseColours(simple);
if (msg != null)
throw new SyntaxException(scanner, msg);
Xpm xpm = new Xpm();
xpm.setColourInfo(colourInfo);
int height = colourInfo.getHeight();
int width = colourInfo.getWidth();
if (height > 0 && width > 0) {
colourInfo.setHasBitmap(true);
Image image;
if (colourInfo.getNumberOfColours() == 0)
image = readTrueImage(scanner, colourInfo);
else
image = readImage(scanner, colourInfo);
xpm.setImage(image);
}
hasXpm = true;
return xpm;
}
protected void warnUnknown(String name) {
if (seen.contains(name))
return;
seen.add(name);
System.out.printf("Warning: tag '%s' not known\n", name);
}
protected void validate(TokenScanner scanner) {
if (!hasXpm)
throw new SyntaxException(scanner, "No XPM tag in section");
}
}