Ticket #16472: 16472.6.patch
File 16472.6.patch, 123.9 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..c5b8d2b5fb 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 477 484 return Objects.hash(height, width, isNewGpsData, 478 485 elevation, exifCoor, exifGpsTime, exifImgDir, exifOrientation, exifTime, 479 486 iptcCaption, iptcHeadline, iptcKeywords, iptcObjectName, 480 file, gpsTime, pos, speed, tmp );487 file, gpsTime, pos, speed, tmp, cameraProjection); 481 488 } 482 489 483 490 @Override … … public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType 504 511 && Objects.equals(gpsTime, other.gpsTime) 505 512 && Objects.equals(pos, other.pos) 506 513 && Objects.equals(speed, other.speed) 507 && Objects.equals(tmp, other.tmp); 514 && Objects.equals(tmp, other.tmp) 515 && cameraProjection == other.cameraProjection; 508 516 } 509 517 510 518 /** … … public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType 753 761 ifNotNull(ExifReader.readKeywords(dirIptc), this::setIptcKeywords); 754 762 ifNotNull(ExifReader.readObjectName(dirIptc), this::setIptcObjectName); 755 763 } 764 765 for (XmpDirectory xmpDirectory : metadata.getDirectoriesOfType(XmpDirectory.class)) { 766 Map<String, String> properties = xmpDirectory.getXmpProperties(); 767 final String projectionType = "GPano:ProjectionType"; 768 if (properties.containsKey(projectionType)) { 769 Stream.of(Projections.values()).filter(p -> p.name().equalsIgnoreCase(properties.get(projectionType))) 770 .findFirst().ifPresent(projection -> this.cameraProjection = projection); 771 break; 772 } 773 } 774 } 775 776 /** 777 * Reads the image represented by this entry in the given target dimension. 778 * @param target the desired dimension used for {@linkplain IIOParam#setSourceSubsampling subsampling} or {@code null} 779 * @return the read image, or {@code null} 780 * @throws IOException if any I/O error occurs 781 */ 782 public BufferedImage read(Dimension target) throws IOException { 783 throw new UnsupportedOperationException("read not implemented for " + this.getClass().getSimpleName()); 756 784 } 757 785 758 786 private static class NoMetadataReaderWarning extends Exception { … … public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType 767 795 } 768 796 } 769 797 798 /** 799 * Get the projection type for this entry 800 * @return The projection type 801 */ 802 public Projections getProjectionType() { 803 return this.cameraProjection; 804 } 805 770 806 /** 771 807 * Returns a {@link WayPoint} representation of this GPX image entry. 772 808 * @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/GeoImageLayer.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java b/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java index 0c5c8c29ae..55e5d98bc1 100644
a b public class GeoImageLayer extends AbstractModifiableLayer implements 867 867 868 868 @Override 869 869 public void jumpToNextMarker() { 870 data.se lectNextImage();870 data.setSelectedImage(data.getNextImage()); 871 871 } 872 872 873 873 @Override 874 874 public void jumpToPreviousMarker() { 875 data.se lectPreviousImage();875 data.setSelectedImage(data.getPreviousImage()); 876 876 } 877 877 878 878 /** -
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 abe21ec0a1..fa54f6b86e 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 import org.openstreetmap.josm.tools.Utils; 43 46 … … import org.openstreetmap.josm.tools.Utils; 49 52 */ 50 53 public class ImageDisplay extends JComponent implements Destroyable, PreferenceChangedListener, FilterChangeListener { 51 54 55 /** The current image viewer */ 56 private IImageViewer iImageViewer; 57 52 58 /** The file that is currently displayed */ 53 private I mageEntryentry;59 private IImageEntry<?> entry; 54 60 55 61 /** The previous file that is currently displayed. Cleared on paint. Only used to help improve UI error information. */ 56 private I mageEntryoldEntry;62 private IImageEntry<?> oldEntry; 57 63 58 64 /** The image currently displayed */ 59 65 private transient BufferedImage image; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 261 267 /** The thread that reads the images. */ 262 268 protected class LoadImageRunnable implements Runnable { 263 269 264 private final I mageEntryentry;270 private final IImageEntry<?> entry; 265 271 266 LoadImageRunnable(I mageEntryentry) {272 LoadImageRunnable(IImageEntry<?> entry) { 267 273 this.entry = entry; 268 274 } 269 275 … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 295 301 updateProcessedImage(); 296 302 // This will clear the loading info box 297 303 ImageDisplay.this.oldEntry = ImageDisplay.this.entry; 298 visibleRect = new VisRect(0, 0, width, height);304 visibleRect = getIImageViewer(entry).getDefaultVisibleRectangle(ImageDisplay.this, image); 299 305 300 306 selectedRect = null; 301 307 errorLoading = false; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 307 313 } 308 314 } 309 315 310 private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {316 private class ImgDisplayMouseListener extends MouseAdapter { 311 317 312 318 private MouseEvent lastMouseEvent; 313 319 private Point mousePointInImg; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 330 336 } 331 337 332 338 private void mouseWheelMovedImpl(int x, int y, int rotation, boolean refreshMousePointInImg) { 333 ImageEntry entry; 334 Image image; 335 VisRect visibleRect; 339 IImageEntry<?> currentEntry; 340 IImageViewer imageViewer; 341 Image currentImage; 342 VisRect currentVisibleRect; 336 343 337 344 synchronized (ImageDisplay.this) { 338 entry = ImageDisplay.this.entry; 339 image = ImageDisplay.this.image; 340 visibleRect = ImageDisplay.this.visibleRect; 345 currentEntry = ImageDisplay.this.entry; 346 currentImage = ImageDisplay.this.image; 347 currentVisibleRect = ImageDisplay.this.visibleRect; 348 imageViewer = ImageDisplay.this.iImageViewer; 341 349 } 342 350 343 351 selectedRect = null; 344 352 345 if ( image == null)353 if (currentImage == null) 346 354 return; 347 355 348 356 // Calculate the mouse cursor position in image coordinates to center the zoom. 349 357 if (refreshMousePointInImg) 350 mousePointInImg = comp2imgCoord( visibleRect, x, y, getSize());358 mousePointInImg = comp2imgCoord(currentVisibleRect, x, y, getSize()); 351 359 352 360 // Apply the zoom to the visible rectangle in image coordinates 353 361 if (rotation > 0) { 354 visibleRect.width = (int) (visibleRect.width * ZOOM_STEP.get());355 visibleRect.height = (int) (visibleRect.height * ZOOM_STEP.get());362 currentVisibleRect.width = (int) (currentVisibleRect.width * ZOOM_STEP.get()); 363 currentVisibleRect.height = (int) (currentVisibleRect.height * ZOOM_STEP.get()); 356 364 } else { 357 visibleRect.width = (int) (visibleRect.width / ZOOM_STEP.get());358 visibleRect.height = (int) (visibleRect.height / ZOOM_STEP.get());365 currentVisibleRect.width = (int) (currentVisibleRect.width / ZOOM_STEP.get()); 366 currentVisibleRect.height = (int) (currentVisibleRect.height / ZOOM_STEP.get()); 359 367 } 360 368 361 369 // Check that the zoom doesn't exceed MAX_ZOOM:1 362 if (visibleRect.width < getSize().width / MAX_ZOOM.get()) { 363 visibleRect.width = (int) (getSize().width / MAX_ZOOM.get()); 364 } 365 if (visibleRect.height < getSize().height / MAX_ZOOM.get()) { 366 visibleRect.height = (int) (getSize().height / MAX_ZOOM.get()); 367 } 370 ensureMaxZoom(currentVisibleRect); 368 371 369 // Set the same ratio for the visible rectangle and the display area 370 int hFact = visibleRect.height * getSize().width; 371 int wFact = visibleRect.width * getSize().height; 372 if (hFact > wFact) { 373 visibleRect.width = hFact / getSize().height; 372 // The size of the visible rectangle is limited by the image size or the viewer implementation. 373 if (imageViewer != null) { 374 imageViewer.checkAndModifyVisibleRectSize(currentImage, currentVisibleRect); 374 375 } else { 375 visibleRect.height = wFact / getSize().width;376 currentVisibleRect.checkRectSize(); 376 377 } 377 378 378 // The size of the visible rectangle is limited by the image size.379 visibleRect.checkRectSize();380 381 379 // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image. 382 Rectangle drawRect = calculateDrawImageRectangle( visibleRect, getSize());383 visibleRect.x = mousePointInImg.x + ((drawRect.x - x) * visibleRect.width) / drawRect.width;384 visibleRect.y = mousePointInImg.y + ((drawRect.y - y) * visibleRect.height) / drawRect.height;380 Rectangle drawRect = calculateDrawImageRectangle(currentVisibleRect, getSize()); 381 currentVisibleRect.x = mousePointInImg.x + ((drawRect.x - x) * currentVisibleRect.width) / drawRect.width; 382 currentVisibleRect.y = mousePointInImg.y + ((drawRect.y - y) * currentVisibleRect.height) / drawRect.height; 385 383 386 384 // The position is also limited by the image size 387 visibleRect.checkRectPos();385 currentVisibleRect.checkRectPos(); 388 386 389 387 synchronized (ImageDisplay.this) { 390 if (ImageDisplay.this.entry == entry) {391 ImageDisplay.this.visibleRect = visibleRect;388 if (ImageDisplay.this.entry == currentEntry) { 389 ImageDisplay.this.visibleRect = currentVisibleRect; 392 390 } 393 391 } 394 392 ImageDisplay.this.repaint(); … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 416 414 @Override 417 415 public void mouseClicked(MouseEvent e) { 418 416 // Move the center to the clicked point. 419 I mageEntry entry;420 Image image;421 VisRect visibleRect;417 IImageEntry<?> currentEntry; 418 Image currentImage; 419 VisRect currentVisibleRect; 422 420 423 421 synchronized (ImageDisplay.this) { 424 entry = ImageDisplay.this.entry;425 image = ImageDisplay.this.image;426 visibleRect = ImageDisplay.this.visibleRect;422 currentEntry = ImageDisplay.this.entry; 423 currentImage = ImageDisplay.this.image; 424 currentVisibleRect = ImageDisplay.this.visibleRect; 427 425 } 428 426 429 if ( image == null)427 if (currentImage == null) 430 428 return; 431 429 432 430 if (ZOOM_ON_CLICK.get()) { 433 431 // click notions are less coherent than wheel, refresh mousePointInImg on each click 434 432 lastMouseEvent = null; 435 433 436 if (mouseIsZoomSelecting(e) && !isAtMaxZoom( visibleRect)) {434 if (mouseIsZoomSelecting(e) && !isAtMaxZoom(currentVisibleRect)) { 437 435 // zoom in if clicked with the zoom button 438 436 mouseWheelMovedImpl(e.getX(), e.getY(), -1, true); 439 437 return; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 446 444 } 447 445 448 446 // Calculate the translation to set the clicked point the center of the view. 449 Point click = comp2imgCoord( visibleRect, e.getX(), e.getY(), getSize());450 Point center = getCenterImgCoord( visibleRect);447 Point click = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 448 Point center = getCenterImgCoord(currentVisibleRect); 451 449 452 visibleRect.x += click.x - center.x;453 visibleRect.y += click.y - center.y;450 currentVisibleRect.x += click.x - center.x; 451 currentVisibleRect.y += click.y - center.y; 454 452 455 visibleRect.checkRectPos();453 currentVisibleRect.checkRectPos(); 456 454 457 455 synchronized (ImageDisplay.this) { 458 if (ImageDisplay.this.entry == entry) {459 ImageDisplay.this.visibleRect = visibleRect;456 if (ImageDisplay.this.entry == currentEntry) { 457 ImageDisplay.this.visibleRect = currentVisibleRect; 460 458 } 461 459 } 462 460 ImageDisplay.this.repaint(); … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 466 464 * a picture part) */ 467 465 @Override 468 466 public void mousePressed(MouseEvent e) { 469 Image image;470 VisRect visibleRect;467 Image currentImage; 468 VisRect currentVisibleRect; 471 469 472 470 synchronized (ImageDisplay.this) { 473 image = ImageDisplay.this.image;474 visibleRect = ImageDisplay.this.visibleRect;471 currentImage = ImageDisplay.this.image; 472 currentVisibleRect = ImageDisplay.this.visibleRect; 475 473 } 476 474 477 if ( image == null)475 if (currentImage == null) 478 476 return; 479 477 480 478 selectedRect = null; 481 479 482 480 if (mouseIsDragging(e) || mouseIsZoomSelecting(e)) 483 mousePointInImg = comp2imgCoord( visibleRect, e.getX(), e.getY(), getSize());481 mousePointInImg = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 484 482 } 485 483 486 484 @Override … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 488 486 if (!mouseIsDragging(e) && !mouseIsZoomSelecting(e)) 489 487 return; 490 488 491 I mageEntry entry;492 Image image;493 VisRect visibleRect;489 IImageEntry<?> imageEntry; 490 Image currentImage; 491 VisRect currentVisibleRect; 494 492 495 493 synchronized (ImageDisplay.this) { 496 entry = ImageDisplay.this.entry;497 image = ImageDisplay.this.image;498 visibleRect = ImageDisplay.this.visibleRect;494 imageEntry = ImageDisplay.this.entry; 495 currentImage = ImageDisplay.this.image; 496 currentVisibleRect = ImageDisplay.this.visibleRect; 499 497 } 500 498 501 if ( image == null)499 if (currentImage == null) 502 500 return; 503 501 504 502 if (mouseIsDragging(e) && mousePointInImg != null) { 505 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize()); 506 visibleRect.isDragUpdate = true; 507 visibleRect.x += mousePointInImg.x - p.x; 508 visibleRect.y += mousePointInImg.y - p.y; 509 visibleRect.checkRectPos(); 503 Point p = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 504 getIImageViewer(entry).mouseDragged(this.mousePointInImg, p, currentVisibleRect); 505 currentVisibleRect.checkRectPos(); 510 506 synchronized (ImageDisplay.this) { 511 if (ImageDisplay.this.entry == entry) {512 ImageDisplay.this.visibleRect = visibleRect;507 if (ImageDisplay.this.entry == imageEntry) { 508 ImageDisplay.this.visibleRect = currentVisibleRect; 513 509 } 514 510 } 511 // We have to update the mousePointInImg for 360 image panning, as otherwise the panning 512 // never stops. 513 // This does not work well with the perspective viewer at this time (2021-08-26). 514 if (entry != null && Projections.EQUIRECTANGULAR == entry.getProjectionType()) { 515 this.mousePointInImg = p; 516 } 515 517 ImageDisplay.this.repaint(); 516 518 } 517 519 518 520 if (mouseIsZoomSelecting(e) && mousePointInImg != null) { 519 Point p = comp2imgCoord( visibleRect, e.getX(), e.getY(), getSize());520 visibleRect.checkPointInside(p);521 VisRect selectedRect = new VisRect(522 p.x < mousePointInImg.x ? p.x : mousePointInImg.x,523 p.y < mousePointInImg.y ? p.y : mousePointInImg.y,521 Point p = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 522 currentVisibleRect.checkPointInside(p); 523 VisRect selectedRectTemp = new VisRect( 524 Math.min(p.x, mousePointInImg.x), 525 Math.min(p.y, mousePointInImg.y), 524 526 p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x, 525 527 p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y, 526 visibleRect);527 selectedRect .checkRectSize();528 selectedRect .checkRectPos();529 ImageDisplay.this.selectedRect = selectedRect ;528 currentVisibleRect); 529 selectedRectTemp.checkRectSize(); 530 selectedRectTemp.checkRectPos(); 531 ImageDisplay.this.selectedRect = selectedRectTemp; 530 532 ImageDisplay.this.repaint(); 531 533 } 532 533 534 } 534 535 535 536 @Override 536 537 public void mouseReleased(MouseEvent e) { 537 I mageEntry entry;538 Image image;539 VisRect visibleRect;538 IImageEntry<?> currentEntry; 539 Image currentImage; 540 VisRect currentVisibleRect; 540 541 541 542 synchronized (ImageDisplay.this) { 542 entry = ImageDisplay.this.entry;543 image = ImageDisplay.this.image;544 visibleRect = ImageDisplay.this.visibleRect;543 currentEntry = ImageDisplay.this.entry; 544 currentImage = ImageDisplay.this.image; 545 currentVisibleRect = ImageDisplay.this.visibleRect; 545 546 } 546 547 547 if ( image == null)548 if (currentImage == null) 548 549 return; 549 550 550 551 if (mouseIsDragging(e)) { 551 visibleRect.isDragUpdate = false;552 currentVisibleRect.isDragUpdate = false; 552 553 } 553 554 554 555 if (mouseIsZoomSelecting(e) && selectedRect != null) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 556 557 int oldHeight = selectedRect.height; 557 558 558 559 // Check that the zoom doesn't exceed MAX_ZOOM:1 559 if (selectedRect.width < getSize().width / MAX_ZOOM.get()) { 560 selectedRect.width = (int) (getSize().width / MAX_ZOOM.get()); 561 } 562 if (selectedRect.height < getSize().height / MAX_ZOOM.get()) { 563 selectedRect.height = (int) (getSize().height / MAX_ZOOM.get()); 564 } 565 566 // Set the same ratio for the visible rectangle and the display area 567 int hFact = selectedRect.height * getSize().width; 568 int wFact = selectedRect.width * getSize().height; 569 if (hFact > wFact) { 570 selectedRect.width = hFact / getSize().height; 571 } else { 572 selectedRect.height = wFact / getSize().width; 573 } 560 ensureMaxZoom(selectedRect); 574 561 575 562 // Keep the center of the selection 576 563 if (selectedRect.width != oldWidth) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 585 572 } 586 573 587 574 synchronized (ImageDisplay.this) { 588 if ( entry == ImageDisplay.this.entry) {575 if (currentEntry == ImageDisplay.this.entry) { 589 576 if (selectedRect == null) { 590 ImageDisplay.this.visibleRect = visibleRect;577 ImageDisplay.this.visibleRect = currentVisibleRect; 591 578 } else { 592 579 ImageDisplay.this.visibleRect.setBounds(selectedRect); 593 580 selectedRect = null; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 596 583 } 597 584 ImageDisplay.this.repaint(); 598 585 } 599 600 @Override601 public void mouseEntered(MouseEvent e) {602 // Do nothing603 }604 605 @Override606 public void mouseExited(MouseEvent e) {607 // Do nothing608 }609 610 @Override611 public void mouseMoved(MouseEvent e) {612 // Do nothing613 }614 586 } 615 587 616 588 /** 617 589 * Constructs a new {@code ImageDisplay} with no image processor. 618 590 */ 619 591 public ImageDisplay() { 620 this(image -> image);592 this(imageObject -> imageObject); 621 593 } 622 594 623 595 /** … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 652 624 * Sets a new source image to be displayed by this {@code ImageDisplay}. 653 625 * @param entry new source image 654 626 * @return a {@link Future} representing pending completion of the image loading task 655 * @since 18150 627 * @since 18150 (xxx for IImageEntry) 656 628 */ 657 public Future<?> setImage(I mageEntryentry) {629 public Future<?> setImage(IImageEntry<?> entry) { 658 630 LoadImageRunnable runnable = setImage0(entry); 659 631 return runnable != null ? MainApplication.worker.submit(runnable) : null; 660 632 } 661 633 662 protected LoadImageRunnable setImage0(I mageEntryentry) {634 protected LoadImageRunnable setImage0(IImageEntry<?> entry) { 663 635 synchronized (this) { 664 636 this.oldEntry = this.entry; 665 637 this.entry = entry; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 707 679 708 680 private void updateProcessedImage() { 709 681 processedImage = image == null ? null : imageProcessor.process(image); 710 GuiHelper.runInEDT( () -> repaint());682 GuiHelper.runInEDT(this::repaint); 711 683 } 712 684 713 685 @Override 714 686 public void paintComponent(Graphics g) { 715 687 super.paintComponent(g); 716 688 717 ImageEntry entry; 718 ImageEntry oldEntry; 719 BufferedImage image; 720 VisRect visibleRect; 721 boolean errorLoading; 689 IImageEntry<?> currentEntry; 690 IImageEntry<?> currentOldEntry; 691 IImageViewer currentImageViewer; 692 BufferedImage currentImage; 693 VisRect currentVisibleRect; 694 boolean currentErrorLoading; 722 695 723 696 synchronized (this) { 724 image = this.processedImage;725 entry = this.entry;726 oldEntry = this.oldEntry;727 visibleRect = this.visibleRect;728 errorLoading = this.errorLoading;697 currentImage = this.processedImage; 698 currentEntry = this.entry; 699 currentOldEntry = this.oldEntry; 700 currentVisibleRect = this.visibleRect; 701 currentErrorLoading = this.errorLoading; 729 702 } 730 703 731 704 if (g instanceof Graphics2D) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 734 707 735 708 Dimension size = getSize(); 736 709 // Draw the image first, then draw error information 737 if (image != null && (entry != null || oldEntry != null)) { 738 Rectangle r = new Rectangle(visibleRect); 739 Rectangle target = calculateDrawImageRectangle(visibleRect, size); 740 741 g.drawImage(image, 742 target.x, target.y, target.x + target.width, target.y + target.height, 743 r.x, r.y, r.x + r.width, r.y + r.height, null); 744 745 if (selectedRect != null) { 746 Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y, size); 747 Point bottomRight = img2compCoord(visibleRect, 748 selectedRect.x + selectedRect.width, 749 selectedRect.y + selectedRect.height, size); 750 g.setColor(new Color(128, 128, 128, 180)); 751 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y); 752 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height); 753 g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height); 754 g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y); 755 g.setColor(Color.black); 756 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); 757 } 758 if (errorLoading && entry != null) { 759 String loadingStr = tr("Error on file {0}", entry.getDisplayName()); 710 if (currentImage != null && (currentEntry != null || currentOldEntry != null)) { 711 currentImageViewer = this.getIImageViewer(currentEntry); 712 Rectangle r = new Rectangle(currentVisibleRect); 713 Rectangle target = calculateDrawImageRectangle(currentVisibleRect, size); 714 715 currentImageViewer.paintImage(g, currentImage, target, r); 716 paintSelectedRect(g, target, currentVisibleRect, size); 717 if (currentErrorLoading && currentEntry != null) { 718 String loadingStr = tr("Error on file {0}", currentEntry.getDisplayName()); 760 719 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g); 761 g.drawString(loadingStr, 762 (int) ((size.width - noImageSize.getWidth()) / 2), 720 g.drawString(loadingStr, (int) ((size.width - noImageSize.getWidth()) / 2), 763 721 (int) ((size.height - noImageSize.getHeight()) / 2)); 764 722 } 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 } 723 paintOsdText(g); 793 724 } 725 paintErrorMessage(g, currentEntry, currentOldEntry, currentImage, currentErrorLoading, size); 726 } 727 728 /** 729 * Paint an error message 730 * @param g The graphics to paint on 731 * @param imageEntry The current image entry 732 * @param oldImageEntry The old image entry 733 * @param bufferedImage The image being painted 734 * @param currentErrorLoading If there was an error loading the image 735 * @param size The size of the component 736 */ 737 private void paintErrorMessage(Graphics g, IImageEntry<?> imageEntry, IImageEntry<?> oldImageEntry, 738 BufferedImage bufferedImage, boolean currentErrorLoading, Dimension size) { 794 739 final String errorMessage; 795 740 // If the new entry is null, then there is no image. 796 if ( entry == null) {741 if (imageEntry == null) { 797 742 if (emptyText == null) { 798 743 emptyText = tr("No image"); 799 744 } 800 745 errorMessage = emptyText; 801 } else if ( image == null || !Objects.equals(entry, oldEntry)) {746 } else if (bufferedImage == null || !Objects.equals(imageEntry, oldImageEntry)) { 802 747 // The image is not necessarily null when loading anymore. If the oldEntry is not the same as the new entry, 803 748 // we are probably still loading the image. (oldEntry gets set to entry when the image finishes loading). 804 if (! errorLoading) {805 errorMessage = tr("Loading {0}", entry.getDisplayName());749 if (!currentErrorLoading) { 750 errorMessage = tr("Loading {0}", imageEntry.getDisplayName()); 806 751 } else { 807 errorMessage = tr("Error on file {0}", entry.getDisplayName());752 errorMessage = tr("Error on file {0}", imageEntry.getDisplayName()); 808 753 } 809 754 } else { 810 755 errorMessage = null; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 830 775 } 831 776 } 832 777 778 /** 779 * Paint OSD text 780 * @param g The graphics to paint on 781 */ 782 private void paintOsdText(Graphics g) { 783 if (osdText != null) { 784 FontMetrics metrics = g.getFontMetrics(g.getFont()); 785 int ascent = metrics.getAscent(); 786 Color bkground = new Color(255, 255, 255, 128); 787 int lastPos = 0; 788 int pos = osdText.indexOf('\n'); 789 int x = 3; 790 int y = 3; 791 String line; 792 while (pos > 0) { 793 line = osdText.substring(lastPos, pos); 794 Rectangle2D lineSize = metrics.getStringBounds(line, g); 795 g.setColor(bkground); 796 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 797 g.setColor(Color.black); 798 g.drawString(line, x, y + ascent); 799 y += (int) lineSize.getHeight(); 800 lastPos = pos + 1; 801 pos = osdText.indexOf('\n', lastPos); 802 } 803 804 line = osdText.substring(lastPos); 805 Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g); 806 g.setColor(bkground); 807 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 808 g.setColor(Color.black); 809 g.drawString(line, x, y + ascent); 810 } 811 } 812 813 /** 814 * Paint the selected rectangle 815 * @param g The graphics to paint on 816 * @param target The target area (i.e., the selection) 817 * @param visibleRectTemp The current visible rect 818 * @param size The size of the component 819 */ 820 private void paintSelectedRect(Graphics g, Rectangle target, VisRect visibleRectTemp, Dimension size) { 821 if (selectedRect != null) { 822 Point topLeft = img2compCoord(visibleRectTemp, selectedRect.x, selectedRect.y, size); 823 Point bottomRight = img2compCoord(visibleRectTemp, 824 selectedRect.x + selectedRect.width, 825 selectedRect.y + selectedRect.height, size); 826 g.setColor(new Color(128, 128, 128, 180)); 827 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y); 828 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height); 829 g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height); 830 g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y); 831 g.setColor(Color.black); 832 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); 833 } 834 } 835 833 836 static Point img2compCoord(VisRect visibleRect, int xImg, int yImg, Dimension compSize) { 834 837 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize); 835 838 return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width, … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 853 856 visibleRect.y + visibleRect.height / 2); 854 857 } 855 858 859 /** 860 * calculateDrawImageRectangle 861 * 862 * @param visibleRect the part of the image that should be drawn (in image coordinates) 863 * @param compSize the part of the component where the image should be drawn (in component coordinates) 864 * @return the part of compRect with the same width/height ratio as the image 865 */ 856 866 static VisRect calculateDrawImageRectangle(VisRect visibleRect, Dimension compSize) { 857 867 return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, compSize.width, compSize.height)); 858 868 } … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 906 916 * the component size. 907 917 */ 908 918 public void zoomBestFitOrOne() { 909 I mageEntry entry;910 Image image;911 VisRect visibleRect;919 IImageEntry<?> currentEntry; 920 Image currentImage; 921 VisRect currentVisibleRect; 912 922 913 923 synchronized (this) { 914 entry = this.entry;915 image = this.image;916 visibleRect = this.visibleRect;924 currentEntry = this.entry; 925 currentImage = this.image; 926 currentVisibleRect = this.visibleRect; 917 927 } 918 928 919 if ( image == null)929 if (currentImage == null) 920 930 return; 921 931 922 if ( visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) {932 if (currentVisibleRect.width != currentImage.getWidth(null) || currentVisibleRect.height != currentImage.getHeight(null)) { 923 933 // The display is not at best fit. => Zoom to best fit 924 visibleRect.reset();934 currentVisibleRect.reset(); 925 935 } else { 926 936 // The display is at best fit => zoom to 1:1 927 Point center = getCenterImgCoord( visibleRect);928 visibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2,937 Point center = getCenterImgCoord(currentVisibleRect); 938 currentVisibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2, 929 939 getWidth(), getHeight()); 930 visibleRect.checkRectSize();931 visibleRect.checkRectPos();940 currentVisibleRect.checkRectSize(); 941 currentVisibleRect.checkRectPos(); 932 942 } 933 943 934 944 synchronized (this) { 935 if (this.entry == entry) {936 this.visibleRect = visibleRect;945 if (this.entry == currentEntry) { 946 this.visibleRect = currentVisibleRect; 937 947 } 938 948 } 939 949 repaint(); 940 950 } 951 952 /** 953 * Get the image viewer for an entry 954 * @param entry The entry to get the viewer for. May be {@code null}. 955 * @return The new image viewer, may be {@code null} 956 */ 957 private IImageViewer getIImageViewer(IImageEntry<?> entry) { 958 IImageViewer imageViewer; 959 IImageEntry<?> imageEntry; 960 synchronized (this) { 961 imageViewer = this.iImageViewer; 962 imageEntry = entry == null ? this.entry : entry; 963 } 964 if (imageEntry == null || (imageViewer != null && imageViewer.getSupportedProjections().contains(imageEntry.getProjectionType()))) { 965 return imageViewer; 966 } 967 try { 968 imageViewer = ImageProjectionRegistry.getViewer(imageEntry.getProjectionType()).getConstructor().newInstance(); 969 } catch (ReflectiveOperationException e) { 970 throw new JosmRuntimeException(e); 971 } 972 synchronized (this) { 973 if (imageEntry.equals(this.entry)) { 974 this.removeComponentListener(this.iImageViewer); 975 this.iImageViewer = imageViewer; 976 imageViewer.componentResized(new ComponentEvent(this, ComponentEvent.COMPONENT_RESIZED)); 977 this.addComponentListener(this.iImageViewer); 978 } 979 } 980 return imageViewer; 981 } 982 983 /** 984 * Ensure that a rectangle isn't zoomed in too much 985 * @param rectangle The rectangle to get (typically the visible area) 986 */ 987 private void ensureMaxZoom(final Rectangle rectangle) { 988 if (rectangle.width < getSize().width / MAX_ZOOM.get()) { 989 rectangle.width = (int) (getSize().width / MAX_ZOOM.get()); 990 } 991 if (rectangle.height < getSize().height / MAX_ZOOM.get()) { 992 rectangle.height = (int) (getSize().height / MAX_ZOOM.get()); 993 } 994 995 // Set the same ratio for the visible rectangle and the display area 996 int hFact = rectangle.height * getSize().width; 997 int wFact = rectangle.width * getSize().height; 998 if (hFact > wFact) { 999 rectangle.width = hFact / getSize().height; 1000 } else { 1001 rectangle.height = wFact / getSize().width; 1002 } 1003 } 1004 1005 /** 1006 * Update the visible rectangle (ensure zoom does not exceed specified values). 1007 * Specifically only visible for {@link IImageViewer} implementations. 1008 * @since xxx 1009 */ 1010 public void updateVisibleRectangle() { 1011 final VisRect currentVisibleRect; 1012 final Image mouseImage; 1013 final IImageViewer iImageViewer; 1014 synchronized (this) { 1015 currentVisibleRect = this.visibleRect; 1016 mouseImage = this.image; 1017 iImageViewer = this.getIImageViewer(this.entry); 1018 } 1019 if (mouseImage != null && currentVisibleRect != null && iImageViewer != null) { 1020 final Image maxImageSize = iImageViewer.getMaxImageSize(this, mouseImage); 1021 final VisRect maxVisibleRect = new VisRect(0, 0, maxImageSize.getWidth(null), maxImageSize.getHeight(null)); 1022 maxVisibleRect.setRect(currentVisibleRect); 1023 ensureMaxZoom(maxVisibleRect); 1024 1025 maxVisibleRect.checkRectSize(); 1026 synchronized (this) { 1027 this.visibleRect = maxVisibleRect; 1028 } 1029 } 1030 } 941 1031 } -
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..d5a94886b1 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} 141 196 * @return the read image, or {@code null} 142 197 * @throws IOException if any I/O error occurs 143 198 */ 199 @Override 144 200 public BufferedImage read(Dimension target) throws IOException { 145 201 URL imageUrl = getImageUrl(); 146 202 Logging.info(tr("Loading {0}", imageUrl)); -
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..72da2a3a31
- + 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); 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 /** 207 * Rotate a vector using the current rotation 208 * @param vec The vector to rotate 209 * @return A rotated vector 210 */ 211 private Vector3D rotate(final Vector3D vec) { 212 // @formatting:off 213 /* Full rotation matrix for a yaw-pitch-roll 214 * yaw = alpha, pitch = beta, roll = gamma (typical representations) 215 * [cos(alpha), -sin(alpha), 0 ] [cos(beta), 0, sin(beta) ] [1, 0 , 0 ] [x] [x1] 216 * |sin(alpha), cos(alpha), 0 | . |0 , 1, 0 | . |0, cos(gamma), -sin(gamma)| . |y| = |y1| 217 * [0 , 0 , 1 ] [-sin(beta), 0, cos(beta)] [0, sin(gamma), cos(gamma) ] [z] [z1] 218 * which becomes 219 * x1 = y(cos(alpha)sin(beta)sin(gamma) - sin(alpha)cos(gamma)) + z(cos(alpha)sin(beta)cos(gamma) + sin(alpha)sin(gamma)) 220 * + x cos(alpha)cos(beta) 221 * y1 = y(sin(alpha)sin(beta)sin(gamma) + cos(alpha)cos(gamma)) + z(sin(alpha)sin(beta)cos(gamma) - cos(alpha)sin(gamma)) 222 * + x sin(alpha)cos(beta) 223 * z1 = y cos(beta)sin(gamma) + z cos(beta)cos(gamma) - x sin(beta) 224 */ 225 // @formatting:on 226 double vecX; 227 double vecY; 228 double vecZ; 229 // We only do pitch/roll (we specifically do not do roll -- this would lead to tilting the image) 230 // So yaw (alpha) -> azimuthalAngle, pitch (beta) -> polarAngle, roll (gamma) -> 0 (sin(gamma) -> 0, cos(gamma) -> 1) 231 // 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. 232 // Ironically enough, the alpha (yaw) and gama (roll) got reversed somewhere. TODO figure out where and fix this. 233 final int gamma = 0; 234 final double sinAlpha = Math.sin(gamma); 235 final double cosAlpha = Math.cos(gamma); 236 final double cosGamma = this.rotation.getAzimuthalAngleCos(); 237 final double sinGamma = this.rotation.getAzimuthalAngleSin(); 238 final double cosBeta = this.rotation.getPolarAngleCos(); 239 final double sinBeta = this.rotation.getPolarAngleSin(); 240 final double x = vec.getX(); 241 final double y = YAW_DIRECTION * vec.getY(); 242 final double z = vec.getZ(); 243 vecX = y * (cosAlpha * sinBeta * sinGamma - sinAlpha * cosGamma) 244 + z * (cosAlpha * sinBeta * cosGamma + sinAlpha * sinGamma) + x * cosAlpha * cosBeta; 245 vecY = y * (sinAlpha * sinBeta * sinGamma + cosAlpha * cosGamma) 246 + z * (sinAlpha * sinBeta * cosGamma - cosAlpha * sinGamma) + x * sinAlpha * cosBeta; 247 vecZ = y * cosBeta * sinGamma + z * cosBeta * cosGamma - x * sinBeta; 248 return new Vector3D(vecX, YAW_DIRECTION * vecY, vecZ); 249 } 250 251 public void mapping(BufferedImage sourceImage, BufferedImage targetImage) { 252 DataBuffer sourceBuffer = sourceImage.getRaster().getDataBuffer(); 253 DataBuffer targetBuffer = targetImage.getRaster().getDataBuffer(); 254 // Faster mapping 255 if (sourceBuffer.getDataType() == DataBuffer.TYPE_INT && targetBuffer.getDataType() == DataBuffer.TYPE_INT) { 256 int[] sourceImageBuffer = ((DataBufferInt) sourceImage.getRaster().getDataBuffer()).getData(); 257 int[] targetImageBuffer = ((DataBufferInt) targetImage.getRaster().getDataBuffer()).getData(); 258 IntStream.range(0, targetImage.getHeight()).parallel() 259 .forEach(y -> IntStream.range(0, targetImage.getWidth()).forEach(x -> { 260 final Point2D.Double p = mapPoint(x, y); 261 int tx = (int) (p.x * (sourceImage.getWidth() - 1)); 262 int ty = (int) (p.y * (sourceImage.getHeight() - 1)); 263 int color = sourceImageBuffer[ty * sourceImage.getWidth() + tx]; 264 targetImageBuffer[y * targetImage.getWidth() + x] = color; 265 })); 266 } else if (sourceBuffer.getDataType() == DataBuffer.TYPE_DOUBLE && targetBuffer.getDataType() == DataBuffer.TYPE_DOUBLE) { 267 double[] sourceImageBuffer = ((DataBufferDouble) sourceImage.getRaster().getDataBuffer()).getData(); 268 double[] targetImageBuffer = ((DataBufferDouble) targetImage.getRaster().getDataBuffer()).getData(); 269 IntStream.range(0, targetImage.getHeight()).parallel() 270 .forEach(y -> IntStream.range(0, targetImage.getWidth()).forEach(x -> { 271 final Point2D.Double p = mapPoint(x, y); 272 int tx = (int) (p.x * (sourceImage.getWidth() - 1)); 273 int ty = (int) (p.y * (sourceImage.getHeight() - 1)); 274 double color = sourceImageBuffer[ty * sourceImage.getWidth() + tx]; 275 targetImageBuffer[y * targetImage.getWidth() + x] = color; 276 })); 277 } else { 278 IntStream.range(0, targetImage.getHeight()).parallel() 279 .forEach(y -> IntStream.range(0, targetImage.getWidth()).parallel().forEach(x -> { 280 final Point2D.Double p = mapPoint(x, y); 281 targetImage.setRGB(x, y, sourceImage.getRGB((int) (p.x * (sourceImage.getWidth() - 1)), 282 (int) (p.y * (sourceImage.getHeight() - 1)))); 283 })); 284 } 285 } 286 287 /** 288 * Map a real point to the displayed point. This method uses cached vectors. 289 * @param x The original x coordinate 290 * @param y The original y coordinate 291 * @return The scaled (0-1) point in the image. Use {@code p.x * (image.getWidth() - 1)} or {@code p.y * image.getHeight() - 1}. 292 */ 293 public final Point2D.Double mapPoint(final int x, final int y) { 294 final Vector3D vec = getVector3D(x, y); 295 return UVMapping.getTextureCoordinate(vec); 296 } 297 298 /** 299 * Map a real point to the displayed point. This function does not use cached vectors. 300 * @param x The original x coordinate 301 * @param y The original y coordinate 302 * @return The scaled (0-1) point in the image. Use {@code p.x * (image.getWidth() - 1)} or {@code p.y * image.getHeight() - 1}. 303 */ 304 public final Point2D.Double mapPoint(final double x, final double y) { 305 final Vector3D vec = getVector3D(x, y); 306 return UVMapping.getTextureCoordinate(vec); 307 } 308 } -
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..03e282a2cc
- + 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.Disabled; 13 import org.junit.jupiter.api.Test; 14 import org.junit.jupiter.params.ParameterizedTest; 15 import org.junit.jupiter.params.provider.Arguments; 16 import org.junit.jupiter.params.provider.MethodSource; 17 18 class CameraPlaneTest { 19 20 private static final int CAMERA_PLANE_WIDTH = 800; 21 private static final int CAMERA_PLANE_HEIGHT = 600; 22 23 private CameraPlane cameraPlane; 24 25 @BeforeEach 26 void setUp() { 27 this.cameraPlane = new CameraPlane(CAMERA_PLANE_WIDTH, CAMERA_PLANE_HEIGHT); 28 } 29 30 @Test 31 @Disabled("Currently broken") 32 void testSetRotation() { 33 Vector3D vec = new Vector3D(0, 0, 1); 34 cameraPlane.setRotation(vec); 35 Vector3D out = cameraPlane.getRotation(); 36 assertAll(() -> assertEquals(280.0830152838839, out.getRadialDistance(), 0.001), 37 () -> assertEquals(0, out.getPolarAngle(), 0.001), () -> assertEquals(0, out.getAzimuthalAngle(), 0.001)); 38 } 39 40 @Test 41 @Disabled("Currently broken") 42 void testGetVector3D() { 43 Vector3D vec = new Vector3D(0, 0, 1); 44 cameraPlane.setRotation(vec); 45 Vector3D out = cameraPlane.getVector3D(new Point(CAMERA_PLANE_WIDTH / 2, CAMERA_PLANE_HEIGHT / 2)); 46 assertAll(() -> assertEquals(0.0, out.getX(), 1.0E-04), () -> assertEquals(0.0, out.getY(), 1.0E-04), 47 () -> assertEquals(1.0, out.getZ(), 1.0E-04)); 48 } 49 50 static Stream<Arguments> testGetVector3DFloat() { 51 return Stream 52 .of(Arguments.of(new Vector3D(0, 0, 1), new Point(CAMERA_PLANE_WIDTH / 2, CAMERA_PLANE_HEIGHT / 2))); 53 } 54 55 /** 56 * This tests a method which does not cache, and more importantly, is what is used to create the sphere. 57 * The vector is normalized. 58 * (0, 0) is the center of the image 59 * 60 * @param expected The expected vector 61 * @param toCheck The point to check 62 */ 63 @ParameterizedTest 64 @MethodSource 65 void testGetVector3DFloat(final Vector3D expected, final Point toCheck) { 66 Vector3D out = cameraPlane.getVector3D(toCheck.getX(), toCheck.getY()); 67 assertAll(() -> assertEquals(expected.getX(), out.getX(), 1.0E-04), 68 () -> assertEquals(expected.getY(), out.getY(), 1.0E-04), 69 () -> assertEquals(expected.getZ(), out.getZ(), 1.0E-04), () -> assertEquals(1, 70 Math.sqrt(Math.pow(out.getX(), 2) + Math.pow(out.getY(), 2) + Math.pow(out.getZ(), 2)), 1.0E-04)); 71 } 72 73 @Test 74 @Disabled("Currently broken") 75 void testMapping() { 76 Vector3D vec = new Vector3D(0, 0, 1); 77 cameraPlane.setRotation(vec); 78 Vector3D out = cameraPlane.getVector3D(new Point(300, 200)); 79 Point2D map = UVMapping.getTextureCoordinate(out); 80 assertAll(() -> assertEquals(0.44542099, map.getX(), 1e-8), () -> assertEquals(0.39674936, map.getY(), 1e-8)); 81 } 82 } -
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 }