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

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

fix #10987 - EDT violation during GeoImageLayer merge

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