package net.sourceforge.gpstools;
/* gpsdings
* Copyright (C) 2006-2007 Moritz Ringler
* $Id: ExifLocator.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.awt.Dimension;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.File;
import java.io.BufferedOutputStream;
import java.io.PrintStream;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.text.DateFormat;
import java.util.TimeZone;
import java.util.Date;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Locale;
import java.util.Set;
import java.util.Comparator;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.zip.ZipOutputStream;
import java.util.zip.ZipEntry;
import net.sourceforge.gpstools.exif.*;
import net.sourceforge.gpstools.xmp.*;
import net.sourceforge.gpstools.utils.*;
import net.sourceforge.gpstools.gpx.*;
import net.sourceforge.gpstools.kml.GpxKMLWriter;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.MissingOptionException;
import org.apache.commons.cli.OptionBuilder;
import org.xml.sax.SAXException;
//ToDo: consider separating a) geo-locating jpegs b) producing output etc.
/**
* Geo-references jpeg files based on their exif information. ExifLocator can
* use GPS Exif data if present or the OriginalDateTime Exif tag in conjunction
* with a time-to-location mapping established from one or more gps tracklogs in
* GPX format. This class can produce several forms of output from the
* geo-referenced jpegs, and it can write the gps information back to the jpeg
* files (either their exif headers or directly into the image). ExifLocator can
* be used via its command line interface or via the API methods documented
* below.
**/
public class ExifLocator extends GPSDings {
private final TrackInterpolator interp = new TrackInterpolator();
private GpsExifWriter exifWriter = null;
private Dimension imageDimension;
private JSTemplateHandler jsHandler;
private CalibratedExifReader exifReader;
private GpsPhotoStampFormat stampformat;
private JpegTransformer transformer;
private boolean addTrackView = false;
private File targetDir;
private final static Comparator<WptType> defaultComparator = new Comparator<WptType>() {
@Override
public int compare(WptType pt1, WptType pt2) {
final Date d1 = pt1.getTime();
final Date d2 = pt2.getTime();
int result = 0;
if (d1 == null) {
result = (d2 == null) ? compareByHashCode(pt1, pt2) : -1;
} else if (d2 == null) {
result = 1;
} else {
result = d1.compareTo(d2);
if (result == 0) {
result = compareByHashCode(pt1, pt2);
}
}
return result;
}
private int compareByHashCode(WptType pt1, WptType pt2) {
return pt1.hashCode() - pt2.hashCode();
}
};
private Set<PhotoWpt> data = new TreeSet<PhotoWpt>(defaultComparator);
private List<GpxType> gpxTrks = null;
private synchronized boolean hasGPX() {
return gpxTrks != null;
}
/** Constructs a new instance of this class. */
public ExifLocator() {
// sole constructor
}
/**
* Adds the track data in <code>gpx</code> to the internal interpolator.
* This data will be used in future calls to {@link #locate locate} to find
* the geolocation of digital photographs from their time stamps. There is
* currently no way to remove GPX data that you added. You must construct a
* new ExifLocator.
*
* @param gpx
* GPX data with timed gps tracks to use for geo-locating digital
* photographs; can be read from a GPX file <code>f</code>
* through
* <code>(GpxType) org.exolab.castor.xml.Unmarshaller.unmarshal({@link net.sourceforge.gpstools.gpx.Gpx Gpx.class},
new {@link org.xml.sax.InputSource InputSource}(new {@link java.io.FileInputStream FileInputStream}(f)))</code>
*/
public synchronized void addGPX(GpxType gpx) {
if (gpx.getTrkCount() > 0) {
if (gpxTrks == null) {
gpxTrks = new ArrayList<GpxType>();
}
gpxTrks.add(gpx);
interp.addAllTracks(gpx);
}
}
/** Returns whether a track view is added to the images. */
public boolean getAddTrackView() {
return addTrackView;
}
/** Sets whether a track view is added to the images. */
public void setAddTrackView(boolean b) {
addTrackView = b;
}
/**
* (Re-)sets the collection of geolocated jpegs of this locator from
* <code>newdata</code>.
*
* @param newdata
* a map of jpeg files and their associated geolocations
* @see #setData(Set) setData(Set<PhotoWpt> data)
*/
public void setData(Map<File, Wpt> newdata) {
newdata.getClass(); // if data is null we throw a NullPointerException
data.clear();
for (File jpeg : newdata.keySet()) {
data.add(new PhotoWpt(jpeg, newdata.get(jpeg)));
}
}
/**
* (Re-)sets the collection of geolocated jpegs of this locator from
* <code>newdata</code>.
*
* @param newdata
* a set of geo-located photographs
* @see #setData(Map) setData(Map<File, Wpt> data)
* @see #locate locate
* @see #transformJpegs transformJpegs
*/
public void setData(Set<PhotoWpt> newdata) {
data.clear();
data.addAll(newdata);
}
/**
* Sets the preferred image dimension of this locator to the specified width
* and height. The specified width and height are used in javascript output
* and for scaling down jpegs in kmz mode or when a non-null target
* directory has been specified. option is specified on the command line.
*
* @param w
* the new preferred image width
* @param h
* the new preferred image height
* @see ExifLocator#print(Output)
* @see #transformJpegs transformJpegs
* @see #maybeTransformJpeg maybeTransformJpeg
*/
public void setImageDimension(int w, int h) {
if (w <= 0 || h <= 0) {
throw new IllegalArgumentException(
"Both image width and height must be larger than zero.");
}
imageDimension = new Dimension(w, h);
}
/**
* Tries to geolocate the specified jpeg file. If <code>readExifTag</code>
* is <code>true</code> this exif locator will first look for EXIF GPS
* information and use it if present. If such information is not available
* or if <code>readExifTag</code> is <code>false</code> this exif locator
* will try to find the geo-location of the jpeg by reading the
* OriginalDateTime from the jpeg's EXIF tag, converting it to UTC and using
* the UTC-to-geolocation mapping established from the gps tracks added with
* {@link #addGPX addGPX} to find the corresponding geolocation. The
* returned PhotoWpt is <em>not</em> automatically added to this exif
* locator's collection of geo-located pictures, use {@link #setData(Set)
* setData}.
*
* @param jpeg
* the jpeg file to locate. Must have an exif header with at
* least an OriginalDateTime tag.
* @param readExifTag
* whether to read EXIF GPS information if present
* @return the geo-located jpeg or <code>null</code> if the jpeg couldn't be
* geo-located.
*/
public PhotoWpt locate(File jpeg, boolean readExifTag) {
message("Locating " + jpeg);
/* read exif or xmp gps tag if it exists */
Wpt wpt = null;
if (readExifTag) {
try {
wpt = getExifReader().readGPSTag(jpeg);
} catch (Exception ex) {
handleException(null, ex);
}
}
/* try to locate the picture using its timestamp and the gpx tracks */
if ((wpt == null) && hasGPX()) {
try {
wpt = interp.getWpt(getExifReader().readOriginalUTCTime(jpeg));
} catch (org.apache.commons.math.FunctionEvaluationException fee) {
if (fee.getMessage().indexOf("Argument outside domain") >= 0) {
String d = null;
try {
d = (new SimpleDateFormat(ISO_DATE + 'Z'))
.format(getExifReader().readOriginalUTCTime(
jpeg));
} catch (Exception wonthappen) {
throw new Error(wonthappen);
}
message("The point in time this picture was taken ("
+ d
+ ") does not fall into the period covered by the current gps track data.");
}
} catch (Exception ex) {
handleException(null, ex);
}
}
/* return the result */
return (wpt == null) ? null : new PhotoWpt(jpeg, wpt);
}
/**
* Generates output.
*
* @throws IOException
* if an I/O error occurs.
*/
@Override
public void print() throws IOException {
if (output.size() == 0) {
print(Output.Text);
} else {
super.print();
}
}
/**
* Generates output of the specified type.
*
* @param out
* the type of output to produce.
* @throws IOException
* if an I/O error occurs.
*/
@Override
public void print(Output out) throws IOException {
if (data.isEmpty()) {
message("The list of geo-located pictures is empty. No output is generated.");
return;
}
File f = output.get(out);
OutputStream os = (f == null) ? System.out : new BufferedOutputStream(
new FileOutputStream(f));
try {
switch (out) {
case XML:
printGPX(os);
break;
case Text:
printPlainText(os);
break;
case MapJS:
printJS(os);
break;
case KMZ:
createKMZ(os);
break;
default:
printKML(os);
}
} finally {
if (os != System.out) {
os.close();
}
}
}
private void printKML(OutputStream out) throws IOException {
message("Writing KML");
try {
Gpx gpx = new Gpx();
for (PhotoWpt wpt : data) {
Wpt photoPoint = GPXUtils.getInstance().copyWpt(wpt);
String photo = wpt.getJpeg().getAbsolutePath();
photoPoint.setDesc("<p>" + photo + "</p><p><a href=\"" + photo
+ "\">" + "<img src=\"" + photo
+ "\" width=\"200\" height=\"150\">"
+ "<br>Show image in web browser</a></p>");
gpx.addWpt(photoPoint);
}
/* GpxKMLWriter automatically uses UTF-8 encoding */
GpxKMLWriter kw = new GpxKMLWriter(out, "GPSDings Pictures", false);
kw.writeGpx(gpx);
kw.endDocument();
} catch (SAXException ex) {
throw new IOException("SAXException: " + ex.getMessage());
}
}
private static void addJpeg(InputStream in, String entry, byte[] buffer,
ZipOutputStream zippo) throws IOException {
final int bufflength = buffer.length;
try {
zippo.putNextEntry(new ZipEntry(entry));
int count = 0;
while ((count = in.read(buffer, 0, bufflength)) != -1) {
zippo.write(buffer, 0, count);
}
} finally {
try {
zippo.closeEntry();
} finally {
in.close();
}
}
}
private synchronized JpegTransformer getJpegTransformer() {
if (transformer == null) {
transformer = new JpegTransformer();
}
return transformer;
}
/**
* Sets the stamp format that will be used to write text into geo-located
* jpegs.
*
* @param pattern
* the stamp pattern to use. If <code>pattern</code> is
* <code>null</code> text stamping will be de-activated.
* @param loc
* the locale to use for formatting numeric variables in the
* pattern. <code>null</code> is equivalent to
* {@link java.util.Locale#getDefault}.
* @see net.sourceforge.gpstools.utils.GpsPhotoStampFormat
* GpsPhotoStampFormat
* @see #transformJpegs transformJpegs
* @see #maybeTransformJpeg maybeTransformJpeg
**/
public synchronized void setStampFormat(String pattern, Locale loc) {
if (pattern == null) {
stampformat = null;
} else {
stampformat = new GpsPhotoStampFormat(pattern, loc);
}
}
/**
* Sets the target directory where transformed jpeg files will be stored.
*
* @param f
* the directory to store processed jpegs in or <code>null</code>
* if jpegs should be processed in place.
* @see #transformJpegs transformJpegs
* @see #maybeTransformJpeg maybeTransformJpeg
*/
public void setTargetDir(File f) {
targetDir = f;
}
private synchronized String getStampText(Wpt wpt) {
return (stampformat == null) ? null : stampformat.format(wpt);
}
private void createKMZ(OutputStream out) throws IOException {
message("Writing KMZ");
Map<String, String> resourceMap = new TreeMap<String, String>();
ZipOutputStream zippo = new ZipOutputStream(out);
byte[] buffer = new byte[65536];
try {
Gpx gpx = new Gpx();
JpegTransformer imgTransformer = getJpegTransformer();
for (PhotoWpt wpt : data) {
Wpt photoPoint = GPXUtils.getInstance().copyWpt(wpt);
String resourceBase = "photos/" + wpt.getJpeg().getName();
String resource = resourceBase;
String path = wpt.getJpeg().getAbsolutePath();
for (int count = 1; resourceMap.get(resource) != null
&& !resourceMap.get(resource).equals(path); count++) {
resource = resourceBase + "-" + count;
}
if (resourceMap.get(resource) == null) {
message("Adding " + path + " as " + resource);
InputStream image = imgTransformer.transform(wpt.getJpeg(),
getStampText(wpt), imageDimension);
addJpeg(image, resource, buffer, zippo);
resourceMap.put(resource, path);
}
photoPoint.setDesc("<p><img src=\"" + resource + "\">");
gpx.addWpt(photoPoint);
}
if (imgTransformer != null) {
imgTransformer.releaseResources();
}
/* GpxKMLWriter automatically uses UTF-8 encoding */
zippo.putNextEntry(new ZipEntry("photo.kml"));
BufferedOutputStream bos = new BufferedOutputStream(zippo);
GpxKMLWriter kw = new GpxKMLWriter(bos, "GPSDings Pictures", false);
kw.writeGpx(gpx);
kw.endDocument();
bos.flush();
zippo.closeEntry();
zippo.close();
} catch (SAXException ex) {
throw new IOException("SAXException: " + ex.getMessage());
}
}
private void printGPX(OutputStream out) throws IOException {
message("Writing GPX");
Gpx gpx = new Gpx();
gpx.setVersion("1.1");
gpx.setCreator("net.sourceforge.gpstools.ExifLocator");
GPXUtils gutil = GPXUtils.getInstance();
for (Wpt pt : data) {
// We must use copyWpt here to have an object of
// class Wpt.class, otherwise XML marshalling will not work
// since the marshalling descriptor is not inherited. Maybe we could
// also add an entry for PhotoWpt in .castor.cdr
// instead
gpx.addWpt(Wpt.class.equals(pt.getClass()) ? pt : gutil.copyWpt(pt));
}
GPSDings.writeGPX(gpx, out);
}
private void printPlainText(OutputStream out) throws IOException {
message("Writing plain text");
PrintStream ps = new PrintStream(out, true);
for (Wpt pt : data) {
interp.printWpt(ps, pt);
}
}
private synchronized void printJS(OutputStream out) throws IOException {
message("Writing Google Maps Javascript");
if (jsHandler == null) {
jsHandler = new JSTemplateHandler();
}
getTemplateProcessor().readWriteTemplate(
"/net/sourceforge/gpstools/res/ExiflocPhoto.js.template", out,
jsHandler);
}
/**
* Adds GPS EXIF information to the headers of the geo-located jpegs if
* there is a non-null GpsExifWriter.
*
* @see #setData(Map) setData
* @see #setData(Set) setData
* @see #setGpsExifWriter setGpsExifWriter
*/
public void maybeUpdateExif() {
if (exifWriter != null) {
File jpeg;
long time = System.nanoTime();
int n = data.size();
String type = (exifWriter instanceof XMPWriter) ? "XMP" : "Exif";
String msg = "Writing " + type + " GPS info to ";
for (PhotoWpt wpt : data) {
jpeg = wpt.getJpeg();
message(msg + jpeg);
if (!exifWriter.writeExifGpsInfo(jpeg, wpt, true)) {
message(msg + jpeg + " failed.");
n--;
}
}
if (n != 0) {
System.err.println(type + " gps info written to " + n
+ " images");
System.err.println("Average time per image "
+ (System.nanoTime() - time) / 1e9 / n + " s");
}
}
}
/**
* Copies, resizes, and/or text-stamps the geo-located jpegs.
*
* @see #setData(Map) setData
* @see #setData(Set) setData
* @see #setTargetDir setTargetDir
* @see #setImageDimension setImageDimension
* @see #setStampFormat setStampFormat
* @see #setTextCorner(String) setTextCorner
* @see #setTextCorner(int) setTextCorner
* @see #setTextAlpha setTextAlpha
* @return the set of geo-located jpegs after applying the current
* transformations. If the exif locator has a non-null
* {@link #setTargetDir targetDir directory} the file paths of the
* transformed jpegs will differ from those of the input files.
*/
public Set<PhotoWpt> transformJpegs() throws IOException {
Set<PhotoWpt> newpts = new TreeSet<PhotoWpt>(defaultComparator);
if (targetDir == null) {
newpts.addAll(data);
} else {
JpegTransformer t = getJpegTransformer();
GPXPath tracks = null;
if (hasGPX() && getAddTrackView()) {
tracks = new GPXPath(gpxTrks);
}
for (PhotoWpt wpt : data) {
PhotoWpt pt = maybeTransformJpeg(wpt, t, tracks);
if (pt != null) {
newpts.add(pt);
}
}
t.releaseResources();
}
return newpts;
}
/**
* Copies, resizes, and/or text-stamps a geo-located jpeg using the
* specified transformer.
*
* @param pt
* the geo-located jpeg to transform
* @param t
* the transformer to use
* @see #setTargetDir setTargetDir
* @see #setImageDimension setImageDimension
* @see #setStampFormat setStampFormat
* @return the geo-located jpeg after applying the current transformations.
* If the exif locator has a non-null {@link #setTargetDir target
* directory} the file path of the transformed jpeg will differ from
* that of the input file.
*/
public PhotoWpt maybeTransformJpeg(PhotoWpt pt, JpegTransformer t,
GPXPath tracks) throws IOException {
PhotoWpt result = pt;
if (targetDir != null) {
File f = pt.getJpeg();
String name = f.getName();
targetDir.mkdirs();
File target = new File(targetDir, name);
InputStream in = (tracks != null) ? t.transform(f,
getStampText(pt), imageDimension, tracks,
tracks.project(pt)) : t.transform(f, getStampText(pt),
imageDimension);
result = null;
try {
OutputStream out = new FileOutputStream(target);
byte[] buffer = new byte[4096];
int count;
while ((count = in.read(buffer, 0, 4096)) != -1) {
out.write(buffer, 0, count);
}
out.close();
result = new PhotoWpt(target, GPXUtils.getInstance()
.copyWpt(pt));
} finally {
in.close();
}
}
return result;
}
/**
* Sets the exif writer to use for writing gps information to the exif
* headers of geo-located jpegs.
*
* @param writer
* the exif writer or <code>null</code> to disable exif
* modification
* @see #maybeUpdateExif maybeUpdateExif
*/
public void setGpsExifWriter(GpsExifWriter writer) {
exifWriter = writer;
}
/**
* Command line interface.
*
* @param argv
* the command line arguments
* @see <a href="http://gpstools.sf.net/exifloc.html">GPSdings exifloc
* online documentation</a>
*/
public static void main(String[] argv) throws Exception {
// java.util.Locale.setDefault(java.util.Locale.US);
(new ExiflocCommandLine(argv)).execute();
}
private class JSTemplateHandler implements TemplateHandler {
private final GpsFormat format = GpsFormat.getInstance();
public JSTemplateHandler() {
// sole constructor
}
@Override
public void handleTemplateEvent(TemplateEvent e) throws IOException {
Appendable app = e.getDest();
String var = e.getVariable();
if ("photowpts".equals(var)) {
addPhotoWpts(app);
} else {
message("Skipping unknown variable " + var);
}
}
private void addPhotoWpts(Appendable photojs) throws IOException {
Dimension jpegSize = null;
boolean comma = false;
for (PhotoWpt wpt : data) {
File jpeg = wpt.getJpeg();
try {
// message("Getting dimensions for " + jpeg);
jpegSize = getJPEGDimension(jpeg);
// message(jpegSize);
} catch (Exception ex) {
message(ex.getMessage());
message("Cannot retrieve height and width for file " + jpeg);
continue;
}
if (comma) {
photojs.append(",\n");
} else {
comma = true;
}
String name = "'" + wpt.getName().replace('\'', ' ').trim()
+ "'";
photojs.append("{lat:");
photojs.append(format.asLatLon(wpt.getLat()));
photojs.append(",lon:").append(format.asLatLon(wpt.getLon()));
photojs.append(",desc:").append(name);
photojs.append(",img: { src: ").append(name);
photojs.append(",width:")
.append(String.valueOf(jpegSize.width));
photojs.append(",height:").append(
String.valueOf(jpegSize.height));
photojs.append("}}");
}
}
}
/**
* Returns the dimension of the specified jpeg image. This method will first
* try to read the image dimension from the jpeg header; if this fails it
* will decode the jpeg and determine the dimension of the decoded image.
*
* @param f
* a jpeg file
* @return the size of the image stored in <code>f</code>
* @throws IOException
* if an I/O error occurs or if <code>f</code> is no jpeg
* @see #getExifReader getExifReader
*/
public Dimension getJPEGDimension(File f) throws IOException {
// if(imageDimension != null){
// return imageDimension;
// }
// message("Getting dimensions for " + f);
Dimension result = null;
try {
result = getExifReader().readJPEGDimension(f);
} catch (Exception ignore) {
// handled below.
}
if (result == null) {
result = JpegTransformer.getJPEGDimensionByDecoding(f);
}
if (imageDimension != null) {
float scale = Math.min(1.0f * imageDimension.width / result.width,
1.0f * imageDimension.height / result.width);
if (scale < 1) {
result.setSize(Math.round(result.width * scale),
Math.round(result.height * scale));
}
}
// message(result);
return result;
}
/**
* Returns the exif reader used by this exif locator to read Exif
* information from digital photographs.
*
* @return the exif reader used by this exif locator
*/
public synchronized CalibratedExifReader getExifReader() {
if (exifReader == null) {
exifReader = new CalibratedExifReader();
}
return exifReader;
}
public synchronized void setExifReader(CalibratedExifReader reader) {
exifReader = reader;
}
private static class ExiflocCommandLine extends
AbstractCommandLine<ExifLocator> {
private boolean readGpsTag = false;
private boolean guessUTCOffset = false;
private GpxType xgpx = null;
public static final String OPT_GPX = "gpx";
public static final String OPT_DATE_FORMAT = "date-format";
public static final String OPT_CALIBRATE = "calibrate";
public static final String OPT_CALIBRATE_EXIF = "Calibrate";
public static final String OPT_UTC_OFFSET = "utc-offset";
public static final String OPT_TEXT = "text";
public static final String OPT_XML = "GPX";
public static final String OPT_EXIV2 = "exif";
public static final String OPT_EXIF = "Exif";
public static final String OPT_XMP = "XMP";
public static final String OPT_READ_XMP = "xmp";
public static final String OPT_JS = "javascript";
public static final String OPT_IMAGE_SIZE = "image-size";
public static final String OPT_KML = "kml";
public static final String OPT_KMZ = "kmz";
public static final String OPT_READ_GPS = "read-gps-tag";
public static final String OPT_COPY_TO = "copy-to";
public static final String OPT_STAMP = "stamp";
public static final String OPT_TRACK = "Track";
public ExiflocCommandLine(String[] argv) {
super(new ExifLocator(), argv);
}
@SuppressWarnings("static-access")
@Override
protected Options createOptions() {
Options options = super.createOptions();
options.addOption(makeOption(OPT_GPX, File.class, 1, "gpxfile"));
String longOption = OPT_DATE_FORMAT;
options.addOption(OptionBuilder.withLongOpt(longOption)
.isRequired(false).hasArg(true).withType(String.class)
// .withDescription(desc.getString(opt) + " " + ISO_DATE)
.create(longOption.charAt(0)));
OptionGroup calib = new OptionGroup();
calib.addOption(makeOption(OPT_CALIBRATE, Date.class, 1,
"dateTimeList"));
calib.addOption(makeOption(OPT_CALIBRATE_EXIF, Date.class, 1,
"jpegDateTimeList"));
calib.addOption(makeOption(OPT_UTC_OFFSET, Double.class, 1, "secs"));
options.addOptionGroup(calib);
OptionGroup ogExif = new OptionGroup();
ogExif.addOption(OptionBuilder.withLongOpt(OPT_EXIV2)
.isRequired(false).hasOptionalArg().withType(String.class)
// .withDescription(desc.getString(opt) + " " + ISO_DATE)
.create(OPT_EXIV2.charAt(0)));
// addOption(makeOption(OPT_EXIV2, String.class, 1, "file"));
ogExif.addOption(makeOption(OPT_EXIF, Boolean.class, 0, null));
ogExif.addOption(makeOption(OPT_XMP, Boolean.class, 0, null));
options.addOptionGroup(ogExif);
options.addOption(makeOption(OPT_READ_XMP, Boolean.class, 0, null));
options.addOption(makeOption(OPT_TEXT, File.class, 1, "file"));
options.addOption(makeOption(OPT_XML, File.class, 1, "file"));
options.addOption(makeOption(OPT_KML, File.class, 1, "file"));
options.addOption(makeOption('z', OPT_KMZ, File.class, 1, "file"));
options.addOption(makeOption('y', OPT_COPY_TO, File.class, 1,
"file"));
options.addOption(makeOption(OPT_STAMP, String.class, 3,
"pattern position alpha"));
options.addOption(makeOption('T', OPT_TRACK, String.class, 5,
"position size bg track marker"));
options.addOption(makeOption(OPT_JS, File.class, 1, "file"));
options.addOption(makeOption(OPT_READ_GPS, Boolean.class, 0, null));
options.addOption(makeOption(OPT_IMAGE_SIZE, Integer.class, 2,
"width height"));
return options;
}
@Override
protected CommandLine processOptions(Options options)
throws org.apache.commons.cli.ParseException, IOException,
java.text.ParseException {
CommandLine cl = super.processOptions(options);
char c;
/* readGpsTag option */
opt = OPT_READ_GPS;
c = opt.charAt(0);
if (cl.hasOption(c)) {
readGpsTag = true;
}
// OPT_READ_XMP must be processed before OPT_CALIBRATE_EXIF
/* xmp option */
opt = OPT_READ_XMP;
c = opt.charAt(0);
if (cl.hasOption(c)) {
app.setExifReader(new CalibratedExifReader(new XMPReader()));
}
/* gpx option */
opt = OPT_GPX;
c = opt.charAt(0);
if (cl.hasOption(c)) {
// throw new
// MissingOptionException("The -g, --gpx option is required.");
File fgpx = (File) cl.getOptionObject(c);
InputStream in = new BufferedInputStream(new FileInputStream(
fgpx));
try {
xgpx = GPSDings.readGPX(in);
} finally {
in.close();
}
app.addGPX(xgpx);
} else if (!readGpsTag) {
throw new MissingOptionException(
"You must use at least one of the -g and the -r options.");
}
/* output options */
opt = OPT_XML;
c = opt.charAt(0);
if (cl.hasOption(c)) {
app.enableOutput(Output.XML, cl.getOptionValue(c));
}
opt = OPT_TEXT;
c = opt.charAt(0);
if (cl.hasOption(c)) {
app.enableOutput(Output.Text, cl.getOptionValue(c));
}
opt = OPT_JS;
c = opt.charAt(0);
if (cl.hasOption(c)) {
app.enableOutput(Output.MapJS, cl.getOptionValue(c));
}
opt = OPT_KML;
c = opt.charAt(0);
if (cl.hasOption(c)) {
app.enableOutput(Output.KML, cl.getOptionValue(c));
}
c = 'z';
if (cl.hasOption(c)) {
app.enableOutput(Output.KMZ, cl.getOptionValue(c));
}
opt = OPT_EXIV2;
c = opt.charAt(0);
if (cl.hasOption(c)) {
String s = cl.getOptionValue(c);
WriterFactory factory = new WriterFactory();
GpsExifWriter writer = null;
try {
writer = (s == null) ? factory.getWriter(s,
WriterFactory.ExifWriter.Libexiv2) : factory
.getWriter(s);
} catch (InstantiationException ex) {
System.err.println(ex);
System.err
.println("Cannot load the specified Exif writer. Consider using the -E option");
System.exit(1);
}
app.setGpsExifWriter(writer);
}
opt = OPT_XMP;
c = opt.charAt(0);
if (cl.hasOption(c)) {
WriterFactory factory = new WriterFactory();
GpsExifWriter writer = null;
try {
writer = factory.getWriter(null,
WriterFactory.ExifWriter.XMP);
} catch (InstantiationException ex) {
System.err.println(ex);
System.err.println("Cannot load the XMP writer.");
System.exit(1);
}
app.setGpsExifWriter(writer);
}
opt = OPT_EXIF;
c = opt.charAt(0);
if (cl.hasOption(c)) {
try {
app.setGpsExifWriter((new WriterFactory()).getWriter(null,
WriterFactory.ExifWriter.Default));
} catch (InstantiationException neverHappens) {
throw new Error(neverHappens);
}
}
/* image-size option */
opt = OPT_IMAGE_SIZE;
c = opt.charAt(0);
if (cl.hasOption(c)) {
String[] wh = cl.getOptionValues(c);
app.setImageDimension(Integer.parseInt(wh[0]),
Integer.parseInt(wh[1]));
if (!(cl.hasOption('y') || cl.hasOption('z') || cl
.hasOption('j'))) {
System.err
.println("Warning: -i option found, but neither -y nor -z.");
System.err.println(" Jpegs will not be resized.");
}
}
/* track option */
opt = OPT_TRACK;
c = 'T';
if (cl.hasOption(c)) {
if (cl.hasOption('g')) {
if (cl.hasOption('y') || cl.hasOption('z')) {
app.setAddTrackView(true);
String[] possza = cl.getOptionValues(c);
JpegTransformer t = app.getJpegTransformer();
t.setTrackCorner(possza[0]);
t.setTrackSize(Float.parseFloat(possza[1]));
t.setTrackColor(JpegTransformer.C_BACKGROUND,
parseColor(possza[2]));
t.setTrackColor(JpegTransformer.C_TRACK,
parseColor(possza[3]));
t.setTrackColor(JpegTransformer.C_MARKER,
parseColor(possza[4]));
} else {
System.err
.println("Warning: -T option found, but neither -y nor -z.");
System.err
.println(" Jpegs will not be stamped.");
}
} else {
System.err
.println("Warning: -T option without -g has no effect.");
}
}
/* stamp option */
opt = OPT_STAMP;
c = opt.charAt(0);
if (cl.hasOption(c)) {
String[] patternposition = cl.getOptionValues(c);
String pattern = patternposition[0];
if (pattern.charAt(0) == '@') {
BufferedReader in = new BufferedReader(new FileReader(
pattern.substring(1)));
StringBuilder sb = new StringBuilder();
for (String line = in.readLine(); line != null; line = in
.readLine()) {
sb.append(line);
}
pattern = sb.toString();
}
app.setStampFormat(pattern, null);
app.getJpegTransformer().setTextCorner(patternposition[1]);
app.getJpegTransformer().setTextAlpha(
Float.parseFloat(patternposition[2]));
if (!(cl.hasOption('y') || cl.hasOption('z'))) {
System.err
.println("Warning: -s option found, but neither -y nor -z.");
System.err.println(" Jpegs will not be stamped.");
}
}
/* copyto option */
c = 'y';
if (cl.hasOption(c)) {
String path = cl.getOptionValue(c);
File f = new File(path);
if (f.exists() && !f.isDirectory()) {
throw new IllegalArgumentException(
"Illegal Argument for the -y/--copy-to option: "
+ path + "\nNot a directory.");
}
app.setTargetDir(f);
}
/* date-format option */
opt = OPT_DATE_FORMAT;
c = opt.charAt(0);
String dfpattern = cl.getOptionValue(c, ISO_DATE);
DateFormat df = new SimpleDateFormat(dfpattern);
df.setTimeZone(UTC);
// OPT_READ_XMP must be processed before OPT_CALIBRATE_EXIF
/* calibrate options */
opt = OPT_CALIBRATE;
c = opt.charAt(0);
if (cl.hasOption(c)) {
app.getExifReader().calibrateFromDateMap(
(new DateMapParser(df)).parse(cl.getOptionValue(c)));
} else if (cl.hasOption(OPT_CALIBRATE_EXIF.charAt(0))) {
/* calibrateExif option */
opt = OPT_CALIBRATE_EXIF;
c = opt.charAt(0);
app.getExifReader()
.calibrateFromDateMap(
(new FileDateMapParser(df)).parse(cl
.getOptionValue(c)));
/* utc-offset option */
} else if (cl.hasOption(OPT_UTC_OFFSET.charAt(0))) {
opt = OPT_UTC_OFFSET;
c = opt.charAt(0);
app.getExifReader()
.setUTCOffset(
Math.round(Double.parseDouble(cl
.getOptionValue(c)) * 1000));
} else {
// by default use computer time zone
guessUTCOffset = true;
}
return cl;
}
private TimeZone getGpxTimeZone() {
TimeZone result = null;
app.message("Trying to retrieve the local timezone from the geonames.org webservice.");
try {
result = GPXUtils.getInstance().getTimeZone(xgpx);
} catch (GPXUtils.GeonamesException ex) {
app.message(ex.toString());
app.message("Caused by " + ex.getCause().toString());
} catch (GPXUtils.TimeZoneBoundaryException ex) {
app.message(ex.toString());
}
return result;
}
private TimeZone getJpegTimeZone(File[] jpegs) {
TimeZone result = null;
app.message("Trying to retrieve the local timezone from the geonames.org webservice.");
GpsExifReader exr = app.getExifReader();
try {
for (File jpeg : jpegs) {
try {
WptType pt = exr.readGPSTag(jpeg);
if (pt != null) {
result = GPXUtils.getInstance().getTimeZone(pt);
break;
}
} catch (IOException ex) {
app.message(ex.toString());
}
}
} catch (GPXUtils.GeonamesException ex) {
app.message(ex.toString());
app.message("Caused by " + ex.getCause().toString());
} catch (Exception ex) {
app.message(ex.toString());
}
return result;
}
@Override
public void execute() {
processArguments();
try {
File[] jpegs = getInputFiles();
if (jpegs == null) {
System.exit(1);
}
Set<PhotoWpt> points = new TreeSet<PhotoWpt>(defaultComparator);
PhotoWpt pt;
if (guessUTCOffset) {
TimeZone tz = null;
if (xgpx != null) {
tz = getGpxTimeZone();
} else if (readGpsTag) {
tz = getJpegTimeZone(jpegs);
}
if (tz == null) {
app.message("Using computer timezone.");
tz = TimeZone.getDefault();
}
for (File jpeg : jpegs) {
try {
Date d = app.getExifReader().readOriginalTime(jpeg);
if (d != null) {
long dl = d.getTime();
int offs = tz.getOffset(dl);
dl -= offs;
offs = tz.getOffset(dl);
d = new Date(dl);
app.getExifReader().setUTCOffset(offs);
app.message("Assuming that EXIF DateTimeOriginal tags are given in "
+ tz.getDisplayName(
tz.inDaylightTime(d),
TimeZone.LONG, Locale.US) + ":");
app.message("\tUTC offset at " + d + " was "
+ offs / 1000 + " s.");
break;
}
} catch (IOException ex) {
// try next picture
}
}
}
for (File jpeg : jpegs) {
pt = app.locate(jpeg, readGpsTag);
if (pt != null) {
boolean b = points.add(pt);
if (!b) {
app.message(jpeg.getName()
+ " is already contained in the ExifLocator's dataset.");
}
}
}
app.setData(points);
app.setData(app.transformJpegs());
app.maybeUpdateExif();
app.print();
} catch (Exception ex) {
System.err.println(ex.getClass().getName() + ": "
+ ex.getMessage());
ex.printStackTrace();
System.exit(1);
}
}
@Override
public void printHelp(PrintStream out) {
try {
super.printHelp(out);
cat("exifloc.txt");
} catch (Exception ex) {
throw new Error(ex);
}
}
}
/**
* A geo-located digital photograph. The jpeg file is accessible through
* {@link #getJpeg getJpeg}, the geo-location through the diverse
* {@link net.sourceforge.gpstools.gpx.WptType Wpt} methods.
*/
public static class PhotoWpt extends Wpt {
private static final long serialVersionUID = 4758326140499795677L;
private final File jpeg;
/**
* Constructs a new PhotoWpt from the specified jpeg file and
* geo-location.
*
* @param jpeg
* a digital photograph file
* @param wpt
* the geo-information to associate with that file
*/
public PhotoWpt(File jpeg, Wpt wpt) {
this.jpeg = jpeg;
GPXUtils.getInstance().copyProperties(wpt, this);
String name = jpeg.getName();
setName(name);
setSym("Scenic Area");
name = jpeg.getPath();
setCmt(name);
setDesc(name);
}
/**
* Returns the jpeg file with the digital photograph.
*
* @return the jpeg file passed to the constructor
*/
public File getJpeg() {
return jpeg;
}
}
}