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

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

sonar - do not copy collection contents from one to another with a loop

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