From 23959e65a6a0d581e657b07186d18b7a1ac5afeb Mon Sep 17 00:00:00 2001 From: activityworkshop Date: Sat, 14 Feb 2015 14:39:06 +0100 Subject: [PATCH] Version 2, March 2007 --- tim/prune/data/Photo.java | 63 +++ tim/prune/data/PhotoList.java | 109 ++++ tim/prune/data/PointScaler.java | 315 ++++++++++++ tim/prune/drew/jpeg/ExifReader.java | 494 ++++++++++++++++++ tim/prune/drew/jpeg/JpegData.java | 196 +++++++ tim/prune/drew/jpeg/JpegException.java | 25 + tim/prune/drew/jpeg/JpegSegmentData.java | 115 +++++ tim/prune/drew/jpeg/JpegSegmentReader.java | 171 +++++++ tim/prune/drew/jpeg/Rational.java | 94 ++++ tim/prune/edit/EditFieldsTableModel.java | 164 ++++++ tim/prune/edit/FieldEdit.java | 40 ++ tim/prune/edit/FieldEditList.java | 40 ++ tim/prune/edit/PointEditor.java | 176 +++++++ tim/prune/edit/PointNameEditor.java | 199 +++++++ tim/prune/gui/PhotoListModel.java | 57 +++ tim/prune/gui/WaypointListModel.java | 62 +++ tim/prune/lang/prune-texts_es.properties | 248 +++++++++ tim/prune/load/JpegLoader.java | 324 ++++++++++++ tim/prune/save/PovExporter.java | 570 +++++++++++++++++++++ tim/prune/threedee/Java3DWindow.java | 560 ++++++++++++++++++++ tim/prune/threedee/ThreeDException.java | 17 + tim/prune/threedee/ThreeDModel.java | 239 +++++++++ tim/prune/threedee/ThreeDWindow.java | 22 + tim/prune/threedee/WindowFactory.java | 49 ++ tim/prune/undo/UndoEditPoint.java | 56 ++ tim/prune/undo/UndoLoadPhotos.java | 54 ++ 26 files changed, 4459 insertions(+) create mode 100644 tim/prune/data/Photo.java create mode 100644 tim/prune/data/PhotoList.java create mode 100644 tim/prune/data/PointScaler.java create mode 100644 tim/prune/drew/jpeg/ExifReader.java create mode 100644 tim/prune/drew/jpeg/JpegData.java create mode 100644 tim/prune/drew/jpeg/JpegException.java create mode 100644 tim/prune/drew/jpeg/JpegSegmentData.java create mode 100644 tim/prune/drew/jpeg/JpegSegmentReader.java create mode 100644 tim/prune/drew/jpeg/Rational.java create mode 100644 tim/prune/edit/EditFieldsTableModel.java create mode 100644 tim/prune/edit/FieldEdit.java create mode 100644 tim/prune/edit/FieldEditList.java create mode 100644 tim/prune/edit/PointEditor.java create mode 100644 tim/prune/edit/PointNameEditor.java create mode 100644 tim/prune/gui/PhotoListModel.java create mode 100644 tim/prune/gui/WaypointListModel.java create mode 100644 tim/prune/lang/prune-texts_es.properties create mode 100644 tim/prune/load/JpegLoader.java create mode 100644 tim/prune/save/PovExporter.java create mode 100644 tim/prune/threedee/Java3DWindow.java create mode 100644 tim/prune/threedee/ThreeDException.java create mode 100644 tim/prune/threedee/ThreeDModel.java create mode 100644 tim/prune/threedee/ThreeDWindow.java create mode 100644 tim/prune/threedee/WindowFactory.java create mode 100644 tim/prune/undo/UndoEditPoint.java create mode 100644 tim/prune/undo/UndoLoadPhotos.java diff --git a/tim/prune/data/Photo.java b/tim/prune/data/Photo.java new file mode 100644 index 0000000..52743f7 --- /dev/null +++ b/tim/prune/data/Photo.java @@ -0,0 +1,63 @@ +package tim.prune.data; + +import java.io.File; + +/** + * Class to represent a photo and link to DataPoint + */ +public class Photo +{ + /** File where photo is stored */ + private File _file = null; + /** Associated DataPoint if correlated */ + private DataPoint _dataPoint = null; + + + /** + * Constructor + * @param inFile File object for photo + */ + public Photo(File inFile) + { + _file = inFile; + // TODO: Cache photo file contents to allow thumbnail preview + } + + + /** + * @return File object where photo resides + */ + public File getFile() + { + return _file; + } + + + /** + * Set the data point associated with the photo + * @param inPoint DataPoint with coordinates etc + */ + public void setDataPoint(DataPoint inPoint) + { + _dataPoint = inPoint; + } + + /** + * @return the DataPoint object + */ + public DataPoint getDataPoint() + { + return _dataPoint; + } + + /** + * Check if a Photo object refers to the same File as another + * @param inOther other Photo object + * @return true if the Files are the same + */ + public boolean equals(Photo inOther) + { + return (inOther != null && inOther.getFile() != null && getFile() != null + && inOther.getFile().equals(getFile())); + } +} diff --git a/tim/prune/data/PhotoList.java b/tim/prune/data/PhotoList.java new file mode 100644 index 0000000..ac10edf --- /dev/null +++ b/tim/prune/data/PhotoList.java @@ -0,0 +1,109 @@ +package tim.prune.data; + +import java.util.ArrayList; + +/** + * Class to hold a list of Photos + */ +public class PhotoList +{ + private ArrayList _photos = null; + + /** + * @return the number of photos in the list + */ + public int getNumPhotos() + { + if (_photos == null) return 0; + return _photos.size(); + } + + + /** + * Add a List of Photos + * @param inList List containing Photo objects + */ + public void addPhoto(Photo inPhoto) + { + // Make sure array is initialised + if (_photos == null) + { + _photos = new ArrayList(); + } + // Add the photo + if (inPhoto != null) + { + _photos.add(inPhoto); + } + } + + + /** + * Checks if the specified Photo is already in the list + * @param inPhoto Photo object to check + * @return true if it's already in the list + */ + public boolean contains(Photo inPhoto) + { + // Check if we need to check + if (getNumPhotos() <= 0 || inPhoto == null || inPhoto.getFile() == null) + return false; + // Loop around photos in list + for (int i=0; i= getNumPhotos()) return null; + return (Photo) _photos.get(inIndex); + } + + + /** + * Crop the photo list to the specified size + * @param inIndex previous size + */ + public void cropTo(int inIndex) + { + if (inIndex <= 0) + { + // delete whole list + _photos.clear(); + } + else + { + // delete photos to previous size + while (_photos.size() > inIndex) + { + _photos.remove(_photos.size()-1); + } + } + } + + /** + * @return array of file names + */ + public String[] getNameList() + { + String[] names = new String[getNumPhotos()]; + for (int i=0; i 0) + { + for (p=0; p getMaximumVert() ? + getMaximumHoriz():getMaximumVert(); + // calculate boundaries in degrees + double minLong = getUnscaledLongitude(-maxValue); + double maxLong = getUnscaledLongitude(maxValue); + double minLat = getUnscaledLatitude(-maxValue); + double maxLat = getUnscaledLatitude(maxValue); + // work out what line separation to use to give at least two lines + int sepIndex = -1; + double separation; + int numLatLines = 0, numLonLines = 0; + do + { + sepIndex++; + separation = COORD_SEPARATIONS[sepIndex]; + numLatLines = getNumLinesBetween(minLat, maxLat, separation); + numLonLines = getNumLinesBetween(minLong, maxLong, separation); + } + while ((numLonLines <= 1 || numLatLines <= 1) && sepIndex < MAX_COORD_SEPARATION_INDEX); + // create lines based on this separation + _latLinesDegs = getLines(minLat, maxLat, separation, numLatLines); + _lonLinesDegs = getLines(minLong, maxLong, separation, numLonLines); + // scale lines also + _latLinesScaled = new double[numLatLines]; + for (int i=0; i= inMin) numLines++; + value += inSeparation; + } + return numLines; + } + + + /** + * Get the line values in the given range using the specified separation + * @param inMin minimum value + * @param inMax maximum value + * @param inSeparation line separation + * @param inCount number of lines already counted + * @return array of line values + */ + private static double[] getLines(double inMin, double inMax, double inSeparation, int inCount) + { + double[] values = new double[inCount]; + // Start looking from round number of degrees below minimum + double value = (int) inMin; + if (inMin < 0.0) value = value - 1.0; + // Loop until bigger than maximum + int numLines = 0; + while (value < inMax) + { + if (value >= inMin) + { + values[numLines] = value; + numLines++; + } + value += inSeparation; + } + return values; + } + + /** + * @return array of latitude lines in degrees + */ + public double[] getLatitudeLines() + { + return _latLinesDegs; + } + /** + * @return array of longitude lines in degrees + */ + public double[] getLongitudeLines() + { + return _lonLinesDegs; + } + /** + * @return array of latitude lines in scaled units + */ + public double[] getScaledLatitudeLines() + { + return _latLinesScaled; + } + /** + * @return array of longitude lines in scaled units + */ + public double[] getScaledLongitudeLines() + { + return _lonLinesScaled; + } +} diff --git a/tim/prune/drew/jpeg/ExifReader.java b/tim/prune/drew/jpeg/ExifReader.java new file mode 100644 index 0000000..4e58199 --- /dev/null +++ b/tim/prune/drew/jpeg/ExifReader.java @@ -0,0 +1,494 @@ +package tim.prune.drew.jpeg; + +import java.io.File; +import java.util.HashMap; + +/** + * Extracts Exif data from a JPEG header segment + * Based on Drew Noakes' Metadata extractor at http://drewnoakes.com + * which in turn is based on code from Jhead http://www.sentex.net/~mwandel/jhead/ + */ +public class ExifReader +{ + /** The JPEG segment as an array of bytes */ + private final byte[] _data; + + /** + * Represents the native byte ordering used in the JPEG segment. If true, + * then we're using Motorola ordering (Big endian), else we're using Intel + * ordering (Little endian). + */ + private boolean _isMotorolaByteOrder; + + /** + * The number of bytes used per format descriptor. + */ + private static final int[] BYTES_PER_FORMAT = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8}; + + /** + * The number of formats known. + */ + private static final int MAX_FORMAT_CODE = 12; + + // Format types + // Note: Cannot use the DataFormat enumeration in the case statement that uses these tags. + // Is there a better way? + private static final int FMT_BYTE = 1; + private static final int FMT_STRING = 2; + private static final int FMT_USHORT = 3; + private static final int FMT_ULONG = 4; + private static final int FMT_URATIONAL = 5; + private static final int FMT_SBYTE = 6; + private static final int FMT_UNDEFINED = 7; + private static final int FMT_SSHORT = 8; + private static final int FMT_SLONG = 9; + private static final int FMT_SRATIONAL = 10; + private static final int FMT_SINGLE = 11; + private static final int FMT_DOUBLE = 12; + + public static final int TAG_EXIF_OFFSET = 0x8769; + public static final int TAG_INTEROP_OFFSET = 0xA005; + public static final int TAG_GPS_INFO_OFFSET = 0x8825; + public static final int TAG_MAKER_NOTE = 0x927C; + + public static final int TIFF_HEADER_START_OFFSET = 6; + + /** GPS tag version GPSVersionID 0 0 BYTE 4 */ + public static final int TAG_GPS_VERSION_ID = 0x0000; + /** North or South Latitude GPSLatitudeRef 1 1 ASCII 2 */ + public static final int TAG_GPS_LATITUDE_REF = 0x0001; + /** Latitude GPSLatitude 2 2 RATIONAL 3 */ + public static final int TAG_GPS_LATITUDE = 0x0002; + /** East or West Longitude GPSLongitudeRef 3 3 ASCII 2 */ + public static final int TAG_GPS_LONGITUDE_REF = 0x0003; + /** Longitude GPSLongitude 4 4 RATIONAL 3 */ + public static final int TAG_GPS_LONGITUDE = 0x0004; + /** Altitude reference GPSAltitudeRef 5 5 BYTE 1 */ + public static final int TAG_GPS_ALTITUDE_REF = 0x0005; + /** Altitude GPSAltitude 6 6 RATIONAL 1 */ + public static final int TAG_GPS_ALTITUDE = 0x0006; + /** GPS time (atomic clock) GPSTimeStamp 7 7 RATIONAL 3 */ + public static final int TAG_GPS_TIMESTAMP = 0x0007; + /** GPS date (atomic clock) GPSDateStamp 23 1d RATIONAL 3 */ + public static final int TAG_GPS_DATESTAMP = 0x001d; + + /** + * Creates an ExifReader for a Jpeg file. + * @param inFile File object to attempt to read from + * @throws JpegProcessingException on failure + */ + public ExifReader(File inFile) throws JpegException + { + JpegSegmentData segments = JpegSegmentReader.readSegments(inFile); + _data = segments.getSegment(JpegSegmentReader.SEGMENT_APP1); + } + + /** + * Performs the Exif data extraction + * @return the GPS data found in the file + */ + public JpegData extract() + { + JpegData metadata = new JpegData(); + if (_data==null) + return metadata; + + // check for the header length + if (_data.length<=14) + { + metadata.addError("Exif data segment must contain at least 14 bytes"); + return metadata; + } + + // check for the header preamble + if (!"Exif\0\0".equals(new String(_data, 0, 6))) + { + metadata.addError("Exif data segment doesn't begin with 'Exif'"); + return metadata; + } + + // this should be either "MM" or "II" + String byteOrderIdentifier = new String(_data, 6, 2); + if (!setByteOrder(byteOrderIdentifier)) + { + metadata.addError("Unclear distinction between Motorola/Intel byte ordering: " + byteOrderIdentifier); + return metadata; + } + + // Check the next two values are 0x2A as expected + if (get16Bits(8)!=0x2a) + { + metadata.addError("Invalid Exif start - should have 0x2A at offset 8 in Exif header"); + return metadata; + } + + int firstDirectoryOffset = get32Bits(10) + TIFF_HEADER_START_OFFSET; + + // Check that offset is within range + if (firstDirectoryOffset>=_data.length - 1) + { + metadata.addError("First exif directory offset is beyond end of Exif data segment"); + // First directory normally starts 14 bytes in -- try it here and catch another error in the worst case + firstDirectoryOffset = 14; + } + + HashMap processedDirectoryOffsets = new HashMap(); + + // 0th IFD (we merge with Exif IFD) + processDirectory(metadata, false, processedDirectoryOffsets, firstDirectoryOffset, TIFF_HEADER_START_OFFSET); + + return metadata; + } + + + /** + * Set the byte order identifier + * @param byteOrderIdentifier String from exif + * @return true if recognised, false otherwise + */ + private boolean setByteOrder(String byteOrderIdentifier) + { + if ("MM".equals(byteOrderIdentifier)) { + _isMotorolaByteOrder = true; + } else if ("II".equals(byteOrderIdentifier)) { + _isMotorolaByteOrder = false; + } else { + return false; + } + return true; + } + + + /** + * Recursive call to process one of the nested Tiff IFD directories. + * 2 bytes: number of tags + * for each tag + * 2 bytes: tag type + * 2 bytes: format code + * 4 bytes: component count + */ + private void processDirectory(JpegData inMetadata, boolean inIsGPS, HashMap inDirectoryOffsets, + int inDirOffset, int inTiffHeaderOffset) + { + // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist + if (inDirectoryOffsets.containsKey(new Integer(inDirOffset))) + return; + + // remember that we've visited this directory so that we don't visit it again later + inDirectoryOffsets.put(new Integer(inDirOffset), "processed"); + + if (inDirOffset >= _data.length || inDirOffset < 0) + { + inMetadata.addError("Ignored directory marked to start outside data segment"); + return; + } + + // First two bytes in the IFD are the number of tags in this directory + int dirTagCount = get16Bits(inDirOffset); + // If no tags, exit without complaint + if (dirTagCount == 0) return; + + if (!isDirectoryLengthValid(inDirOffset, inTiffHeaderOffset)) + { + inMetadata.addError("Directory length is not valid"); + return; + } + + inMetadata.setExifDataPresent(); + // Handle each tag in this directory + for (int tagNumber = 0; tagNumber MAX_FORMAT_CODE) + { + inMetadata.addError("Invalid format code: " + formatCode); + continue; + } + + // 4 bytes dictate the number of components in this tag's data + final int componentCount = get32Bits(tagOffset + 4); + if (componentCount < 0) + { + inMetadata.addError("Negative component count in EXIF"); + continue; + } + // each component may have more than one byte... calculate the total number of bytes + final int byteCount = componentCount * BYTES_PER_FORMAT[formatCode]; + final int tagValueOffset = calculateTagValueOffset(byteCount, tagOffset, inTiffHeaderOffset); + if (tagValueOffset < 0 || tagValueOffset > _data.length) + { + inMetadata.addError("Illegal pointer offset value in EXIF"); + continue; + } + + // Check that this tag isn't going to allocate outside the bounds of the data array. + // This addresses an uncommon OutOfMemoryError. + if (byteCount < 0 || tagValueOffset + byteCount > _data.length) + { + inMetadata.addError("Illegal number of bytes: " + byteCount); + continue; + } + + // Calculate the value as an offset for cases where the tag represents a directory + final int subdirOffset = inTiffHeaderOffset + get32Bits(tagValueOffset); + + // TODO: Also look for timestamp(s) in Exif for correlation - which directory? + switch (tagType) + { + case TAG_EXIF_OFFSET: + // ignore + continue; + case TAG_INTEROP_OFFSET: + // ignore + continue; + case TAG_GPS_INFO_OFFSET: + processDirectory(inMetadata, true, inDirectoryOffsets, subdirOffset, inTiffHeaderOffset); + continue; + case TAG_MAKER_NOTE: + // ignore + continue; + default: + // not a known directory, so must just be a normal tag + // ignore if we're not in gps directory + if (inIsGPS) + processGpsTag(inMetadata, tagType, tagValueOffset, componentCount, formatCode); + break; + } + } + + // at the end of each IFD is an optional link to the next IFD + final int finalTagOffset = calculateTagOffset(inDirOffset, dirTagCount); + int nextDirectoryOffset = get32Bits(finalTagOffset); + if (nextDirectoryOffset != 0) + { + nextDirectoryOffset += inTiffHeaderOffset; + if (nextDirectoryOffset>=_data.length) + { + // Last 4 bytes of IFD reference another IFD with an address that is out of bounds + return; + } + else if (nextDirectoryOffset < inDirOffset) + { + // Last 4 bytes of IFD reference another IFD with an address before the start of this directory + return; + } + // the next directory is of same type as this one + processDirectory(inMetadata, false, inDirectoryOffsets, nextDirectoryOffset, inTiffHeaderOffset); + } + } + + + /** + * Check if the directory length is valid + * @param dirStartOffset start offset for directory + * @param tiffHeaderOffset Tiff header offeset + * @return true if length is valid + */ + private boolean isDirectoryLengthValid(int inDirStartOffset, int inTiffHeaderOffset) + { + int dirTagCount = get16Bits(inDirStartOffset); + int dirLength = (2 + (12 * dirTagCount) + 4); + if (dirLength + inDirStartOffset + inTiffHeaderOffset >= _data.length) + { + // Note: Files that had thumbnails trimmed with jhead 1.3 or earlier might trigger this + return false; + } + return true; + } + + + /** + * Process a GPS tag and put the contents in the given metadata + * @param inMetadata metadata holding extracted values + * @param inTagType tag type (eg latitude) + * @param inTagValueOffset start offset in data array + * @param inComponentCount component count for tag + * @param inFormatCode format code, eg byte + */ + private void processGpsTag(JpegData inMetadata, int inTagType, int inTagValueOffset, + int inComponentCount, int inFormatCode) + { + // Only interested in tags latref, lat, longref, lon, altref, alt and gps timestamp + switch (inTagType) + { + case TAG_GPS_LATITUDE_REF: + inMetadata.setLatitudeRef(readString(inTagValueOffset, inFormatCode, inComponentCount)); + break; + case TAG_GPS_LATITUDE: + inMetadata.setLatitude(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount)); + break; + case TAG_GPS_LONGITUDE_REF: + inMetadata.setLongitudeRef(readString(inTagValueOffset, inFormatCode, inComponentCount)); + break; + case TAG_GPS_LONGITUDE: + inMetadata.setLongitude(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount)); + break; + case TAG_GPS_ALTITUDE_REF: + inMetadata.setAltitudeRef(_data[inTagValueOffset]); + break; + case TAG_GPS_ALTITUDE: + inMetadata.setAltitude(readRational(inTagValueOffset, inFormatCode, inComponentCount)); + break; + case TAG_GPS_TIMESTAMP: + inMetadata.setTimestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount)); + break; + case TAG_GPS_DATESTAMP: + inMetadata.setDatestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount)); + break; + default: // ignore all other tags + } + } + + + /** + * Calculate the tag value offset + * @param inByteCount + * @param inDirEntryOffset + * @param inTiffHeaderOffset + * @return new offset + */ + private int calculateTagValueOffset(int inByteCount, int inDirEntryOffset, int inTiffHeaderOffset) + { + if (inByteCount > 4) + { + // If it's bigger than 4 bytes, the dir entry contains an offset. + // dirEntryOffset must be passed, as some makernote implementations (e.g. FujiFilm) incorrectly use an + // offset relative to the start of the makernote itself, not the TIFF segment. + final int offsetVal = get32Bits(inDirEntryOffset + 8); + if (offsetVal + inByteCount > _data.length) + { + // Bogus pointer offset and / or bytecount value + return -1; // signal error + } + return inTiffHeaderOffset + offsetVal; + } + else + { + // 4 bytes or less and value is in the dir entry itself + return inDirEntryOffset + 8; + } + } + + + /** + * Creates a String from the _data buffer starting at the specified offset, + * and ending where byte=='\0' or where length==maxLength. + * @param inOffset start offset + * @param inFormatCode format code - should be string + * @param inMaxLength max length of string + * @return contents of tag, or null if format incorrect + */ + private String readString(int inOffset, int inFormatCode, int inMaxLength) + { + if (inFormatCode != FMT_STRING) return null; + // Calculate length + int length = 0; + while ((inOffset + length)<_data.length + && _data[inOffset + length]!='\0' + && length < inMaxLength) + { + length++; + } + return new String(_data, inOffset, length); + } + + /** + * Creates a Rational from the _data buffer starting at the specified offset + * @param inOffset start offset + * @param inFormatCode format code - should be srational or urational + * @param inCount component count - should be 1 + * @return contents of tag as a Rational object + */ + private Rational readRational(int inOffset, int inFormatCode, int inCount) + { + // Check the format is a single rational as expected + if (inFormatCode != FMT_SRATIONAL && inFormatCode != FMT_URATIONAL + || inCount != 1) return null; + return new Rational(get32Bits(inOffset), get32Bits(inOffset + 4)); + } + + + /** + * Creates a Rational array from the _data buffer starting at the specified offset + * @param inOffset start offset + * @param inFormatCode format code - should be srational or urational + * @param inCount component count - number of components + * @return contents of tag as an array of Rational objects + */ + private Rational[] readRationalArray(int inOffset, int inFormatCode, int inCount) + { + // Check the format is rational as expected + if (inFormatCode != FMT_SRATIONAL && inFormatCode != FMT_URATIONAL) + return null; + // Build array of Rationals + Rational[] answer = new Rational[inCount]; + for (int i=0; i_data.length) + throw new ArrayIndexOutOfBoundsException("attempt to read data outside of exif segment (index " + offset + " where max index is " + (_data.length - 1) + ")"); + + if (_isMotorolaByteOrder) { + // Motorola - MSB first + return (_data[offset] << 8 & 0xFF00) | (_data[offset + 1] & 0xFF); + } else { + // Intel ordering - LSB first + return (_data[offset + 1] << 8 & 0xFF00) | (_data[offset] & 0xFF); + } + } + + + /** + * Get a 32 bit value from file's native byte order. + */ + private int get32Bits(int offset) + { + if (offset < 0 || offset+4 > _data.length) + throw new ArrayIndexOutOfBoundsException("attempt to read data outside of exif segment (index " + + offset + " where max index is " + (_data.length - 1) + ")"); + + if (_isMotorolaByteOrder) + { + // Motorola - MSB first + return (_data[offset] << 24 & 0xFF000000) | + (_data[offset + 1] << 16 & 0xFF0000) | + (_data[offset + 2] << 8 & 0xFF00) | + (_data[offset + 3] & 0xFF); + } + else + { + // Intel ordering - LSB first + return (_data[offset + 3] << 24 & 0xFF000000) | + (_data[offset + 2] << 16 & 0xFF0000) | + (_data[offset + 1] << 8 & 0xFF00) | + (_data[offset] & 0xFF); + } + } +} diff --git a/tim/prune/drew/jpeg/JpegData.java b/tim/prune/drew/jpeg/JpegData.java new file mode 100644 index 0000000..51995c9 --- /dev/null +++ b/tim/prune/drew/jpeg/JpegData.java @@ -0,0 +1,196 @@ +package tim.prune.drew.jpeg; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class to hold the GPS data extracted from a Jpeg including position and time + * All contents are in Rational format + */ +public class JpegData +{ + private boolean _exifDataPresent = false; + private char _latitudeRef = '\0'; + private char _longitudeRef = '\0'; + private byte _altitudeRef = 0; + private Rational[] _latitude = null; + private Rational[] _longitude = null; + private Rational _altitude = null; + private Rational[] _timestamp = null; + private Rational[] _datestamp = null; + private ArrayList _errors = null; + + + /** + * Set the exif data present flag + */ + public void setExifDataPresent() + { + _exifDataPresent = true; + } + /** + * @return true if exif data found + */ + public boolean getExifDataPresent() + { + return _exifDataPresent; + } + + /** + * Set the latitude reference (N/S) + * @param inChar character representing reference + */ + public void setLatitudeRef(char inChar) + { + _latitudeRef = inChar; + } + + /** + * Set the latitude reference (N/S) + * @param inString string containing reference + */ + public void setLatitudeRef(String inString) + { + if (inString != null && inString.length() == 1) + setLatitudeRef(inString.charAt(0)); + } + + /** + * Set the latitude + * @param inValues array of three Rationals for deg-min-sec + */ + public void setLatitude(Rational[] inValues) + { + if (inValues != null && inValues.length == 3) + _latitude = inValues; + } + + /** + * Set the longitude reference (E/W) + * @param inChar character representing reference + */ + public void setLongitudeRef(char inChar) + { + _longitudeRef = inChar; + } + + /** + * Set the longitude reference (E/W) + * @param inString string containing reference + */ + public void setLongitudeRef(String inString) + { + if (inString != null && inString.length() == 1) + setLongitudeRef(inString.charAt(0)); + } + + /** + * Set the longitude + * @param inValues array of three Rationals for deg-min-sec + */ + public void setLongitude(Rational[] inValues) + { + if (inValues != null && inValues.length == 3) + _longitude = inValues; + } + + /** + * Set the altitude reference (sea level / not) + * @param inByte byte representing reference + */ + public void setAltitudeRef(byte inByte) + { + _altitudeRef = inByte; + } + + /** + * Set the altitude + * @param inRational Rational number representing altitude + */ + public void setAltitude(Rational inRational) + { + _altitude = inRational; + } + + /** + * Set the timestamp + * @param inValues array of Rationals holding timestamp + */ + public void setTimestamp(Rational[] inValues) + { + _timestamp = inValues; + } + + /** + * Set the datestamp + * @param inValues array of Rationals holding datestamp + */ + public void setDatestamp(Rational[] inValues) + { + _datestamp = inValues; + } + + /** @return latitude ref as char */ + public char getLatitudeRef() { return _latitudeRef; } + /** @return latitude as array of 3 Rationals */ + public Rational[] getLatitude() { return _latitude; } + /** @return longitude ref as char */ + public char getLongitudeRef() { return _longitudeRef; } + /** @return longitude as array of 3 Rationals */ + public Rational[] getLongitude() { return _longitude; } + /** @return altitude ref as byte (should be 0) */ + public byte getAltitudeRef() { return _altitudeRef; } + /** @return altitude as Rational */ + public Rational getAltitude() { return _altitude; } + /** @return timestamp as array of 3 Rationals */ + public Rational[] getTimestamp() { return _timestamp; } + /** @return timestamp as array of 3 Rationals */ + public Rational[] getDatestamp() { return _datestamp; } + + /** + * @return true if data looks valid, ie has at least lat and long + * (altitude and timestamp optional). + */ + public boolean isValid() + { + return (_latitudeRef == 'N' || _latitudeRef == 'n' || _latitudeRef == 'S' || _latitudeRef == 's') + && _latitude != null + && (_longitudeRef == 'E' || _longitudeRef == 'e' || _longitudeRef == 'W' || _longitudeRef == 'w') + && _longitude != null; + } + + /** + * Add the given error message to the list of errors + * @param inError String containing error message + */ + public void addError(String inError) + { + if (_errors == null) _errors = new ArrayList(); + _errors.add(inError); + } + + /** + * @return the number of errors, if any + */ + public int getNumErrors() + { + if (_errors == null) return 0; + return _errors.size(); + } + + /** + * @return true if errors occurred + */ + public boolean hasErrors() + { + return getNumErrors() > 0; + } + + /** + * @return all errors as a list + */ + public List getErrors() + { + return _errors; + } +} diff --git a/tim/prune/drew/jpeg/JpegException.java b/tim/prune/drew/jpeg/JpegException.java new file mode 100644 index 0000000..5c75766 --- /dev/null +++ b/tim/prune/drew/jpeg/JpegException.java @@ -0,0 +1,25 @@ +package tim.prune.drew.jpeg; + +/** + * Class to indicate a fatal exception processing a jpeg, + * including IO errors and exif errors + */ +public class JpegException extends Exception +{ + /** + * @param message description of error + */ + public JpegException(String message) + { + super(message); + } + + /** + * @param message description of error + * @param cause Throwable which caused the error + */ + public JpegException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/tim/prune/drew/jpeg/JpegSegmentData.java b/tim/prune/drew/jpeg/JpegSegmentData.java new file mode 100644 index 0000000..6a0d6a4 --- /dev/null +++ b/tim/prune/drew/jpeg/JpegSegmentData.java @@ -0,0 +1,115 @@ +package tim.prune.drew.jpeg; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Class to hold a collection of Jpeg data segments + * Each marker represents a list of multiple byte arrays + * Based on Drew Noakes' Metadata extractor at http://drewnoakes.com + */ +public class JpegSegmentData +{ + /** A map of byte[], keyed by the segment marker */ + private final HashMap _segmentDataMap; + + + /** + * Constructor for an empty collection + */ + public JpegSegmentData() + { + _segmentDataMap = new HashMap(10); + } + + /** + * Add a segment to the collection + * @param inSegmentMarker marker byte + * @param inSegmentBytes data of segment + */ + public void addSegment(byte inSegmentMarker, byte[] inSegmentBytes) + { + // System.out.println("Adding segment: " + inSegmentMarker); + List segmentList = getOrCreateSegmentList(inSegmentMarker); + segmentList.add(inSegmentBytes); + } + + + /** + * Get the first segment with the given marker + * @param inSegmentMarker marker byte + * @return first segment with that marker + */ + public byte[] getSegment(byte inSegmentMarker) + { + return getSegment(inSegmentMarker, 0); + } + + + /** + * Get the nth segment with the given marker + * @param inSegmentMarker marker byte + * @param inOccurrence occurrence to get, starting at 0 + * @return byte array from specified segment + */ + public byte[] getSegment(byte inSegmentMarker, int inOccurrence) + { + final List segmentList = getSegmentList(inSegmentMarker); + + if (segmentList==null || segmentList.size()<=inOccurrence) + return null; + else + return (byte[]) segmentList.get(inOccurrence); + } + + + /** + * Get the number of segments with the given marker + * @param inSegmentMarker marker byte + * @return number of segments + */ + public int getSegmentCount(byte inSegmentMarker) + { + final List segmentList = getSegmentList(inSegmentMarker); + if (segmentList == null) + return 0; + else + return segmentList.size(); + } + + + /** + * Get the list of segments with the given marker + * @param inSegmentMarker marker byte + * @return list of segments + */ + private List getSegmentList(byte inSegmentMarker) + { + return (List)_segmentDataMap.get(new Byte(inSegmentMarker)); + } + + + /** + * Get the specified segment if it exists, otherwise create new one + * @param inSegmentMarker marker byte + * @return list of segments + */ + private List getOrCreateSegmentList(byte inSegmentMarker) + { + List segmentList = null; + Byte key = new Byte(inSegmentMarker); + if (_segmentDataMap.containsKey(key)) + { + // list already exists + segmentList = (List)_segmentDataMap.get(key); + } + else + { + // create new list and add it + segmentList = new ArrayList(); + _segmentDataMap.put(key, segmentList); + } + return segmentList; + } +} diff --git a/tim/prune/drew/jpeg/JpegSegmentReader.java b/tim/prune/drew/jpeg/JpegSegmentReader.java new file mode 100644 index 0000000..20eb83c --- /dev/null +++ b/tim/prune/drew/jpeg/JpegSegmentReader.java @@ -0,0 +1,171 @@ +package tim.prune.drew.jpeg; + +import java.io.*; + +/** + * Class to perform read functions of Jpeg files, returning specific file segments + * Based on Drew Noakes' Metadata extractor at http://drewnoakes.com + */ +public class JpegSegmentReader +{ + /** Start of scan marker */ + private static final byte SEGMENT_SOS = (byte)0xDA; + + /** End of image marker */ + private static final byte MARKER_EOI = (byte)0xD9; + + /** APP0 Jpeg segment identifier -- Jfif data. */ + public static final byte SEGMENT_APP0 = (byte)0xE0; + /** APP1 Jpeg segment identifier -- where Exif data is kept. */ + public static final byte SEGMENT_APP1 = (byte)0xE1; + /** APP2 Jpeg segment identifier. */ + public static final byte SEGMENT_APP2 = (byte)0xE2; + /** APP3 Jpeg segment identifier. */ + public static final byte SEGMENT_APP3 = (byte)0xE3; + /** APP4 Jpeg segment identifier. */ + public static final byte SEGMENT_APP4 = (byte)0xE4; + /** APP5 Jpeg segment identifier. */ + public static final byte SEGMENT_APP5 = (byte)0xE5; + /** APP6 Jpeg segment identifier. */ + public static final byte SEGMENT_APP6 = (byte)0xE6; + /** APP7 Jpeg segment identifier. */ + public static final byte SEGMENT_APP7 = (byte)0xE7; + /** APP8 Jpeg segment identifier. */ + public static final byte SEGMENT_APP8 = (byte)0xE8; + /** APP9 Jpeg segment identifier. */ + public static final byte SEGMENT_APP9 = (byte)0xE9; + /** APPA Jpeg segment identifier -- can hold Unicode comments. */ + public static final byte SEGMENT_APPA = (byte)0xEA; + /** APPB Jpeg segment identifier. */ + public static final byte SEGMENT_APPB = (byte)0xEB; + /** APPC Jpeg segment identifier. */ + public static final byte SEGMENT_APPC = (byte)0xEC; + /** APPD Jpeg segment identifier -- IPTC data in here. */ + public static final byte SEGMENT_APPD = (byte)0xED; + /** APPE Jpeg segment identifier. */ + public static final byte SEGMENT_APPE = (byte)0xEE; + /** APPF Jpeg segment identifier. */ + public static final byte SEGMENT_APPF = (byte)0xEF; + /** Start Of Image segment identifier. */ + public static final byte SEGMENT_SOI = (byte)0xD8; + /** Define Quantization Table segment identifier. */ + public static final byte SEGMENT_DQT = (byte)0xDB; + /** Define Huffman Table segment identifier. */ + public static final byte SEGMENT_DHT = (byte)0xC4; + /** Start-of-Frame Zero segment identifier. */ + public static final byte SEGMENT_SOF0 = (byte)0xC0; + /** Jpeg comment segment identifier. */ + public static final byte SEGMENT_COM = (byte)0xFE; + + /** Magic numbers to mark the beginning of all Jpegs */ + private static final int MAGIC_JPEG_BYTE_1 = 0xFF; + private static final int MAGIC_JPEG_BYTE_2 = 0xD8; + + + /** + * Obtain the Jpeg segment data from the specified file + * @param inFile File to read + * @return Jpeg segment data from file + * @throws JpegException on file read errors or exif data errors + */ + public static JpegSegmentData readSegments(File inFile) throws JpegException + { + JpegSegmentData segmentData = new JpegSegmentData(); + + BufferedInputStream bStream = null; + + try + { + bStream = new BufferedInputStream(new FileInputStream(inFile)); + int offset = 0; + // first two bytes should be jpeg magic number + int magic1 = bStream.read() & 0xFF; + int magic2 = bStream.read() & 0xFF; + checkMagicNumbers(magic1, magic2); + + offset += 2; + // Loop around segments found + do + { + // next byte is 0xFF + byte segmentIdentifier = (byte) (bStream.read() & 0xFF); + if ((segmentIdentifier & 0xFF) != 0xFF) + { + throw new JpegException("expected jpeg segment start identifier 0xFF at offset " + + offset + ", not 0x" + Integer.toHexString(segmentIdentifier & 0xFF)); + } + offset++; + // next byte is + byte thisSegmentMarker = (byte) (bStream.read() & 0xFF); + offset++; + // next 2-bytes are : [high-byte] [low-byte] + byte[] segmentLengthBytes = new byte[2]; + bStream.read(segmentLengthBytes, 0, 2); + offset += 2; + int segmentLength = ((segmentLengthBytes[0] << 8) & 0xFF00) | (segmentLengthBytes[1] & 0xFF); + // segment length includes size bytes, so subtract two + segmentLength -= 2; + if (segmentLength > bStream.available()) + throw new JpegException("segment size would extend beyond file stream length"); + else if (segmentLength < 0) + throw new JpegException("segment size would be less than zero"); + byte[] segmentBytes = new byte[segmentLength]; + bStream.read(segmentBytes, 0, segmentLength); + offset += segmentLength; + if ((thisSegmentMarker & 0xFF) == (SEGMENT_SOS & 0xFF)) + { + // The 'Start-Of-Scan' segment comes last so break out of loop + break; + } + else if ((thisSegmentMarker & 0xFF) == (MARKER_EOI & 0xFF)) + { + // the 'End-Of-Image' segment - should already have exited by now + break; + } + else + { + segmentData.addSegment(thisSegmentMarker, segmentBytes); + } + // loop through to the next segment + } + while (true); + } + catch (FileNotFoundException fnfe) + { + throw new JpegException("Jpeg file not found"); + } + catch (IOException ioe) + { + throw new JpegException("IOException processing Jpeg file: " + ioe.getMessage(), ioe); + } + finally + { + try + { + if (bStream != null) { + bStream.close(); + } + } + catch (IOException ioe) { + throw new JpegException("IOException processing Jpeg file: " + ioe.getMessage(), ioe); + } + } + // Return the result + return segmentData; + } + + + /** + * Helper method that validates the Jpeg file's magic number. + * @param inMagic1 first half of magic number + * @param inMagic2 second half of magic number + * @throws JpegException if numbers do not match magic numbers expected + */ + private static void checkMagicNumbers(int inMagic1, int inMagic2) throws JpegException + { + if (inMagic1 != MAGIC_JPEG_BYTE_1 || inMagic2 != MAGIC_JPEG_BYTE_2) + { + throw new JpegException("not a jpeg file"); + } + } +} \ No newline at end of file diff --git a/tim/prune/drew/jpeg/Rational.java b/tim/prune/drew/jpeg/Rational.java new file mode 100644 index 0000000..78bd72b --- /dev/null +++ b/tim/prune/drew/jpeg/Rational.java @@ -0,0 +1,94 @@ +package tim.prune.drew.jpeg; + +/** + * Immutable class for holding a rational number without loss of precision. + * Based on Drew Noakes' Metadata extractor at http://drewnoakes.com + */ +public class Rational +{ + /** Holds the numerator */ + private final int _numerator; + + /** Holds the denominator */ + private final int _denominator; + + /** + * Constructor + * @param inNumerator numerator of fraction (upper number) + * @param inDenominator denominator of fraction (lower number) + */ + public Rational(int inNumerator, int inDenominator) + { + // Could throw exception if denominator is zero + _numerator = inNumerator; + _denominator = inDenominator; + } + + + /** + * @return the value of the specified number as a double. + * This may involve rounding. + */ + public double doubleValue() + { + if (_denominator == 0) return 0.0; + return (double)_numerator / (double)_denominator; + } + + /** + * @return the value of the specified number as an int. + * This may involve rounding or truncation. + */ + public final int intValue() + { + if (_denominator == 0) return 0; + return _numerator / _denominator; + } + + /** + * @return the denominator. + */ + public final int getDenominator() + { + return _denominator; + } + + /** + * @return the numerator. + */ + public final int getNumerator() + { + return _numerator; + } + + /** + * Checks if this rational number is an Integer, either positive or negative. + */ + public boolean isInteger() + { + // number is integer if the denominator is 1, or if the remainder is zero + return (_denominator == 1 + || (_denominator != 0 && (_numerator % _denominator == 0))); + } + + + /** + * @return a string representation of the object of form numerator/denominator. + */ + public String toString() + { + return "" + _numerator + "/" + _denominator; + } + + + /** + * Compares two Rational instances, returning true if they are equal + * @param inOther the Rational to compare this instance to. + * @return true if instances are equal, otherwise false. + */ + public boolean equals(Rational inOther) + { + // Could also attempt to simplify fractions to lowest common denominator before compare + return _numerator == inOther._numerator && _denominator == inOther._denominator; + } +} \ No newline at end of file diff --git a/tim/prune/edit/EditFieldsTableModel.java b/tim/prune/edit/EditFieldsTableModel.java new file mode 100644 index 0000000..575b0aa --- /dev/null +++ b/tim/prune/edit/EditFieldsTableModel.java @@ -0,0 +1,164 @@ +package tim.prune.edit; + +import javax.swing.table.AbstractTableModel; + +import tim.prune.I18nManager; + +/** + * Class to hold table model information for edit dialog + */ +public class EditFieldsTableModel extends AbstractTableModel +{ + private String[] _fieldNames = null; + private String[] _fieldValues = null; + private boolean[] _valueChanged = null; + + + /** + * Constructor giving list size + */ + public EditFieldsTableModel(int inSize) + { + _fieldNames = new String[inSize]; + _fieldValues = new String[inSize]; + _valueChanged = new boolean[inSize]; + } + + + /** + * Set the given data in the array + * @param inName field name + * @param inValue field value + * @param inIndex index to place in array + */ + public void addFieldInfo(String inName, String inValue, int inIndex) + { + _fieldNames[inIndex] = inName; + _fieldValues[inIndex] = inValue; + _valueChanged[inIndex] = false; + } + + + /** + * @see javax.swing.table.TableModel#getColumnCount() + */ + public int getColumnCount() + { + return 3; + } + + + /** + * @see javax.swing.table.TableModel#getRowCount() + */ + public int getRowCount() + { + return _fieldNames.length; + } + + + /** + * @see javax.swing.table.TableModel#getValueAt(int, int) + */ + public Object getValueAt(int inRowIndex, int inColumnIndex) + { + if (inColumnIndex == 0) + { + return _fieldNames[inRowIndex]; + } + else if (inColumnIndex == 1) + { + return _fieldValues[inRowIndex]; + } + return new Boolean(_valueChanged[inRowIndex]); + } + + + /** + * @return true if cell is editable + */ + public boolean isCellEditable(int inRowIndex, int inColumnIndex) + { + // no + return false; + } + + + /** + * Set the given cell value + * @see javax.swing.table.TableModel#setValueAt(java.lang.Object, int, int) + */ + public void setValueAt(Object inValue, int inRowIndex, int inColumnIndex) + { + // ignore edits + } + + + /** + * @return Class of cell data + */ + public Class getColumnClass(int inColumnIndex) + { + if (inColumnIndex <= 1) return String.class; + return Boolean.class; + } + + + /** + * Get the name of the column + */ + public String getColumnName(int inColNum) + { + if (inColNum == 0) return I18nManager.getText("dialog.pointedit.table.field"); + else if (inColNum == 1) return I18nManager.getText("dialog.pointedit.table.value"); + return I18nManager.getText("dialog.pointedit.table.changed"); + } + + + /** + * Update the value of the given row + * @param inRowNum number of row, starting at 0 + * @param inValue new value + * @return true if data updated + */ + public boolean updateValue(int inRowNum, String inValue) + { + String currValue = _fieldValues[inRowNum]; + // ignore empty-to-empty changes + if ((currValue == null || currValue.equals("")) && (inValue == null || inValue.equals(""))) + { + return false; + } + // ignore changes when strings equal + if (currValue == null || inValue == null || !currValue.equals(inValue)) + { + // really changed + _fieldValues[inRowNum] = inValue; + _valueChanged[inRowNum] = true; + fireTableRowsUpdated(inRowNum, inRowNum); + return true; + } + return false; + } + + + /** + * Get the value at the given index + * @param inIndex index of field, starting at 0 + * @return string value + */ + public String getValue(int inIndex) + { + return _fieldValues[inIndex]; + } + + /** + * Get the changed flag at the given index + * @param inIndex index of field, starting at 0 + * @return true if field changed + */ + public boolean getChanged(int inIndex) + { + return _valueChanged[inIndex]; + } +} diff --git a/tim/prune/edit/FieldEdit.java b/tim/prune/edit/FieldEdit.java new file mode 100644 index 0000000..5877ab1 --- /dev/null +++ b/tim/prune/edit/FieldEdit.java @@ -0,0 +1,40 @@ +package tim.prune.edit; + +import tim.prune.data.Field; + +/** + * Class to hold a single field edit including Field and new value + */ +public class FieldEdit +{ + private Field _field = null; + private String _value = null; + + /** + * Constructor + * @param inField field to edit + * @param inValue new value + */ + public FieldEdit(Field inField, String inValue) + { + _field = inField; + _value = inValue; + } + + + /** + * @return the field + */ + public Field getField() + { + return _field; + } + + /** + * @return the value + */ + public String getValue() + { + return _value; + } +} diff --git a/tim/prune/edit/FieldEditList.java b/tim/prune/edit/FieldEditList.java new file mode 100644 index 0000000..70bd263 --- /dev/null +++ b/tim/prune/edit/FieldEditList.java @@ -0,0 +1,40 @@ +package tim.prune.edit; + +import java.util.ArrayList; + +/** + * Class to hold a list of field edits + */ +public class FieldEditList +{ + private ArrayList _editList = new ArrayList(); + + + /** + * Add an edit to the list + * @param inEdit FieldEdit + */ + public void addEdit(FieldEdit inEdit) + { + if (inEdit != null) + _editList.add(inEdit); + } + + /** + * @return number of edits in list + */ + public int getNumEdits() + { + return _editList.size(); + } + + /** + * Get the edit at the specified index + * @param inIndex index to get, starting at 0 + * @return FieldEdit + */ + public FieldEdit getEdit(int inIndex) + { + return (FieldEdit) _editList.get(inIndex); + } +} diff --git a/tim/prune/edit/PointEditor.java b/tim/prune/edit/PointEditor.java new file mode 100644 index 0000000..ea67a0a --- /dev/null +++ b/tim/prune/edit/PointEditor.java @@ -0,0 +1,176 @@ +package tim.prune.edit; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import tim.prune.App; +import tim.prune.I18nManager; +import tim.prune.data.DataPoint; +import tim.prune.data.Field; +import tim.prune.data.FieldList; +import tim.prune.data.Track; + +/** + * Class to manage the display and editing of point data + */ +public class PointEditor +{ + private App _app = null; + private JFrame _parentFrame = null; + private JDialog _dialog = null; + private JTable _table = null; + private Track _track = null; + private DataPoint _point = null; + private EditFieldsTableModel _model = null; + private JButton _editButton = null; + private JButton _okButton = null; + + + /** + * Constructor + * @param inApp application object to inform of success + * @param inParentFrame parent frame + */ + public PointEditor(App inApp, JFrame inParentFrame) + { + _app = inApp; + _parentFrame = inParentFrame; + } + + + /** + * Show the edit point dialog + * @param inTrack track object + * @param inPoint point to edit + */ + public void showDialog(Track inTrack, DataPoint inPoint) + { + _track = inTrack; + _point = inPoint; + _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.pointedit.title"), true); + _dialog.setLocationRelativeTo(_parentFrame); + // Check field list + FieldList fieldList = _track.getFieldList(); + int numFields = fieldList.getNumFields(); + // Create table model for point editor + _model = new EditFieldsTableModel(numFields); + for (int i=0; i mayor compresión) +dialog.compresstrack.text=Track comprimido +dialog.compresstrack.single.text=punto eliminado +dialog.compresstrack.multi.text=puntos eliminados +dialog.compresstrack.nonefound=Ningún punto eliminado +dialog.openoptions.title=Opciones de abrir +dialog.openoptions.filesnippet=Extraer archivo +dialog.load.table.field=Campo +dialog.load.table.datatype=Tipo de datos +dialog.load.table.description=Descripción +dialog.delimiter.label=Separador de campo +dialog.delimiter.comma=Coma , +dialog.delimiter.tab=Tabulador +dialog.delimiter.space=Espacio +dialog.delimiter.semicolon=Punto y coma ; +dialog.delimiter.other=Otro +dialog.openoptions.deliminfo.records=datos, con +dialog.openoptions.deliminfo.fields=campos +dialog.openoptions.deliminfo.norecords=Ningun dato +dialog.openoptions.tabledesc=Extraer archivo +dialog.openoptions.altitudeunits=Unidades altitud +dialog.jpegload.subdirectories=Incluir subdirectorios +dialog.jpegload.progress.title=Cargando fotos +dialog.jpegload.progress=Por favor espere mientras se buscan las fotos +dialog.jpegload.title=Fotos cargadas +dialog.jpegload.photoadded=Foto incluida +dialog.jpegload.photosadded=Fotos incluidas +dialog.saveoptions.title=Guardar archivo +dialog.save.fieldstosave=Campos a guardar +dialog.save.table.field=Campo +dialog.save.table.hasdata=Contiene datos +dialog.save.table.save=Guardar +dialog.save.headerrow=Título fila +dialog.save.coordinateunits=Unidades de las coordenadas +dialog.save.units.original=Original +dialog.save.altitudeunits=Unidades de las altitudes +dialog.save.oktitle=Guardando archivo +dialog.save.ok1=Guardando +dialog.save.ok2=puntos al archivo +dialog.save.overwrite.title=El archivo ya existe +dialog.save.overwrite.text=El archivo ya existe, desea sobreescribirlo? +dialog.exportkml.title=Exportar KML +dialog.exportkml.text=Introduzca breve descripción para los datos +dialog.exportkml.filetype=Archivos KML +dialog.exportpov.title=Exportar POV +dialog.exportpov.text=Introdzca los Parametros para exportar +dialog.exportpov.font=Fuente +dialog.exportpov.camerax=Camera X +dialog.exportpov.cameray=Camera Y +dialog.exportpov.cameraz=Camera Z +dialog.exportpov.filetype=Archivos POV +dialog.exportpov.warningtracksize=This track has a large number of points, which Java3D might not be able to display.\nAre you sure you want to continue? +dialog.confirmreversetrack.title=Confirmar inversión +dialog.confirmreversetrack.text=Este track contiene información sobre la fecha, que estará fuera de secuencia después de la inversión. Esta seguro que desea invertir esta sección? +dialog.interpolate.title=Interpolar puntos +dialog.interpolate.parameter.text=Número de los puntos a insertar entre los puntos elegidos +dialog.undo.title=Deshacer +dialog.undo.pretext=Por favor, seleccione la operación(es) a deshacer +dialog.confirmundo.title=Operación(es) no realizada(s) +dialog.confirmundo.single.text=operación no realizada +dialog.confirmundo.multiple.text=operación(es) no realizada(s) +dialog.undo.none.title=No se puede deshacer +dialog.undo.none.text=Ninguna operación a deshacer +dialog.clearundo.title=Despejar la lista de deshacer +dialog.clearundo.text=Esta seguro que desea despejar la lista de deshacer?, se perderá toda la información! +dialog.pointedit.title=Editar punto +dialog.pointedit.text=Seleccione cada campo a editar y use el botón 'Editar' para modificar el valor +dialog.pointedit.table.field=Campo +dialog.pointedit.table.value=Valor +dialog.pointedit.table.changed=Modificado +dialog.pointedit.changevalue.text=Introduzca el nuevo valor de campo +dialog.pointedit.changevalue.title=Editar campo +dialog.pointnameedit.title=Editar nombre de waypoint +dialog.pointnameedit.name=Nombre de waypoint +dialog.pointnameedit.uppercase=Maysculas +dialog.pointnameedit.lowercase=minsculas +dialog.pointnameedit.sentencecase=Mezcla +dialog.about.title=Acerca de Prune +dialog.about.version=Versión +dialog.about.build=Construir +dialog.about.summarytext1=Prune es un programa para cargar, mostrar y editar datos de receptores GPS. +dialog.about.summarytext2=Distribuido bajo el GNU GPL para uso libre y gratuito.
Se permite (y se anima) la copia, redistribución y modificación de acuerdo
a las condiciones incluidas en el archivo licence.txt. +dialog.about.summarytext3=Por favor, ver http://activityworkshop.net/ para más información y guías del usuario. +dialog.about.translatedby=Traducción en español por activityworkshop y un alma muy gentil! + +# 3d window +dialog.3d.title=Prune vista 3-D +dialog.3d.altitudecap=Escala de las altitudes + +# Buttons +button.ok=Aceptar +button.back=Anterior +button.next=Siguiente +button.finish=Completar +button.cancel=Cancelar +button.overwrite=Sobreescribir +button.moveup=Mover hacia arriba +button.movedown=Mover hacia abajo +button.startrange=Fijar comienzo +button.endrange=Fijar final +button.deletepoint=Eliminar punto +button.deleterange=Eliminar rango +button.edit=Editar +button.exit=Salir +button.close=Cerrar +button.continue=Continúe + +# Display components +display.nodata=Ningún dato cargado +display.noaltitudes=Los datos del track no incluyen altitudes +details.trackdetails=Detalles del track +details.notrack=Ningún track cargado +details.track.points=Puntos +details.track.file=Archivo +details.track.numfiles=Número de archivos +details.pointdetails=Detalles del punto +details.index.selected=Indice seleccionado +details.index.of=de +details.nopointselection=Ningún punto seleccionado +details.photofile=Archivo de fotos +details.norangeselection=Ningún rango seleccionado +details.rangedetails=Detalles del rango +details.range.selected=Rango seleccionado +details.range.to=hacia +details.altitude.to=hacia +details.range.climb=Ascenso +details.range.descent=Descenso +details.distanceunits=Unidades de distancia +display.range.time.secs=s +display.range.time.mins=m +display.range.time.hours=h +display.range.time.days=d +details.waypointsphotos.waypoints=Waypoints +details.waypointsphotos.photos=Fotos + +# Field names +fieldname.latitude=Latitud +fieldname.longitude=Longitud +fieldname.altitude=Altitud +fieldname.timestamp=Información de tiempo +fieldname.waypointname=Nombre +fieldname.waypointtype=Tipo +fieldname.newsegment=Segmento +fieldname.custom=Personalizado +fieldname.prefix=Campo +fieldname.distance=Distancia +fieldname.duration=Duración + +# Measurement units +units.metres=Metros +units.metres.short=m +units.feet=Pies +units.feet.short=ft +units.kilometres=Kilómetros +units.kilometres.short=km +units.miles=Millas +units.miles.short=mi +units.degminsec=Gra-min-seg +units.degmin=Gra-min +units.deg=Grados + +# Cardinals for 3d plots +cardinal.n=N +cardinal.s=S +cardinal.e=E +cardinal.w=O + +# Undo operations +undo.load=cargar datos +undo.loadphotos=cargar fotos +undo.editpoint=editar punto +undo.deletepoint=eliminar punto +undo.deleterange=eliminar rango +undo.compress=comprimir track +undo.insert=insertar puntos +undo.deleteduplicates=eliminar duplicados +undo.reverse=invertir rango +undo.rearrangewaypoints=reordenar waypoints + +# Error messages +error.save.dialogtitle=Fallo al guardar datos +error.save.nodata=Ningún dato salvado +error.save.failed=Fallo al guardar datos al archivo: +error.load.dialogtitle=Fallo al cargar datos +error.load.noread=No se puede leer el fichero +error.jpegload.dialogtitle=Error cargando fotos +error.jpegload.nofilesfound=Ningún archivo encontrado +error.jpegload.nojpegsfound=Ningún archivo jpeg encontrado +error.jpegload.noexiffound=Ninguna información EXIF encontrada +error.jpegload.nogpsfound=Ninguna información GPS encontrada +error.undofailed.title=Fallo al deshacer +error.undofailed.text=No ha sido posible deshacer la operación +error.function.noop.title=La función no se ha efectuado +error.rearrange.noop=Reordenación de waypoints no se ha efectuado +error.function.notimplemented=Esta función aún no ha sido implementada +error.function.notavailable.title=Función no disponible +error.function.nojava3d=Esta función requiere la librería Java3d, disponible en Sun.com o Blackdown.org. +error.3d.title=Fallo al mostrar 3-D +error.3d=Ha ocurrido un error con la función 3-D diff --git a/tim/prune/load/JpegLoader.java b/tim/prune/load/JpegLoader.java new file mode 100644 index 0000000..6fe52a1 --- /dev/null +++ b/tim/prune/load/JpegLoader.java @@ -0,0 +1,324 @@ +package tim.prune.load; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.ArrayList; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JProgressBar; + +import tim.prune.App; +import tim.prune.I18nManager; +import tim.prune.data.Altitude; +import tim.prune.data.DataPoint; +import tim.prune.data.Latitude; +import tim.prune.data.Longitude; +import tim.prune.data.Photo; +import tim.prune.drew.jpeg.ExifReader; +import tim.prune.drew.jpeg.JpegData; +import tim.prune.drew.jpeg.JpegException; +import tim.prune.drew.jpeg.Rational; + +/** + * Class to manage the loading of Jpegs and dealing with the GPS data from them + */ +public class JpegLoader implements Runnable +{ + private App _app = null; + private JFrame _parentFrame = null; + private JFileChooser _fileChooser = null; + private JCheckBox _subdirCheckbox = null; + private JDialog _progressDialog = null; + private JProgressBar _progressBar = null; + private int[] _fileCounts = null; + private boolean _cancelled = false; + private ArrayList _photos = null; + + + /** + * Constructor + * @param inApp Application object to inform of photo load + * @param inParentFrame parent frame to reference for dialogs + */ + public JpegLoader(App inApp, JFrame inParentFrame) + { + _app = inApp; + _parentFrame = inParentFrame; + } + + /** + * Select an input file and open the GUI frame + * to select load options + */ + public void openFile() + { + if (_fileChooser == null) + { + _fileChooser = new JFileChooser(); + _fileChooser.setMultiSelectionEnabled(true); + _fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + _subdirCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.subdirectories")); + _subdirCheckbox.setSelected(true); + _fileChooser.setAccessory(_subdirCheckbox); + } + if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION) + { + // Bring up dialog before starting + showDialog(); + new Thread(this).start(); + } + } + + + /** + * Show the main dialog + */ + private void showDialog() + { + _progressDialog = new JDialog(_parentFrame, I18nManager.getText("dialog.jpegload.progress.title")); + _progressDialog.setLocationRelativeTo(_parentFrame); + _progressBar = new JProgressBar(0, 100); + _progressBar.setValue(0); + _progressBar.setStringPainted(true); + _progressBar.setString(""); + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); + panel.add(new JLabel(I18nManager.getText("dialog.jpegload.progress"))); + panel.add(_progressBar); + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _cancelled = true; + } + }); + panel.add(cancelButton); + _progressDialog.getContentPane().add(panel); + _progressDialog.pack(); + _progressDialog.show(); + } + + + /** + * Run method for performing tasks in separate thread + */ + public void run() + { + // Initialise arrays, errors, summaries + _fileCounts = new int[4]; // files, jpegs, exifs, gps + _photos = new ArrayList(); + // Loop over selected files/directories + File[] files = _fileChooser.getSelectedFiles(); + int numFiles = countFileList(files, true, _subdirCheckbox.isSelected()); + // if (false) System.out.println("Found " + numFiles + " files"); + _progressBar.setMaximum(numFiles); + _progressBar.setValue(0); + _cancelled = false; + processFileList(files, true, _subdirCheckbox.isSelected()); + _progressDialog.hide(); + if (_cancelled) return; + // System.out.println("Finished - counts are: " + _fileCounts[0] + ", " + _fileCounts[1] + ", " + _fileCounts[2] + ", " + _fileCounts[3]); + if (_fileCounts[0] == 0) + { + JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nofilesfound"), + I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE); + } + else if (_fileCounts[1] == 0) + { + JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nojpegsfound"), + I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE); + } + else if (_fileCounts[2] == 0) + { + JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.noexiffound"), + I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE); + } + else if (_fileCounts[3] == 0) + { + JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nogpsfound"), + I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE); + } + else + { + // Load information into dialog for confirmation + _app.informPhotosLoaded(_photos); + } + } + + + /** + * Process a list of files and/or directories + * @param inFiles array of file/directories + * @param inFirstDir true if first directory + * @param inDescend true to descend to subdirectories + */ + private void processFileList(File[] inFiles, boolean inFirstDir, boolean inDescend) + { + if (inFiles != null) + { + // Loop over elements in array + for (int i=0; i 0) +// System.out.println("Number of errors was: " + jpegData.getNumErrors() + ": " + jpegData.getErrors().get(0)); + if (jpegData.getExifDataPresent()) + _fileCounts[2]++; // exif found + if (jpegData.isValid()) + { +// if (false && jpegData.getTimestamp() != null) +// System.out.println("Timestamp is " + jpegData.getTimestamp()[0].toString() + ":" + jpegData.getTimestamp()[1].toString() + ":" + jpegData.getTimestamp()[2].toString()); +// if (false && jpegData.getDatestamp() != null) +// System.out.println("Datestamp is " + jpegData.getDatestamp()[0].toString() + ":" + jpegData.getDatestamp()[1].toString() + ":" + jpegData.getDatestamp()[2].toString()); + // Make DataPoint and Photo + DataPoint point = createDataPoint(jpegData); + Photo photo = new Photo(inFile); + point.setPhoto(photo); + photo.setDataPoint(point); + _photos.add(photo); +// System.out.println("Made photo: " + photo.getFile().getAbsolutePath() + " with the datapoint: " +// + point.getLatitude().output(Latitude.FORMAT_DEG_MIN_SEC) + ", " +// + point.getLongitude().output(Longitude.FORMAT_DEG_MIN_SEC) + ", " +// + point.getAltitude().getValue(Altitude.FORMAT_METRES)); + _fileCounts[3]++; + } + } + catch (JpegException jpe) { // don't list errors, just count them + } + } + + + /** + * Process the given directory, by looping over its contents + * and recursively through its subdirectories + * @param inDirectory directory to read + * @param inDescend true to descend subdirectories + */ + private void processDirectory(File inDirectory, boolean inDescend) + { + File[] files = inDirectory.listFiles(); + processFileList(files, false, inDescend); + } + + + /** + * Recursively count the selected Files so we can draw a progress bar + * @param inFiles file list + * @param inFirstDir true if first directory + * @param inDescend true to descend to subdirectories + * @return count of the files selected + */ + private int countFileList(File[] inFiles, boolean inFirstDir, boolean inDescend) + { + int fileCount = 0; + if (inFiles != null) + { + // Loop over elements in array + for (int i=0; i 0.0) + { + _cameraX = "" + (inX / cameraDist * DEFAULT_CAMERA_DISTANCE); + _cameraY = "" + (inY / cameraDist * DEFAULT_CAMERA_DISTANCE); + // Careful! Need to convert from java3d (right-handed) to povray (left-handed) coordinate system! + _cameraZ = "" + (-inZ / cameraDist * DEFAULT_CAMERA_DISTANCE); + } + } + + + /** + * @param inAltitudeCap altitude cap to use + */ + public void setAltitudeCap(int inAltitudeCap) + { + _altitudeCap = inAltitudeCap; + if (_altitudeCap < ThreeDModel.MINIMUM_ALTITUDE_CAP) + { + _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP; + } + } + + + /** + * Show the dialog to select options and export file + */ + public void showDialog() + { + // Make dialog window to select angles, colours etc + if (_dialog == null) + { + _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportpov.title"), true); + _dialog.setLocationRelativeTo(_parentFrame); + _dialog.getContentPane().add(makeDialogComponents()); + } + + // Set angles + _cameraXField.setText(_cameraX); + _cameraYField.setText(_cameraY); + _cameraZField.setText(_cameraZ); + // Set vertical scale + _altitudeCapField.setText("" + _altitudeCap); + // Show dialog + _dialog.pack(); + _dialog.show(); + } + + + /** + * Make the dialog components to select the export options + * @return Component holding gui elements + */ + private Component makeDialogComponents() + { + JPanel panel = new JPanel(); + panel.setLayout(new BorderLayout()); + panel.add(new JLabel(I18nManager.getText("dialog.exportpov.text")), BorderLayout.NORTH); + // OK, Cancel buttons + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + JButton okButton = new JButton(I18nManager.getText("button.ok")); + okButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + doExport(); + _dialog.dispose(); + } + }); + buttonPanel.add(okButton); + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _dialog.dispose(); + } + }); + buttonPanel.add(cancelButton); + panel.add(buttonPanel, BorderLayout.SOUTH); + + // central panel + JPanel centralPanel = new JPanel(); + centralPanel.setLayout(new GridLayout(0, 2, 10, 4)); + + JLabel fontLabel = new JLabel(I18nManager.getText("dialog.exportpov.font")); + fontLabel.setHorizontalAlignment(SwingConstants.TRAILING); + centralPanel.add(fontLabel); + _fontName = new JTextField(DEFAULT_FONT_FILE, 12); + _fontName.setAlignmentX(Component.LEFT_ALIGNMENT); + centralPanel.add(_fontName); + //coordinates of camera + JLabel cameraXLabel = new JLabel(I18nManager.getText("dialog.exportpov.camerax")); + cameraXLabel.setHorizontalAlignment(SwingConstants.TRAILING); + centralPanel.add(cameraXLabel); + _cameraXField = new JTextField("" + _cameraX); + centralPanel.add(_cameraXField); + JLabel cameraYLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameray")); + cameraYLabel.setHorizontalAlignment(SwingConstants.TRAILING); + centralPanel.add(cameraYLabel); + _cameraYField = new JTextField("" + _cameraY); + centralPanel.add(_cameraYField); + JLabel cameraZLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameraz")); + cameraZLabel.setHorizontalAlignment(SwingConstants.TRAILING); + centralPanel.add(cameraZLabel); + _cameraZField = new JTextField("" + _cameraZ); + centralPanel.add(_cameraZField); + // Altitude capping + JLabel altitudeCapLabel = new JLabel(I18nManager.getText("dialog.3d.altitudecap")); + altitudeCapLabel.setHorizontalAlignment(SwingConstants.TRAILING); + centralPanel.add(altitudeCapLabel); + _altitudeCapField = new JTextField("" + _altitudeCap); + centralPanel.add(_altitudeCapField); + + JPanel flowPanel = new JPanel(); + flowPanel.add(centralPanel); + panel.add(flowPanel, BorderLayout.CENTER); + return panel; + } + + + /** + * Select the file and export data to it + */ + private void doExport() + { + // Copy camera coordinates + _cameraX = checkCoordinate(_cameraXField.getText()); + _cameraY = checkCoordinate(_cameraYField.getText()); + _cameraZ = checkCoordinate(_cameraZField.getText()); + + // OK pressed, so choose output file + boolean fileSaved = false; + if (_fileChooser == null) + _fileChooser = new JFileChooser(); + _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG); + _fileChooser.setFileFilter(new FileFilter() { + public boolean accept(File f) + { + return (f != null && (f.isDirectory() || f.getName().toLowerCase().endsWith(".pov"))); + } + public String getDescription() + { + return I18nManager.getText("dialog.exportpov.filetype"); + } + }); + _fileChooser.setAcceptAllFileFilterUsed(false); + + // Allow choose again if an existing file is selected + boolean chooseAgain = false; + do + { + chooseAgain = false; + if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION) + { + // OK pressed and file chosen + File file = _fileChooser.getSelectedFile(); + if (!file.getName().toLowerCase().endsWith(".pov")) + { + file = new File(file.getAbsolutePath() + ".pov"); + } + // Check if file exists and if necessary prompt for overwrite + Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")}; + if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame, + I18nManager.getText("dialog.save.overwrite.text"), + I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1]) + == JOptionPane.YES_OPTION) + { + // Export the file + if (exportFile(file)) + { + fileSaved = true; + } + else + { + // export failed so need to choose again + chooseAgain = true; + } + } + else + { + // overwrite cancelled so need to choose again + chooseAgain = true; + } + } + } while (chooseAgain); + } + + + /** + * Export the track data to the specified file + * @param inFile File object to save to + * @return true if successful + */ + private boolean exportFile(File inFile) + { + FileWriter writer = null; + // find out the line separator for this system + String lineSeparator = System.getProperty("line.separator"); + try + { + // create and scale model + ThreeDModel model = new ThreeDModel(_track); + try + { + // try to use given altitude cap + _altitudeCap = Integer.parseInt(_altitudeCapField.getText()); + model.setAltitudeCap(_altitudeCap); + } + catch (NumberFormatException nfe) {} + model.scale(); + + // Create file and write basics + writer = new FileWriter(inFile); + writeStartOfFile(writer, model.getModelSize(), lineSeparator); + + // write out lat/long lines using model + writeLatLongLines(writer, model, lineSeparator); + + // write out points + writeDataPoints(writer, model, lineSeparator); + + // everything worked + JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.ok1") + + " " + _track.getNumPoints() + " " + I18nManager.getText("dialog.save.ok2") + + " " + inFile.getAbsolutePath(), + I18nManager.getText("dialog.save.oktitle"), JOptionPane.INFORMATION_MESSAGE); + return true; + } + catch (IOException ioe) + { + JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + ioe.getMessage(), + I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE); + } + finally + { + // close file ignoring exceptions + try + { + writer.close(); + } + catch (Exception e) {} + } + return false; + } + + + /** + * Write the start of the Pov file, including base plane and lights + * @param inWriter Writer to use for writing file + * @param inModelSize model size + * @param inLineSeparator line separator to use + * @throws IOException on file writing error + */ + private void writeStartOfFile(FileWriter inWriter, double inModelSize, String inLineSeparator) + throws IOException + { + inWriter.write("// Pov file produced by Prune - see http://activityworkshop.net/"); + inWriter.write(inLineSeparator); + inWriter.write(inLineSeparator); + // Select font based on user input + String fontPath = _fontName.getText(); + if (fontPath == null || fontPath.equals("")) + { + fontPath = DEFAULT_FONT_FILE; + } + // Set up output + String[] outputLines = { + "global_settings { ambient_light rgb <4, 4, 4> }", "", + "// Background and camera", + "background { color rgb <0, 0, 0> }", + // camera position + "camera {", + " location <" + _cameraX + ", " + _cameraY + ", " + _cameraZ + ">", + " look_at <0, 0, 0>", + "}", "", + // global declares + "// Global declares", + "#declare lat_line =", + " cylinder {", + " <-" + inModelSize + ", 0.1, 0>,", + " <" + inModelSize + ", 0.1, 0>,", + " 0.1 // Radius", + " pigment { color rgb <0.5 0.5 0.5> }", + " }", + "#declare lon_line =", + " cylinder {", + " <0, 0.1, -" + inModelSize + ">,", + " <0, 0.1, " + inModelSize + ">,", + " 0.1 // Radius", + " pigment { color rgb <0.5 0.5 0.5> }", + " }", + "#declare point_rod =", + " cylinder {", + " <0, 0, 0>,", + " <0, 1, 0>,", + " 0.15", + " open", + " pigment { color rgb <0.5 0.5 0.5> }", + " }", "", + // TODO: Export rods to POV? How to store in data? + "#declare waypoint_sphere =", + " sphere {", + " <0, 0, 0>, 0.4", + " texture {", + " pigment {color rgb <0.1 0.1 1.0>}", + " finish { phong 1 }", + " }", + " }", + "#declare track_sphere0 =", + " sphere {", + " <0, 0, 0>, 0.3", // size should depend on model size + " texture {", + " pigment {color rgb <0.2 1.0 0.2>}", + " finish { phong 1 }", + " }", + " }", + "#declare track_sphere1 =", + " sphere {", + " <0, 0, 0>, 0.3", // size should depend on model size + " texture {", + " pigment {color rgb <0.6 1.0 0.2>}", + " finish { phong 1 }", + " }", + " }", + "#declare track_sphere2 =", + " sphere {", + " <0, 0, 0>, 0.3", // size should depend on model size + " texture {", + " pigment {color rgb <1.0 1.0 0.1>}", + " finish { phong 1 }", + " }", + " }", + "#declare track_sphere3 =", + " sphere {", + " <0, 0, 0>, 0.3", // size should depend on model size + " texture {", + " pigment {color rgb <1.0 1.0 1.0>}", + " finish { phong 1 }", + " }", + " }", + "#declare track_sphere4 =", + " sphere {", + " <0, 0, 0>, 0.3", // size should depend on model size + " texture {", + " pigment {color rgb <0.1 1.0 1.0>}", + " finish { phong 1 }", + " }", + " }", "", + "// Base plane", + "box {", + " <-" + inModelSize + ", -0.15, -" + inModelSize + ">, // Near lower left corner", + " <" + inModelSize + ", 0.15, " + inModelSize + "> // Far upper right corner", + " pigment { color rgb <0.5 0.75 0.8> }", + "}", "", + // write cardinals + "// Cardinal letters N,S,E,W", + "text {", + " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.n") + "\" 0.3, 0", + " pigment { color rgb <1 1 1> }", + " translate <0, 0.2, " + inModelSize + ">", + "}", + "text {", + " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.s") + "\" 0.3, 0", + " pigment { color rgb <1 1 1> }", + " translate <0, 0.2, -" + inModelSize + ">", + "}", + "text {", + " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.e") + "\" 0.3, 0", + " pigment { color rgb <1 1 1> }", + " translate <" + (inModelSize * 0.97) + ", 0.2, 0>", + "}", + "text {", + " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.w") + "\" 0.3, 0", + " pigment { color rgb <1 1 1> }", + " translate <-" + (inModelSize * 1.03) + ", 0.2, 0>", + "}", "", + // TODO: Light positions should relate to model size + "// lights", + "light_source { <-1, 9, -4> color rgb <0.5 0.5 0.5>}", + "light_source { <1, 6, -14> color rgb <0.6 0.6 0.6>}", + "light_source { <11, 12, 8> color rgb <0.3 0.3 0.3>}", + "", + }; + // write strings to file + int numLines = outputLines.length; + for (int i=0; i }"); + inWriter.write(inLineSeparator); + } + numlines = inModel.getNumLongitudeLines(); + for (int i=0; i }"); + inWriter.write(inLineSeparator); + } + inWriter.write(inLineSeparator); + } + + + /** + * Write out all the data points to the file + * @param inWriter Writer to use for writing file + * @param inModel model object for getting data points + * @param inLineSeparator line separator to use + * @throws IOException on file writing error + */ + private void writeDataPoints(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator) + throws IOException + { + inWriter.write("// Data points:"); + inWriter.write(inLineSeparator); + int numPoints = inModel.getNumPoints(); + for (int i=0; i }"); + } + else + { + // normal track point ball + inWriter.write("object { track_sphere" + checkHeightCode(inModel.getPointHeightCode(i)) + + " translate <" + inModel.getScaledHorizValue(i) + "," + inModel.getScaledAltValue(i) + + "," + inModel.getScaledVertValue(i) + "> }"); + } + inWriter.write(inLineSeparator); + // vertical rod (if altitude positive) + if (inModel.getScaledAltValue(i) > 0.0) + { + inWriter.write("object { point_rod translate <" + inModel.getScaledHorizValue(i) + ",0," + + inModel.getScaledVertValue(i) + "> scale <1," + inModel.getScaledAltValue(i) + ",1> }"); + inWriter.write(inLineSeparator); + } + } + inWriter.write(inLineSeparator); + } + + + /** + * @param inCode height code to check + * @return validated height code within range 0 to max + */ + private static byte checkHeightCode(byte inCode) + { + final byte maxHeightCode = 4; + if (inCode < 0) return 0; + if (inCode > maxHeightCode) return maxHeightCode; + return inCode; + } + + + /** + * Check the given coordinate + * @param inString String entered by user + * @return validated String value + */ + private static String checkCoordinate(String inString) + { + double value = 0.0; + try + { + value = Double.parseDouble(inString); + } + catch (Exception e) {} // ignore parse failures + return "" + value; + } +} diff --git a/tim/prune/threedee/Java3DWindow.java b/tim/prune/threedee/Java3DWindow.java new file mode 100644 index 0000000..e65d53d --- /dev/null +++ b/tim/prune/threedee/Java3DWindow.java @@ -0,0 +1,560 @@ +package tim.prune.threedee; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsEnvironment; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.geom.GeneralPath; + +import javax.media.j3d.AmbientLight; +import javax.media.j3d.Appearance; +import javax.media.j3d.BoundingSphere; +import javax.media.j3d.BranchGroup; +import javax.media.j3d.Canvas3D; +import javax.media.j3d.Font3D; +import javax.media.j3d.FontExtrusion; +import javax.media.j3d.GraphicsConfigTemplate3D; +import javax.media.j3d.Group; +import javax.media.j3d.Material; +import javax.media.j3d.PointLight; +import javax.media.j3d.Shape3D; +import javax.media.j3d.Text3D; +import javax.media.j3d.Transform3D; +import javax.media.j3d.TransformGroup; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.vecmath.Color3f; +import javax.vecmath.Matrix3d; +import javax.vecmath.Point3d; +import javax.vecmath.Point3f; +import javax.vecmath.Vector3d; + +import com.sun.j3d.utils.behaviors.vp.OrbitBehavior; +import com.sun.j3d.utils.geometry.Box; +import com.sun.j3d.utils.geometry.Cylinder; +import com.sun.j3d.utils.geometry.Sphere; +import com.sun.j3d.utils.universe.SimpleUniverse; + +import tim.prune.App; +import tim.prune.I18nManager; +import tim.prune.data.Altitude; +import tim.prune.data.Track; + + +/** + * Class to hold main window for java3d view of data + */ +public class Java3DWindow implements ThreeDWindow +{ + private App _app = null; + private Track _track = null; + private JFrame _parentFrame = null; + private JFrame _frame = null; + private OrbitBehavior _orbit = null; + private int _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP; + + /** only prompt about big track size once */ + private static boolean TRACK_SIZE_WARNING_GIVEN = false; + + // Constants + private static final double INITIAL_Y_ROTATION = -25.0; + private static final double INITIAL_X_ROTATION = 15.0; + private static final int INITIAL_ALTITUDE_CAP = 500; + private static final String CARDINALS_FONT = "Arial"; + private static final int MAX_TRACK_SIZE = 2500; // threshold for warning + + + /** + * Constructor + * @param inApp App object to use for callbacks + * @param inFrame parent frame + */ + public Java3DWindow(App inApp, JFrame inFrame) + { + _app = inApp; + _parentFrame = inFrame; + } + + + /** + * Set the track object + * @param inTrack Track object + */ + public void setTrack(Track inTrack) + { + _track = inTrack; + } + + + /** + * Show the window + */ + public void show() throws ThreeDException + { + // Get the altitude cap to use + String altitudeUnits = getAltitudeUnitsLabel(_track); + Object altCapString = JOptionPane.showInputDialog(_parentFrame, + I18nManager.getText("dialog.3d.altitudecap") + " (" + altitudeUnits + ")", + I18nManager.getText("dialog.3d.title"), + JOptionPane.QUESTION_MESSAGE, null, null, "" + _altitudeCap); + if (altCapString == null) return; + try + { + _altitudeCap = Integer.parseInt(altCapString.toString()); + } + catch (Exception e) {} // Ignore parse errors + + // Set up the graphics config + GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration(); + if (config == null) + { + // Config shouldn't be null, but we can try to create a new one as a workaround + GraphicsConfigTemplate3D gc = new GraphicsConfigTemplate3D(); + gc.setDepthSize(0); + config = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getBestConfiguration(gc); + } + + if (config == null) + { + // Second attempt also failed, going to have to give up here. + throw new ThreeDException("Couldn't create graphics config"); + } + + // Check number of points in model isn't too big, and suggest compression + Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")}; + if (_track.getNumPoints() > MAX_TRACK_SIZE && !TRACK_SIZE_WARNING_GIVEN) + { + if (JOptionPane.showOptionDialog(_frame, + I18nManager.getText("dialog.exportpov.warningtracksize"), + I18nManager.getText("dialog.exportpov.title"), JOptionPane.OK_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1]) + == JOptionPane.OK_OPTION) + { + // opted to continue, don't show warning again + TRACK_SIZE_WARNING_GIVEN = true; + } + else + { + // opted to cancel - show warning again next time + return; + } + } + + Canvas3D canvas = new Canvas3D(config); + canvas.setSize(400, 300); + + // Create the scene and attach it to the virtual universe + BranchGroup scene = createSceneGraph(); + SimpleUniverse u = new SimpleUniverse(canvas); + + // This will move the ViewPlatform back a bit so the + // objects in the scene can be viewed. + u.getViewingPlatform().setNominalViewingTransform(); + + // Add behaviour to rotate using mouse + _orbit = new OrbitBehavior(canvas, OrbitBehavior.REVERSE_ALL | + OrbitBehavior.STOP_ZOOM); + BoundingSphere bounds = new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0); + _orbit.setSchedulingBounds(bounds); + u.getViewingPlatform().setViewPlatformBehavior(_orbit); + u.addBranchGraph(scene); + + // Don't reuse _frame object from last time, because data and/or scale might be different + // Need to regenerate everything + _frame = new JFrame(I18nManager.getText("dialog.3d.title")); + _frame.getContentPane().setLayout(new BorderLayout()); + _frame.getContentPane().add(canvas, BorderLayout.CENTER); + // Make panel for render, close buttons + JPanel panel = new JPanel(); + panel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + // Add callback button for render + JButton renderButton = new JButton(I18nManager.getText("menu.file.exportpov")); + renderButton.addActionListener(new ActionListener() + { + /** Render button pressed */ + public void actionPerformed(ActionEvent e) + { + if (_orbit != null) + { + callbackRender(); + } + }}); + panel.add(renderButton); + JButton closeButton = new JButton(I18nManager.getText("button.close")); + closeButton.addActionListener(new ActionListener() + { + /** Close button pressed - clean up */ + public void actionPerformed(ActionEvent e) + { + _frame.dispose(); + _frame = null; + _orbit = null; + }}); + panel.add(closeButton); + _frame.getContentPane().add(panel, BorderLayout.SOUTH); + _frame.setSize(500, 350); + _frame.pack(); + + // show frame + _frame.show(); + if (_frame.getState() == JFrame.ICONIFIED) + { + _frame.setState(JFrame.NORMAL); + } + } + + + /** + * Create the whole scenery from the given track + * @return all objects in the scene + */ + private BranchGroup createSceneGraph() + { + // Create the root of the branch graph + BranchGroup objRoot = new BranchGroup(); + + // Create the transform group node and initialize it. + // Enable the TRANSFORM_WRITE capability so it can be spun by the mouse + TransformGroup objTrans = new TransformGroup(); + objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + + // Create a translation + Transform3D shiftz = new Transform3D(); + shiftz.setScale(0.055); + TransformGroup shiftTrans = new TransformGroup(shiftz); + + objRoot.addChild(shiftTrans); + Transform3D rotTrans = new Transform3D(); + rotTrans.rotY(Math.toRadians(INITIAL_Y_ROTATION)); + Transform3D rot2 = new Transform3D(); + rot2.rotX(Math.toRadians(INITIAL_X_ROTATION)); + TransformGroup tg2 = new TransformGroup(rot2); + objTrans.setTransform(rotTrans); + shiftTrans.addChild(tg2); + tg2.addChild(objTrans); + + // Base plane + Appearance planeAppearance = null; + Box plane = null; + Transform3D planeShift = null; + TransformGroup planeTrans = null; + planeAppearance = new Appearance(); + planeAppearance.setMaterial(new Material(new Color3f(0.1f, 0.2f, 0.2f), + new Color3f(0.0f, 0.0f, 0.0f), new Color3f(0.3f, 0.4f, 0.4f), + new Color3f(0.3f, 0.3f, 0.3f), 0.0f)); + plane = new Box(10f, 0.04f, 10f, planeAppearance); + objTrans.addChild(plane); + + // N, S, E, W + GeneralPath bevelPath = new GeneralPath(); + bevelPath.moveTo(0.0f, 0.0f); + for (int i=0; i<91; i+= 5) + bevelPath.lineTo((float) (0.1 - 0.1 * Math.cos(Math.toRadians(i))), + (float) (0.1 * Math.sin(Math.toRadians(i)))); + for (int i=90; i>0; i-=5) + bevelPath.lineTo((float) (0.3 + 0.1 * Math.cos(Math.toRadians(i))), + (float) (0.1 * Math.sin(Math.toRadians(i)))); + Font3D compassFont = new Font3D( + new Font(CARDINALS_FONT, Font.PLAIN, 1), + new FontExtrusion(bevelPath)); + objTrans.addChild(createCompassPoint(I18nManager.getText("cardinal.n"), new Point3f(0f, 0f, -10f), compassFont)); + objTrans.addChild(createCompassPoint(I18nManager.getText("cardinal.s"), new Point3f(0f, 0f, 10f), compassFont)); + objTrans.addChild(createCompassPoint(I18nManager.getText("cardinal.w"), new Point3f(-11f, 0f, 0f), compassFont)); + objTrans.addChild(createCompassPoint(I18nManager.getText("cardinal.e"), new Point3f(10f, 0f, 0f), compassFont)); + + // create and scale model + ThreeDModel model = new ThreeDModel(_track); + model.setAltitudeCap(_altitudeCap); + model.scale(); + + // Lat/Long lines + objTrans.addChild(createLatLongs(model)); + + // Add points to model + objTrans.addChild(createDataPoints(model)); + + // Create lights + BoundingSphere bounds = + new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0); + AmbientLight aLgt = new AmbientLight(new Color3f(1.0f, 1.0f, 1.0f)); + aLgt.setInfluencingBounds(bounds); + objTrans.addChild(aLgt); + + PointLight pLgt = new PointLight(new Color3f(1.0f, 1.0f, 1.0f), + new Point3f(0f, 0f, 2f), + new Point3f(0.25f, 0.05f, 0.0f) ); + pLgt.setInfluencingBounds(bounds); + objTrans.addChild(pLgt); + + PointLight pl2 = new PointLight(new Color3f(0.8f, 0.9f, 0.4f), + new Point3f(6f, 1f, 6f), + new Point3f(0.2f, 0.1f, 0.05f) ); + pl2.setInfluencingBounds(bounds); + objTrans.addChild(pl2); + + PointLight pl3 = new PointLight(new Color3f(0.7f, 0.7f, 0.7f), + new Point3f(0.0f, 12f, -2f), + new Point3f(0.1f, 0.1f, 0.0f) ); + pl3.setInfluencingBounds(bounds); + objTrans.addChild(pl3); + + // Have Java 3D perform optimizations on this scene graph. + objRoot.compile(); + + return objRoot; + } + + + /** + * Create a text object for compass point, N S E or W + * @param text text to display + * @param locn position at which to display + * @param font 3d font to use + * @return Shape3D object + */ + private Shape3D createCompassPoint(String inText, Point3f inLocn, Font3D inFont) + { + Text3D txt = new Text3D(inFont, inText, inLocn, Text3D.ALIGN_FIRST, Text3D.PATH_RIGHT); + Material mat = new Material(new Color3f(0.5f, 0.5f, 0.55f), + new Color3f(0.05f, 0.05f, 0.1f), new Color3f(0.3f, 0.4f, 0.5f), + new Color3f(0.4f, 0.5f, 0.7f), 70.0f); + mat.setLightingEnable(true); + Appearance app = new Appearance(); + app.setMaterial(mat); + Shape3D shape = new Shape3D(txt, app); + return shape; + } + + + /** + * Create all the latitude and longitude lines on the base plane + * @param inModel model containing data + * @return Group object containing cylinders for lat and long lines + */ + private static Group createLatLongs(ThreeDModel inModel) + { + Group group = new Group(); + int numlines = inModel.getNumLatitudeLines(); + for (int i=0; i= 5) mat.setDiffuseColor(new Color3f(1.0f, 1.0f, 1.0f)); + // return object + return mat; + } + + + /** + * Create a ball at the given point + * @param inPosition scaled position of point + * @param inSphere sphere object + * @param inMaterial material object + * @return Group containing sphere + */ + private static Group createBall(Point3d inPosition, Sphere inSphere, Material inMaterial) + { + Group group = new Group(); + // Create ball and add to group + Transform3D ballShift = new Transform3D(); + ballShift.setTranslation(new Vector3d(inPosition)); + TransformGroup ballShiftTrans = new TransformGroup(ballShift); + inMaterial.setLightingEnable(true); + Appearance ballApp = new Appearance(); + ballApp.setMaterial(inMaterial); + inSphere.setAppearance(ballApp); + ballShiftTrans.addChild(inSphere); + group.addChild(ballShiftTrans); + // Also create rod for ball to sit on + Cylinder rod = new Cylinder(0.1f, (float) inPosition.y); + Material rodMat = new Material(new Color3f(0.2f, 0.2f, 0.2f), + new Color3f(0.0f, 0.0f, 0.0f), new Color3f(0.2f, 0.2f, 0.2f), + new Color3f(0.05f, 0.05f, 0.05f), 0.4f); + rodMat.setLightingEnable(true); + Appearance rodApp = new Appearance(); + rodApp.setMaterial(rodMat); + rod.setAppearance(rodApp); + Transform3D rodShift = new Transform3D(); + rodShift.setTranslation(new Vector3d(inPosition.x, + inPosition.y/2.0, inPosition.z)); + TransformGroup rodShiftTrans = new TransformGroup(rodShift); + rodShiftTrans.addChild(rod); + group.addChild(rodShiftTrans); + // return the pair + return group; + } + + + /** + * Calculate the angles and call them back to the app + */ + private void callbackRender() + { + Transform3D trans3d = new Transform3D(); + _orbit.getViewingPlatform().getViewPlatformTransform().getTransform(trans3d); + Matrix3d matrix = new Matrix3d(); + trans3d.get(matrix); + Point3d point = new Point3d(0.0, 0.0, 1.0); + matrix.transform(point); + // Set up initial rotations + Transform3D firstTran = new Transform3D(); + firstTran.rotY(Math.toRadians(-INITIAL_Y_ROTATION)); + Transform3D secondTran = new Transform3D(); + secondTran.rotX(Math.toRadians(-INITIAL_X_ROTATION)); + // Apply inverse rotations in reverse order to test point + Point3d result = new Point3d(); + secondTran.transform(point, result); + firstTran.transform(result); + // Callback settings to App + _app.exportPov(result.x, result.y, result.z, _altitudeCap); + } + + + /** + * Get a units label for the altitudes in the given Track + * @param inTrack Track object + * @return units label for altitude used in Track + */ + private static String getAltitudeUnitsLabel(Track inTrack) + { + int altitudeFormat = inTrack.getAltitudeRange().getFormat(); + if (altitudeFormat == Altitude.FORMAT_METRES) + return I18nManager.getText("units.metres.short"); + return I18nManager.getText("units.feet.short"); + } + +} diff --git a/tim/prune/threedee/ThreeDException.java b/tim/prune/threedee/ThreeDException.java new file mode 100644 index 0000000..45cfce0 --- /dev/null +++ b/tim/prune/threedee/ThreeDException.java @@ -0,0 +1,17 @@ +package tim.prune.threedee; + +/** + * Class to hold Exceptions thrown by 3d routines + */ +public class ThreeDException extends Exception +{ + + /** + * Constructor + * @param message error text + */ + public ThreeDException(String message) + { + super(message); + } +} diff --git a/tim/prune/threedee/ThreeDModel.java b/tim/prune/threedee/ThreeDModel.java new file mode 100644 index 0000000..db748c0 --- /dev/null +++ b/tim/prune/threedee/ThreeDModel.java @@ -0,0 +1,239 @@ +package tim.prune.threedee; + +import tim.prune.data.Altitude; +import tim.prune.data.DataPoint; +import tim.prune.data.PointScaler; +import tim.prune.data.Track; + +/** + * Class to hold a 3d model of the track data, + * including all points and scaling operations. + * Used by java3d and also Pov export functions + */ +public class ThreeDModel +{ + private Track _track = null; + private PointScaler _scaler = null; + private double _modelSize; + private int _altitudeCap = -1; + private double _scaleFactor = 1.0; + private double _altFactor = 1.0; + // TODO: How to store rods (lifts) in data? + private byte[] _pointTypes = null; + private byte[] _pointHeights = null; + + private static final double DEFAULT_MODEL_SIZE = 10.0; + public static final int MINIMUM_ALTITUDE_CAP = 100; + + // Constants for point types + public static final byte POINT_TYPE_WAYPOINT = 1; + public static final byte POINT_TYPE_NORMAL_POINT = 2; + + + /** + * Constructor + * @param inTrack Track object + */ + public ThreeDModel(Track inTrack) + { + this(inTrack, DEFAULT_MODEL_SIZE); + } + + + /** + * Constructor + * @param inTrack Track object + * @param inSize model size + */ + public ThreeDModel(Track inTrack, double inSize) + { + _track = inTrack; + _modelSize = inSize; + if (_modelSize <= 0.0) _modelSize = DEFAULT_MODEL_SIZE; + } + + + /** + * @return the number of points in the model + */ + public int getNumPoints() + { + if (_track == null) return 0; + return _track.getNumPoints(); + } + + + /** + * Set the altitude cap + * @param inAltitudeCap altitude range to cap to (ignored if less than data range) + */ + public void setAltitudeCap(int inAltitudeCap) + { + _altitudeCap = inAltitudeCap; + if (_altitudeCap < MINIMUM_ALTITUDE_CAP) + { + _altitudeCap = MINIMUM_ALTITUDE_CAP; + } + } + + + /** + * Scale all points and calculate factors + */ + public void scale() + { + // Use PointScaler to sort out x and y values + _scaler = new PointScaler(_track); + _scaler.scale(); + // Calculate scale factor to fit within box + _scaleFactor = 1.0; + if (_scaler.getMaximumHoriz() > 0.0 || _scaler.getMaximumVert() > 0.0) + { + if (_scaler.getMaximumHoriz() > _scaler.getMaximumVert()) + { + // scale limited by longitude + _scaleFactor = _modelSize / _scaler.getMaximumHoriz(); + } + else + { + // scale limited by latitude + _scaleFactor = _modelSize / _scaler.getMaximumVert(); + } + } + // calculate altitude scale factor + _altFactor = 1.0; + if (_scaler.getMaximumAlt() >= 0) + { + // limit by altitude cap or by data range? + if (_scaler.getMaximumAlt() > _altitudeCap) + { + // data is bigger than cap + _altFactor = _modelSize / _scaler.getMaximumAlt(); + } + else + { + // capped + _altFactor = _modelSize / _altitudeCap; + } + } + // calculate lat/long lines + _scaler.calculateLatLongLines(); + + // calculate point types and height codes + calculatePointTypes(); + } + + + /** + * Calculate the point types and height codes + */ + private void calculatePointTypes() + { + int numPoints = getNumPoints(); + _pointTypes = new byte[numPoints]; + _pointHeights = new byte[numPoints]; + // Loop over points in track + for (int i=0; i 0) + desc = desc + " (" + _numLoaded + ")"; + return desc; + } + + + /** + * Perform the undo operation on the given Track + * Delete both track points and Photo objects + * @param inTrackInfo TrackInfo object on which to perform the operation + */ + public void performUndo(TrackInfo inTrackInfo) throws UndoException + { + // crop track to previous size + int cropIndex = inTrackInfo.getTrack().getNumPoints() - _numLoaded; + inTrackInfo.getTrack().cropTo(cropIndex); + // crop photo list to previous size + // (currently it is assumed that the number of points is the same as number of photos) + cropIndex = inTrackInfo.getPhotoList().getNumPhotos() - _numLoaded; + inTrackInfo.getPhotoList().cropTo(cropIndex); + // clear selection + inTrackInfo.getSelection().clearAll(); + } +} \ No newline at end of file -- 2.43.0