source: josm/trunk/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java@ 8506

Last change on this file since 8506 was 8449, checked in by Don-vip, 9 years ago

remove extra whitespaces

  • Property svn:eol-style set to native
File size: 39.7 KB
RevLine 
[8378]1// License: GPL. For details, see LICENSE file.
[2566]2package org.openstreetmap.josm.gui.layer.geoimage;
3
[304]4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
[2617]7import java.awt.AlphaComposite;
[7912]8import java.awt.BasicStroke;
[2617]9import java.awt.Color;
10import java.awt.Composite;
11import java.awt.Dimension;
[323]12import java.awt.Graphics2D;
[304]13import java.awt.Image;
14import java.awt.Point;
15import java.awt.Rectangle;
[7912]16import java.awt.RenderingHints;
[304]17import java.awt.event.MouseAdapter;
18import java.awt.event.MouseEvent;
[323]19import java.awt.image.BufferedImage;
[2621]20import java.beans.PropertyChangeEvent;
[2617]21import java.beans.PropertyChangeListener;
[304]22import java.io.File;
23import java.io.IOException;
24import java.text.ParseException;
25import java.util.ArrayList;
[2566]26import java.util.Arrays;
[6392]27import java.util.Calendar;
[2621]28import java.util.Collection;
[2566]29import java.util.Collections;
[6392]30import java.util.GregorianCalendar;
[2621]31import java.util.HashSet;
[2566]32import java.util.LinkedHashSet;
[2904]33import java.util.LinkedList;
[1704]34import java.util.List;
[6316]35import java.util.Set;
[6392]36import java.util.TimeZone;
[7935]37import java.util.concurrent.ExecutorService;
38import java.util.concurrent.Executors;
[7983]39import java.util.concurrent.ThreadFactory;
[304]40
[3408]41import javax.swing.Action;
[304]42import javax.swing.Icon;
[2627]43import javax.swing.JLabel;
[304]44import javax.swing.JOptionPane;
[2627]45import javax.swing.SwingConstants;
[304]46
47import org.openstreetmap.josm.Main;
[7764]48import org.openstreetmap.josm.actions.LassoModeAction;
[304]49import org.openstreetmap.josm.actions.RenameLayerAction;
[2629]50import org.openstreetmap.josm.actions.mapmode.MapMode;
[5430]51import org.openstreetmap.josm.actions.mapmode.SelectAction;
[2450]52import org.openstreetmap.josm.data.Bounds;
[304]53import org.openstreetmap.josm.data.coor.LatLon;
[582]54import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
[2627]55import org.openstreetmap.josm.gui.ExtendedDialog;
[2629]56import org.openstreetmap.josm.gui.MapFrame;
[4010]57import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener;
[304]58import org.openstreetmap.josm.gui.MapView;
[4010]59import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
[4627]60import org.openstreetmap.josm.gui.NavigatableComponent;
[304]61import org.openstreetmap.josm.gui.PleaseWaitRunnable;
62import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
[2904]63import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
[2566]64import org.openstreetmap.josm.gui.layer.GpxLayer;
[4751]65import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer;
66import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker;
67import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
[2566]68import org.openstreetmap.josm.gui.layer.Layer;
[7983]69import org.openstreetmap.josm.gui.util.GuiHelper;
[8405]70import org.openstreetmap.josm.io.JpgImporter;
[304]71import org.openstreetmap.josm.tools.ExifReader;
72import org.openstreetmap.josm.tools.ImageProvider;
[6524]73import org.openstreetmap.josm.tools.Utils;
[316]74
[2566]75import com.drew.imaging.jpeg.JpegMetadataReader;
[2749]76import com.drew.lang.CompoundException;
[2566]77import com.drew.metadata.Directory;
78import com.drew.metadata.Metadata;
[4241]79import com.drew.metadata.MetadataException;
[6127]80import com.drew.metadata.exif.ExifIFD0Directory;
[2566]81import com.drew.metadata.exif.GpsDirectory;
82
[6209]83/**
84 * Layer displaying geottaged pictures.
85 */
[4751]86public class GeoImageLayer extends Layer implements PropertyChangeListener, JumpToMarkerLayer {
[444]87
[2566]88 List<ImageEntry> data;
[2646]89 GpxLayer gpxLayer;
[2711]90
[2566]91 private Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker");
92 private Icon selectedIcon = ImageProvider.get("dialogs/geoimage/photo-marker-selected");
[1704]93
[2566]94 private int currentPhoto = -1;
[1704]95
[2617]96 boolean useThumbs = false;
[8285]97 private ExecutorService thumbsLoaderExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
[7935]98 @Override
99 public Thread newThread(Runnable r) {
100 Thread t = new Thread(r);
101 t.setPriority(Thread.MIN_PRIORITY);
102 return t;
103 }
104 });
[8285]105 private ThumbsLoader thumbsloader;
106 private boolean thumbsLoaderRunning = false;
[7935]107 volatile boolean thumbsLoaded = false;
[2617]108 private BufferedImage offscreenBuffer;
109 boolean updateOffscreenBuffer = true;
[2592]110
[2566]111 /** Loads a set of images, while displaying a dialog that indicates what the plugin is currently doing.
112 * In facts, this object is instantiated with a list of files. These files may be JPEG files or
113 * directories. In case of directories, they are scanned to find all the images they contain.
114 * Then all the images that have be found are loaded as ImageEntry instances.
115 */
116 private static final class Loader extends PleaseWaitRunnable {
[1704]117
[4310]118 private boolean canceled = false;
[2566]119 private GeoImageLayer layer;
120 private Collection<File> selection;
[7005]121 private Set<String> loadedDirectories = new HashSet<>();
[6316]122 private Set<String> errorMessages;
[2646]123 private GpxLayer gpxLayer;
[1704]124
[2566]125 protected void rememberError(String message) {
[2592]126 this.errorMessages.add(message);
[1704]127 }
128
[2566]129 public Loader(Collection<File> selection, GpxLayer gpxLayer) {
130 super(tr("Extracting GPS locations from EXIF"));
131 this.selection = selection;
[2646]132 this.gpxLayer = gpxLayer;
[7005]133 errorMessages = new LinkedHashSet<>();
[1704]134 }
135
[2566]136 @Override protected void realRun() throws IOException {
[1704]137
[2566]138 progressMonitor.subTask(tr("Starting directory scan"));
[7005]139 Collection<File> files = new ArrayList<>();
[2566]140 try {
141 addRecursiveFiles(files, selection);
[6287]142 } catch (IllegalStateException e) {
143 rememberError(e.getMessage());
[1704]144 }
145
[4310]146 if (canceled)
[2566]147 return;
148 progressMonitor.subTask(tr("Read photos..."));
149 progressMonitor.setTicksCount(files.size());
[1704]150
[2566]151 progressMonitor.subTask(tr("Read photos..."));
152 progressMonitor.setTicksCount(files.size());
[1704]153
[2566]154 // read the image files
[7005]155 List<ImageEntry> data = new ArrayList<>(files.size());
[1704]156
[2566]157 for (File f : files) {
[1704]158
[4310]159 if (canceled) {
[2566]160 break;
161 }
[1704]162
[2566]163 progressMonitor.subTask(tr("Reading {0}...", f.getName()));
164 progressMonitor.worked(1);
[1704]165
[2566]166 ImageEntry e = new ImageEntry();
[1704]167
[2566]168 // Changed to silently cope with no time info in exif. One case
169 // of person having time that couldn't be parsed, but valid GPS info
[1704]170
[2566]171 try {
[2931]172 e.setExifTime(ExifReader.readTime(f));
[6450]173 } catch (ParseException ex) {
[2931]174 e.setExifTime(null);
[2566]175 }
[2931]176 e.setFile(f);
[2566]177 extractExif(e);
178 data.add(e);
[1863]179 }
[2646]180 layer = new GeoImageLayer(data, gpxLayer);
[2566]181 files.clear();
[1704]182 }
[444]183
[2566]184 private void addRecursiveFiles(Collection<File> files, Collection<File> sel) {
185 boolean nullFile = false;
[2322]186
[2566]187 for (File f : sel) {
[316]188
[4310]189 if(canceled) {
[2566]190 break;
[1863]191 }
192
[2566]193 if (f == null) {
194 nullFile = true;
195
196 } else if (f.isDirectory()) {
197 String canonical = null;
198 try {
199 canonical = f.getCanonicalPath();
200 } catch (IOException e) {
[6643]201 Main.error(e);
[2566]202 rememberError(tr("Unable to get canonical path for directory {0}\n",
[2621]203 f.getAbsolutePath()));
[1169]204 }
[316]205
[2566]206 if (canonical == null || loadedDirectories.contains(canonical)) {
207 continue;
208 } else {
209 loadedDirectories.add(canonical);
210 }
[316]211
[8405]212 File[] children = f.listFiles(JpgImporter.FILE_FILTER_WITH_FOLDERS);
[2566]213 if (children != null) {
214 progressMonitor.subTask(tr("Scanning directory {0}", f.getPath()));
[6783]215 addRecursiveFiles(files, Arrays.asList(children));
[2566]216 } else {
[2592]217 rememberError(tr("Error while getting files from directory {0}\n", f.getPath()));
[2566]218 }
[316]219
[2566]220 } else {
[2621]221 files.add(f);
[1169]222 }
[2566]223 }
[316]224
[6287]225 if (nullFile) {
226 throw new IllegalStateException(tr("One of the selected files was null"));
227 }
[1169]228 }
[2566]229
230 protected String formatErrorMessages() {
[3662]231 StringBuilder sb = new StringBuilder();
[2592]232 sb.append("<html>");
233 if (errorMessages.size() == 1) {
234 sb.append(errorMessages.iterator().next());
235 } else {
[6524]236 sb.append(Utils.joinAsHtmlUnorderedList(errorMessages));
[2592]237 }
238 sb.append("</html>");
239 return sb.toString();
[2566]240 }
241
[1169]242 @Override protected void finish() {
[2592]243 if (!errorMessages.isEmpty()) {
244 JOptionPane.showMessageDialog(
245 Main.parent,
246 formatErrorMessages(),
247 tr("Error"),
248 JOptionPane.ERROR_MESSAGE
[4751]249 );
[2592]250 }
[1865]251 if (layer != null) {
[1169]252 Main.main.addLayer(layer);
[2566]253
[6093]254 if (!canceled && !layer.data.isEmpty()) {
[2566]255 boolean noGeotagFound = true;
256 for (ImageEntry e : layer.data) {
[2662]257 if (e.getPos() != null) {
[2566]258 noGeotagFound = false;
259 }
260 }
261 if (noGeotagFound) {
262 new CorrelateGpxWithImages(layer).actionPerformed(null);
263 }
264 }
[1865]265 }
[1169]266 }
[2322]267
[2566]268 @Override protected void cancel() {
[4310]269 canceled = true;
[1811]270 }
[2592]271 }
[316]272
[1169]273 public static void create(Collection<File> files, GpxLayer gpxLayer) {
274 Loader loader = new Loader(files, gpxLayer);
275 Main.worker.execute(loader);
276 }
[316]277
[6209]278 /**
279 * Constructs a new {@code GeoImageLayer}.
280 * @param data The list of images to display
281 * @param gpxLayer The associated GPX layer
282 */
[5505]283 public GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer) {
[6392]284 this(data, gpxLayer, null, false);
285 }
[2566]286
[6392]287 /**
288 * Constructs a new {@code GeoImageLayer}.
289 * @param data The list of images to display
290 * @param gpxLayer The associated GPX layer
291 * @param name Layer name
292 * @since 6392
293 */
294 public GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer, final String name) {
295 this(data, gpxLayer, name, false);
296 }
297
298 /**
299 * Constructs a new {@code GeoImageLayer}.
300 * @param data The list of images to display
301 * @param gpxLayer The associated GPX layer
302 * @param useThumbs Thumbnail display flag
303 * @since 6392
304 */
305 public GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer, boolean useThumbs) {
306 this(data, gpxLayer, null, useThumbs);
307 }
308
309 /**
310 * Constructs a new {@code GeoImageLayer}.
311 * @param data The list of images to display
312 * @param gpxLayer The associated GPX layer
313 * @param name Layer name
314 * @param useThumbs Thumbnail display flag
315 * @since 6392
316 */
317 public GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer, final String name, boolean useThumbs) {
318 super(name != null ? name : tr("Geotagged Images"));
[1169]319 Collections.sort(data);
320 this.data = data;
[2646]321 this.gpxLayer = gpxLayer;
[6392]322 this.useThumbs = useThumbs;
[1169]323 }
[316]324
[2566]325 @Override
326 public Icon getIcon() {
327 return ImageProvider.get("dialogs/geoimage");
328 }
[2969]329
[7005]330 private static List<Action> menuAdditions = new LinkedList<>();
[3408]331 public static void registerMenuAddition(Action addition) {
[2904]332 menuAdditions.add(addition);
[2566]333 }
[316]334
[2566]335 @Override
[3408]336 public Action[] getMenuEntries() {
[1704]337
[7005]338 List<Action> entries = new ArrayList<>();
[3408]339 entries.add(LayerListDialog.getInstance().createShowHideLayerAction());
340 entries.add(LayerListDialog.getInstance().createDeleteLayerAction());
341 entries.add(new RenameLayerAction(null, this));
342 entries.add(SeparatorLayerAction.INSTANCE);
343 entries.add(new CorrelateGpxWithImages(this));
[7935]344 entries.add(new ShowThumbnailAction(this));
[2931]345 if (!menuAdditions.isEmpty()) {
[3408]346 entries.add(SeparatorLayerAction.INSTANCE);
347 entries.addAll(menuAdditions);
[2931]348 }
[3408]349 entries.add(SeparatorLayerAction.INSTANCE);
[4751]350 entries.add(new JumpToNextMarker(this));
351 entries.add(new JumpToPreviousMarker(this));
352 entries.add(SeparatorLayerAction.INSTANCE);
[3408]353 entries.add(new LayerListPopup.InfoAction(this));
[1704]354
[6083]355 return entries.toArray(new Action[entries.size()]);
[2969]356
[2566]357 }
[1704]358
[8041]359 /**
360 * Prepare the string that is displayed if layer information is requested.
361 * @return String with layer information
362 */
[2904]363 private String infoText() {
[8041]364 int tagged = 0;
365 int newdata = 0;
366 for (ImageEntry e : data) {
[2662]367 if (e.getPos() != null) {
[8041]368 tagged++;
[2621]369 }
[8041]370 if (e.hasNewGpsData()) {
371 newdata++;
372 }
373 }
374 return "<html>"
375 + trn("{0} image loaded.", "{0} images loaded.", data.size(), data.size())
376 + " " + trn("{0} was found to be GPS tagged.", "{0} were found to be GPS tagged.", tagged, tagged)
377 + (newdata > 0 ? "<br>" + trn("{0} has updated GPS data.", "{0} have updated GPS data.", newdata, newdata) : "")
378 + "</html>";
[2566]379 }
[2969]380
[2904]381 @Override public Object getInfoComponent() {
382 return infoText();
383 }
[1704]384
[2566]385 @Override
[2904]386 public String getToolTipText() {
387 return infoText();
388 }
389
390 @Override
[2566]391 public boolean isMergable(Layer other) {
392 return other instanceof GeoImageLayer;
393 }
[317]394
[2566]395 @Override
396 public void mergeFrom(Layer from) {
397 GeoImageLayer l = (GeoImageLayer) from;
398
[7935]399 // Stop to load thumbnails on both layers. Thumbnail loading will continue the next time
400 // the layer is painted.
401 stopLoadThumbs();
402 l.stopLoadThumbs();
403
[7983]404 final ImageEntry selected = l.currentPhoto >= 0 ? l.data.get(l.currentPhoto) : null;
[1704]405
[2566]406 data.addAll(l.data);
407 Collections.sort(data);
[1704]408
[2566]409 // Supress the double photos.
410 if (data.size() > 1) {
411 ImageEntry cur;
412 ImageEntry prev = data.get(data.size() - 1);
413 for (int i = data.size() - 2; i >= 0; i--) {
414 cur = data.get(i);
[2931]415 if (cur.getFile().equals(prev.getFile())) {
[2566]416 data.remove(i);
417 } else {
418 prev = cur;
419 }
420 }
421 }
[1704]422
[7983]423 if (selected != null && !data.isEmpty()) {
424 GuiHelper.runInEDTAndWait(new Runnable() {
425 @Override
426 public void run() {
[8449]427 for (int i = 0; i < data.size(); i++) {
[7983]428 if (selected.equals(data.get(i))) {
429 currentPhoto = i;
430 ImageViewerDialog.showImage(GeoImageLayer.this, data.get(i));
431 break;
432 }
433 }
[2566]434 }
[7983]435 });
[2566]436 }
[1704]437
[2566]438 setName(l.getName());
[7784]439 thumbsLoaded &= l.thumbsLoaded;
[2617]440 }
[1704]441
[2617]442 private Dimension scaledDimension(Image thumb) {
443 final double d = Main.map.mapView.getDist100Pixel();
[2662]444 final double size = 10 /*meter*/; /* size of the photo on the map */
[2617]445 double s = size * 100 /*px*/ / d;
446
447 final double sMin = ThumbsLoader.minSize;
448 final double sMax = ThumbsLoader.maxSize;
449
450 if (s < sMin) {
451 s = sMin;
452 }
453 if (s > sMax) {
454 s = sMax;
455 }
456 final double f = s / sMax; /* scale factor */
457
458 if (thumb == null)
459 return null;
460
461 return new Dimension(
[2621]462 (int) Math.round(f * thumb.getWidth(null)),
463 (int) Math.round(f * thumb.getHeight(null)));
[2566]464 }
[1704]465
[2566]466 @Override
467 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
[5901]468 int width = mv.getWidth();
469 int height = mv.getHeight();
[2617]470 Rectangle clip = g.getClipBounds();
471 if (useThumbs) {
[6392]472 if (!thumbsLoaded) {
[7935]473 startLoadThumbs();
[6392]474 }
475
[2617]476 if (null == offscreenBuffer || offscreenBuffer.getWidth() != width // reuse the old buffer if possible
477 || offscreenBuffer.getHeight() != height) {
478 offscreenBuffer = new BufferedImage(width, height,
479 BufferedImage.TYPE_INT_ARGB);
480 updateOffscreenBuffer = true;
481 }
[2566]482
[2617]483 if (updateOffscreenBuffer) {
484 Graphics2D tempG = offscreenBuffer.createGraphics();
485 tempG.setColor(new Color(0,0,0,0));
486 Composite saveComp = tempG.getComposite();
487 tempG.setComposite(AlphaComposite.Clear); // remove the old images
488 tempG.fillRect(0, 0, width, height);
489 tempG.setComposite(saveComp);
490
491 for (ImageEntry e : data) {
[2662]492 if (e.getPos() == null) {
[2617]493 continue;
[2621]494 }
[2662]495 Point p = mv.getPoint(e.getPos());
[2617]496 if (e.thumbnail != null) {
497 Dimension d = scaledDimension(e.thumbnail);
498 Rectangle target = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
499 if (clip.intersects(target)) {
500 tempG.drawImage(e.thumbnail, target.x, target.y, target.width, target.height, null);
501 }
[8342]502 } else { // thumbnail not loaded yet
[2617]503 icon.paintIcon(mv, tempG,
[2621]504 p.x - icon.getIconWidth() / 2,
505 p.y - icon.getIconHeight() / 2);
[2617]506 }
507 }
508 updateOffscreenBuffer = false;
509 }
510 g.drawImage(offscreenBuffer, 0, 0, null);
[8342]511 } else {
[2617]512 for (ImageEntry e : data) {
[2662]513 if (e.getPos() == null) {
[2617]514 continue;
[2621]515 }
[2662]516 Point p = mv.getPoint(e.getPos());
[2592]517 icon.paintIcon(mv, g,
[2621]518 p.x - icon.getIconWidth() / 2,
519 p.y - icon.getIconHeight() / 2);
[2566]520 }
[1704]521 }
[317]522
[2566]523 if (currentPhoto >= 0 && currentPhoto < data.size()) {
524 ImageEntry e = data.get(currentPhoto);
[317]525
[2662]526 if (e.getPos() != null) {
527 Point p = mv.getPoint(e.getPos());
[2566]528
[7912]529 int imgWidth = 100;
530 int imgHeight = 100;
[6392]531 if (useThumbs && e.thumbnail != null) {
[2617]532 Dimension d = scaledDimension(e.thumbnail);
[7912]533 imgWidth = d.width;
534 imgHeight = d.height;
[8342]535 } else {
[7912]536 imgWidth = selectedIcon.getIconWidth();
537 imgHeight = selectedIcon.getIconHeight();
538 }
[3261]539
[7912]540 if (e.getExifImgDir() != null) {
541 // Multiplier must be larger than sqrt(2)/2=0.71.
542 double arrowlength = Math.max(25, Math.max(imgWidth, imgHeight) * 0.85);
543 double arrowwidth = arrowlength / 1.4;
[3261]544
[7912]545 double dir = e.getExifImgDir();
546 // Rotate 90 degrees CCW
[8444]547 double headdir = (dir < 90) ? dir + 270 : dir - 90;
548 double leftdir = (headdir < 90) ? headdir + 270 : headdir - 90;
549 double rightdir = (headdir > 270) ? headdir - 270 : headdir + 90;
[3261]550
[7912]551 double ptx = p.x + Math.cos(Math.toRadians(headdir)) * arrowlength;
552 double pty = p.y + Math.sin(Math.toRadians(headdir)) * arrowlength;
[3261]553
[7912]554 double ltx = p.x + Math.cos(Math.toRadians(leftdir)) * arrowwidth/2;
555 double lty = p.y + Math.sin(Math.toRadians(leftdir)) * arrowwidth/2;
[3261]556
[7912]557 double rtx = p.x + Math.cos(Math.toRadians(rightdir)) * arrowwidth/2;
558 double rty = p.y + Math.sin(Math.toRadians(rightdir)) * arrowwidth/2;
[3261]559
[7912]560 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
561 g.setColor(new Color(255, 255, 255, 192));
562 int[] xar = {(int) ltx, (int) ptx, (int) rtx, (int) ltx};
563 int[] yar = {(int) lty, (int) pty, (int) rty, (int) lty};
564 g.fillPolygon(xar, yar, 4);
565 g.setColor(Color.black);
566 g.setStroke(new BasicStroke(1.2f));
567 g.drawPolyline(xar, yar, 3);
568 }
569
570 if (useThumbs && e.thumbnail != null) {
571 g.setColor(new Color(128, 0, 0, 122));
572 g.fillRect(p.x - imgWidth / 2, p.y - imgHeight / 2, imgWidth, imgHeight);
573 } else {
[2617]574 selectedIcon.paintIcon(mv, g,
[7912]575 p.x - imgWidth / 2,
576 p.y - imgHeight / 2);
[3261]577
[2617]578 }
[1152]579 }
[2566]580 }
581 }
[316]582
[2566]583 @Override
584 public void visitBoundingBox(BoundingXYVisitor v) {
[2621]585 for (ImageEntry e : data) {
[2662]586 v.visit(e.getPos());
[2621]587 }
[2566]588 }
[1704]589
[6209]590 /**
591 * Extract GPS metadata from image EXIF
[2566]592 *
[6209]593 * If successful, fills in the LatLon and EastNorth attributes of passed in image
[2566]594 */
595 private static void extractExif(ImageEntry e) {
[1704]596
[6127]597 Metadata metadata;
598 Directory dirExif;
599 GpsDirectory dirGps;
[3408]600
[2566]601 try {
[3365]602 metadata = JpegMetadataReader.readMetadata(e.getFile());
[8243]603 dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
604 dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
[7004]605 } catch (CompoundException | IOException p) {
[3365]606 e.setExifCoor(null);
607 e.setPos(null);
608 return;
609 }
[3408]610
[3365]611 try {
[6292]612 if (dirExif != null) {
613 int orientation = dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION);
614 e.setExifOrientation(orientation);
615 }
[4241]616 } catch (MetadataException ex) {
[6792]617 Main.debug(ex.getMessage());
[4241]618 }
[4751]619
[6127]620 if (dirGps == null) {
621 e.setExifCoor(null);
622 e.setPos(null);
623 return;
624 }
625
[4241]626 try {
[8132]627 double speed = dirGps.getDouble(GpsDirectory.TAG_SPEED);
628 String speedRef = dirGps.getString(GpsDirectory.TAG_SPEED_REF);
[8373]629 if ("M".equalsIgnoreCase(speedRef)) {
630 // miles per hour
631 speed *= 1.609344;
632 } else if ("N".equalsIgnoreCase(speedRef)) {
633 // knots == nautical miles per hour
634 speed *= 1.852;
[7912]635 }
[8373]636 // default is K (km/h)
[7912]637 e.setSpeed(speed);
638 } catch (Exception ex) {
639 Main.debug(ex.getMessage());
640 }
641
642 try {
[8132]643 double ele = dirGps.getDouble(GpsDirectory.TAG_ALTITUDE);
644 int d = dirGps.getInt(GpsDirectory.TAG_ALTITUDE_REF);
[5242]645 if (d == 1) {
646 ele *= -1;
647 }
648 e.setElevation(ele);
649 } catch (MetadataException ex) {
[6792]650 Main.debug(ex.getMessage());
[5242]651 }
652
653 try {
[6209]654 LatLon latlon = ExifReader.readLatLon(dirGps);
655 e.setExifCoor(latlon);
[6128]656 e.setPos(e.getExifCoor());
657
[3387]658 } catch (Exception ex) { // (other exceptions, e.g. #5271)
[6248]659 Main.error("Error reading EXIF from file: "+ex);
[3387]660 e.setExifCoor(null);
661 e.setPos(null);
[3365]662 }
[1704]663
[3365]664 try {
[6209]665 Double direction = ExifReader.readDirection(dirGps);
[3408]666 if (direction != null) {
667 e.setExifImgDir(direction.doubleValue());
668 }
[3411]669 } catch (Exception ex) { // (CompoundException and other exceptions, e.g. #5271)
[6792]670 Main.debug(ex.getMessage());
[2566]671 }
[6392]672
673 // Time and date. We can have these cases:
674 // 1) GPS_TIME_STAMP not set -> date/time will be null
675 // 2) GPS_DATE_STAMP not set -> use EXIF date or set to default
676 // 3) GPS_TIME_STAMP and GPS_DATE_STAMP are set
[8132]677 int[] timeStampComps = dirGps.getIntArray(GpsDirectory.TAG_TIME_STAMP);
[6392]678 if (timeStampComps != null) {
679 int gpsHour = timeStampComps[0];
680 int gpsMin = timeStampComps[1];
681 int gpsSec = timeStampComps[2];
682 Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
683
684 // We have the time. Next step is to check if the GPS date stamp is set.
685 // dirGps.getString() always succeeds, but the return value might be null.
[8132]686 String dateStampStr = dirGps.getString(GpsDirectory.TAG_DATE_STAMP);
[6392]687 if (dateStampStr != null && dateStampStr.matches("^\\d+:\\d+:\\d+$")) {
688 String[] dateStampComps = dateStampStr.split(":");
689 cal.set(Calendar.YEAR, Integer.parseInt(dateStampComps[0]));
690 cal.set(Calendar.MONTH, Integer.parseInt(dateStampComps[1]) - 1);
691 cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStampComps[2]));
[8342]692 } else {
[6450]693 // No GPS date stamp in EXIF data. Copy it from EXIF time.
[6392]694 // Date is not set if EXIF time is not available.
[6450]695 if (e.hasExifTime()) {
[6392]696 // Time not set yet, so we can copy everything, not just date.
[6450]697 cal.setTime(e.getExifTime());
[6392]698 }
699 }
700
701 cal.set(Calendar.HOUR_OF_DAY, gpsHour);
702 cal.set(Calendar.MINUTE, gpsMin);
703 cal.set(Calendar.SECOND, gpsSec);
704
705 e.setExifGpsTime(cal.getTime());
706 }
[2566]707 }
708
709 public void showNextPhoto() {
[8318]710 if (data != null && !data.isEmpty()) {
[2566]711 currentPhoto++;
712 if (currentPhoto >= data.size()) {
713 currentPhoto = data.size() - 1;
[1704]714 }
[2566]715 ImageViewerDialog.showImage(this, data.get(currentPhoto));
716 } else {
717 currentPhoto = -1;
[1704]718 }
[2749]719 Main.map.repaint();
[2566]720 }
[1704]721
[2566]722 public void showPreviousPhoto() {
[6093]723 if (data != null && !data.isEmpty()) {
[2566]724 currentPhoto--;
725 if (currentPhoto < 0) {
726 currentPhoto = 0;
[1169]727 }
[2566]728 ImageViewerDialog.showImage(this, data.get(currentPhoto));
729 } else {
730 currentPhoto = -1;
[1169]731 }
[2749]732 Main.map.repaint();
[1169]733 }
[2592]734
[6456]735 public void showFirstPhoto() {
[8318]736 if (data != null && !data.isEmpty()) {
[6456]737 currentPhoto = 0;
738 ImageViewerDialog.showImage(this, data.get(currentPhoto));
739 } else {
740 currentPhoto = -1;
741 }
742 Main.map.repaint();
743 }
744
745 public void showLastPhoto() {
[8318]746 if (data != null && !data.isEmpty()) {
[6456]747 currentPhoto = data.size() - 1;
748 ImageViewerDialog.showImage(this, data.get(currentPhoto));
749 } else {
750 currentPhoto = -1;
751 }
752 Main.map.repaint();
753 }
754
[2566]755 public void checkPreviousNextButtons() {
756 ImageViewerDialog.setNextEnabled(currentPhoto < data.size() - 1);
757 ImageViewerDialog.setPreviousEnabled(currentPhoto > 0);
758 }
[316]759
[2566]760 public void removeCurrentPhoto() {
[8318]761 if (data != null && !data.isEmpty() && currentPhoto >= 0 && currentPhoto < data.size()) {
[2566]762 data.remove(currentPhoto);
763 if (currentPhoto >= data.size()) {
764 currentPhoto = data.size() - 1;
765 }
766 if (currentPhoto >= 0) {
767 ImageViewerDialog.showImage(this, data.get(currentPhoto));
768 } else {
769 ImageViewerDialog.showImage(this, null);
770 }
[2627]771 updateOffscreenBuffer = true;
[2749]772 Main.map.repaint();
[1865]773 }
[1169]774 }
[316]775
[2627]776 public void removeCurrentPhotoFromDisk() {
777 ImageEntry toDelete = null;
[8318]778 if (data != null && !data.isEmpty() && currentPhoto >= 0 && currentPhoto < data.size()) {
[2627]779 toDelete = data.get(currentPhoto);
780
781 int result = new ExtendedDialog(
782 Main.parent,
783 tr("Delete image file from disk"),
784 new String[] {tr("Cancel"), tr("Delete")})
[8061]785 .setButtonIcons(new String[] {"cancel", "dialogs/delete"})
[2749]786 .setContent(new JLabel(tr("<html><h3>Delete the file {0} from disk?<p>The image file will be permanently lost!</h3></html>"
[2931]787 ,toDelete.getFile().getName()), ImageProvider.get("dialogs/geoimage/deletefromdisk"),SwingConstants.LEFT))
[2749]788 .toggleEnable("geoimage.deleteimagefromdisk")
789 .setCancelButton(1)
790 .setDefaultButton(2)
791 .showDialog()
792 .getValue();
[2627]793
[8395]794 if (result == 2) {
[2627]795 data.remove(currentPhoto);
796 if (currentPhoto >= data.size()) {
797 currentPhoto = data.size() - 1;
798 }
799 if (currentPhoto >= 0) {
800 ImageViewerDialog.showImage(this, data.get(currentPhoto));
801 } else {
802 ImageViewerDialog.showImage(this, null);
803 }
804
[2931]805 if (toDelete.getFile().delete()) {
[8376]806 Main.info("File "+toDelete.getFile()+" deleted. ");
[2627]807 } else {
808 JOptionPane.showMessageDialog(
[2749]809 Main.parent,
810 tr("Image file could not be deleted."),
811 tr("Error"),
812 JOptionPane.ERROR_MESSAGE
[4751]813 );
[2627]814 }
815
816 updateOffscreenBuffer = true;
[2749]817 Main.map.repaint();
[2627]818 }
819 }
820 }
821
[7788]822 public void copyCurrentPhotoPath() {
823 ImageEntry toCopy = null;
[8318]824 if (data != null && !data.isEmpty() && currentPhoto >= 0 && currentPhoto < data.size()) {
[7788]825 toCopy = data.get(currentPhoto);
826 String copyString = toCopy.getFile().toString();
827 Utils.copyToClipboard(copyString);
828 }
829 }
830
[6392]831 /**
832 * Removes a photo from the list of images by index.
833 * @param idx Image index
834 * @since 6392
835 */
836 public void removePhotoByIdx(int idx) {
837 if (idx >= 0 && data != null && idx < data.size()) {
838 data.remove(idx);
839 }
840 }
841
842 /**
843 * Returns the image that matches the position of the mouse event.
844 * @param evt Mouse event
845 * @return Image at mouse position, or {@code null} if there is no image at the mouse position
846 * @since 6392
847 */
848 public ImageEntry getPhotoUnderMouse(MouseEvent evt) {
849 if (data != null) {
850 for (int idx = data.size() - 1; idx >= 0; --idx) {
851 ImageEntry img = data.get(idx);
852 if (img.getPos() == null) {
853 continue;
854 }
855 Point p = Main.map.mapView.getPoint(img.getPos());
856 Rectangle r;
857 if (useThumbs && img.thumbnail != null) {
858 Dimension d = scaledDimension(img.thumbnail);
859 r = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
860 } else {
861 r = new Rectangle(p.x - icon.getIconWidth() / 2,
862 p.y - icon.getIconHeight() / 2,
863 icon.getIconWidth(),
864 icon.getIconHeight());
865 }
866 if (r.contains(evt.getPoint())) {
867 return img;
868 }
869 }
870 }
871 return null;
872 }
873
874 /**
875 * Clears the currentPhoto, i.e. remove select marker, and optionally repaint.
876 * @param repaint Repaint flag
877 * @since 6392
878 */
879 public void clearCurrentPhoto(boolean repaint) {
880 currentPhoto = -1;
881 if (repaint) {
882 updateBufferAndRepaint();
883 }
884 }
885
886 /**
887 * Clears the currentPhoto of the other GeoImageLayer's. Otherwise there could be multiple selected photos.
888 */
889 private void clearOtherCurrentPhotos() {
890 for (GeoImageLayer layer:
891 Main.map.mapView.getLayersOfType(GeoImageLayer.class)) {
892 if (layer != this) {
893 layer.clearCurrentPhoto(false);
894 }
895 }
896 }
897
[8126]898 private static volatile List<MapMode> supportedMapModes = null;
[6392]899
900 /**
901 * Registers a map mode for which the functionality of this layer should be available.
902 * @param mapMode Map mode to be registered
903 * @since 6392
904 */
905 public static void registerSupportedMapMode(MapMode mapMode) {
906 if (supportedMapModes == null) {
[7005]907 supportedMapModes = new ArrayList<>();
[6392]908 }
909 supportedMapModes.add(mapMode);
910 }
911
912 /**
913 * Determines if the functionality of this layer is available in
[7764]914 * the specified map mode. {@link SelectAction} and {@link LassoModeAction} are supported by default,
[6392]915 * other map modes can be registered.
916 * @param mapMode Map mode to be checked
917 * @return {@code true} if the map mode is supported,
918 * {@code false} otherwise
919 */
920 private static final boolean isSupportedMapMode(MapMode mapMode) {
[7764]921 if (mapMode instanceof SelectAction || mapMode instanceof LassoModeAction) {
922 return true;
923 }
[6392]924 if (supportedMapModes != null) {
925 for (MapMode supmmode: supportedMapModes) {
926 if (mapMode == supmmode) {
927 return true;
928 }
929 }
930 }
931 return false;
932 }
933
[2566]934 private MouseAdapter mouseAdapter = null;
[2629]935 private MapModeChangeListener mapModeListener = null;
[2566]936
[5505]937 @Override
938 public void hookUpMapView() {
[2566]939 mouseAdapter = new MouseAdapter() {
[5430]940 private final boolean isMapModeOk() {
[6392]941 return Main.map.mapMode == null || isSupportedMapMode(Main.map.mapMode);
[5430]942 }
[2566]943 @Override public void mousePressed(MouseEvent e) {
944
[2621]945 if (e.getButton() != MouseEvent.BUTTON1)
[1169]946 return;
[5430]947 if (isVisible() && isMapModeOk()) {
[2621]948 Main.map.mapView.repaint();
[2566]949 }
[1169]950 }
[316]951
[2566]952 @Override public void mouseReleased(MouseEvent ev) {
[2621]953 if (ev.getButton() != MouseEvent.BUTTON1)
[2566]954 return;
[5430]955 if (data == null || !isVisible() || !isMapModeOk())
[2566]956 return;
957
958 for (int i = data.size() - 1; i >= 0; --i) {
959 ImageEntry e = data.get(i);
[2662]960 if (e.getPos() == null) {
[2566]961 continue;
[2621]962 }
[2662]963 Point p = Main.map.mapView.getPoint(e.getPos());
[2617]964 Rectangle r;
[6392]965 if (useThumbs && e.thumbnail != null) {
[2617]966 Dimension d = scaledDimension(e.thumbnail);
967 r = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
968 } else {
969 r = new Rectangle(p.x - icon.getIconWidth() / 2,
[2621]970 p.y - icon.getIconHeight() / 2,
971 icon.getIconWidth(),
972 icon.getIconHeight());
[2617]973 }
[2566]974 if (r.contains(ev.getPoint())) {
[6392]975 clearOtherCurrentPhotos();
[2566]976 currentPhoto = i;
977 ImageViewerDialog.showImage(GeoImageLayer.this, e);
[2749]978 Main.map.repaint();
[2566]979 break;
980 }
981 }
[1169]982 }
[2566]983 };
[2629]984
985 mapModeListener = new MapModeChangeListener() {
[6084]986 @Override
[2629]987 public void mapModeChange(MapMode oldMapMode, MapMode newMapMode) {
[6392]988 if (newMapMode == null || isSupportedMapMode(newMapMode)) {
[2629]989 Main.map.mapView.addMouseListener(mouseAdapter);
990 } else {
991 Main.map.mapView.removeMouseListener(mouseAdapter);
992 }
993 }
994 };
995
[2749]996 MapFrame.addMapModeChangeListener(mapModeListener);
[2629]997 mapModeListener.mapModeChange(null, Main.map.mapMode);
998
[2621]999 MapView.addLayerChangeListener(new LayerChangeListener() {
[6084]1000 @Override
[2566]1001 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
[2629]1002 if (newLayer == GeoImageLayer.this) {
1003 // only in select mode it is possible to click the images
1004 Main.map.selectSelectTool(false);
[2566]1005 }
[1704]1006 }
[316]1007
[6084]1008 @Override
[2566]1009 public void layerAdded(Layer newLayer) {
1010 }
[316]1011
[6084]1012 @Override
[2566]1013 public void layerRemoved(Layer oldLayer) {
1014 if (oldLayer == GeoImageLayer.this) {
[7935]1015 stopLoadThumbs();
[2566]1016 Main.map.mapView.removeMouseListener(mouseAdapter);
[2749]1017 MapFrame.removeMapModeChangeListener(mapModeListener);
[2566]1018 currentPhoto = -1;
1019 data.clear();
1020 data = null;
[2621]1021 // stop listening to layer change events
1022 MapView.removeLayerChangeListener(this);
[1865]1023 }
[1169]1024 }
[2566]1025 });
[5505]1026
1027 Main.map.mapView.addPropertyChangeListener(this);
1028 if (Main.map.getToggleDialog(ImageViewerDialog.class) == null) {
1029 ImageViewerDialog.newInstance();
1030 Main.map.addToggleDialog(ImageViewerDialog.getInstance());
1031 }
[1169]1032 }
[2617]1033
[6084]1034 @Override
[2617]1035 public void propertyChange(PropertyChangeEvent evt) {
[4627]1036 if (NavigatableComponent.PROPNAME_CENTER.equals(evt.getPropertyName()) || NavigatableComponent.PROPNAME_SCALE.equals(evt.getPropertyName())) {
[2617]1037 updateOffscreenBuffer = true;
[2606]1038 }
1039 }
[2711]1040
[7935]1041 /**
1042 * Start to load thumbnails.
1043 */
1044 public synchronized void startLoadThumbs() {
1045 if (useThumbs && !thumbsLoaded && !thumbsLoaderRunning) {
1046 stopLoadThumbs();
[2662]1047 thumbsloader = new ThumbsLoader(this);
[7954]1048 thumbsLoaderExecutor.submit(thumbsloader);
[7935]1049 thumbsLoaderRunning = true;
[2662]1050 }
1051 }
[2711]1052
[7935]1053 /**
1054 * Stop to load thumbnails.
[7983]1055 *
[7935]1056 * Can be called at any time to make sure that the
1057 * thumbnail loader is stopped.
1058 */
1059 public synchronized void stopLoadThumbs() {
1060 if (thumbsloader != null) {
1061 thumbsloader.stop = true;
1062 }
1063 thumbsLoaderRunning = false;
1064 }
1065
1066 /**
1067 * Called to signal that the loading of thumbnails has finished.
[7983]1068 *
[7935]1069 * Usually called from {@link ThumbsLoader} in another thread.
1070 */
1071 public void thumbsLoaded() {
1072 thumbsLoaded = true;
1073 }
1074
[2662]1075 public void updateBufferAndRepaint() {
1076 updateOffscreenBuffer = true;
1077 Main.map.mapView.repaint();
1078 }
[2969]1079
[8041]1080 /**
1081 * Get list of images in layer.
1082 * @return List of images in layer
1083 */
[2904]1084 public List<ImageEntry> getImages() {
[7005]1085 List<ImageEntry> copy = new ArrayList<>(data.size());
[2904]1086 for (ImageEntry ie : data) {
[8041]1087 copy.add(ie);
[2904]1088 }
1089 return copy;
1090 }
[4751]1091
[6209]1092 /**
1093 * Returns the associated GPX layer.
1094 * @return The associated GPX layer
1095 */
[5505]1096 public GpxLayer getGpxLayer() {
1097 return gpxLayer;
1098 }
1099
[6084]1100 @Override
[4751]1101 public void jumpToNextMarker() {
1102 showNextPhoto();
1103 }
1104
[6084]1105 @Override
[4751]1106 public void jumpToPreviousMarker() {
1107 showPreviousPhoto();
1108 }
[6392]1109
1110 /**
1111 * Returns the current thumbnail display status.
1112 * {@code true}: thumbnails are displayed, {@code false}: an icon is displayed instead of thumbnails.
1113 * @return Current thumbnail display status
1114 * @since 6392
1115 */
1116 public boolean isUseThumbs() {
1117 return useThumbs;
1118 }
1119
1120 /**
1121 * Enables or disables the display of thumbnails. Does not update the display.
1122 * @param useThumbs New thumbnail display status
1123 * @since 6392
1124 */
1125 public void setUseThumbs(boolean useThumbs) {
1126 this.useThumbs = useThumbs;
1127 if (useThumbs && !thumbsLoaded) {
[7935]1128 startLoadThumbs();
1129 } else if (!useThumbs) {
1130 stopLoadThumbs();
[6392]1131 }
1132 }
[316]1133}
Note: See TracBrowser for help on using the repository browser.