/*
* 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<>();
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 ("Type".
equalsIgnoreCase(name
)) {
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 ("SubType".
equalsIgnoreCase(name
)) {
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 ("Xpm".
equalsIgnoreCase(name
)) {
Xpm xpm = readXpm
(scanner, value, current.
simpleBitmap());
current.
setXpm(xpm
);
} else if ("FontStyle".
equalsIgnoreCase(name
)) {
int font = decodeFontStyle
(value
);
current.
setFontStyle(font
);
} else if ("CustomColor".
equalsIgnoreCase(name
) ||
"ExtendedLabels".
equals(name
)) {
// These are just noise, the appropriate flag is set if any feature is used.
} else if ("DaycustomColor".
equalsIgnoreCase(name
)) {
current.
setDayFontColor(value
);
} else if ("NightcustomColor".
equalsIgnoreCase(name
)) {
current.
setNightCustomColor(value
);
} else if ("Comment".
equalsIgnoreCase(name
)) {
// a comment that is ignored.
} else {
return false;
}
return true;
}
protected int decodeFontStyle
(String value
) {
if (value.
startsWith("NoLabel") ||
"nolabel".
equalsIgnoreCase(value
)) {
return 1;
} else if ("SmallFont".
equalsIgnoreCase(value
) ||
"Small".
equalsIgnoreCase(value
)) {
return 2;
} else if ("NormalFont".
equalsIgnoreCase(value
) ||
"Normal".
equalsIgnoreCase(value
)) {
return 3;
} else if ("LargeFont".
equalsIgnoreCase(value
) ||
"Large".
equalsIgnoreCase(value
)) {
return 4;
} else if ("Default".
equalsIgnoreCase(value
)) {
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 static 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 ("none".
equalsIgnoreCase(colour
)) {
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 static 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 static 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
);
xpmCheck
(scanner, colourInfo
);
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");
}
/**
* Check the colourInfo against any restrictions that apply to the element type.
*
* Subtypes make checks as appropriate, there are no common restrictions.
*/
protected void xpmCheck
(TokenScanner scanner, ColourInfo colourInfo
) {
}
}