package net.sourceforge.gpstools.exif;
/* gpsdings
* Copyright (C) 2006 Moritz Ringler
* $Id: CalibratedExifReader.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.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.Date;
import java.util.Collections;
import java.util.TreeMap;
import java.util.SortedMap;
import net.sourceforge.gpstools.gpx.Wpt;
import org.apache.commons.math.FunctionEvaluationException;
import net.sourceforge.gpstools.math.LinearFunction;
import org.apache.commons.math.analysis.SplineInterpolator;
import org.apache.commons.math.analysis.UnivariateRealFunction;
/** Adds a time calibration to a GPSExifReader that allows to read dates
in UTC. This addresses two problems: A. The camera may be off with respect
to the correct time for the time zone it is in. B. The time zone is not stored
in Exif.
**/
public class CalibratedExifReader implements GpsExifReader{
private static final UnivariateRealFunction IDENTITY = LinearFunction.withUnitySlope(0.0);
private static final Long LONG_ZERO = new Long(0l);
private static class TimeCalibrator extends SplineInterpolator{
private static class TimeCalibration implements UnivariateRealFunction{
private final double minx;
private final double minOffset;
private final double maxx;
private final double maxOffset;
private final UnivariateRealFunction inter;
public TimeCalibration(double minX, double minY, double maxX, double maxY, UnivariateRealFunction interpol){
minx = minX;
maxx = maxX;
minOffset = minY - minX;
maxOffset = maxY - maxX;
inter = interpol;
}
@Override
public double value(double x) throws FunctionEvaluationException{
double result = 0;
if( x < minx){
result = x + minOffset;
} else if (x > maxx){
result = x + maxOffset;
} else {
result = inter.value(x);
}
return result;
}
}
@Override
public UnivariateRealFunction interpolate(double[] x, double[] y){
final int n = x.length;
if(y.length != n || n == 0){
throw new IllegalArgumentException("Both arrays must have the same length > 0");
}
double minX = x[0];
double minY = y[0];
double maxX = x[0];
double maxY = y[0];
for(int i=1; i< n; i++){
if(x[i] < minX){
minX = x[i];
minY = y[i];
} else if (x[i] > maxX){
maxX = x[i];
maxY = y[i];
}
}
UnivariateRealFunction result = null;
switch(n){
case 1:
result = LinearFunction.withUnitySlope(y[0] - x[0]);
break;
case 2:
if(minX == maxX){
throw new IllegalArgumentException("Identical x-values.");
}
double y0 = (maxX * minY - minX * maxY)/(maxX - minX);
double m = (maxY - minY)/(maxX - minX);
result = new TimeCalibration(minX, minY, maxX, maxY, new LinearFunction(y0, m));
break;
default:
result = new TimeCalibration(minX, minY, maxX, maxY, super.interpolate(x, y));
}
return result;
}
}
private UnivariateRealFunction fCalib = IDENTITY;
private final GpsExifReader delegate;
private final SortedMap<Long, Long> calibration =
Collections.synchronizedSortedMap(new TreeMap<Long, Long>());
private final Object sync = new Object();
/** Constructs a new CalibratedExifReader with an new
GpsExifReader delegate of unspecified type.
**/
public CalibratedExifReader(){
this((new ReaderFactory()).getReader());
}
/** Constructs a new CalibratedExifReader that uses the
specified GpsExifReader delegate to access raw Exif information.
**/
public CalibratedExifReader(GpsExifReader delegate){
this.delegate = delegate;
calibration.put(LONG_ZERO, LONG_ZERO);
}
/** Returns the OriginalDateTime from the exif header of the jpeg
file adjusted to UTC using the current time calibration. */
public Date readOriginalUTCTime(File jpeg) throws IOException, ParseException, FunctionEvaluationException{
return adjustDate(readOriginalTime(jpeg));
}
/** Adjusts the Date d to UTC using the current time calibration. **/
protected Date adjustDate(Date d) throws FunctionEvaluationException{
synchronized(sync){
long millis = d.getTime();
millis = Math.round(fCalib.value(millis));
d.setTime(millis);
}
return d;
}
/** Calibrates this exif reader using a linear calibration function
with unity slope. The time returned by readOriginalUTCTime(jpeg) will be
<pre> result = readOriginalTime(jpeg) - millis </pre>
**/
public void setUTCOffset(final long millis){
synchronized(sync){
fCalib = LinearFunction.withUnitySlope(- millis);
calibration.clear();
calibration.put(LONG_ZERO, -millis);
}
}
/** Calibrates this exif reader by interpolating
calibration points supplied as two Date arrays of equal length.
@throws IllegalArgumentException if cameraTimes or utcTimes is
empty or if they have unequal lengths
@throws NullPointerException if any of the arguments is <code>null</code>
**/
public void calibrate(Date[] cameraTimes, Date[] utcTimes){
final int nc = cameraTimes.length;
if( utcTimes.length != nc){
throw new IllegalArgumentException("Both Date arrays must have the same length.");
}
double[] camera = new double[nc];
double[] utc = new double[nc];
synchronized(sync){
long lcam, lutc;
calibration.clear();
for(int i=0; i<nc; i++){
lcam = cameraTimes[i].getTime();
lutc = utcTimes[i].getTime();
camera[i] = lcam;
utc[i] = lutc;
calibration.put(lcam, lutc); //auto-boxing
}
}
fCalib = (new TimeCalibrator()).interpolate(camera, utc);
}
/** Calibrates this exif reader by interpolating
calibration points supplied as two long arrays of equal length.
The long values are understood as milliseconds since the epoch
(January 1, 1970, 00:00:00 GMT).
@throws IllegalArgumentException if cameraTimes or utcTimes is
empty or if they have unequal lengths
@throws NullPointerException if any of the arguments is <code>null</code>
**/
public void calibrate(long[] cameraTimes, long[] utcTimes){
final int n = cameraTimes.length;
if(n != utcTimes.length){
throw new IllegalArgumentException("cameraTimes and utcTimes must have the same length.");
}
double[] camera = new double[n];
double[] utc = new double[n];
synchronized(sync){
calibration.clear();
for(int i=0; i<n; i++){
camera[i] = cameraTimes[i];
utc[i] = utcTimes[i];
calibration.put(cameraTimes[i], utcTimes[i]); //uses auto-boxing
}
fCalib = (new TimeCalibrator()).interpolate(camera, utc);
}
}
/** Calibrates this exif reader by interpolating
calibration points supplied as a sorted map of Dates.
@throws IllegalArgumentException if calib is empty
@throws NullPointerException if calib is <code>null</code>
**/
public void calibrateFromDateMap(SortedMap<Date, Date> calib){
final int n = calib.size();
calibrate(
calib.keySet().toArray(new Date[n]),
calib.values().toArray(new Date[n])
);
}
/** Calibrates this exif reader by interpolating
calibration points supplied as a sorted map of longs.
The long values are understood as milliseconds since the epoch
(January 1, 1970, 00:00:00 GMT).
@throws IllegalArgumentException if calib is empty
@throws NullPointerException if calib is <code>null</code>
**/
public void calibrateFromLongMap(SortedMap<Long, Long> calib){
final int n = calib.size();
double[] cam = new double[n];
double[] utc = new double[n];
int i = 0;
for(Long key : calib.keySet()){
cam[i] = key.doubleValue();
utc[i] = calib.get(key).doubleValue();
i++;
}
synchronized(sync){
calibration.clear();
calibration.putAll(calib);
fCalib = (new TimeCalibrator()).interpolate(cam, utc);
}
}
/** Returns an unmodifiable view of this CalibratedExifReader's
calibration points. When the calibration is changed the
returned Map will change, too. */
public SortedMap<Long, Long> calibration(){
synchronized(sync){
return Collections.unmodifiableSortedMap(calibration);
}
}
@Override
public Date parseExifDate(String str) throws ParseException{
return delegate.parseExifDate(str);
}
@Override
public Date readOriginalTime(File jpeg) throws IOException, ParseException{
Date d = delegate.readOriginalTime(jpeg);
if (d == null){
System.err.println("No original date-time information found in " + jpeg.getName());
}
return d;
}
@Override
public Wpt readGPSTag(File jpeg) throws IOException, ParseException{
Wpt result = delegate.readGPSTag(jpeg);
if(result != null && result.getTime() == null){
try{
result.setTime(readOriginalUTCTime(jpeg));
} catch (Exception ex){
throw new Error(ex);
}
}
return result;
}
@Override
public Dimension readJPEGDimension(File f) throws IOException, ParseException{
return delegate.readJPEGDimension(f);
}
}