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