Ticket #16472: 16472.3.patch
File 16472.3.patch, 122.0 KB (added by , 4 years ago) |
---|
-
src/org/openstreetmap/josm/data/ImageData.java
diff --git a/src/org/openstreetmap/josm/data/ImageData.java b/src/org/openstreetmap/josm/data/ImageData.java index 7fba8372a5..da289dca82 100644
a b public class ImageData implements Data { 131 131 return selectedImagesIndex.stream().filter(i -> i > -1 && i < data.size()).map(data::get).collect(Collectors.toList()); 132 132 } 133 133 134 /** 135 * Get the first image on the layer 136 * @return The first image 137 * @since xxx 138 */ 139 public ImageEntry getFirstImage() { 140 if (!this.data.isEmpty()) { 141 return this.data.get(0); 142 } 143 return null; 144 } 145 134 146 /** 135 147 * Select the first image of the sequence 148 * @deprecated Use {@link #getFirstImage()} in conjunction with {@link #setSelectedImage} 136 149 */ 150 @Deprecated 137 151 public void selectFirstImage() { 138 152 if (!data.isEmpty()) { 139 153 setSelectedImageIndex(0); 140 154 } 141 155 } 142 156 157 /** 158 * Get the last image in the layer 159 * @return The last image 160 * @since xxx 161 */ 162 public ImageEntry getLastImage() { 163 if (!this.data.isEmpty()) { 164 return this.data.get(this.data.size() - 1); 165 } 166 return null; 167 } 168 143 169 /** 144 170 * Select the last image of the sequence 171 * @deprecated Use {@link #getLastImage()} with {@link #setSelectedImage} 145 172 */ 173 @Deprecated 146 174 public void selectLastImage() { 147 175 setSelectedImageIndex(data.size() - 1); 148 176 } … … public class ImageData implements Data { 165 193 return this.geoImages.search(bounds.toBBox()); 166 194 } 167 195 196 /** 197 * Get the image next to the current image 198 * @return The next image 199 * @since xxx 200 */ 201 public ImageEntry getNextImage() { 202 if (this.hasNextImage()) { 203 return this.data.get(this.selectedImagesIndex.get(0) + 1); 204 } 205 return null; 206 } 207 168 208 /** 169 209 * Select the next image of the sequence 210 * @deprecated Use {@link #getNextImage()} in conjunction with {@link #setSelectedImage} 170 211 */ 212 @Deprecated 171 213 public void selectNextImage() { 172 214 if (hasNextImage()) { 173 215 setSelectedImageIndex(selectedImagesIndex.get(0) + 1); 174 216 } 175 217 } 176 218 219 /** 220 * Get the image previous to the current image 221 * @return The previous image 222 * @since xxx 223 */ 224 public ImageEntry getPreviousImage() { 225 if (this.hasPreviousImage()) { 226 return this.data.get(Integer.max(0, selectedImagesIndex.get(0) - 1)); 227 } 228 return null; 229 } 230 177 231 /** 178 232 * Check if there is a previous image in the sequence 179 233 * @return {@code true} is there is a previous image, {@code false} otherwise … … public class ImageData implements Data { 184 238 185 239 /** 186 240 * Select the previous image of the sequence 241 * @deprecated Use {@link #getPreviousImage()} with {@link #setSelectedImage} 187 242 */ 243 @Deprecated 188 244 public void selectPreviousImage() { 189 245 if (data.isEmpty()) { 190 246 return; -
src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java
diff --git a/src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java b/src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java index 030e2db117..d06b607693 100644
a b package org.openstreetmap.josm.data.gpx; 3 3 4 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 5 6 import java.awt.Dimension; 7 import java.awt.image.BufferedImage; 6 8 import java.io.File; 7 9 import java.io.IOException; 8 10 import java.time.Instant; 9 11 import java.util.Date; 10 12 import java.util.List; 11 13 import java.util.Locale; 14 import java.util.Map; 12 15 import java.util.Objects; 13 16 import java.util.function.Consumer; 14 15 import org.openstreetmap.josm.data.IQuadBucketType; 16 import org.openstreetmap.josm.data.coor.CachedLatLon; 17 import org.openstreetmap.josm.data.coor.LatLon; 18 import org.openstreetmap.josm.data.osm.BBox; 19 import org.openstreetmap.josm.tools.ExifReader; 20 import org.openstreetmap.josm.tools.JosmRuntimeException; 21 import org.openstreetmap.josm.tools.Logging; 17 import java.util.stream.Stream; 18 import javax.imageio.IIOParam; 22 19 23 20 import com.drew.imaging.jpeg.JpegMetadataReader; 24 21 import com.drew.imaging.jpeg.JpegProcessingException; … … import com.drew.metadata.exif.ExifIFD0Directory; 33 30 import com.drew.metadata.exif.GpsDirectory; 34 31 import com.drew.metadata.iptc.IptcDirectory; 35 32 import com.drew.metadata.jpeg.JpegDirectory; 33 import com.drew.metadata.xmp.XmpDirectory; 34 import org.openstreetmap.josm.data.IQuadBucketType; 35 import org.openstreetmap.josm.data.coor.CachedLatLon; 36 import org.openstreetmap.josm.data.coor.LatLon; 37 import org.openstreetmap.josm.data.imagery.street_level.Projections; 38 import org.openstreetmap.josm.data.osm.BBox; 39 import org.openstreetmap.josm.tools.ExifReader; 40 import org.openstreetmap.josm.tools.JosmRuntimeException; 41 import org.openstreetmap.josm.tools.Logging; 36 42 37 43 /** 38 44 * Stores info about each image … … public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType 44 50 private LatLon exifCoor; 45 51 private Double exifImgDir; 46 52 private Instant exifTime; 53 private Projections cameraProjection = Projections.UNKNOWN; 47 54 /** 48 55 * Flag isNewGpsData indicates that the GPS data of the image is new or has changed. 49 56 * GPS data includes the position, speed, elevation, time (e.g. as extracted from the GPS track). … … public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType 753 760 ifNotNull(ExifReader.readKeywords(dirIptc), this::setIptcKeywords); 754 761 ifNotNull(ExifReader.readObjectName(dirIptc), this::setIptcObjectName); 755 762 } 763 764 for (XmpDirectory xmpDirectory : metadata.getDirectoriesOfType(XmpDirectory.class)) { 765 Map<String, String> properties = xmpDirectory.getXmpProperties(); 766 final String projectionType = "GPano:ProjectionType"; 767 if (properties.containsKey(projectionType)) { 768 Stream.of(Projections.values()).filter(p -> p.name().equalsIgnoreCase(properties.get(projectionType))) 769 .findFirst().ifPresent(projection -> this.cameraProjection = projection); 770 break; 771 } 772 } 773 } 774 775 /** 776 * Reads the image represented by this entry in the given target dimension. 777 * @param target the desired dimension used for {@linkplain IIOParam#setSourceSubsampling subsampling} or {@code null} 778 * @return the read image, or {@code null} 779 * @throws IOException if any I/O error occurs 780 */ 781 public BufferedImage read(Dimension target) throws IOException { 782 throw new UnsupportedOperationException("read not implemented for " + this.getClass().getSimpleName()); 756 783 } 757 784 758 785 private static class NoMetadataReaderWarning extends Exception { … … public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType 767 794 } 768 795 } 769 796 797 /** 798 * Get the projection type for this entry 799 * @return The projection type 800 */ 801 public Projections getProjectionType() { 802 return this.cameraProjection; 803 } 804 770 805 /** 771 806 * Returns a {@link WayPoint} representation of this GPX image entry. 772 807 * @return a {@code WayPoint} representation of this GPX image entry (containing position, instant and elevation) -
new file src/org/openstreetmap/josm/data/imagery/street_level/IImageEntry.java
diff --git a/src/org/openstreetmap/josm/data/imagery/street_level/IImageEntry.java b/src/org/openstreetmap/josm/data/imagery/street_level/IImageEntry.java new file mode 100644 index 0000000000..685b8f8d5a
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.street_level; 3 4 import java.awt.Dimension; 5 import java.awt.image.BufferedImage; 6 import java.io.File; 7 import java.io.IOException; 8 import java.time.Instant; 9 import java.util.List; 10 import javax.imageio.IIOParam; 11 12 import org.openstreetmap.josm.data.coor.ILatLon; 13 import org.openstreetmap.josm.gui.layer.geoimage.ImageViewerDialog; 14 15 /** 16 * An interface for image entries that will be shown in {@link org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay} 17 * @author Taylor Smock 18 * @since xxx 19 */ 20 public interface IImageEntry<I extends IImageEntry<I>> { 21 /** 22 * Select the next image 23 * @param imageViewerDialog The image viewer to update 24 */ 25 default void selectNextImage(final ImageViewerDialog imageViewerDialog) { 26 imageViewerDialog.displayImage(this.getNextImage()); 27 } 28 29 /** 30 * Get what would be the next image 31 * @return The next image 32 */ 33 I getNextImage(); 34 35 /** 36 * Select the previous image 37 * @param imageViewerDialog The image viewer to update 38 */ 39 default void selectPreviousImage(final ImageViewerDialog imageViewerDialog) { 40 imageViewerDialog.displayImage(this.getPreviousImage()); 41 } 42 43 /** 44 * Get the previous image 45 * @return The previous image 46 */ 47 I getPreviousImage(); 48 49 /** 50 * Select the first image for the data or sequence 51 * @param imageViewerDialog The image viewer to update 52 */ 53 default void selectFirstImage(final ImageViewerDialog imageViewerDialog) { 54 imageViewerDialog.displayImage(this.getFirstImage()); 55 } 56 57 /** 58 * Get the first image for the data or sequence 59 * @return The first image 60 */ 61 I getFirstImage(); 62 63 /** 64 * Select the last image for the data or sequence 65 * @param imageViewerDialog The image viewer to update 66 */ 67 default void selectLastImage(final ImageViewerDialog imageViewerDialog) { 68 imageViewerDialog.displayImage(this.getLastImage()); 69 } 70 71 /** 72 * Get the last image for the data or sequence 73 * @return The last image 74 */ 75 I getLastImage(); 76 77 /** 78 * Remove the image 79 * @return {@code true} if removal was successful 80 * @throws UnsupportedOperationException If the implementation does not support removal. 81 * Use {@link #isRemoveSupported()}} to check for support. 82 */ 83 default boolean remove() { 84 throw new UnsupportedOperationException("remove is not supported for " + this.getClass().getSimpleName()); 85 } 86 87 /** 88 * Check if image removal is supported 89 * @return {@code true} if removal is supported 90 */ 91 default boolean isRemoveSupported() { 92 return false; 93 } 94 95 /** 96 * Returns a display name for this entry (shown in image viewer title bar) 97 * @return a display name for this entry 98 */ 99 String getDisplayName(); 100 101 /** 102 * Reads the image represented by this entry in the given target dimension. 103 * @param target the desired dimension used for {@linkplain IIOParam#setSourceSubsampling subsampling} or {@code null} 104 * @return the read image, or {@code null} 105 * @throws IOException if any I/O error occurs 106 */ 107 BufferedImage read(Dimension target) throws IOException; 108 109 /** 110 * Sets the width of this ImageEntry. 111 * @param width set the width of this ImageEntry 112 */ 113 void setWidth(int width); 114 115 /** 116 * Sets the height of this ImageEntry. 117 * @param height set the height of this ImageEntry 118 */ 119 void setHeight(int height); 120 121 /** 122 * Returns associated file. 123 * @return associated file 124 */ 125 File getFile(); 126 127 /** 128 * Returns the position value. The position value from the temporary copy 129 * is returned if that copy exists. 130 * @return the position value 131 */ 132 ILatLon getPos(); 133 134 /** 135 * Returns the speed value. The speed value from the temporary copy is 136 * returned if that copy exists. 137 * @return the speed value 138 */ 139 Double getSpeed(); 140 141 /** 142 * Returns the elevation value. The elevation value from the temporary 143 * copy is returned if that copy exists. 144 * @return the elevation value 145 */ 146 Double getElevation(); 147 148 /** 149 * Returns the image direction. The image direction from the temporary 150 * copy is returned if that copy exists. 151 * @return The image camera angle 152 */ 153 Double getExifImgDir(); 154 155 /** 156 * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy. 157 * @return {@code true} if this entry has a EXIF time 158 * @since 6450 159 */ 160 boolean hasExifTime(); 161 162 /** 163 * Returns EXIF time 164 * @return EXIF time 165 */ 166 Instant getExifInstant(); 167 168 /** 169 * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy. 170 * @return {@code true} if this entry has a GPS time 171 */ 172 boolean hasGpsTime(); 173 174 /** 175 * Returns the GPS time value. The GPS time value from the temporary copy 176 * is returned if that copy exists. 177 * @return the GPS time value 178 */ 179 Instant getGpsInstant(); 180 181 /** 182 * Returns the IPTC caption. 183 * @return the IPTC caption 184 */ 185 String getIptcCaption(); 186 187 /** 188 * Returns the IPTC headline. 189 * @return the IPTC headline 190 */ 191 String getIptcHeadline(); 192 193 /** 194 * Returns the IPTC keywords. 195 * @return the IPTC keywords 196 */ 197 List<String> getIptcKeywords(); 198 199 /** 200 * Returns the IPTC object name. 201 * @return the IPTC object name 202 */ 203 String getIptcObjectName(); 204 205 /** 206 * Get the camera projection type 207 * @return the camera projection type 208 */ 209 default Projections getProjectionType() { 210 return Projections.PERSPECTIVE; 211 } 212 } -
new file src/org/openstreetmap/josm/data/imagery/street_level/Projections.java
diff --git a/src/org/openstreetmap/josm/data/imagery/street_level/Projections.java b/src/org/openstreetmap/josm/data/imagery/street_level/Projections.java new file mode 100644 index 0000000000..ce5c2b58ea
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.street_level; 3 4 /** 5 * Projections for street level imagery 6 * @author Taylor Smock 7 * @since xxx 8 */ 9 public enum Projections { 10 /** This is the image type from most cameras */ 11 PERSPECTIVE(1), 12 /** This will probably not be seen often in JOSM, but someone might have a synchronized pair of fisheye camers */ 13 FISHEYE(1), 14 /** 360 imagery using the equirectangular method (single image) */ 15 EQUIRECTANGULAR(1), 16 /** 360 imagery using a cube map */ 17 CUBE_MAP(6), 18 /* 19 * Additional known projections: Equi-Angular Cubemap (EAC) from Google and the Pyramid format from Facebook 20 * Neither are particularly well-documented at this point, although I believe the Pyramid format uses 30 images. 21 */ 22 /** In the event that we have no clue what the projection should be. Defaults to perspective viewing. */ 23 UNKNOWN(Integer.MAX_VALUE); 24 25 private final int expectedImages; 26 27 /** 28 * Create a new Projections enum 29 * @param expectedImages The maximum images for the projection type 30 */ 31 Projections(final int expectedImages) { 32 this.expectedImages = expectedImages; 33 } 34 35 /** 36 * Get the maximum number of expected images for the projection 37 * @return The number of expected images 38 */ 39 public int getExpectedImages() { 40 return this.expectedImages; 41 } 42 } -
src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java index cece730ab4..8728e886bc 100644
a b import java.awt.Image; 12 12 import java.awt.Point; 13 13 import java.awt.Rectangle; 14 14 import java.awt.RenderingHints; 15 import java.awt.event.ComponentEvent; 16 import java.awt.event.MouseAdapter; 15 17 import java.awt.event.MouseEvent; 16 import java.awt.event.MouseListener;17 import java.awt.event.MouseMotionListener;18 18 import java.awt.event.MouseWheelEvent; 19 import java.awt.event.MouseWheelListener;20 19 import java.awt.geom.Rectangle2D; 21 20 import java.awt.image.BufferedImage; 22 21 import java.io.IOException; 23 22 import java.util.Objects; 24 23 import java.util.concurrent.Future; 25 26 24 import javax.swing.JComponent; 27 25 import javax.swing.SwingUtilities; 28 26 27 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 28 import org.openstreetmap.josm.data.imagery.street_level.Projections; 29 29 import org.openstreetmap.josm.data.preferences.BooleanProperty; 30 30 import org.openstreetmap.josm.data.preferences.DoubleProperty; 31 31 import org.openstreetmap.josm.data.preferences.IntegerProperty; 32 32 import org.openstreetmap.josm.gui.MainApplication; 33 import org.openstreetmap.josm.gui.layer.geoimage.viewers.projections.IImageViewer; 34 import org.openstreetmap.josm.gui.layer.geoimage.viewers.projections.ImageProjectionRegistry; 33 35 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings; 34 36 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener; 35 37 import org.openstreetmap.josm.gui.util.GuiHelper; … … import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 38 40 import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 39 41 import org.openstreetmap.josm.tools.Destroyable; 40 42 import org.openstreetmap.josm.tools.ImageProcessor; 43 import org.openstreetmap.josm.tools.JosmRuntimeException; 41 44 import org.openstreetmap.josm.tools.Logging; 42 45 43 46 /** … … import org.openstreetmap.josm.tools.Logging; 48 51 */ 49 52 public class ImageDisplay extends JComponent implements Destroyable, PreferenceChangedListener, FilterChangeListener { 50 53 54 /** The current image viewer */ 55 private IImageViewer iImageViewer; 56 51 57 /** The file that is currently displayed */ 52 private I mageEntryentry;58 private IImageEntry<?> entry; 53 59 54 60 /** The previous file that is currently displayed. Cleared on paint. Only used to help improve UI error information. */ 55 private I mageEntryoldEntry;61 private IImageEntry<?> oldEntry; 56 62 57 63 /** The image currently displayed */ 58 64 private transient BufferedImage image; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 245 251 /** The thread that reads the images. */ 246 252 protected class LoadImageRunnable implements Runnable { 247 253 248 private final I mageEntryentry;254 private final IImageEntry<?> entry; 249 255 250 LoadImageRunnable(I mageEntryentry) {256 LoadImageRunnable(IImageEntry<?> entry) { 251 257 this.entry = entry; 252 258 } 253 259 … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 279 285 updateProcessedImage(); 280 286 // This will clear the loading info box 281 287 ImageDisplay.this.oldEntry = ImageDisplay.this.entry; 282 visibleRect = new VisRect(0, 0, width, height);288 visibleRect = getIImageViewer(entry).getDefaultVisibleRectangle(ImageDisplay.this, image); 283 289 284 290 selectedRect = null; 285 291 errorLoading = false; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 291 297 } 292 298 } 293 299 294 private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {300 private class ImgDisplayMouseListener extends MouseAdapter { 295 301 296 302 private MouseEvent lastMouseEvent; 297 303 private Point mousePointInImg; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 314 320 } 315 321 316 322 private void mouseWheelMovedImpl(int x, int y, int rotation, boolean refreshMousePointInImg) { 317 ImageEntry entry; 318 Image image; 319 VisRect visibleRect; 323 IImageEntry<?> currentEntry; 324 IImageViewer imageViewer; 325 Image currentImage; 326 VisRect currentVisibleRect; 320 327 321 328 synchronized (ImageDisplay.this) { 322 entry = ImageDisplay.this.entry; 323 image = ImageDisplay.this.image; 324 visibleRect = ImageDisplay.this.visibleRect; 329 currentEntry = ImageDisplay.this.entry; 330 currentImage = ImageDisplay.this.image; 331 currentVisibleRect = ImageDisplay.this.visibleRect; 332 imageViewer = ImageDisplay.this.iImageViewer; 325 333 } 326 334 327 335 selectedRect = null; 328 336 329 if ( image == null)337 if (currentImage == null) 330 338 return; 331 339 332 340 // Calculate the mouse cursor position in image coordinates to center the zoom. 333 341 if (refreshMousePointInImg) 334 mousePointInImg = comp2imgCoord( visibleRect, x, y, getSize());342 mousePointInImg = comp2imgCoord(currentVisibleRect, x, y, getSize()); 335 343 336 344 // Apply the zoom to the visible rectangle in image coordinates 337 345 if (rotation > 0) { 338 visibleRect.width = (int) (visibleRect.width * ZOOM_STEP.get());339 visibleRect.height = (int) (visibleRect.height * ZOOM_STEP.get());346 currentVisibleRect.width = (int) (currentVisibleRect.width * ZOOM_STEP.get()); 347 currentVisibleRect.height = (int) (currentVisibleRect.height * ZOOM_STEP.get()); 340 348 } else { 341 visibleRect.width = (int) (visibleRect.width / ZOOM_STEP.get());342 visibleRect.height = (int) (visibleRect.height / ZOOM_STEP.get());349 currentVisibleRect.width = (int) (currentVisibleRect.width / ZOOM_STEP.get()); 350 currentVisibleRect.height = (int) (currentVisibleRect.height / ZOOM_STEP.get()); 343 351 } 344 352 345 353 // Check that the zoom doesn't exceed MAX_ZOOM:1 346 if (visibleRect.width < getSize().width / MAX_ZOOM.get()) { 347 visibleRect.width = (int) (getSize().width / MAX_ZOOM.get()); 348 } 349 if (visibleRect.height < getSize().height / MAX_ZOOM.get()) { 350 visibleRect.height = (int) (getSize().height / MAX_ZOOM.get()); 351 } 354 ensureMaxZoom(currentVisibleRect); 352 355 353 // Set the same ratio for the visible rectangle and the display area 354 int hFact = visibleRect.height * getSize().width; 355 int wFact = visibleRect.width * getSize().height; 356 if (hFact > wFact) { 357 visibleRect.width = hFact / getSize().height; 356 // The size of the visible rectangle is limited by the image size or the viewer implementation. 357 if (imageViewer != null) { 358 imageViewer.checkAndModifyVisibleRectSize(currentImage, currentVisibleRect); 358 359 } else { 359 visibleRect.height = wFact / getSize().width;360 currentVisibleRect.checkRectSize(); 360 361 } 361 362 362 // The size of the visible rectangle is limited by the image size.363 visibleRect.checkRectSize();364 365 363 // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image. 366 Rectangle drawRect = calculateDrawImageRectangle( visibleRect, getSize());367 visibleRect.x = mousePointInImg.x + ((drawRect.x - x) * visibleRect.width) / drawRect.width;368 visibleRect.y = mousePointInImg.y + ((drawRect.y - y) * visibleRect.height) / drawRect.height;364 Rectangle drawRect = calculateDrawImageRectangle(currentVisibleRect, getSize()); 365 currentVisibleRect.x = mousePointInImg.x + ((drawRect.x - x) * currentVisibleRect.width) / drawRect.width; 366 currentVisibleRect.y = mousePointInImg.y + ((drawRect.y - y) * currentVisibleRect.height) / drawRect.height; 369 367 370 368 // The position is also limited by the image size 371 visibleRect.checkRectPos();369 currentVisibleRect.checkRectPos(); 372 370 373 371 synchronized (ImageDisplay.this) { 374 if (ImageDisplay.this.entry == entry) {375 ImageDisplay.this.visibleRect = visibleRect;372 if (ImageDisplay.this.entry == currentEntry) { 373 ImageDisplay.this.visibleRect = currentVisibleRect; 376 374 } 377 375 } 378 376 ImageDisplay.this.repaint(); … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 400 398 @Override 401 399 public void mouseClicked(MouseEvent e) { 402 400 // Move the center to the clicked point. 403 I mageEntry entry;404 Image image;405 VisRect visibleRect;401 IImageEntry<?> currentEntry; 402 Image currentImage; 403 VisRect currentVisibleRect; 406 404 407 405 synchronized (ImageDisplay.this) { 408 entry = ImageDisplay.this.entry;409 image = ImageDisplay.this.image;410 visibleRect = ImageDisplay.this.visibleRect;406 currentEntry = ImageDisplay.this.entry; 407 currentImage = ImageDisplay.this.image; 408 currentVisibleRect = ImageDisplay.this.visibleRect; 411 409 } 412 410 413 if ( image == null)411 if (currentImage == null) 414 412 return; 415 413 416 414 if (ZOOM_ON_CLICK.get()) { 417 415 // click notions are less coherent than wheel, refresh mousePointInImg on each click 418 416 lastMouseEvent = null; 419 417 420 if (mouseIsZoomSelecting(e) && !isAtMaxZoom( visibleRect)) {418 if (mouseIsZoomSelecting(e) && !isAtMaxZoom(currentVisibleRect)) { 421 419 // zoom in if clicked with the zoom button 422 420 mouseWheelMovedImpl(e.getX(), e.getY(), -1, true); 423 421 return; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 430 428 } 431 429 432 430 // Calculate the translation to set the clicked point the center of the view. 433 Point click = comp2imgCoord( visibleRect, e.getX(), e.getY(), getSize());434 Point center = getCenterImgCoord( visibleRect);431 Point click = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 432 Point center = getCenterImgCoord(currentVisibleRect); 435 433 436 visibleRect.x += click.x - center.x;437 visibleRect.y += click.y - center.y;434 currentVisibleRect.x += click.x - center.x; 435 currentVisibleRect.y += click.y - center.y; 438 436 439 visibleRect.checkRectPos();437 currentVisibleRect.checkRectPos(); 440 438 441 439 synchronized (ImageDisplay.this) { 442 if (ImageDisplay.this.entry == entry) {443 ImageDisplay.this.visibleRect = visibleRect;440 if (ImageDisplay.this.entry == currentEntry) { 441 ImageDisplay.this.visibleRect = currentVisibleRect; 444 442 } 445 443 } 446 444 ImageDisplay.this.repaint(); … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 450 448 * a picture part) */ 451 449 @Override 452 450 public void mousePressed(MouseEvent e) { 453 Image image;454 VisRect visibleRect;451 Image currentImage; 452 VisRect currentVisibleRect; 455 453 456 454 synchronized (ImageDisplay.this) { 457 image = ImageDisplay.this.image;458 visibleRect = ImageDisplay.this.visibleRect;455 currentImage = ImageDisplay.this.image; 456 currentVisibleRect = ImageDisplay.this.visibleRect; 459 457 } 460 458 461 if ( image == null)459 if (currentImage == null) 462 460 return; 463 461 464 462 selectedRect = null; 465 463 466 464 if (mouseIsDragging(e) || mouseIsZoomSelecting(e)) 467 mousePointInImg = comp2imgCoord( visibleRect, e.getX(), e.getY(), getSize());465 mousePointInImg = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 468 466 } 469 467 470 468 @Override … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 472 470 if (!mouseIsDragging(e) && !mouseIsZoomSelecting(e)) 473 471 return; 474 472 475 I mageEntry entry;476 Image image;477 VisRect visibleRect;473 IImageEntry<?> imageEntry; 474 Image currentImage; 475 VisRect currentVisibleRect; 478 476 479 477 synchronized (ImageDisplay.this) { 480 entry = ImageDisplay.this.entry;481 image = ImageDisplay.this.image;482 visibleRect = ImageDisplay.this.visibleRect;478 imageEntry = ImageDisplay.this.entry; 479 currentImage = ImageDisplay.this.image; 480 currentVisibleRect = ImageDisplay.this.visibleRect; 483 481 } 484 482 485 if ( image == null)483 if (currentImage == null) 486 484 return; 487 485 488 486 if (mouseIsDragging(e) && mousePointInImg != null) { 489 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize()); 490 visibleRect.isDragUpdate = true; 491 visibleRect.x += mousePointInImg.x - p.x; 492 visibleRect.y += mousePointInImg.y - p.y; 493 visibleRect.checkRectPos(); 487 Point p = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 488 getIImageViewer(entry).mouseDragged(this.mousePointInImg, p, currentVisibleRect); 489 currentVisibleRect.checkRectPos(); 494 490 synchronized (ImageDisplay.this) { 495 if (ImageDisplay.this.entry == entry) {496 ImageDisplay.this.visibleRect = visibleRect;491 if (ImageDisplay.this.entry == imageEntry) { 492 ImageDisplay.this.visibleRect = currentVisibleRect; 497 493 } 498 494 } 495 // We have to update the mousePointInImg for 360 image panning, as otherwise the panning 496 // never stops. 497 // This does not work well with the perspective viewer at this time (2021-08-26). 498 if (entry != null && Projections.EQUIRECTANGULAR == entry.getProjectionType()) { 499 this.mousePointInImg = p; 500 } 499 501 ImageDisplay.this.repaint(); 500 502 } 501 503 502 504 if (mouseIsZoomSelecting(e) && mousePointInImg != null) { 503 Point p = comp2imgCoord( visibleRect, e.getX(), e.getY(), getSize());504 visibleRect.checkPointInside(p);505 VisRect selectedRect = new VisRect(506 p.x < mousePointInImg.x ? p.x : mousePointInImg.x,507 p.y < mousePointInImg.y ? p.y : mousePointInImg.y,505 Point p = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 506 currentVisibleRect.checkPointInside(p); 507 VisRect selectedRectTemp = new VisRect( 508 Math.min(p.x, mousePointInImg.x), 509 Math.min(p.y, mousePointInImg.y), 508 510 p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x, 509 511 p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y, 510 visibleRect);511 selectedRect .checkRectSize();512 selectedRect .checkRectPos();513 ImageDisplay.this.selectedRect = selectedRect ;512 currentVisibleRect); 513 selectedRectTemp.checkRectSize(); 514 selectedRectTemp.checkRectPos(); 515 ImageDisplay.this.selectedRect = selectedRectTemp; 514 516 ImageDisplay.this.repaint(); 515 517 } 516 517 518 } 518 519 519 520 @Override 520 521 public void mouseReleased(MouseEvent e) { 521 I mageEntry entry;522 Image image;523 VisRect visibleRect;522 IImageEntry<?> currentEntry; 523 Image currentImage; 524 VisRect currentVisibleRect; 524 525 525 526 synchronized (ImageDisplay.this) { 526 entry = ImageDisplay.this.entry;527 image = ImageDisplay.this.image;528 visibleRect = ImageDisplay.this.visibleRect;527 currentEntry = ImageDisplay.this.entry; 528 currentImage = ImageDisplay.this.image; 529 currentVisibleRect = ImageDisplay.this.visibleRect; 529 530 } 530 531 531 if ( image == null)532 if (currentImage == null) 532 533 return; 533 534 534 535 if (mouseIsDragging(e)) { 535 visibleRect.isDragUpdate = false;536 currentVisibleRect.isDragUpdate = false; 536 537 } 537 538 538 539 if (mouseIsZoomSelecting(e) && selectedRect != null) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 540 541 int oldHeight = selectedRect.height; 541 542 542 543 // Check that the zoom doesn't exceed MAX_ZOOM:1 543 if (selectedRect.width < getSize().width / MAX_ZOOM.get()) { 544 selectedRect.width = (int) (getSize().width / MAX_ZOOM.get()); 545 } 546 if (selectedRect.height < getSize().height / MAX_ZOOM.get()) { 547 selectedRect.height = (int) (getSize().height / MAX_ZOOM.get()); 548 } 549 550 // Set the same ratio for the visible rectangle and the display area 551 int hFact = selectedRect.height * getSize().width; 552 int wFact = selectedRect.width * getSize().height; 553 if (hFact > wFact) { 554 selectedRect.width = hFact / getSize().height; 555 } else { 556 selectedRect.height = wFact / getSize().width; 557 } 544 ensureMaxZoom(selectedRect); 558 545 559 546 // Keep the center of the selection 560 547 if (selectedRect.width != oldWidth) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 569 556 } 570 557 571 558 synchronized (ImageDisplay.this) { 572 if ( entry == ImageDisplay.this.entry) {559 if (currentEntry == ImageDisplay.this.entry) { 573 560 if (selectedRect == null) { 574 ImageDisplay.this.visibleRect = visibleRect;561 ImageDisplay.this.visibleRect = currentVisibleRect; 575 562 } else { 576 563 ImageDisplay.this.visibleRect.setBounds(selectedRect); 577 564 selectedRect = null; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 580 567 } 581 568 ImageDisplay.this.repaint(); 582 569 } 583 584 @Override585 public void mouseEntered(MouseEvent e) {586 // Do nothing587 }588 589 @Override590 public void mouseExited(MouseEvent e) {591 // Do nothing592 }593 594 @Override595 public void mouseMoved(MouseEvent e) {596 // Do nothing597 }598 570 } 599 571 600 572 /** 601 573 * Constructs a new {@code ImageDisplay} with no image processor. 602 574 */ 603 575 public ImageDisplay() { 604 this(image -> image);576 this(imageObject -> imageObject); 605 577 } 606 578 607 579 /** … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 636 608 * Sets a new source image to be displayed by this {@code ImageDisplay}. 637 609 * @param entry new source image 638 610 * @return a {@link Future} representing pending completion of the image loading task 639 * @since 18150 611 * @since 18150 (xxx for IImageEntry) 640 612 */ 641 public Future<?> setImage(I mageEntryentry) {613 public Future<?> setImage(IImageEntry<?> entry) { 642 614 LoadImageRunnable runnable = setImage0(entry); 643 615 return runnable != null ? MainApplication.worker.submit(runnable) : null; 644 616 } 645 617 646 protected LoadImageRunnable setImage0(I mageEntryentry) {618 protected LoadImageRunnable setImage0(IImageEntry<?> entry) { 647 619 synchronized (this) { 648 620 this.oldEntry = this.entry; 649 621 this.entry = entry; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 691 663 692 664 private void updateProcessedImage() { 693 665 processedImage = image == null ? null : imageProcessor.process(image); 694 GuiHelper.runInEDT( () -> repaint());666 GuiHelper.runInEDT(this::repaint); 695 667 } 696 668 697 669 @Override 698 670 public void paintComponent(Graphics g) { 699 ImageEntry entry; 700 ImageEntry oldEntry; 701 BufferedImage image; 702 VisRect visibleRect; 703 boolean errorLoading; 671 IImageEntry<?> currentEntry; 672 IImageEntry<?> currentOldEntry; 673 IImageViewer currentImageViewer; 674 BufferedImage currentImage; 675 VisRect currentVisibleRect; 676 boolean currentErrorLoading; 704 677 705 678 synchronized (this) { 706 image = this.processedImage;707 entry = this.entry;708 oldEntry = this.oldEntry;709 visibleRect = this.visibleRect;710 errorLoading = this.errorLoading;679 currentImage = this.processedImage; 680 currentEntry = this.entry; 681 currentOldEntry = this.oldEntry; 682 currentVisibleRect = this.visibleRect; 683 currentErrorLoading = this.errorLoading; 711 684 } 712 685 713 686 if (g instanceof Graphics2D) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 716 689 717 690 Dimension size = getSize(); 718 691 // Draw the image first, then draw error information 719 if (image != null && (entry != null || oldEntry != null)) { 720 Rectangle r = new Rectangle(visibleRect); 721 Rectangle target = calculateDrawImageRectangle(visibleRect, size); 722 723 g.drawImage(image, 724 target.x, target.y, target.x + target.width, target.y + target.height, 725 r.x, r.y, r.x + r.width, r.y + r.height, null); 726 727 if (selectedRect != null) { 728 Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y, size); 729 Point bottomRight = img2compCoord(visibleRect, 730 selectedRect.x + selectedRect.width, 731 selectedRect.y + selectedRect.height, size); 732 g.setColor(new Color(128, 128, 128, 180)); 733 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y); 734 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height); 735 g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height); 736 g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y); 737 g.setColor(Color.black); 738 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); 739 } 740 if (errorLoading && entry != null) { 741 String loadingStr = tr("Error on file {0}", entry.getDisplayName()); 692 if (currentImage != null && (currentEntry != null || currentOldEntry != null)) { 693 currentImageViewer = this.getIImageViewer(currentEntry); 694 Rectangle r = new Rectangle(currentVisibleRect); 695 Rectangle target = calculateDrawImageRectangle(currentVisibleRect, size); 696 697 currentImageViewer.paintImage(g, currentImage, target, r); 698 paintSelectedRect(g, target, currentVisibleRect, size); 699 if (currentErrorLoading && currentEntry != null) { 700 String loadingStr = tr("Error on file {0}", currentEntry.getDisplayName()); 742 701 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g); 743 g.drawString(loadingStr, 744 (int) ((size.width - noImageSize.getWidth()) / 2), 702 g.drawString(loadingStr, (int) ((size.width - noImageSize.getWidth()) / 2), 745 703 (int) ((size.height - noImageSize.getHeight()) / 2)); 746 704 } 747 if (osdText != null) { 748 FontMetrics metrics = g.getFontMetrics(g.getFont()); 749 int ascent = metrics.getAscent(); 750 Color bkground = new Color(255, 255, 255, 128); 751 int lastPos = 0; 752 int pos = osdText.indexOf('\n'); 753 int x = 3; 754 int y = 3; 755 String line; 756 while (pos > 0) { 757 line = osdText.substring(lastPos, pos); 758 Rectangle2D lineSize = metrics.getStringBounds(line, g); 759 g.setColor(bkground); 760 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 761 g.setColor(Color.black); 762 g.drawString(line, x, y + ascent); 763 y += (int) lineSize.getHeight(); 764 lastPos = pos + 1; 765 pos = osdText.indexOf('\n', lastPos); 766 } 767 768 line = osdText.substring(lastPos); 769 Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g); 770 g.setColor(bkground); 771 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 772 g.setColor(Color.black); 773 g.drawString(line, x, y + ascent); 774 } 705 paintOsdText(g); 775 706 } 707 paintErrorMessage(g, currentEntry, currentOldEntry, currentImage, currentErrorLoading, size); 708 } 709 710 /** 711 * Paint an error message 712 * @param g The graphics to paint on 713 * @param imageEntry The current image entry 714 * @param oldImageEntry The old image entry 715 * @param bufferedImage The image being painted 716 * @param currentErrorLoading If there was an error loading the image 717 * @param size The size of the component 718 */ 719 private void paintErrorMessage(Graphics g, IImageEntry<?> imageEntry, IImageEntry<?> oldImageEntry, 720 BufferedImage bufferedImage, boolean currentErrorLoading, Dimension size) { 776 721 final String errorMessage; 777 722 // If the new entry is null, then there is no image. 778 if ( entry == null) {723 if (imageEntry == null) { 779 724 if (emptyText == null) { 780 725 emptyText = tr("No image"); 781 726 } 782 727 errorMessage = emptyText; 783 } else if ( image == null || !Objects.equals(entry, oldEntry)) {728 } else if (bufferedImage == null || !Objects.equals(imageEntry, oldImageEntry)) { 784 729 // The image is not necessarily null when loading anymore. If the oldEntry is not the same as the new entry, 785 730 // we are probably still loading the image. (oldEntry gets set to entry when the image finishes loading). 786 if (! errorLoading) {787 errorMessage = tr("Loading {0}", entry.getDisplayName());731 if (!currentErrorLoading) { 732 errorMessage = tr("Loading {0}", imageEntry.getDisplayName()); 788 733 } else { 789 errorMessage = tr("Error on file {0}", entry.getDisplayName());734 errorMessage = tr("Error on file {0}", imageEntry.getDisplayName()); 790 735 } 791 736 } else { 792 737 errorMessage = null; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 812 757 } 813 758 } 814 759 760 /** 761 * Paint OSD text 762 * @param g The graphics to paint on 763 */ 764 private void paintOsdText(Graphics g) { 765 if (osdText != null) { 766 FontMetrics metrics = g.getFontMetrics(g.getFont()); 767 int ascent = metrics.getAscent(); 768 Color bkground = new Color(255, 255, 255, 128); 769 int lastPos = 0; 770 int pos = osdText.indexOf('\n'); 771 int x = 3; 772 int y = 3; 773 String line; 774 while (pos > 0) { 775 line = osdText.substring(lastPos, pos); 776 Rectangle2D lineSize = metrics.getStringBounds(line, g); 777 g.setColor(bkground); 778 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 779 g.setColor(Color.black); 780 g.drawString(line, x, y + ascent); 781 y += (int) lineSize.getHeight(); 782 lastPos = pos + 1; 783 pos = osdText.indexOf('\n', lastPos); 784 } 785 786 line = osdText.substring(lastPos); 787 Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g); 788 g.setColor(bkground); 789 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 790 g.setColor(Color.black); 791 g.drawString(line, x, y + ascent); 792 } 793 } 794 795 /** 796 * Paint the selected rectangle 797 * @param g The graphics to paint on 798 * @param target The target area (i.e., the selection) 799 * @param visibleRectTemp The current visible rect 800 * @param size The size of the component 801 */ 802 private void paintSelectedRect(Graphics g, Rectangle target, VisRect visibleRectTemp, Dimension size) { 803 if (selectedRect != null) { 804 Point topLeft = img2compCoord(visibleRectTemp, selectedRect.x, selectedRect.y, size); 805 Point bottomRight = img2compCoord(visibleRectTemp, 806 selectedRect.x + selectedRect.width, 807 selectedRect.y + selectedRect.height, size); 808 g.setColor(new Color(128, 128, 128, 180)); 809 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y); 810 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height); 811 g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height); 812 g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y); 813 g.setColor(Color.black); 814 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); 815 } 816 } 817 815 818 static Point img2compCoord(VisRect visibleRect, int xImg, int yImg, Dimension compSize) { 816 819 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize); 817 820 return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width, … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 835 838 visibleRect.y + visibleRect.height / 2); 836 839 } 837 840 841 /** 842 * calculateDrawImageRectangle 843 * 844 * @param visibleRect the part of the image that should be drawn (in image coordinates) 845 * @param compSize the part of the component where the image should be drawn (in component coordinates) 846 * @return the part of compRect with the same width/height ratio as the image 847 */ 838 848 static VisRect calculateDrawImageRectangle(VisRect visibleRect, Dimension compSize) { 839 849 return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, compSize.width, compSize.height)); 840 850 } … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 888 898 * the component size. 889 899 */ 890 900 public void zoomBestFitOrOne() { 891 I mageEntry entry;892 Image image;893 VisRect visibleRect;901 IImageEntry<?> currentEntry; 902 Image currentImage; 903 VisRect currentVisibleRect; 894 904 895 905 synchronized (this) { 896 entry = this.entry;897 image = this.image;898 visibleRect = this.visibleRect;906 currentEntry = this.entry; 907 currentImage = this.image; 908 currentVisibleRect = this.visibleRect; 899 909 } 900 910 901 if ( image == null)911 if (currentImage == null) 902 912 return; 903 913 904 if ( visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) {914 if (currentVisibleRect.width != currentImage.getWidth(null) || currentVisibleRect.height != currentImage.getHeight(null)) { 905 915 // The display is not at best fit. => Zoom to best fit 906 visibleRect.reset();916 currentVisibleRect.reset(); 907 917 } else { 908 918 // The display is at best fit => zoom to 1:1 909 Point center = getCenterImgCoord( visibleRect);910 visibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2,919 Point center = getCenterImgCoord(currentVisibleRect); 920 currentVisibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2, 911 921 getWidth(), getHeight()); 912 visibleRect.checkRectSize();913 visibleRect.checkRectPos();922 currentVisibleRect.checkRectSize(); 923 currentVisibleRect.checkRectPos(); 914 924 } 915 925 916 926 synchronized (this) { 917 if (this.entry == entry) {918 this.visibleRect = visibleRect;927 if (this.entry == currentEntry) { 928 this.visibleRect = currentVisibleRect; 919 929 } 920 930 } 921 931 repaint(); 922 932 } 933 934 /** 935 * Get the image viewer for an entry 936 * @param entry The entry to get the viewer for. May be {@code null}. 937 * @return The new image viewer, may be {@code null} 938 */ 939 private IImageViewer getIImageViewer(IImageEntry<?> entry) { 940 IImageViewer imageViewer; 941 IImageEntry<?> imageEntry; 942 synchronized (this) { 943 imageViewer = this.iImageViewer; 944 imageEntry = entry == null ? this.entry : entry; 945 } 946 if (imageEntry == null || imageViewer != null && imageViewer.getSupportedProjections().contains(imageEntry.getProjectionType())) { 947 return imageViewer; 948 } 949 try { 950 imageViewer = ImageProjectionRegistry.getViewer(imageEntry.getProjectionType()).getConstructor().newInstance(); 951 } catch (ReflectiveOperationException e) { 952 throw new JosmRuntimeException(e); 953 } 954 synchronized (this) { 955 if (imageEntry.equals(this.entry)) { 956 this.removeComponentListener(this.iImageViewer); 957 this.iImageViewer = imageViewer; 958 imageViewer.componentResized(new ComponentEvent(this, ComponentEvent.COMPONENT_RESIZED)); 959 this.addComponentListener(this.iImageViewer); 960 } 961 } 962 return imageViewer; 963 } 964 965 /** 966 * Ensure that a rectangle isn't zoomed in too much 967 * @param rectangle The rectangle to get (typically the visible area) 968 */ 969 private void ensureMaxZoom(final Rectangle rectangle) { 970 if (rectangle.width < getSize().width / MAX_ZOOM.get()) { 971 rectangle.width = (int) (getSize().width / MAX_ZOOM.get()); 972 } 973 if (rectangle.height < getSize().height / MAX_ZOOM.get()) { 974 rectangle.height = (int) (getSize().height / MAX_ZOOM.get()); 975 } 976 977 // Set the same ratio for the visible rectangle and the display area 978 int hFact = rectangle.height * getSize().width; 979 int wFact = rectangle.width * getSize().height; 980 if (hFact > wFact) { 981 rectangle.width = hFact / getSize().height; 982 } else { 983 rectangle.height = wFact / getSize().width; 984 } 985 } 986 987 /** 988 * Update the visible rectangle (ensure zoom does not exceed specified values). 989 * Specifically only visible for {@link IImageViewer} implementations. 990 * @since xxx 991 */ 992 public void updateVisibleRectangle() { 993 final VisRect currentVisibleRect; 994 final Image mouseImage; 995 final IImageViewer iImageViewer; 996 synchronized (this) { 997 currentVisibleRect = this.visibleRect; 998 mouseImage = this.image; 999 iImageViewer = this.getIImageViewer(this.entry); 1000 } 1001 if (mouseImage != null && currentVisibleRect != null && iImageViewer != null) { 1002 final Image maxImageSize = iImageViewer.getMaxImageSize(this, mouseImage); 1003 final VisRect maxVisibleRect = new VisRect(0, 0, maxImageSize.getWidth(null), maxImageSize.getHeight(null)); 1004 maxVisibleRect.setRect(currentVisibleRect); 1005 ensureMaxZoom(maxVisibleRect); 1006 1007 maxVisibleRect.checkRectSize(); 1008 synchronized (this) { 1009 this.visibleRect = maxVisibleRect; 1010 } 1011 } 1012 } 923 1013 } -
src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java index c0d3412f4e..8847011a33 100644
a b import java.net.MalformedURLException; 15 15 import java.net.URL; 16 16 import java.util.Collections; 17 17 import java.util.Objects; 18 19 18 import javax.imageio.IIOParam; 20 19 import javax.imageio.ImageReadParam; 21 20 import javax.imageio.ImageReader; 22 21 23 22 import org.openstreetmap.josm.data.ImageData; 24 23 import org.openstreetmap.josm.data.gpx.GpxImageEntry; 24 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 25 25 import org.openstreetmap.josm.tools.ExifReader; 26 26 import org.openstreetmap.josm.tools.ImageProvider; 27 27 import org.openstreetmap.josm.tools.Logging; … … import org.openstreetmap.josm.tools.Logging; 30 30 * Stores info about each image, with an optional thumbnail 31 31 * @since 2662 32 32 */ 33 public class ImageEntry extends GpxImageEntry {33 public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry> { 34 34 35 35 private Image thumbnail; 36 36 private ImageData dataSet; … … public class ImageEntry extends GpxImageEntry { 135 135 return Objects.equals(thumbnail, other.thumbnail) && Objects.equals(dataSet, other.dataSet); 136 136 } 137 137 138 @Override 139 public ImageEntry getNextImage() { 140 return this.dataSet.getNextImage(); 141 } 142 143 @Override 144 public void selectNextImage(final ImageViewerDialog imageViewerDialog) { 145 IImageEntry.super.selectNextImage(imageViewerDialog); 146 this.dataSet.setSelectedImage(this.getNextImage()); 147 } 148 149 @Override 150 public ImageEntry getPreviousImage() { 151 return this.dataSet.getPreviousImage(); 152 } 153 154 @Override 155 public void selectPreviousImage(ImageViewerDialog imageViewerDialog) { 156 IImageEntry.super.selectPreviousImage(imageViewerDialog); 157 this.dataSet.setSelectedImage(this.getPreviousImage()); 158 } 159 160 @Override 161 public ImageEntry getFirstImage() { 162 return this.dataSet.getFirstImage(); 163 } 164 165 @Override 166 public void selectFirstImage(ImageViewerDialog imageViewerDialog) { 167 IImageEntry.super.selectFirstImage(imageViewerDialog); 168 this.dataSet.setSelectedImage(this.getFirstImage()); 169 } 170 171 @Override 172 public ImageEntry getLastImage() { 173 return this.dataSet.getLastImage(); 174 } 175 176 @Override 177 public void selectLastImage(ImageViewerDialog imageViewerDialog) { 178 IImageEntry.super.selectLastImage(imageViewerDialog); 179 this.dataSet.setSelectedImage(this.getLastImage()); 180 } 181 182 @Override 183 public boolean isRemoveSupported() { 184 return true; 185 } 186 187 @Override 188 public boolean remove() { 189 this.dataSet.removeImage(this, false); 190 return true; 191 } 192 138 193 /** 139 194 * Reads the image represented by this entry in the given target dimension. 140 195 * @param target the desired dimension used for {@linkplain IIOParam#setSourceSubsampling subsampling} or {@code null} -
src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java index fc74335f22..bf668a17ae 100644
a b import java.awt.event.WindowEvent; 15 15 import java.time.ZoneOffset; 16 16 import java.time.format.DateTimeFormatter; 17 17 import java.time.format.FormatStyle; 18 import java.util.ArrayList; 18 19 import java.util.Collections; 19 20 import java.util.List; 20 21 import java.util.Optional; 21 22 import java.util.concurrent.Future; 22 23 import java.util.stream.Collectors; 23 24 import javax.swing.AbstractAction; 24 25 import javax.swing.Box; 25 26 import javax.swing.JButton; … … import javax.swing.SwingConstants; 32 33 import org.openstreetmap.josm.actions.JosmAction; 33 34 import org.openstreetmap.josm.data.ImageData; 34 35 import org.openstreetmap.josm.data.ImageData.ImageDataUpdateListener; 36 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 35 37 import org.openstreetmap.josm.gui.ExtendedDialog; 36 38 import org.openstreetmap.josm.gui.MainApplication; 37 39 import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils; 38 import org.openstreetmap.josm.gui.dialogs.DialogsPanel .Action;40 import org.openstreetmap.josm.gui.dialogs.DialogsPanel; 39 41 import org.openstreetmap.josm.gui.dialogs.ToggleDialog; 40 42 import org.openstreetmap.josm.gui.dialogs.layer.LayerVisibilityAction; 41 43 import org.openstreetmap.josm.gui.layer.Layer; … … import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings; 49 51 import org.openstreetmap.josm.tools.ImageProvider; 50 52 import org.openstreetmap.josm.tools.Logging; 51 53 import org.openstreetmap.josm.tools.Shortcut; 52 import org.openstreetmap.josm.tools.Utils;53 54 import org.openstreetmap.josm.tools.date.DateUtils; 54 55 55 56 /** … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 220 221 221 222 @Override 222 223 public void actionPerformed(ActionEvent e) { 223 if ( currentData!= null) {224 currentData.selectNextImage();224 if (ImageViewerDialog.this.currentEntry != null) { 225 ImageViewerDialog.this.currentEntry.selectNextImage(ImageViewerDialog.this); 225 226 } 226 227 } 227 228 } … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 235 236 236 237 @Override 237 238 public void actionPerformed(ActionEvent e) { 238 if ( currentData!= null) {239 currentData.selectPreviousImage();239 if (ImageViewerDialog.this.currentEntry != null) { 240 ImageViewerDialog.this.currentEntry.selectPreviousImage(ImageViewerDialog.this); 240 241 } 241 242 } 242 243 } … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 250 251 251 252 @Override 252 253 public void actionPerformed(ActionEvent e) { 253 if ( currentData!= null) {254 currentData.selectFirstImage();254 if (ImageViewerDialog.this.currentEntry != null) { 255 ImageViewerDialog.this.currentEntry.selectFirstImage(ImageViewerDialog.this); 255 256 } 256 257 } 257 258 } … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 265 266 266 267 @Override 267 268 public void actionPerformed(ActionEvent e) { 268 if ( currentData!= null) {269 currentData.selectLastImage();269 if (ImageViewerDialog.this.currentEntry != null) { 270 ImageViewerDialog.this.currentEntry.selectLastImage(ImageViewerDialog.this); 270 271 } 271 272 } 272 273 } … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 308 309 309 310 @Override 310 311 public void actionPerformed(ActionEvent e) { 311 if (currentData != null) { 312 currentData.removeSelectedImages(); 312 if (ImageViewerDialog.this.currentEntry != null) { 313 IImageEntry<?> imageEntry = ImageViewerDialog.this.currentEntry; 314 if (imageEntry.isRemoveSupported()) { 315 imageEntry.remove(); 316 } 313 317 } 314 318 } 315 319 } … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 324 328 325 329 @Override 326 330 public void actionPerformed(ActionEvent e) { 327 if (currentData != null && currentData.getSelectedImage() != null) { 328 List<ImageEntry> toDelete = currentData.getSelectedImages(); 331 if (currentEntry != null) { 332 List<IImageEntry<?>> toDelete = currentEntry instanceof ImageEntry ? 333 new ArrayList<>(((ImageEntry) currentEntry).getDataSet().getSelectedImages()) 334 : Collections.singletonList(currentEntry); 329 335 int size = toDelete.size(); 330 336 331 337 int result = new ExtendedDialog( … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 346 352 .getValue(); 347 353 348 354 if (result == 2) { 349 for (ImageEntry delete : toDelete) { 350 if (Utils.deleteFile(delete.getFile())) { 351 currentData.removeImage(delete, false); 355 final List<ImageData> imageDataCollection = toDelete.stream().filter(ImageEntry.class::isInstance) 356 .map(ImageEntry.class::cast).map(ImageEntry::getDataSet).distinct().collect(Collectors.toList()); 357 for (IImageEntry<?> delete : toDelete) { 358 if (delete.isRemoveSupported() && delete.remove()) { 352 359 Logging.info("File {0} deleted.", delete.getFile()); 353 360 } else { 354 361 JOptionPane.showMessageDialog( … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 359 366 ); 360 367 } 361 368 } 362 currentData.notifyImageUpdate(); 363 currentData.updateSelectedImage(); 369 imageDataCollection.forEach(data -> { 370 data.notifyImageUpdate(); 371 data.updateSelectedImage(); 372 }); 364 373 } 365 374 } 366 375 } … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 375 384 376 385 @Override 377 386 public void actionPerformed(ActionEvent e) { 378 if (current Data!= null) {379 ClipboardUtils.copyString(String.valueOf(current Data.getSelectedImage().getFile()));387 if (currentEntry != null) { 388 ClipboardUtils.copyString(String.valueOf(currentEntry.getFile())); 380 389 } 381 390 } 382 391 } … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 425 434 return wasEnabled; 426 435 } 427 436 428 private transient ImageData currentData; 429 private transient ImageEntry currentEntry; 437 private transient IImageEntry<?> currentEntry; 430 438 431 439 /** 432 440 * Displays a single image for the given layer. 433 * @param data the image data441 * @param ignoredData the image data 434 442 * @param entry image entry 435 443 * @see #displayImages 436 444 */ 437 public void displayImage(ImageData data, ImageEntry entry) { 438 displayImages(data, Collections.singletonList(entry)); 445 public void displayImage(ImageData ignoredData, ImageEntry entry) { 446 displayImages(Collections.singletonList(entry)); 447 } 448 449 /** 450 * Displays a single image for the given layer. 451 * @param entry image entry 452 * @see #displayImages 453 */ 454 public void displayImage(IImageEntry<?> entry) { 455 this.displayImages(Collections.singletonList(entry)); 439 456 } 440 457 441 458 /** 442 459 * Displays images for the given layer. 443 * @param data the image data444 460 * @param entries image entries 445 * @since 15333461 * @since xxx 446 462 */ 447 public void displayImages( ImageData data, List<ImageEntry> entries) {463 public void displayImages(List<IImageEntry<?>> entries) { 448 464 boolean imageChanged; 449 I mageEntryentry = entries != null && entries.size() == 1 ? entries.get(0) : null;465 IImageEntry<?> entry = entries != null && entries.size() == 1 ? entries.get(0) : null; 450 466 451 467 synchronized (this) { 452 468 // TODO: pop up image dialog but don't load image again … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 457 473 MainApplication.getMap().mapView.zoomTo(entry.getPos()); 458 474 } 459 475 460 currentData = data;461 476 currentEntry = entry; 462 477 } 463 478 464 479 if (entry != null) { 465 setNextEnabled(data.hasNextImage()); 466 setPreviousEnabled(data.hasPreviousImage()); 467 btnDelete.setEnabled(true); 468 btnDeleteFromDisk.setEnabled(entry.getFile() != null); 469 btnCopyPath.setEnabled(true); 470 471 if (imageChanged) { 472 cancelLoadingImage(); 473 // Set only if the image is new to preserve zoom and position if the same image is redisplayed 474 // (e.g. to update the OSD). 475 imgLoadingFuture = imgDisplay.setImage(entry); 476 } 477 setTitle(tr("Geotagged Images") + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : "")); 478 StringBuilder osd = new StringBuilder(entry.getDisplayName()); 479 if (entry.getElevation() != null) { 480 osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation()))); 481 } 482 if (entry.getSpeed() != null) { 483 osd.append(tr("\nSpeed: {0} km/h", Math.round(entry.getSpeed()))); 484 } 485 if (entry.getExifImgDir() != null) { 486 osd.append(tr("\nDirection {0}\u00b0", Math.round(entry.getExifImgDir()))); 487 } 488 489 DateTimeFormatter dtf = DateUtils.getDateTimeFormatter(FormatStyle.SHORT, FormatStyle.MEDIUM) 490 // Set timezone to UTC since UTC is assumed when parsing the EXIF timestamp, 491 // see see org.openstreetmap.josm.tools.ExifReader.readTime(com.drew.metadata.Metadata) 492 .withZone(ZoneOffset.UTC); 493 494 if (entry.hasExifTime()) { 495 osd.append(tr("\nEXIF time: {0}", dtf.format(entry.getExifInstant()))); 496 } 497 if (entry.hasGpsTime()) { 498 osd.append(tr("\nGPS time: {0}", dtf.format(entry.getGpsInstant()))); 499 } 500 Optional.ofNullable(entry.getIptcCaption()).map(s -> tr("\nCaption: {0}", s)).ifPresent(osd::append); 501 Optional.ofNullable(entry.getIptcHeadline()).map(s -> tr("\nHeadline: {0}", s)).ifPresent(osd::append); 502 Optional.ofNullable(entry.getIptcKeywords()).map(s -> tr("\nKeywords: {0}", s)).ifPresent(osd::append); 503 Optional.ofNullable(entry.getIptcObjectName()).map(s -> tr("\nObject name: {0}", s)).ifPresent(osd::append); 504 505 imgDisplay.setOsdText(osd.toString()); 480 this.updateButtonsNonNullEntry(entry, imageChanged); 506 481 } else { 507 boolean hasMultipleImages = entries != null && entries.size() > 1; 508 // if this method is called to reinitialize dialog content with a blank image, 509 // do not actually show the dialog again with a blank image if currently hidden (fix #10672) 510 setTitle(tr("Geotagged Images")); 511 imgDisplay.setImage(null); 512 imgDisplay.setOsdText(""); 513 setNextEnabled(false); 514 setPreviousEnabled(false); 515 btnDelete.setEnabled(hasMultipleImages); 516 btnDeleteFromDisk.setEnabled(hasMultipleImages); 517 btnCopyPath.setEnabled(false); 518 if (hasMultipleImages) { 519 imgDisplay.setEmptyText(tr("Multiple images selected")); 520 btnFirst.setEnabled(!isFirstImageSelected(data)); 521 btnLast.setEnabled(!isLastImageSelected(data)); 522 } 523 imgDisplay.setImage(null); 524 imgDisplay.setOsdText(""); 482 this.updateButtonsNullEntry(entries); 525 483 return; 526 484 } 527 485 if (!isDialogShowing()) { … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 530 488 } else { 531 489 if (isDocked && isCollapsed) { 532 490 expand(); 533 dialogsPanel.reconstruct( Action.COLLAPSED_TO_DEFAULT, this);491 dialogsPanel.reconstruct(DialogsPanel.Action.COLLAPSED_TO_DEFAULT, this); 534 492 } 535 493 } 536 494 } 537 495 538 private static boolean isLastImageSelected(ImageData data) { 539 return data.isImageSelected(data.getImages().get(data.getImages().size() - 1)); 496 /** 497 * Update buttons for null entry 498 * @param entries {@code true} if multiple images are selected 499 */ 500 private void updateButtonsNullEntry(List<IImageEntry<?>> entries) { 501 boolean hasMultipleImages = entries != null && entries.size() > 1; 502 // if this method is called to reinitialize dialog content with a blank image, 503 // do not actually show the dialog again with a blank image if currently hidden (fix #10672) 504 setTitle(tr("Geotagged Images")); 505 imgDisplay.setImage(null); 506 imgDisplay.setOsdText(""); 507 setNextEnabled(false); 508 setPreviousEnabled(false); 509 btnDelete.setEnabled(hasMultipleImages); 510 btnDeleteFromDisk.setEnabled(hasMultipleImages); 511 btnCopyPath.setEnabled(false); 512 if (hasMultipleImages) { 513 imgDisplay.setEmptyText(tr("Multiple images selected")); 514 btnFirst.setEnabled(!isFirstImageSelected(entries)); 515 btnLast.setEnabled(!isLastImageSelected(entries)); 516 } 517 imgDisplay.setImage(null); 518 imgDisplay.setOsdText(""); 519 } 520 521 /** 522 * Update the image viewer buttons for the new entry 523 * @param entry The new entry 524 * @param imageChanged {@code true} if it is not the same image as the previous image. 525 */ 526 private void updateButtonsNonNullEntry(IImageEntry<?> entry, boolean imageChanged) { 527 setNextEnabled(entry.getNextImage() != null); 528 setPreviousEnabled(entry.getPreviousImage() != null); 529 btnDelete.setEnabled(true); 530 btnDeleteFromDisk.setEnabled(entry.getFile() != null); 531 btnCopyPath.setEnabled(true); 532 533 if (imageChanged) { 534 cancelLoadingImage(); 535 // Set only if the image is new to preserve zoom and position if the same image is redisplayed 536 // (e.g. to update the OSD). 537 imgLoadingFuture = imgDisplay.setImage(entry); 538 } 539 setTitle(tr("Geotagged Images") + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : "")); 540 StringBuilder osd = new StringBuilder(entry.getDisplayName()); 541 if (entry.getElevation() != null) { 542 osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation()))); 543 } 544 if (entry.getSpeed() != null) { 545 osd.append(tr("\nSpeed: {0} km/h", Math.round(entry.getSpeed()))); 546 } 547 if (entry.getExifImgDir() != null) { 548 osd.append(tr("\nDirection {0}\u00b0", Math.round(entry.getExifImgDir()))); 549 } 550 551 DateTimeFormatter dtf = DateUtils.getDateTimeFormatter(FormatStyle.SHORT, FormatStyle.MEDIUM) 552 // Set timezone to UTC since UTC is assumed when parsing the EXIF timestamp, 553 // see see org.openstreetmap.josm.tools.ExifReader.readTime(com.drew.metadata.Metadata) 554 .withZone(ZoneOffset.UTC); 555 556 if (entry.hasExifTime()) { 557 osd.append(tr("\nEXIF time: {0}", dtf.format(entry.getExifInstant()))); 558 } 559 if (entry.hasGpsTime()) { 560 osd.append(tr("\nGPS time: {0}", dtf.format(entry.getGpsInstant()))); 561 } 562 Optional.ofNullable(entry.getIptcCaption()).map(s -> tr("\nCaption: {0}", s)).ifPresent(osd::append); 563 Optional.ofNullable(entry.getIptcHeadline()).map(s -> tr("\nHeadline: {0}", s)).ifPresent(osd::append); 564 Optional.ofNullable(entry.getIptcKeywords()).map(s -> tr("\nKeywords: {0}", s)).ifPresent(osd::append); 565 Optional.ofNullable(entry.getIptcObjectName()).map(s -> tr("\nObject name: {0}", s)).ifPresent(osd::append); 566 567 imgDisplay.setOsdText(osd.toString()); 568 } 569 570 /** 571 * Displays images for the given layer. 572 * @param ignoredData the image data (unused, may be {@code null}) 573 * @param entries image entries 574 * @since 15333 (xxx for IImageEntry<?>) 575 * @deprecated Use {@link #displayImages(List)} (The data param is no longer used) 576 */ 577 @Deprecated 578 public void displayImages(ImageData ignoredData, List<IImageEntry<?>> entries) { 579 this.displayImages(entries); 580 } 581 582 private static boolean isLastImageSelected(List<IImageEntry<?>> data) { 583 return data.stream().anyMatch(image -> data.contains(image.getLastImage())); 540 584 } 541 585 542 private static boolean isFirstImageSelected( ImageDatadata) {543 return data. isImageSelected(data.getImages().get(0));586 private static boolean isFirstImageSelected(List<IImageEntry<?>> data) { 587 return data.stream().anyMatch(image -> data.contains(image.getFirstImage())); 544 588 } 545 589 546 590 /** … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 575 619 /** 576 620 * Returns the currently displayed image. 577 621 * @return Currently displayed image or {@code null} 578 * @since 6392 622 * @since 6392 (xxx for IImageEntry<?>) 579 623 */ 580 public static I mageEntrygetCurrentImage() {624 public static IImageEntry<?> getCurrentImage() { 581 625 return getInstance().currentEntry; 582 626 } 583 627 … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 598 642 599 643 @Override 600 644 public void layerRemoving(LayerRemoveEvent e) { 601 if (e.getRemovedLayer() instanceof GeoImageLayer ) {645 if (e.getRemovedLayer() instanceof GeoImageLayer && this.currentEntry instanceof ImageEntry) { 602 646 ImageData removedData = ((GeoImageLayer) e.getRemovedLayer()).getImageData(); 603 if (removedData == currentData) {604 displayImages(null , null);647 if (removedData == ((ImageEntry) this.currentEntry).getDataSet()) { 648 displayImages(null); 605 649 } 606 650 removedData.removeImageDataUpdateListener(this); 607 651 } … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 626 670 } 627 671 628 672 private void showLayer(Layer newLayer) { 629 if (currentData == null && newLayer instanceof GeoImageLayer) { 630 ((GeoImageLayer) newLayer).getImageData().selectFirstImage(); 673 if (this.currentEntry == null && newLayer instanceof GeoImageLayer) { 674 ImageData imageData = ((GeoImageLayer) newLayer).getImageData(); 675 imageData.setSelectedImage(imageData.getFirstImage()); 631 676 } 632 677 } 633 678 … … public final class ImageViewerDialog extends ToggleDialog implements LayerChange 640 685 641 686 @Override 642 687 public void selectedImageChanged(ImageData data) { 643 displayImages( data, data.getSelectedImages());688 displayImages(new ArrayList<>(data.getSelectedImages())); 644 689 } 645 690 646 691 @Override 647 692 public void imageDataUpdated(ImageData data) { 648 displayImages( data, data.getSelectedImages());693 displayImages(new ArrayList<>(data.getSelectedImages())); 649 694 } 650 695 } -
new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Equirectangular.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Equirectangular.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Equirectangular.java new file mode 100644 index 0000000000..29ae11ae29
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.geoimage.viewers.projections; 3 4 import java.awt.Component; 5 import java.awt.Graphics; 6 import java.awt.Image; 7 import java.awt.Point; 8 import java.awt.Rectangle; 9 import java.awt.event.ComponentAdapter; 10 import java.awt.event.ComponentEvent; 11 import java.awt.image.BufferedImage; 12 import java.util.Collections; 13 import java.util.Set; 14 15 import org.openstreetmap.josm.data.imagery.street_level.Projections; 16 import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay; 17 import org.openstreetmap.josm.gui.util.GuiHelper; 18 import org.openstreetmap.josm.gui.util.imagery.CameraPlane; 19 import org.openstreetmap.josm.gui.util.imagery.Vector3D; 20 21 /** 22 * A class for showing 360 images that use the equirectangular projection 23 * @author Taylor Smock 24 * @since xxx 25 */ 26 public class Equirectangular extends ComponentAdapter implements IImageViewer { 27 private volatile CameraPlane cameraPlane; 28 private volatile BufferedImage offscreenImage; 29 30 @Override 31 public Set<Projections> getSupportedProjections() { 32 return Collections.singleton(Projections.EQUIRECTANGULAR); 33 } 34 35 @Override 36 public void paintImage(Graphics g, BufferedImage image, Rectangle target, Rectangle visibleRect) { 37 final CameraPlane currentCameraPlane; 38 final BufferedImage currentOffscreenImage; 39 synchronized (this) { 40 currentCameraPlane = this.cameraPlane; 41 currentOffscreenImage = this.offscreenImage; 42 } 43 currentCameraPlane.mapping(image, currentOffscreenImage); 44 if (target == null) { 45 target = new Rectangle(0, 0, currentOffscreenImage.getWidth(null), currentOffscreenImage.getHeight(null)); 46 } 47 g.drawImage(currentOffscreenImage, target.x, target.y, target.x + target.width, target.y + target.height, 48 visibleRect.x, visibleRect.y, visibleRect.x + visibleRect.width, visibleRect.y + visibleRect.height, 49 null); 50 } 51 52 @Override 53 public ImageDisplay.VisRect getDefaultVisibleRectangle(Component component, Image image) { 54 return new ImageDisplay.VisRect(0, 0, component.getSize().width, component.getSize().height); 55 } 56 57 @Override 58 public double getRotation() { 59 return this.cameraPlane.getRotation().getAzimuthalAngle(); 60 } 61 62 @Override 63 public void componentResized(ComponentEvent e) { 64 final Component imgDisplay = e.getComponent(); 65 if (e.getComponent().getWidth() > 0 66 && e.getComponent().getHeight() > 0) { 67 // FIXME: Do something so that the types of the images are the same between the offscreenImage and 68 // the image entry 69 final CameraPlane currentCameraPlane; 70 synchronized (this) { 71 currentCameraPlane = this.cameraPlane; 72 } 73 final BufferedImage temporaryOffscreenImage = new BufferedImage(imgDisplay.getWidth(), imgDisplay.getHeight(), 74 BufferedImage.TYPE_4BYTE_ABGR); 75 76 Vector3D currentRotation = null; 77 if (currentCameraPlane != null) { 78 currentRotation = currentCameraPlane.getRotation(); 79 } 80 final CameraPlane temporaryCameraPlane = new CameraPlane(imgDisplay.getWidth(), imgDisplay.getHeight()); 81 if (currentRotation != null) { 82 temporaryCameraPlane.setRotation(currentRotation); 83 } 84 synchronized (this) { 85 this.cameraPlane = temporaryCameraPlane; 86 this.offscreenImage = temporaryOffscreenImage; 87 } 88 if (imgDisplay instanceof ImageDisplay) { 89 ((ImageDisplay) imgDisplay).updateVisibleRectangle(); 90 } 91 GuiHelper.runInEDT(imgDisplay::revalidate); 92 } 93 } 94 95 @Override 96 public void mouseDragged(final Point from, final Point to, ImageDisplay.VisRect currentVisibleRect) { 97 if (from != null && to != null) { 98 this.cameraPlane.setRotationFromDelta(from, to); 99 } 100 } 101 102 @Override 103 public void checkAndModifyVisibleRectSize(Image image, ImageDisplay.VisRect visibleRect) { 104 IImageViewer.super.checkAndModifyVisibleRectSize(this.offscreenImage, visibleRect); 105 } 106 107 @Override 108 public Image getMaxImageSize(ImageDisplay imageDisplay, Image image) { 109 return this.offscreenImage; 110 } 111 } -
new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/IImageViewer.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/IImageViewer.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/IImageViewer.java new file mode 100644 index 0000000000..1c97a87b76
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.geoimage.viewers.projections; 3 4 import java.awt.Component; 5 import java.awt.Graphics; 6 import java.awt.Image; 7 import java.awt.Point; 8 import java.awt.Rectangle; 9 import java.awt.event.ComponentListener; 10 import java.awt.image.BufferedImage; 11 import java.util.Set; 12 13 import org.openstreetmap.josm.data.imagery.street_level.Projections; 14 import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay; 15 16 /** 17 * An interface for image viewers for specific projections 18 * @since xxx 19 */ 20 public interface IImageViewer extends ComponentListener { 21 /** 22 * Get the supported projections for the image viewer 23 * @return The projections supported. Typically, only one. 24 */ 25 Set<Projections> getSupportedProjections(); 26 27 /** 28 * Paint the image 29 * @param g The graphics to paint on 30 * @param image The image to paint 31 * @param target The target area 32 * @param visibleRect The visible rectangle 33 */ 34 void paintImage(Graphics g, BufferedImage image, Rectangle target, Rectangle visibleRect); 35 36 /** 37 * Get the default visible rectangle for the projection 38 * @param component The component the image will be displayed in 39 * @param image The image that will be shown 40 * @return The default visible rectangle 41 */ 42 ImageDisplay.VisRect getDefaultVisibleRectangle(Component component, Image image); 43 44 /** 45 * Get the current rotation in the image viewer 46 * @return The rotation 47 */ 48 default double getRotation() { 49 return 0; 50 } 51 52 /** 53 * Indicate that the mouse has been dragged to a point 54 * @param from The point the mouse was dragged from 55 * @param to The point the mouse has been dragged to 56 * @param currentVisibleRect The currently visible rectangle (this is updated by the default implementation) 57 */ 58 default void mouseDragged(Point from, Point to, ImageDisplay.VisRect currentVisibleRect) { 59 currentVisibleRect.isDragUpdate = true; 60 currentVisibleRect.x += from.x - to.x; 61 currentVisibleRect.y += from.y - to.y; 62 } 63 64 /** 65 * Check and modify the visible rect size to appropriate dimensions 66 * @param visibleRect the visible rectangle to update 67 * @param image The image to use for checking 68 */ 69 default void checkAndModifyVisibleRectSize(Image image, ImageDisplay.VisRect visibleRect) { 70 if (visibleRect.width > image.getWidth(null)) { 71 visibleRect.width = image.getWidth(null); 72 } 73 if (visibleRect.height > image.getHeight(null)) { 74 visibleRect.height = image.getHeight(null); 75 } 76 } 77 78 /** 79 * Get the maximum image size that can be displayed 80 * @param imageDisplay The image display 81 * @param image The image 82 * @return The maximum image size (may be the original image passed in) 83 */ 84 default Image getMaxImageSize(ImageDisplay imageDisplay, Image image) { 85 return image; 86 } 87 } -
new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/ImageProjectionRegistry.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/ImageProjectionRegistry.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/ImageProjectionRegistry.java new file mode 100644 index 0000000000..24f1f55197
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.geoimage.viewers.projections; 3 4 import java.util.EnumMap; 5 import java.util.Map; 6 import java.util.Objects; 7 import java.util.stream.Collectors; 8 9 import org.openstreetmap.josm.data.imagery.street_level.Projections; 10 import org.openstreetmap.josm.tools.JosmRuntimeException; 11 12 /** 13 * A class that holds a registry of viewers for image projections 14 */ 15 public final class ImageProjectionRegistry { 16 private static final EnumMap<Projections, Class<? extends IImageViewer>> DEFAULT_VIEWERS = new EnumMap<>(Projections.class); 17 18 // Register the default viewers 19 static { 20 try { 21 registerViewer(Perspective.class); 22 registerViewer(Equirectangular.class); 23 } catch (ReflectiveOperationException e) { 24 throw new JosmRuntimeException(e); 25 } 26 } 27 28 private ImageProjectionRegistry() { 29 // Prevent instantiations 30 } 31 32 /** 33 * Register a new viewer 34 * @param clazz The class to register. The class <i>must</i> have a no args constructor 35 * @return {@code true} if something changed 36 * @throws ReflectiveOperationException if there is no no-args constructor, or it is not visible to us. 37 */ 38 public static boolean registerViewer(Class<? extends IImageViewer> clazz) throws ReflectiveOperationException { 39 Objects.requireNonNull(clazz, "null classes are hard to instantiate"); 40 final IImageViewer object = clazz.getConstructor().newInstance(); 41 boolean changed = false; 42 for (Projections projections : object.getSupportedProjections()) { 43 changed = clazz.equals(DEFAULT_VIEWERS.put(projections, clazz)) || changed; 44 } 45 return changed; 46 } 47 48 /** 49 * Remove a viewer 50 * @param clazz The class to remove. 51 * @return {@code true} if something changed 52 */ 53 public static boolean removeViewer(Class<? extends IImageViewer> clazz) { 54 boolean changed = false; 55 for (Projections projections : DEFAULT_VIEWERS.entrySet().stream() 56 .filter(entry -> entry.getValue().equals(clazz)).map(Map.Entry::getKey) 57 .collect(Collectors.toList())) { 58 changed = DEFAULT_VIEWERS.remove(projections, clazz) || changed; 59 } 60 return changed; 61 } 62 63 /** 64 * Get the viewer for a specific projection type 65 * @param projection The projection to view 66 * @return The class to use 67 */ 68 public static Class<? extends IImageViewer> getViewer(Projections projection) { 69 return DEFAULT_VIEWERS.getOrDefault(projection, DEFAULT_VIEWERS.getOrDefault(Projections.UNKNOWN, Perspective.class)); 70 } 71 } -
new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Perspective.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Perspective.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/projections/Perspective.java new file mode 100644 index 0000000000..72ed12adc0
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.geoimage.viewers.projections; 3 4 import java.awt.Component; 5 import java.awt.Graphics; 6 import java.awt.Image; 7 import java.awt.Rectangle; 8 import java.awt.event.ComponentAdapter; 9 import java.awt.image.BufferedImage; 10 import java.util.EnumSet; 11 import java.util.Set; 12 13 import org.openstreetmap.josm.data.imagery.street_level.Projections; 14 import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay; 15 16 /** 17 * The default perspective image viewer class. 18 * This also handles (by default) unknown projections. 19 */ 20 public class Perspective extends ComponentAdapter implements IImageViewer { 21 22 @Override 23 public Set<Projections> getSupportedProjections() { 24 return EnumSet.of(Projections.PERSPECTIVE); 25 } 26 27 @Override 28 public void paintImage(Graphics g, BufferedImage image, Rectangle target, Rectangle r) { 29 g.drawImage(image, 30 target.x, target.y, target.x + target.width, target.y + target.height, 31 r.x, r.y, r.x + r.width, r.y + r.height, null); 32 } 33 34 @Override 35 public ImageDisplay.VisRect getDefaultVisibleRectangle(Component component, Image image) { 36 return new ImageDisplay.VisRect(0, 0, image.getWidth(null), image.getHeight(null)); 37 } 38 } -
new file src/org/openstreetmap/josm/gui/util/imagery/CameraPlane.java
diff --git a/src/org/openstreetmap/josm/gui/util/imagery/CameraPlane.java b/src/org/openstreetmap/josm/gui/util/imagery/CameraPlane.java new file mode 100644 index 0000000000..b4eff0d256
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.util.imagery; 3 4 import java.awt.Point; 5 import java.awt.geom.Point2D; 6 import java.awt.image.BufferedImage; 7 import java.awt.image.DataBuffer; 8 import java.awt.image.DataBufferDouble; 9 import java.awt.image.DataBufferInt; 10 import java.util.stream.IntStream; 11 import javax.annotation.Nullable; 12 13 /** 14 * The plane that the camera appears on and rotates around. 15 */ 16 public class CameraPlane { 17 /** The field of view for the panorama at 0 zoom */ 18 static final double PANORAMA_FOV = Math.toRadians(110); 19 20 /** This determines the yaw direction. We may want to make it a config option, but maybe not */ 21 private static final byte YAW_DIRECTION = -1; 22 23 /** The width of the image */ 24 private final int width; 25 /** The height of the image */ 26 private final int height; 27 28 private final Vector3D[][] vectors; 29 private Vector3D rotation; 30 31 public static final double HALF_PI = Math.PI / 2; 32 public static final double TWO_PI = 2 * Math.PI; 33 34 /** 35 * Create a new CameraPlane with the default FOV (110 degrees). 36 * 37 * @param width The width of the image 38 * @param height The height of the image 39 */ 40 public CameraPlane(int width, int height) { 41 this(width, height, (width / 2d) / Math.tan(PANORAMA_FOV / 2)); 42 } 43 44 /** 45 * Create a new CameraPlane 46 * 47 * @param width The width of the image 48 * @param height The height of the image 49 * @param distance The radial distance of the photosphere 50 */ 51 private CameraPlane(int width, int height, double distance) { 52 this.width = width; 53 this.height = height; 54 this.rotation = new Vector3D(Vector3D.VectorType.RPA, distance, 0, 0); 55 this.vectors = new Vector3D[width][height]; 56 IntStream.range(0, this.height).parallel().forEach(y -> IntStream.range(0, this.width).parallel() 57 .forEach(x -> this.vectors[x][y] = this.getVector3D((double) x, y))); 58 } 59 60 /** 61 * Get the width of the image 62 * @return The width of the image 63 */ 64 public int getWidth() { 65 return this.width; 66 } 67 68 /** 69 * Get the height of the image 70 * @return The height of the image 71 */ 72 public int getHeight() { 73 return this.height; 74 } 75 76 /** 77 * Get the point for a vector 78 * 79 * @param vector the vector for which the corresponding point on the camera plane will be returned 80 * @return the point on the camera plane to which the given vector is mapped, nullable 81 */ 82 @Nullable 83 public Point getPoint(final Vector3D vector) { 84 final Vector3D rotatedVector = rotate(vector, -1); 85 // Currently set to false due to change in painting 86 if (rotatedVector.getZ() < 0) { 87 // Ignores any points "behind the back", so they don't get painted a second time on the other 88 // side of the sphere 89 return null; 90 } 91 // This is a slightly faster than just doing the (brute force) method of Math.max(Math.min)). Reduces if 92 // statements by 1 per call. 93 final long x = Math 94 .round((rotatedVector.getX() / rotatedVector.getZ()) * this.rotation.getRadialDistance() + width / 2d); 95 final long y = Math 96 .round((rotatedVector.getY() / rotatedVector.getZ()) * this.rotation.getRadialDistance() + height / 2d); 97 98 try { 99 return new Point(Math.toIntExact(x), Math.toIntExact(y)); 100 } catch (ArithmeticException e) { 101 return new Point((int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, x)), 102 (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, y))); 103 } 104 } 105 106 /** 107 * Convert a point to a 3D vector 108 * 109 * @param p The point to convert 110 * @return The vector 111 */ 112 public Vector3D getVector3D(final Point p) { 113 return this.getVector3D(p.x, p.y); 114 } 115 116 /** 117 * Convert a point to a 3D vector (vectors are cached) 118 * 119 * @param x The x coordinate 120 * @param y The y coordinate 121 * @return The vector 122 */ 123 public Vector3D getVector3D(final int x, final int y) { 124 Vector3D res; 125 try { 126 res = rotate(vectors[x][y]); 127 } catch (Exception e) { 128 res = Vector3D.DEFAULT_VECTOR_3D; 129 } 130 return res; 131 } 132 133 /** 134 * Convert a point to a 3D vector. Warning: This method does not cache. 135 * 136 * @param x The x coordinate 137 * @param y The y coordinate 138 * @return The vector (the middle of the image is 0, 0) 139 */ 140 public Vector3D getVector3D(final double x, final double y) { 141 return new Vector3D(x - width / 2d, y - height / 2d, this.rotation.getRadialDistance()).normalize(); 142 } 143 144 /** 145 * Set camera plane rotation by current plane position. 146 * 147 * @param p Point within current plane. 148 */ 149 public void setRotation(final Point p) { 150 setRotation(getVector3D(p)); 151 } 152 153 /** 154 * Set the rotation from the difference of two points 155 * 156 * @param from The originating point 157 * @param to The new point 158 */ 159 public void setRotationFromDelta(final Point from, final Point to) { 160 // Bound check (bounds are essentially the image viewer component) 161 if (from.x < 0 || from.y < 0 || to.x < 0 || to.y < 0 162 || from.x > this.vectors.length || from.y > this.vectors[0].length 163 || to.x > this.vectors.length || to.y > this.vectors[0].length) { 164 return; 165 } 166 Vector3D f1 = this.vectors[from.x][from.y]; 167 Vector3D t1 = this.vectors[to.x][to.y]; 168 double deltaPolarAngle = f1.getPolarAngle() - t1.getPolarAngle(); 169 double deltaAzimuthalAngle = t1.getAzimuthalAngle() - f1.getAzimuthalAngle(); 170 double polarAngle = this.rotation.getPolarAngle() + deltaPolarAngle; 171 double azimuthalAngle = this.rotation.getAzimuthalAngle() + deltaAzimuthalAngle; 172 this.setRotation(azimuthalAngle, polarAngle); 173 } 174 175 /** 176 * Set camera plane rotation by spherical vector. 177 * 178 * @param vec vector pointing new view position. 179 */ 180 public void setRotation(Vector3D vec) { 181 setRotation(vec.getPolarAngle(), vec.getAzimuthalAngle()); 182 } 183 184 public Vector3D getRotation() { 185 return this.rotation; 186 } 187 188 synchronized void setRotation(double azimuthalAngle, double polarAngle) { 189 // Note: Something, somewhere, is switching the two. 190 // FIXME: Figure out what is switching them and why 191 // Prevent us from going much outside 2pi 192 if (polarAngle < 0) { 193 polarAngle = polarAngle + TWO_PI; 194 } else if (polarAngle > TWO_PI) { 195 polarAngle = polarAngle - TWO_PI; 196 } 197 // Avoid flipping the camera 198 if (azimuthalAngle > HALF_PI) { 199 azimuthalAngle = HALF_PI; 200 } else if (azimuthalAngle < -HALF_PI) { 201 azimuthalAngle = -HALF_PI; 202 } 203 this.rotation = new Vector3D(Vector3D.VectorType.RPA, this.rotation.getRadialDistance(), polarAngle, azimuthalAngle); 204 } 205 206 private Vector3D rotate(final Vector3D vec) { 207 return rotate(vec, 1); 208 } 209 210 /** 211 * Rotate a vector using the current rotation 212 * @param vec The vector to rotate 213 * @param rotationFactor Used to determine if using left hand rule or right hand rule (1 for RHR) 214 * @return A rotated vector 215 */ 216 private Vector3D rotate(final Vector3D vec, final int rotationFactor) { 217 // @formatting:off 218 /* Full rotation matrix for a yaw-pitch-roll 219 * yaw = alpha, pitch = beta, roll = gamma (typical representations) 220 * [cos(alpha), -sin(alpha), 0 ] [cos(beta), 0, sin(beta) ] [1, 0 , 0 ] [x] [x1] 221 * |sin(alpha), cos(alpha), 0 | . |0 , 1, 0 | . |0, cos(gamma), -sin(gamma)| . |y| = |y1| 222 * [0 , 0 , 1 ] [-sin(beta), 0, cos(beta)] [0, sin(gamma), cos(gamma) ] [z] [z1] 223 * which becomes 224 * x1 = y(cos(alpha)sin(beta)sin(gamma) - sin(alpha)cos(gamma)) + z(cos(alpha)sin(beta)cos(gamma) + sin(alpha)sin(gamma)) 225 * + x cos(alpha)cos(beta) 226 * y1 = y(sin(alpha)sin(beta)sin(gamma) + cos(alpha)cos(gamma)) + z(sin(alpha)sin(beta)cos(gamma) - cos(alpha)sin(gamma)) 227 * + x sin(alpha)cos(beta) 228 * z1 = y cos(beta)sin(gamma) + z cos(beta)cos(gamma) - x sin(beta) 229 */ 230 // @formatting:on 231 double vecX; 232 double vecY; 233 double vecZ; 234 // We only do pitch/roll (we specifically do not do roll -- this would lead to tilting the image) 235 // So yaw (alpha) -> azimuthalAngle, pitch (beta) -> polarAngle, roll (gamma) -> 0 (sin(gamma) -> 0, cos(gamma) -> 1) 236 // gamma is set here just to make it slightly easier to tilt images in the future -- we just have to set the gamma somewhere else. 237 // Ironically enough, the alpha (yaw) and gama (roll) got reversed somewhere. TODO figure out where and fix this. 238 final int gamma = 0; 239 final double sinAlpha = Math.sin(gamma); 240 final double cosAlpha = Math.cos(gamma); 241 final double cosGamma = this.rotation.getAzimuthalAngleCos(); 242 final double sinGamma = this.rotation.getAzimuthalAngleSin(); 243 final double cosBeta = this.rotation.getPolarAngleCos(); 244 final double sinBeta = this.rotation.getPolarAngleSin(); 245 final double x = vec.getX(); 246 final double y = YAW_DIRECTION * vec.getY(); 247 final double z = vec.getZ(); 248 vecX = y * (cosAlpha * sinBeta * sinGamma - sinAlpha * cosGamma) 249 + z * (cosAlpha * sinBeta * cosGamma + sinAlpha * sinGamma) + x * cosAlpha * cosBeta; 250 vecY = y * (sinAlpha * sinBeta * sinGamma + cosAlpha * cosGamma) 251 + z * (sinAlpha * sinBeta * cosGamma - cosAlpha * sinGamma) + x * sinAlpha * cosBeta; 252 vecZ = y * cosBeta * sinGamma + z * cosBeta * cosGamma - x * sinBeta; 253 return new Vector3D(vecX, YAW_DIRECTION * vecY, vecZ); 254 } 255 256 public void mapping(BufferedImage sourceImage, BufferedImage targetImage) { 257 DataBuffer sourceBuffer = sourceImage.getRaster().getDataBuffer(); 258 DataBuffer targetBuffer = targetImage.getRaster().getDataBuffer(); 259 // Faster mapping 260 if (sourceBuffer.getDataType() == DataBuffer.TYPE_INT && targetBuffer.getDataType() == DataBuffer.TYPE_INT) { 261 int[] sourceImageBuffer = ((DataBufferInt) sourceImage.getRaster().getDataBuffer()).getData(); 262 int[] targetImageBuffer = ((DataBufferInt) targetImage.getRaster().getDataBuffer()).getData(); 263 IntStream.range(0, targetImage.getHeight()).parallel() 264 .forEach(y -> IntStream.range(0, targetImage.getWidth()).forEach(x -> { 265 final Point2D.Double p = mapPoint(x, y); 266 int tx = (int) (p.x * (sourceImage.getWidth() - 1)); 267 int ty = (int) (p.y * (sourceImage.getHeight() - 1)); 268 int color = sourceImageBuffer[ty * sourceImage.getWidth() + tx]; 269 targetImageBuffer[y * targetImage.getWidth() + x] = color; 270 })); 271 } else if (sourceBuffer.getDataType() == DataBuffer.TYPE_DOUBLE && targetBuffer.getDataType() == DataBuffer.TYPE_DOUBLE) { 272 double[] sourceImageBuffer = ((DataBufferDouble) sourceImage.getRaster().getDataBuffer()).getData(); 273 double[] targetImageBuffer = ((DataBufferDouble) targetImage.getRaster().getDataBuffer()).getData(); 274 IntStream.range(0, targetImage.getHeight()).parallel() 275 .forEach(y -> IntStream.range(0, targetImage.getWidth()).forEach(x -> { 276 final Point2D.Double p = mapPoint(x, y); 277 int tx = (int) (p.x * (sourceImage.getWidth() - 1)); 278 int ty = (int) (p.y * (sourceImage.getHeight() - 1)); 279 double color = sourceImageBuffer[ty * sourceImage.getWidth() + tx]; 280 targetImageBuffer[y * targetImage.getWidth() + x] = color; 281 })); 282 } else { 283 IntStream.range(0, targetImage.getHeight()).parallel() 284 .forEach(y -> IntStream.range(0, targetImage.getWidth()).parallel().forEach(x -> { 285 final Point2D.Double p = mapPoint(x, y); 286 targetImage.setRGB(x, y, sourceImage.getRGB((int) (p.x * (sourceImage.getWidth() - 1)), 287 (int) (p.y * (sourceImage.getHeight() - 1)))); 288 })); 289 } 290 } 291 292 /** 293 * Map a real point to the displayed point. This method uses cached vectors. 294 * @param x The original x coordinate 295 * @param y The original y coordinate 296 * @return The scaled (0-1) point in the image. Use {@code p.x * (image.getWidth() - 1)} or {@code p.y * image.getHeight() - 1}. 297 */ 298 public final Point2D.Double mapPoint(final int x, final int y) { 299 final Vector3D vec = getVector3D(x, y); 300 return UVMapping.getTextureCoordinate(vec); 301 } 302 303 /** 304 * Map a real point to the displayed point. This function does not use cached vectors. 305 * @param x The original x coordinate 306 * @param y The original y coordinate 307 * @return The scaled (0-1) point in the image. Use {@code p.x * (image.getWidth() - 1)} or {@code p.y * image.getHeight() - 1}. 308 */ 309 public final Point2D.Double mapPoint(final double x, final double y) { 310 final Vector3D vec = getVector3D(x, y); 311 return UVMapping.getTextureCoordinate(vec); 312 } 313 } -
new file src/org/openstreetmap/josm/gui/util/imagery/UVMapping.java
diff --git a/src/org/openstreetmap/josm/gui/util/imagery/UVMapping.java b/src/org/openstreetmap/josm/gui/util/imagery/UVMapping.java new file mode 100644 index 0000000000..1a4c7ab064
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.util.imagery; 3 4 import java.awt.geom.Point2D; 5 6 /** 7 * A utility class for mapping a point onto a spherical coordinate system and vice versa 8 * @since xxx 9 */ 10 public final class UVMapping { 11 private static final double TWO_PI = 2 * Math.PI; 12 private UVMapping() { 13 // Private constructor to avoid instantiation 14 } 15 16 /** 17 * Returns the point of the texture image that is mapped to the given point in 3D space (given as {@link Vector3D}) 18 * See <a href="https://en.wikipedia.org/wiki/UV_mapping">the Wikipedia article on UV mapping</a>. 19 * 20 * @param vector the vector to which the texture point is mapped 21 * @return a point on the texture image somewhere in the rectangle between (0, 0) and (1, 1) 22 */ 23 public static Point2D.Double getTextureCoordinate(final Vector3D vector) { 24 final double u = 0.5 + (Math.atan2(vector.getX(), vector.getZ()) / TWO_PI); 25 final double v = 0.5 + (Math.asin(vector.getY()) / Math.PI); 26 return new Point2D.Double(u, v); 27 } 28 29 /** 30 * For a given point of the texture (i.e. the image), return the point in 3D space where the point 31 * of the texture is mapped to (as {@link Vector3D}). 32 * 33 * @param u x-coordinate of the point on the texture (in the range between 0 and 1, from left to right) 34 * @param v y-coordinate of the point on the texture (in the range between 0 and 1, from top to bottom) 35 * @return the vector from the origin to where the point of the texture is mapped on the sphere 36 */ 37 public static Vector3D getVector(final double u, final double v) { 38 if (u > 1 || u < 0 || v > 1 || v < 0) { 39 throw new IllegalArgumentException("u and v must be between or equal to 0 and 1"); 40 } 41 final double vectorY = Math.cos(v * Math.PI); 42 final double vectorYSquared = Math.pow(vectorY, 2); 43 return new Vector3D(-Math.sin(TWO_PI * u) * Math.sqrt(1 - vectorYSquared), -vectorY, 44 -Math.cos(TWO_PI * u) * Math.sqrt(1 - vectorYSquared)); 45 } 46 } -
new file src/org/openstreetmap/josm/gui/util/imagery/Vector3D.java
diff --git a/src/org/openstreetmap/josm/gui/util/imagery/Vector3D.java b/src/org/openstreetmap/josm/gui/util/imagery/Vector3D.java new file mode 100644 index 0000000000..ba98b7d884
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.util.imagery; 3 4 import javax.annotation.concurrent.Immutable; 5 6 /** 7 * A basic 3D vector class 8 * @author Taylor Smock (documentation, spherical conversions) 9 * @since xxx 10 */ 11 @Immutable 12 public final class Vector3D { 13 /** 14 * This determines how arguments are used in {@link Vector3D#Vector3D(VectorType, double, double, double)}. 15 */ 16 public enum VectorType { 17 /** Standard cartesian coordinates (x, y, z) */ 18 XYZ, 19 /** Physics (radial distance, polar angle, azimuthal angle) */ 20 RPA, 21 /** Mathematics (radial distance, azimuthal angle, polar angle) */ 22 RAP 23 } 24 25 /** A non-null default vector */ 26 public static final Vector3D DEFAULT_VECTOR_3D = new Vector3D(0, 0, 1); 27 28 private final double x; 29 private final double y; 30 private final double z; 31 /* The following are all lazily calculated, but should always be the same */ 32 /** The radius r */ 33 private volatile double radialDistance = Double.NaN; 34 /** The polar angle theta (inclination) */ 35 private volatile double polarAngle = Double.NaN; 36 /** Cosine of polar angle (angle from Z axis, AKA straight up) */ 37 private volatile double polarAngleCos = Double.NaN; 38 /** Sine of polar angle (angle from Z axis, AKA straight up) */ 39 private volatile double polarAngleSin = Double.NaN; 40 /** The azimuthal angle phi */ 41 private volatile double azimuthalAngle = Double.NaN; 42 /** Cosine of azimuthal angle (angle from X axis) */ 43 private volatile double azimuthalAngleCos = Double.NaN; 44 /** Sine of azimuthal angle (angle from X axis) */ 45 private volatile double azimuthalAngleSin = Double.NaN; 46 47 /** 48 * Create a new Vector3D object using the XYZ coordinate system 49 * 50 * @param x The x coordinate 51 * @param y The y coordinate 52 * @param z The z coordinate 53 */ 54 public Vector3D(double x, double y, double z) { 55 this(VectorType.XYZ, x, y, z); 56 } 57 58 /** 59 * Create a new Vector3D object. See ordering in {@link VectorType}. 60 * 61 * @param first The first coordinate 62 * @param second The second coordinate 63 * @param third The third coordinate 64 * @param vectorType The coordinate type (determines how the other variables are treated) 65 */ 66 public Vector3D(VectorType vectorType, double first, double second, double third) { 67 if (vectorType == VectorType.XYZ) { 68 this.x = first; 69 this.y = second; 70 this.z = third; 71 } else { 72 this.radialDistance = first; 73 if (vectorType == VectorType.RPA) { 74 this.azimuthalAngle = third; 75 this.polarAngle = second; 76 } else { 77 this.azimuthalAngle = second; 78 this.polarAngle = third; 79 } 80 // Since we have to run the calculations anyway, ensure they are cached. 81 this.x = this.radialDistance * this.getAzimuthalAngleCos() * this.getPolarAngleSin(); 82 this.y = this.radialDistance * this.getAzimuthalAngleSin() * this.getPolarAngleSin(); 83 this.z = this.radialDistance * this.getPolarAngleCos(); 84 } 85 } 86 87 /** 88 * Get the x coordinate 89 * 90 * @return The x coordinate 91 */ 92 public double getX() { 93 return x; 94 } 95 96 /** 97 * Get the y coordinate 98 * 99 * @return The y coordinate 100 */ 101 public double getY() { 102 return y; 103 } 104 105 /** 106 * Get the z coordinate 107 * 108 * @return The z coordinate 109 */ 110 public double getZ() { 111 return z; 112 } 113 114 /** 115 * Get the radius 116 * 117 * @return The radius 118 */ 119 public double getRadialDistance() { 120 if (Double.isNaN(this.radialDistance)) { 121 this.radialDistance = Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2) + Math.pow(this.z, 2)); 122 } 123 return this.radialDistance; 124 } 125 126 /** 127 * Get the polar angle (inclination) 128 * 129 * @return The polar angle 130 */ 131 public double getPolarAngle() { 132 if (Double.isNaN(this.polarAngle)) { 133 // This was Math.atan(x, z) in the Mapillary plugin 134 // This should be Math.atan(y, z) 135 this.polarAngle = Math.atan2(this.x, this.z); 136 } 137 return this.polarAngle; 138 } 139 140 /** 141 * Get the polar angle cossine (inclination) 142 * 143 * @return The polar angle cosine 144 */ 145 public double getPolarAngleCos() { 146 if (Double.isNaN(this.polarAngleCos)) { 147 this.polarAngleCos = Math.cos(this.getPolarAngle()); 148 } 149 return this.polarAngleCos; 150 } 151 152 /** 153 * Get the polar angle sine (inclination) 154 * 155 * @return The polar angle sine 156 */ 157 public double getPolarAngleSin() { 158 if (Double.isNaN(this.polarAngleSin)) { 159 this.polarAngleSin = Math.sin(this.getPolarAngle()); 160 } 161 return this.polarAngleSin; 162 } 163 164 /** 165 * Get the azimuthal angle 166 * 167 * @return The azimuthal angle 168 */ 169 public double getAzimuthalAngle() { 170 if (Double.isNaN(this.azimuthalAngle)) { 171 if (Double.isNaN(this.radialDistance)) { 172 // Force calculation 173 this.getRadialDistance(); 174 } 175 // Avoid issues where x, y, and z are 0 176 if (this.radialDistance == 0) { 177 this.azimuthalAngle = 0; 178 } else { 179 // This was Math.acos(y / radialDistance) in the Mapillary plugin 180 // This should be Math.acos(z / radialDistance) 181 this.azimuthalAngle = Math.acos(this.y / this.radialDistance); 182 } 183 } 184 return this.azimuthalAngle; 185 } 186 187 /** 188 * Get the azimuthal angle cosine 189 * 190 * @return The azimuthal angle cosine 191 */ 192 public double getAzimuthalAngleCos() { 193 if (Double.isNaN(this.azimuthalAngleCos)) { 194 this.azimuthalAngleCos = Math.cos(this.getAzimuthalAngle()); 195 } 196 return this.azimuthalAngleCos; 197 } 198 199 /** 200 * Get the azimuthal angle sine 201 * 202 * @return The azimuthal angle sine 203 */ 204 public double getAzimuthalAngleSin() { 205 if (Double.isNaN(this.azimuthalAngleSin)) { 206 this.azimuthalAngleSin = Math.sin(this.getAzimuthalAngle()); 207 } 208 return this.azimuthalAngleSin; 209 } 210 211 /** 212 * Normalize the vector 213 * 214 * @return A normalized vector 215 */ 216 public Vector3D normalize() { 217 final double length = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2)); 218 final double newX; 219 final double newY; 220 final double newZ; 221 if (length == 0 || Double.isNaN(length)) { 222 newX = 0; 223 newY = 0; 224 newZ = 0; 225 } else { 226 newX = x / length; 227 newY = y / length; 228 newZ = z / length; 229 } 230 return new Vector3D(newX, newY, newZ); 231 } 232 233 @Override 234 public int hashCode() { 235 return Double.hashCode(this.x) + 31 * Double.hashCode(this.y) + 31 * 31 * Double.hashCode(this.z); 236 } 237 238 @Override 239 public boolean equals(Object o) { 240 if (o instanceof Vector3D) { 241 Vector3D other = (Vector3D) o; 242 return this.x == other.x && this.y == other.y && this.z == other.z; 243 } 244 return false; 245 } 246 247 @Override 248 public String toString() { 249 return "[x=" + this.x + ", y=" + this.y + ", z=" + this.z + ", r=" + this.radialDistance + ", inclination=" 250 + this.polarAngle + ", azimuthal=" + this.azimuthalAngle + "]"; 251 } 252 } -
new file test/unit/org/openstreetmap/josm/gui/util/imagery/CameraPlaneTest.java
diff --git a/test/unit/org/openstreetmap/josm/gui/util/imagery/CameraPlaneTest.java b/test/unit/org/openstreetmap/josm/gui/util/imagery/CameraPlaneTest.java new file mode 100644 index 0000000000..9ba3970471
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.util.imagery; 3 4 import static org.junit.jupiter.api.Assertions.assertAll; 5 import static org.junit.jupiter.api.Assertions.assertEquals; 6 7 import java.awt.Point; 8 import java.awt.geom.Point2D; 9 import java.util.stream.Stream; 10 11 import org.junit.jupiter.api.BeforeEach; 12 import org.junit.jupiter.api.Test; 13 import org.junit.jupiter.params.ParameterizedTest; 14 import org.junit.jupiter.params.provider.Arguments; 15 import org.junit.jupiter.params.provider.MethodSource; 16 17 class CameraPlaneTest { 18 19 private static final int CAMERA_PLANE_WIDTH = 800; 20 private static final int CAMERA_PLANE_HEIGHT = 600; 21 22 private CameraPlane cameraPlane; 23 24 @BeforeEach 25 void setUp() { 26 this.cameraPlane = new CameraPlane(CAMERA_PLANE_WIDTH, CAMERA_PLANE_HEIGHT); 27 } 28 29 @Test 30 void testSetRotation() { 31 Vector3D vec = new Vector3D(0, 0, 1); 32 cameraPlane.setRotation(vec); 33 Vector3D out = cameraPlane.getRotation(); 34 assertAll(() -> assertEquals(280.0830152838839, out.getRadialDistance(), 0.001), 35 () -> assertEquals(0, out.getPolarAngle(), 0.001), () -> assertEquals(0, out.getAzimuthalAngle(), 0.001)); 36 } 37 38 @Test 39 void testGetVector3D() { 40 Vector3D vec = new Vector3D(0, 0, 1); 41 cameraPlane.setRotation(vec); 42 Vector3D out = cameraPlane.getVector3D(new Point(CAMERA_PLANE_WIDTH / 2, CAMERA_PLANE_HEIGHT / 2)); 43 assertAll(() -> assertEquals(0.0, out.getX(), 1.0E-04), () -> assertEquals(0.0, out.getY(), 1.0E-04), 44 () -> assertEquals(1.0, out.getZ(), 1.0E-04)); 45 } 46 47 static Stream<Arguments> testGetVector3DFloat() { 48 return Stream 49 .of(Arguments.of(new Vector3D(0, 0, 1), new Point(CAMERA_PLANE_WIDTH / 2, CAMERA_PLANE_HEIGHT / 2))); 50 } 51 52 /** 53 * This tests a method which does not cache, and more importantly, is what is used to create the sphere. 54 * The vector is normalized. 55 * (0, 0) is the center of the image 56 * 57 * @param expected The expected vector 58 * @param toCheck The point to check 59 */ 60 @ParameterizedTest 61 @MethodSource 62 void testGetVector3DFloat(final Vector3D expected, final Point toCheck) { 63 Vector3D out = cameraPlane.getVector3D(toCheck.getX(), toCheck.getY()); 64 assertAll(() -> assertEquals(expected.getX(), out.getX(), 1.0E-04), 65 () -> assertEquals(expected.getY(), out.getY(), 1.0E-04), 66 () -> assertEquals(expected.getZ(), out.getZ(), 1.0E-04), () -> assertEquals(1, 67 Math.sqrt(Math.pow(out.getX(), 2) + Math.pow(out.getY(), 2) + Math.pow(out.getZ(), 2)), 1.0E-04)); 68 } 69 70 @Test 71 void testMapping() { 72 Vector3D vec = new Vector3D(0, 0, 1); 73 cameraPlane.setRotation(vec); 74 Vector3D out = cameraPlane.getVector3D(new Point(300, 200)); 75 Point2D map = UVMapping.getTextureCoordinate(out); 76 assertAll(() -> assertEquals(0.44542099, map.getX(), 1e-8), () -> assertEquals(0.39674936, map.getY(), 1e-8)); 77 } 78 } -
new file test/unit/org/openstreetmap/josm/gui/util/imagery/UVMappingTest.java
diff --git a/test/unit/org/openstreetmap/josm/gui/util/imagery/UVMappingTest.java b/test/unit/org/openstreetmap/josm/gui/util/imagery/UVMappingTest.java new file mode 100644 index 0000000000..5cefd64834
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.util.imagery; 3 import static org.junit.jupiter.api.Assertions.assertAll; 4 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 5 import static org.junit.jupiter.api.Assertions.assertEquals; 6 import static org.junit.jupiter.api.Assertions.assertThrows; 7 8 import java.awt.geom.Point2D; 9 import java.util.stream.Stream; 10 11 import org.junit.jupiter.params.ParameterizedTest; 12 import org.junit.jupiter.params.provider.Arguments; 13 import org.junit.jupiter.params.provider.MethodSource; 14 import org.junit.jupiter.params.provider.ValueSource; 15 16 /** 17 * A test class for {@link UVMapping} 18 */ 19 class UVMappingTest { 20 private static final double DEFAULT_DELTA = 1e-5; 21 22 static Stream<Arguments> testMapping() { 23 return Stream.of(Arguments.of(0.5, 1, 0, 1, 0), 24 Arguments.of(0.5, 0, 0, -1, 0), 25 Arguments.of(0.25, 0.5, -1, 0, 0), 26 Arguments.of(0.5, 0.5, 0, 0, 1), 27 Arguments.of(0.75, 0.5, 1, 0, 0), 28 Arguments.of(1, 0.5, 0, 0, -1), 29 Arguments.of(0.125, 0.25, -0.5, -1 / Math.sqrt(2), -0.5), 30 Arguments.of(0.625, 0.75, 0.5, 1 / Math.sqrt(2), 0.5) 31 ); 32 } 33 34 /** 35 * Test that UV mapping is reversible for the sphere 36 * @param px The x for the point 37 * @param py The y for the point 38 * @param x The x portion of the vector 39 * @param y The y portion of the vector 40 * @param z The z portion of the vector 41 */ 42 @ParameterizedTest 43 @MethodSource 44 void testMapping(final double px, final double py, final double x, final double y, final double z) { 45 // The mapping must be reversible 46 assertAll(() -> assertPointEquals(new Point2D.Double(px, py), UVMapping.getTextureCoordinate(new Vector3D(x, y, z))), 47 () -> assertVectorEquals(new Vector3D(x, y, z), UVMapping.getVector(px, py))); 48 } 49 50 @ParameterizedTest 51 @ValueSource(floats = {0, 1, 1.1f, 0.9f}) 52 void testGetVectorEdgeCases(final float location) { 53 if (location < 0 || location > 1) { 54 assertAll(() -> assertThrows(IllegalArgumentException.class, () -> UVMapping.getVector(location, 0.5)), 55 () -> assertThrows(IllegalArgumentException.class, () -> UVMapping.getVector(0.5, location))); 56 } else { 57 assertAll(() -> assertDoesNotThrow(() -> UVMapping.getVector(location, 0.5)), 58 () -> assertDoesNotThrow(() -> UVMapping.getVector(0.5, location))); 59 } 60 } 61 62 private static void assertVectorEquals(final Vector3D expected, final Vector3D actual) { 63 final String message = String.format("Expected (%f %f %f), but was (%f %f %f)", expected.getX(), 64 expected.getY(), expected.getZ(), actual.getX(), actual.getY(), actual.getZ()); 65 assertEquals(expected.getX(), actual.getX(), DEFAULT_DELTA, message); 66 assertEquals(expected.getY(), actual.getY(), DEFAULT_DELTA, message); 67 assertEquals(expected.getZ(), actual.getZ(), DEFAULT_DELTA, message); 68 } 69 70 private static void assertPointEquals(final Point2D expected, final Point2D actual) { 71 final String message = String.format("Expected (%f, %f), but was (%f, %f)", expected.getX(), expected.getY(), 72 actual.getX(), actual.getY()); 73 assertEquals(expected.getX(), actual.getX(), DEFAULT_DELTA, message); 74 assertEquals(expected.getY(), actual.getY(), DEFAULT_DELTA, message); 75 } 76 } -
new file test/unit/org/openstreetmap/josm/gui/util/imagery/Vector3DTest.java
diff --git a/test/unit/org/openstreetmap/josm/gui/util/imagery/Vector3DTest.java b/test/unit/org/openstreetmap/josm/gui/util/imagery/Vector3DTest.java new file mode 100644 index 0000000000..5f360956fc
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.util.imagery; 3 4 import static org.junit.jupiter.api.Assertions.assertAll; 5 import static org.junit.jupiter.api.Assertions.assertEquals; 6 import static org.junit.jupiter.api.Assertions.fail; 7 8 import java.util.stream.Stream; 9 10 import org.junit.jupiter.api.Disabled; 11 import org.junit.jupiter.params.ParameterizedTest; 12 import org.junit.jupiter.params.provider.Arguments; 13 import org.junit.jupiter.params.provider.MethodSource; 14 15 /** 16 * Test class for {@link Vector3D} 17 * @author Taylor Smock 18 */ 19 class Vector3DTest { 20 21 static Stream<Arguments> vectorInformation() { 22 return Stream.of( 23 Arguments.of(0, 0, 0, 0), 24 Arguments.of(1, 1, 1, Math.sqrt(3)), 25 Arguments.of(-1, -1, -1, Math.sqrt(3)), 26 Arguments.of(-2, 2, -2, Math.sqrt(12)) 27 ); 28 } 29 30 @ParameterizedTest 31 @MethodSource("vectorInformation") 32 void getX(final double x, final double y, final double z) { 33 final Vector3D vector3D = new Vector3D(x, y, z); 34 assertEquals(x, vector3D.getX()); 35 } 36 37 @ParameterizedTest 38 @MethodSource("vectorInformation") 39 void getY(final double x, final double y, final double z) { 40 final Vector3D vector3D = new Vector3D(x, y, z); 41 assertEquals(y, vector3D.getY()); 42 } 43 44 @ParameterizedTest 45 @MethodSource("vectorInformation") 46 void getZ(final double x, final double y, final double z) { 47 final Vector3D vector3D = new Vector3D(x, y, z); 48 assertEquals(z, vector3D.getZ()); 49 } 50 51 @ParameterizedTest 52 @MethodSource("vectorInformation") 53 void getRadialDistance(final double x, final double y, final double z, final double radialDistance) { 54 final Vector3D vector3D = new Vector3D(x, y, z); 55 assertEquals(radialDistance, vector3D.getRadialDistance()); 56 } 57 58 @ParameterizedTest 59 @MethodSource("vectorInformation") 60 @Disabled("Angle calculations may be corrected") 61 void getPolarAngle() { 62 fail("Not yet implemented"); 63 } 64 65 @ParameterizedTest 66 @MethodSource("vectorInformation") 67 @Disabled("Angle calculations may be corrected") 68 void getAzimuthalAngle() { 69 fail("Not yet implemented"); 70 } 71 72 @ParameterizedTest 73 @MethodSource("vectorInformation") 74 void normalize(final double x, final double y, final double z) { 75 final Vector3D vector3D = new Vector3D(x, y, z); 76 final Vector3D normalizedVector = vector3D.normalize(); 77 assertAll(() -> assertEquals(vector3D.getRadialDistance() == 0 ? 0 : 1, normalizedVector.getRadialDistance()), 78 () -> assertEquals(vector3D.getPolarAngle(), normalizedVector.getPolarAngle()), 79 () -> assertEquals(vector3D.getAzimuthalAngle(), normalizedVector.getAzimuthalAngle())); 80 } 81 82 @ParameterizedTest 83 @MethodSource("vectorInformation") 84 @Disabled("Angle calculations may be corrected") 85 void testToString() { 86 fail("Not yet implemented"); 87 } 88 }