package net.sourceforge.gpstools.xmp;
/* gpsdings
* Copyright (C) 2007 Moritz Ringler
* $Id: XMPHandler.java 441 2010-12-13 20:04:20Z ringler $
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.Date;
import java.math.BigDecimal;
import java.text.ParseException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import net.sourceforge.gpstools.gpx.Wpt;
/** A ContentHandler for parsing the GPS information contained in an
XMP document. **/
class XMPHandler extends DefaultHandler{
private final static String XMP_EXIF_NAMESPACE = "http://ns.adobe.com/exif/1.0/";
private final static String XMP_TIFF_NAMESPACE = "http://ns.adobe.com/tiff/1.0/";
private XMPProperties xmp;
private transient String element;
private transient String namespace;
private transient StringBuilder contents;
@Override
public void startDocument(){
xmp = new XMPProperties();
element = null;
namespace = null;
contents = null;
}
@Override
public void startElement(String uri, String localname, String rawname,
Attributes atts){
if(isKnownNamespace(uri)){
element = localname;
namespace = uri;
contents = null;
} else {
element = null;
namespace = null;
contents = null;
}
}
@Override
public void characters(char[] ch, int start, int length){
if(element != null){
if(contents == null){
contents = new StringBuilder();
}
contents.append(ch, start, length);
}
}
private static boolean isKnownNamespace(String ns){
return XMP_EXIF_NAMESPACE.equals(ns) || XMP_TIFF_NAMESPACE.equals(ns);
}
private void setResultProperty(String namespace, String key, String value) throws ParseException{
try{
if(XMP_EXIF_NAMESPACE.equals(namespace)){
setExifProperty(key, value);
} else if(XMP_TIFF_NAMESPACE.equals(namespace)){
setTiffProperty(key, value);
}
} catch (NumberFormatException nfx){
ParseException px = new ParseException(nfx.getMessage(), 0);
px.initCause(nfx);
throw px;
}
}
@Override
public void endElement(String uri, String localname, String rawname) throws SAXException{
if(uri != null && uri.equals(namespace)){
if(localname.equals(element)){
try{
setResultProperty(uri, localname, contents.toString().trim());
} catch (ParseException ex){
throw new SAXException(ex);
}
contents = null;
namespace = null;
element = null;
} else {
throw new SAXException("Not well-formed:" +
"element <" + element + "> terminated by </" +
localname +">");
}
}
}
public XMPProperties getParseResult(){
return xmp;
}
/** Regular expression used to parse XMP Exif Rationals. **/
private final static Pattern RATIONAL_PATTERN = Pattern.compile("(\\d+)/(\\d+)?");
/** Parses an XMP Exif Rational. **/
private static BigDecimal parseRational(String key, String value) throws ParseException{
Matcher m = RATIONAL_PATTERN.matcher(value);
BigDecimal dresult = null;
if(m.matches()){
int numerator = Integer.parseInt(m.group(1));
int denominator = Integer.parseInt(m.group(2));
if(denominator == 0){
throw new ParseException(key + " value " + value + "has a zero denominator.",0);
}
dresult = new BigDecimal(1.0 * numerator /denominator);
} else {
throw new ParseException(key + " value " + value + "is not a Rational.",0);
}
return dresult;
}
/** Regular expression used to parse XMP Exif GPSCoordinates. **/
private final static Pattern GPS_COORDINATE_PATTERN_A =
Pattern.compile("(\\d+),(\\d+),(\\d+)([NWSE])");
/** Regular expression used to parse XMP Exif GPSCoordinates. **/
private final static Pattern GPS_COORDINATE_PATTERN_B =
Pattern.compile("(\\d+),(\\d+\\.\\d+)([NWSE])");
/** Parses an XMP Exif GPSCoordinate. **/
private static BigDecimal parseCoordinate(String key, String value) throws ParseException{
Matcher m = GPS_COORDINATE_PATTERN_A.matcher(value);
if(m.matches()){
int degrees = Integer.parseInt(m.group(1));
int minutes = Integer.parseInt(m.group(2));
int seconds = Integer.parseInt(m.group(3));
double dval = degrees + (minutes + (seconds/60.))/60.;
char hemi = m.group(4).charAt(0);
if(hemi == 'S' || hemi == 'W'){
dval = dval * -1;
}
return new BigDecimal(dval);
}
m = GPS_COORDINATE_PATTERN_B.matcher(value);
if(m.matches()){
int degrees = Integer.parseInt(m.group(1));
double minutes = Double.parseDouble(m.group(2));
double dval = degrees + minutes/60.;
char hemi = m.group(3).charAt(0);
if(hemi == 'S' || hemi == 'W'){
dval = dval * -1;
}
return new BigDecimal(dval);
}
throw new ParseException(key + " value " + value + "is not a GPSCoordinate.",0);
}
/** Regular expression used to parse XMP Exif Dates. **/
private final static Pattern DATE_PATTERN =
Pattern.compile(
"(\\d{4})" + //year: group 1
"(?:-(\\d{2})" + //month: group 2
"(?:-(\\d{2})" + //day: group 3
"(?:T(\\d{2}):(\\d{2})" + //hours and minutes: group 4 and 5
"(?::(\\d{2}(?:\\.\\d+)?)" + //seconds: group 6
")?"+
"(?:([+-]\\d{2}:\\d{2})|Z)" + //time zone: group 7
")?" +
")?" +
")?");
/** Parses an XMP Exif Date. **/
final static Calendar parseDate(String value) throws ParseException{
Matcher m = DATE_PATTERN.matcher(value);
Calendar dateTime = new GregorianCalendar();
dateTime.setTimeZone(TimeZone.getTimeZone("UTC"));
dateTime.setLenient(false);
dateTime.setTime(new Date(0l));
try{
if(m.matches()){
if( m.group(7) != null){
dateTime.setTimeZone(TimeZone.getTimeZone("GMT"+m.group(7)));
}
dateTime.set(Calendar.YEAR, Integer.parseInt(m.group(1)));
if( m.group(2) != null){
dateTime.set(Calendar.MONTH, Integer.parseInt(m.group(2)) - 1);
}
if( m.group(3) != null){
dateTime.set(Calendar.DAY_OF_MONTH, Integer.parseInt(m.group(3)));
}
if( m.group(4) != null){
dateTime.set(Calendar.HOUR_OF_DAY, Integer.parseInt(m.group(4)));
}
if( m.group(5) != null){
dateTime.set(Calendar.MINUTE, Integer.parseInt(m.group(5)));
}
if( m.group(6) != null){
float fseconds = Float.parseFloat(m.group(6));
int seconds = (int) fseconds;
dateTime.set(Calendar.SECOND, seconds);
int millis = Math.round((fseconds - seconds) * 1000);
if(millis != 0){
dateTime.set(Calendar.MILLISECOND, millis);
}
}
}
} catch (ArrayIndexOutOfBoundsException xx){
ParseException pex = new ParseException("Error parsing date " + value, 0);
pex.initCause(xx);
throw pex;
}
return dateTime;
}
/** Sets an Exif specific property from a raw XMP tag key-value-pair.*/
private void setExifProperty(String key, String value) throws ParseException{
if(key.startsWith("GPS")){
setGPSProperty(key, value);
} else if("DateTimeOriginal".equals(key)){
xmp.dateTimeOriginal = parseDate(value);
} else if("PixelXDimension".equals(key)){
xmp.setDimension(Integer.valueOf(value), null);
} else if("PixelYDimension".equals(key)){
xmp.setDimension(null,Integer.valueOf(value));
}
}
/** Sets an Exif schema for TIFF property from a raw XMP tag key-value-pair.*/
private void setTiffProperty(String key, String value) throws ParseException{
if("ImageWidth".equals(key)){
xmp.setDimension(Integer.valueOf(value), null);
} else if("ImageHeight".equals(key)){
xmp.setDimension(null, Integer.valueOf(value));
}
}
/** Sets the location Wpt properties from a raw XMP tag key-value-pair.*/
private void setGPSProperty(String key, String value) throws ParseException{
Wpt location = xmp.location;
if(key.equals("GPSAltitude")){
BigDecimal val = parseRational(key, value);
if (location.getEle() != null){
val = location.getEle().multiply(val);
}
location.setEle(val);
} else if (key.equals("GPSAltitudeRef")){
int val = Integer.parseInt(value);
switch(val){
case 0: //above sea level: nothing to do
break;
case 1: //below sea level
BigDecimal dval = new BigDecimal(-1);
if(location.getEle() == null){
location.setEle(dval);
} else {
location.setEle(dval.multiply(location.getEle()));
}
break;
default:
throw new ParseException("Illegal value " + value + " for GPSAltitudeRef.",0);
}
} else if (key.equals("GPSLatitude")){
location.setLat(parseCoordinate(key, value));
} else if (key.equals("GPSLongitude")){
location.setLon(parseCoordinate(key, value));
} else if (key.equals("GPSMapDatum")){
if(value == null || value.equals("")){
System.err.println("Warning: GPS datum missing. Assuming WGS84.");
} else if(!value.toUpperCase().replaceAll("[^0-9A-Z]", "").equals("WGS84")){
throw new ParseException("Unsupported map datum: " + value.toString() +
". Currently only the WGS84 map datum is supported.", 0);
}
} else if (key.equals("GPSVersionID")){
if(!"2.0.0.0".equals(value)){
throw new ParseException("Unsupported GPSVersionID: " + value,0);
}
} else if (key.equals("GPSTimeStamp")){
location.setTime(parseDate(value).getTime());
}
}
}