Ticket #21432: 21432.4.patch
File 21432.4.patch, 92.8 KB (added by , 3 years ago) |
---|
-
src/org/openstreetmap/josm/data/cache/BufferedImageCacheEntry.java
diff --git a/src/org/openstreetmap/josm/data/cache/BufferedImageCacheEntry.java b/src/org/openstreetmap/josm/data/cache/BufferedImageCacheEntry.java index 2e637a12d1..41fb48b2a9 100644
a b 2 2 package org.openstreetmap.josm.data.cache; 3 3 4 4 import java.awt.image.BufferedImage; 5 import java.awt.image.RenderedImage; 5 6 import java.io.ByteArrayInputStream; 6 7 import java.io.ByteArrayOutputStream; 7 8 import java.io.IOException; … … public class BufferedImageCacheEntry extends CacheEntry { 37 38 * @return a cache entry for the PNG encoded image 38 39 * @throws UncheckedIOException if an I/O error occurs 39 40 */ 40 public static BufferedImageCacheEntry pngEncoded( BufferedImage img) {41 public static BufferedImageCacheEntry pngEncoded(RenderedImage img) { 41 42 try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { 42 43 ImageIO.write(img, "png", output); 43 44 return new BufferedImageCacheEntry(output.toByteArray()); -
src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
diff --git a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java index f8603239a9..7512641aa9 100644
a b public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements 99 99 private boolean force; 100 100 private final long minimumExpiryTime; 101 101 102 /** 103 * Get the deduplication string for this job 104 * @return The string used for deduplication 105 * @throws IOException See {@link #getUrl()} 106 */ 107 private String getDeduplicationString() throws IOException { 108 // getCacheKey is useful for where the same url might return different items 109 return this.getUrl().toString() + '/' + getCacheKey(); 110 } 111 102 112 /** 103 113 * @param cache cache instance that we will work on 104 114 * @param options options of the request … … public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements 150 160 String deduplicationKey = null; 151 161 if (url != null) { 152 162 // url might be null, for example when Bing Attribution is not loaded yet 153 deduplicationKey = url.toString();163 deduplicationKey = this.getDeduplicationString(); 154 164 } 155 165 if (deduplicationKey == null) { 156 166 Logging.warn("No url returned for: {0}, skipping", getCacheKey()); … … public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements 252 262 private void finishLoading(LoadResult result) { 253 263 Set<ICachedLoaderListener> listeners; 254 264 try { 255 listeners = inProgress.remove( getUrl().toString());265 listeners = inProgress.remove(this.getDeduplicationString()); 256 266 } catch (IOException e) { 257 267 listeners = null; 258 268 Logging.trace(e); … … public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements 319 329 if (!file.exists()) { 320 330 file = new File(fileName.substring("file://".length() - 1)); 321 331 } 322 try (InputStream fileInputStream = Files.newInputStream(file.toPath())){323 cacheData = createCacheEntry( Utils.readBytesFromStream(fileInputStream));332 try { 333 cacheData = createCacheEntry(this.loadObjectBytes(file)); 324 334 cache.put(getCacheKey(), cacheData, attributes); 325 335 return true; 326 336 } catch (IOException e) { … … public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements 331 341 return false; 332 342 } 333 343 344 /** 345 * Load bytes from a file. This is overridable to allow for sparse loading of bytes 346 * @param file The file to load 347 * @return The file bytes 348 * @throws IOException If there is an issue reading the file 349 */ 350 protected byte[] loadObjectBytes(File file) throws IOException { 351 try (InputStream fileInputStream = Files.newInputStream(file.toPath())) { 352 return Utils.readBytesFromStream(fileInputStream); 353 } 354 } 355 334 356 /** 335 357 * @return true if object was successfully downloaded via http, false, if there was a loading failure 336 358 */ -
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 418fcfd520..6c5a91dee8 100644
a b import java.awt.image.BufferedImage; 21 21 import java.io.IOException; 22 22 import java.util.Objects; 23 23 import java.util.concurrent.Future; 24 import java.util.concurrent.atomic.AtomicInteger; 24 25 25 26 import javax.swing.JComponent; 26 27 import javax.swing.SwingUtilities; … … import org.openstreetmap.josm.gui.MainApplication; 34 35 import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable; 35 36 import org.openstreetmap.josm.gui.layer.geoimage.viewers.projections.IImageViewer; 36 37 import org.openstreetmap.josm.gui.layer.geoimage.viewers.projections.ImageProjectionRegistry; 38 import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling; 37 39 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings; 38 40 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener; 39 41 import org.openstreetmap.josm.gui.util.GuiHelper; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 79 81 private boolean errorLoading; 80 82 81 83 /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated 82 * each time the zoom is modified */84 * each time the zoom is modified. Note: This is in the current zoom coordinates, if those change. */ 83 85 private VisRect visibleRect; 84 86 85 87 /** When a selection is done, the rectangle of the selection (in image coordinates) */ … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 87 89 88 90 private final ImgDisplayMouseListener imgMouseListener = new ImgDisplayMouseListener(); 89 91 92 private final AtomicInteger zoom = new AtomicInteger(12); 93 90 94 private String emptyText; 91 95 private String osdText; 92 96 … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 213 217 } 214 218 215 219 public void checkRectPos() { 220 this.checkRectPos(null, 0); 221 } 222 223 /** 224 * Ensure that the rectangle is within bounds 225 * @param imageEntry The current image entry -- if it is a tiling entry, different constraints are needed 226 * @param zoom The current zoom level (only used if tiling) 227 */ 228 public void checkRectPos(final IImageEntry<?> imageEntry, final int zoom) { 229 final int useWidth; 230 final int useHeight; 231 if (imageEntry instanceof IImageTiling) { 232 final IImageTiling<?> tiler = (IImageTiling<?>) imageEntry; 233 useHeight = tiler.getHeight(zoom); 234 useWidth = tiler.getWidth(zoom); 235 } else { 236 useWidth = init.width; 237 useHeight = init.height; 238 } 216 239 if (x < 0) { 217 240 x = 0; 218 241 } 219 242 if (y < 0) { 220 243 y = 0; 221 244 } 222 if ( x + width > init.width) {223 x = init.width - width;245 if (width > useWidth) { 246 width = useWidth; 224 247 } 225 if (y + height > init.height) { 226 y = init.height - height; 248 if (height > useHeight) { 249 height = useHeight; 250 } 251 if (x + width > useWidth) { 252 x = useWidth - width; 253 } 254 if (y + height > useHeight) { 255 y = useHeight - height; 227 256 } 228 257 } 229 258 230 259 public void checkRectSize() { 231 if (width > init.width) { 232 width = init.width; 260 this.checkRectSize(null, 0); 261 } 262 263 /** 264 * Ensure that the rectangle is the appropriate size 265 * @param imageEntry The current image entry -- if it is a tiling entry, different constraints are needed 266 * @param zoom The current zoom level (only used if tiling) 267 */ 268 public void checkRectSize(final IImageEntry<?> imageEntry, final int zoom) { 269 final int useWidth; 270 final int useHeight; 271 if (imageEntry instanceof IImageTiling) { 272 useWidth = ((IImageTiling<?>) imageEntry).getWidth(zoom); 273 useHeight = ((IImageTiling<?>) imageEntry).getHeight(zoom); 274 } else { 275 useWidth = init.width; 276 useHeight = init.height; 277 } 278 if (width > useWidth) { 279 width = useWidth; 233 280 } 234 if (height > init.height) {235 height = init.height;281 if (height > useHeight) { 282 height = useHeight; 236 283 } 237 284 } 238 285 … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 265 312 VisRect other = (VisRect) obj; 266 313 return Objects.equals(init, other.init); 267 314 } 315 316 /** 317 * Create an equivalent rectangle in image coordinates 318 * @param size The component size 319 * @param visibleRect The visible rectangle 320 * @param imageEntry The current image entry 321 * @param zoom The current zoom 322 * @return A copy of this rectangle in image coordinates 323 */ 324 VisRect createImageCoordinateRectangle(Dimension size, VisRect visibleRect, IImageEntry<?> imageEntry, int zoom) { 325 final Point imageUpperLeft = comp2imgCoord(imageEntry, visibleRect, this.x, 326 this.y, size, zoom); 327 final Point imageLowerRight = comp2imgCoord(imageEntry, visibleRect, 328 this.x + this.width, this.y + this.height, size, zoom); 329 return new VisRect(imageUpperLeft.x, imageUpperLeft.y, imageLowerRight.x - imageUpperLeft.x, imageLowerRight.y - imageUpperLeft.y); 330 } 331 332 /** 333 * Create an equivalent rectangle in component coordinates 334 * @param size The component size 335 * @param visibleRect The visible rectangle 336 * @param imageEntry The current image entry 337 * @param zoom The current zoom 338 * @return A copy of this rectangle in component coordinates 339 */ 340 VisRect createCompCoordinateRectangle(Dimension size, VisRect visibleRect, IImageEntry<?> imageEntry, int zoom) { 341 final Point compUpperLeft = img2compCoord(imageEntry, visibleRect, this.x, this.y, size, zoom); 342 final Point compLowerRight = img2compCoord(imageEntry, visibleRect, this.x + this.width, this.y + this.height, size, zoom); 343 return new VisRect(compUpperLeft.x, compUpperLeft.y, compLowerRight.x - compUpperLeft.x, compLowerRight.y - compUpperLeft.y); 344 } 268 345 } 269 346 270 347 /** The thread that reads the images. */ … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 289 366 } 290 367 } 291 368 292 int width = img.getWidth(); 293 int height = img.getHeight(); 294 entry.setWidth(width); 295 entry.setHeight(height); 369 // Only set width/height if the entry is not something that can be tiled 370 // Tiling *requires* knowledge of the actual width/height of the image. 371 if (!(entry instanceof IImageTiling)) { 372 int width = img.getWidth(); 373 int height = img.getHeight(); 374 entry.setWidth(width); 375 entry.setHeight(height); 376 } 296 377 297 378 synchronized (ImageDisplay.this) { 298 379 if (this.entry != ImageDisplay.this.entry) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 301 382 } 302 383 303 384 ImageDisplay.this.image = img; 304 updateProcessedImage();305 385 // This will clear the loading info box 306 386 ImageDisplay.this.oldEntry = ImageDisplay.this.entry; 307 visibleRect = getIImageViewer(entry).getDefaultVisibleRectangle(ImageDisplay.this, image); 387 visibleRect = getIImageViewer(entry).getDefaultVisibleRectangle(ImageDisplay.this, image, this.entry); 388 // Update the visible rectangle 389 ImageDisplay.this.updateVisibleRectangle(); 390 // Update the processed image *after* updating the visible rect -- otherwise, we may try to load a large image fully (tiled) 391 ImageDisplay.this.updateProcessedImage(); 308 392 309 393 selectedRect = null; 310 394 errorLoading = false; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 319 403 private class ImgDisplayMouseListener extends MouseAdapter { 320 404 321 405 private MouseEvent lastMouseEvent; 406 /** The mouse point in image coordinates in the image */ 322 407 private Point mousePointInImg; 323 408 324 409 private boolean mouseIsDragging(MouseEvent e) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 358 443 359 444 // Calculate the mouse cursor position in image coordinates to center the zoom. 360 445 if (refreshMousePointInImg) 361 mousePointInImg = comp2imgCoord(currentVisibleRect, x, y, getSize());446 this.mousePointInImg = comp2imgCoord(currentEntry, currentVisibleRect, x, y, getSize(), zoom.get()); 362 447 363 448 // Apply the zoom to the visible rectangle in image coordinates 449 final int oldZoom = ImageDisplay.this.zoom.get(); 450 final double step; 364 451 if (rotation > 0) { 365 currentVisibleRect.width = (int) (currentVisibleRect.width * ZOOM_STEP.get());366 currentVisibleRect.height = (int) (currentVisibleRect.height * ZOOM_STEP.get());452 final int currentZoom = ImageDisplay.this.zoom.decrementAndGet(); 453 step = currentEntry instanceof IImageTiling ? Math.pow(2, oldZoom - currentZoom) : ZOOM_STEP.get(); 367 454 } else { 368 currentVisibleRect.width = (int) (currentVisibleRect.width / ZOOM_STEP.get()); 369 currentVisibleRect.height = (int) (currentVisibleRect.height / ZOOM_STEP.get()); 455 final int currentZoom = ImageDisplay.this.zoom.incrementAndGet(); 456 step = currentEntry instanceof IImageTiling ? Math.pow(2, oldZoom - currentZoom) : 1 / ZOOM_STEP.get(); 457 } 458 final VisRect oldVisibleRectImageCoordinates = currentVisibleRect.createImageCoordinateRectangle(getSize(), 459 currentVisibleRect, currentEntry, oldZoom); 460 // This ensures that zoom is not out of bounds 461 ensureMaxZoom(currentVisibleRect); 462 final int currentZoom = ImageDisplay.this.zoom.get(); 463 // If the zoom doesn't change, stop 464 if (currentZoom == oldZoom) { 465 return; 370 466 } 467 currentVisibleRect.width = (int) (currentVisibleRect.width * step); 468 currentVisibleRect.height = (int) (currentVisibleRect.height * step); 371 469 372 470 // Check that the zoom doesn't exceed MAX_ZOOM:1 373 ensureMaxZoom(currentVisibleRect); 471 if (!(currentEntry instanceof IImageTiling) || currentZoom > ((IImageTiling<?>) currentEntry).getMaxZoom()) { 472 ensureMaxZoom(currentVisibleRect); 473 } 374 474 375 475 // The size of the visible rectangle is limited by the image size or the viewer implementation. 476 // It can also be influenced by whether or not the current entry allows tiling. Tiling image implementations 477 // don't care what the size of the image buffer is. In fact, the image buffer can be the size of the window. 478 // So the image buffer really only defines the scale. 376 479 if (imageViewer != null) { 377 imageViewer.checkAndModifyVisibleRectSize(currentImage, current VisibleRect);480 imageViewer.checkAndModifyVisibleRectSize(currentImage, currentEntry, currentVisibleRect); 378 481 } else { 379 currentVisibleRect.checkRectSize( );482 currentVisibleRect.checkRectSize(currentEntry, currentZoom); 380 483 } 381 484 382 485 // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image. 383 Rectangle drawRect = calculateDrawImageRectangle(currentVisibleRect, getSize()); 384 currentVisibleRect.x = mousePointInImg.x + ((drawRect.x - x) * currentVisibleRect.width) / drawRect.width; 385 currentVisibleRect.y = mousePointInImg.y + ((drawRect.y - y) * currentVisibleRect.height) / drawRect.height; 486 if (currentEntry instanceof IImageTiling) { 487 final Point pointToUse = mousePointInImg; 488 // Create an equivalent visible rectangle, just in image coordinates instead of image zoom coordinates 489 final VisRect imageVisRect = currentVisibleRect.createImageCoordinateRectangle(getSize(), 490 oldVisibleRectImageCoordinates, currentEntry, oldZoom); 491 imageVisRect.x = pointToUse.x + 492 ((oldVisibleRectImageCoordinates.x - pointToUse.x) * imageVisRect.width) /oldVisibleRectImageCoordinates.width; 493 imageVisRect.y = pointToUse.y + 494 ((oldVisibleRectImageCoordinates.y - pointToUse.y) * imageVisRect.height) / oldVisibleRectImageCoordinates.height; 495 // Convert the equivalent visible rectangle back to image zoom coordinates 496 currentVisibleRect = imageVisRect.createCompCoordinateRectangle(getSize(), currentVisibleRect, currentEntry, currentZoom); 497 } else { 498 final VisRect drawRect = calculateDrawImageRectangle(currentVisibleRect, getSize()); 499 currentVisibleRect.x = mousePointInImg.x + ((drawRect.x - x) * currentVisibleRect.width) / drawRect.width; 500 currentVisibleRect.y = mousePointInImg.y + ((drawRect.y - y) * currentVisibleRect.height) / drawRect.height; 501 } 386 502 387 503 // The position is also limited by the image size 388 currentVisibleRect.checkRectPos( );504 currentVisibleRect.checkRectPos(currentEntry, currentZoom); 389 505 390 506 synchronized (ImageDisplay.this) { 391 507 if (ImageDisplay.this.entry == currentEntry) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 447 563 } 448 564 449 565 // Calculate the translation to set the clicked point the center of the view. 450 Point click = comp2imgCoord(current VisibleRect, e.getX(), e.getY(), getSize());566 Point click = comp2imgCoord(currentEntry, currentVisibleRect, e.getX(), e.getY(), getSize(), zoom.get()); 451 567 Point center = getCenterImgCoord(currentVisibleRect); 452 568 453 569 currentVisibleRect.x += click.x - center.x; 454 570 currentVisibleRect.y += click.y - center.y; 455 571 456 currentVisibleRect.checkRectPos( );572 currentVisibleRect.checkRectPos(currentEntry, ImageDisplay.this.zoom.get()); 457 573 458 574 synchronized (ImageDisplay.this) { 459 575 if (ImageDisplay.this.entry == currentEntry) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 467 583 * a picture part) */ 468 584 @Override 469 585 public void mousePressed(MouseEvent e) { 470 Image currentImage; 471 VisRect currentVisibleRect; 586 final Image currentImage; 587 final VisRect currentVisibleRect; 588 final IImageEntry<?> imageEntry; 472 589 473 590 synchronized (ImageDisplay.this) { 474 591 currentImage = ImageDisplay.this.image; 475 592 currentVisibleRect = ImageDisplay.this.visibleRect; 593 imageEntry = ImageDisplay.this.entry; 476 594 } 477 595 478 596 if (currentImage == null) … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 481 599 selectedRect = null; 482 600 483 601 if (mouseIsDragging(e) || mouseIsZoomSelecting(e)) 484 mousePointInImg = comp2imgCoord( currentVisibleRect, e.getX(), e.getY(), getSize());602 mousePointInImg = comp2imgCoord(imageEntry, currentVisibleRect, e.getX(), e.getY(), getSize(), zoom.get()); 485 603 } 486 604 487 605 @Override … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 503 621 return; 504 622 505 623 if (mouseIsDragging(e) && mousePointInImg != null) { 506 Point p = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 507 getIImageViewer(entry).mouseDragged(this.mousePointInImg, p, currentVisibleRect); 508 currentVisibleRect.checkRectPos(); 624 Point p = comp2imgCoord(imageEntry, currentVisibleRect, e.getX(), e.getY(), getSize(), zoom.get()); 625 // Create an equivalent visible rectangle, just in image coordinates instead of image zoom coordinates 626 final VisRect imageVisRect = currentVisibleRect.createImageCoordinateRectangle(getSize(), currentVisibleRect, 627 imageEntry, zoom.get()); 628 getIImageViewer(entry).mouseDragged(this.mousePointInImg, p, imageVisRect); 629 // Convert the equivalent visible rectangle back to image zoom coordinates 630 currentVisibleRect = imageVisRect.createCompCoordinateRectangle(getSize(), currentVisibleRect, imageEntry, zoom.get()); 631 currentVisibleRect.checkRectPos(imageEntry, ImageDisplay.this.zoom.get()); 509 632 synchronized (ImageDisplay.this) { 510 633 if (ImageDisplay.this.entry == imageEntry) { 511 634 ImageDisplay.this.visibleRect = currentVisibleRect; … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 525 648 } 526 649 527 650 if (mouseIsZoomSelecting(e) && mousePointInImg != null) { 528 Point p = comp2imgCoord( currentVisibleRect, e.getX(), e.getY(), getSize());651 Point p = comp2imgCoord(imageEntry, currentVisibleRect, e.getX(), e.getY(), getSize(), zoom.get()); 529 652 currentVisibleRect.checkPointInside(p); 530 653 VisRect selectedRectTemp = new VisRect( 531 654 Math.min(p.x, mousePointInImg.x), … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 533 656 p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x, 534 657 p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y, 535 658 currentVisibleRect); 536 selectedRectTemp.checkRectSize( );537 selectedRectTemp.checkRectPos( );659 selectedRectTemp.checkRectSize(imageEntry, zoom.get()); 660 selectedRectTemp.checkRectPos(imageEntry, zoom.get()); 538 661 ImageDisplay.this.selectedRect = selectedRectTemp; 539 662 ImageDisplay.this.repaint(); 540 663 } … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 574 697 selectedRect.y -= (selectedRect.height - oldHeight) / 2; 575 698 } 576 699 577 selectedRect.checkRectSize( );578 selectedRect.checkRectPos( );700 selectedRect.checkRectSize(currentEntry, zoom.get()); 701 selectedRect.checkRectPos(currentEntry, zoom.get()); 579 702 } 580 703 581 704 synchronized (ImageDisplay.this) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 642 765 synchronized (this) { 643 766 this.oldEntry = this.entry; 644 767 this.entry = entry; 768 if (entry instanceof IImageTiling) { 769 this.zoom.set(((IImageTiling<?>) entry).getMinZoom() + 1); 770 } 645 771 if (entry == null) { 646 772 image = null; 647 773 updateProcessedImage(); … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 719 845 Rectangle r = new Rectangle(currentVisibleRect); 720 846 Rectangle target = calculateDrawImageRectangle(currentVisibleRect, size); 721 847 722 currentImageViewer.paintImage(g, currentImage, target, r); 848 if (currentEntry instanceof IImageTiling && ((IImageTiling<?>) currentEntry).isTilingEnabled()) { 849 currentImageViewer.paintTiledImage(g, (IImageTiling<?>) currentEntry, target, r, zoom.get(), this); 850 } else { 851 currentImageViewer.paintImage(g, currentImage, target, r); 852 } 723 853 paintSelectedRect(g, target, currentVisibleRect, size); 724 854 if (currentErrorLoading && currentEntry != null) { 725 855 String loadingStr = tr("Error on file {0}", currentEntry.getDisplayName()); … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 826 956 */ 827 957 private void paintSelectedRect(Graphics g, Rectangle target, VisRect visibleRectTemp, Dimension size) { 828 958 if (selectedRect != null) { 829 Point topLeft = img2compCoord( visibleRectTemp, selectedRect.x, selectedRect.y, size);830 Point bottomRight = img2compCoord( visibleRectTemp,959 Point topLeft = img2compCoord(entry, visibleRectTemp, selectedRect.x, selectedRect.y, size, zoom.get()); 960 Point bottomRight = img2compCoord(entry, visibleRectTemp, 831 961 selectedRect.x + selectedRect.width, 832 selectedRect.y + selectedRect.height, size );962 selectedRect.y + selectedRect.height, size, zoom.get()); 833 963 g.setColor(new Color(128, 128, 128, 180)); 834 964 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y); 835 965 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height); … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 840 970 } 841 971 } 842 972 843 static Point img2compCoord(VisRect visibleRect, int xImg, int yImg, Dimension compSize) { 844 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize); 973 /** 974 * Convert an image coordinate to a component coordinate 975 * @param imageEntry The image entry -- only used if tiling 976 * @param visibleRect The visible rectangle 977 * @param xImg The x position in the component 978 * @param yImg The y position in the component 979 * @param compSize The component size 980 * @param zoom The current zoom level 981 * @return The point in the image 982 */ 983 static Point img2compCoord(IImageEntry<?> imageEntry, VisRect visibleRect, int xImg, int yImg, Dimension compSize, int zoom) { 984 final Rectangle drawRect; 985 if (imageEntry instanceof IImageTiling) { 986 final IImageTiling<?> tiler = (IImageTiling<?>) imageEntry; 987 final double scale = tiler.getScale(zoom); 988 // xImg = (visRect.x + xComp) / scale 989 // xImg * scale - visRect.x = xComp 990 return new Point((int) (xImg * scale - visibleRect.x), (int) (yImg * scale - visibleRect.y)); 991 } else { 992 drawRect = calculateDrawImageRectangle(visibleRect, compSize); 993 } 845 994 return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width, 846 995 drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height); 847 996 } 848 997 849 static Point comp2imgCoord(VisRect visibleRect, int xComp, int yComp, Dimension compSize) { 850 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize); 998 /** 999 * Convert a component coordinate to an image coordinate 1000 * @param imageEntry The image entry -- only used if tiling 1001 * @param visibleRect The visible rectangle 1002 * @param xComp The x position in the component 1003 * @param yComp The y position in the component 1004 * @param compSize The component size 1005 * @param zoom The current zoom level 1006 * @return The point in the image 1007 */ 1008 static Point comp2imgCoord(IImageEntry<?> imageEntry, VisRect visibleRect, int xComp, int yComp, Dimension compSize, int zoom) { 1009 final Rectangle drawRect; 1010 if (imageEntry instanceof IImageTiling) { 1011 final IImageTiling<?> tiler = (IImageTiling<?>) imageEntry; 1012 final double scale = tiler.getScale(zoom); 1013 return new Point((int) ((visibleRect.x + xComp) / scale), (int) ((visibleRect.y + yComp) / scale)); 1014 } else { 1015 drawRect = calculateDrawImageRectangle(visibleRect, compSize); 1016 } 851 1017 Point p = new Point( 852 1018 ((xComp - drawRect.x) * visibleRect.width), 853 1019 ((yComp - drawRect.y) * visibleRect.height)); … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 944 1110 Point center = getCenterImgCoord(currentVisibleRect); 945 1111 currentVisibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2, 946 1112 getWidth(), getHeight()); 947 currentVisibleRect.checkRectSize( );948 currentVisibleRect.checkRectPos( );1113 currentVisibleRect.checkRectSize(currentEntry, this.zoom.get()); 1114 currentVisibleRect.checkRectPos(currentEntry, this.zoom.get()); 949 1115 } 950 1116 951 1117 synchronized (this) { … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 1009 1175 rectangle.height = (int) (getSize().height / MAX_ZOOM.get()); 1010 1176 } 1011 1177 1012 // Set the same ratio for the visible rectangle and the display area 1013 int hFact = rectangle.height * getSize().width; 1014 int wFact = rectangle.width * getSize().height; 1015 if (hFact > wFact) { 1016 rectangle.width = hFact / getSize().height; 1178 1179 final IImageEntry<?> currentEntry; 1180 synchronized (this) { 1181 currentEntry = this.entry; 1182 } 1183 if (currentEntry instanceof IImageTiling) { 1184 final int currentZoom = this.zoom.get(); 1185 IImageTiling<?> imageTiling = (IImageTiling<?>) currentEntry; 1186 if (currentZoom > imageTiling.getMaxZoom()) { 1187 this.zoom.set(imageTiling.getMaxZoom()); 1188 } else if (currentZoom < imageTiling.getMinZoom()) { 1189 this.zoom.set(imageTiling.getMinZoom()); 1190 } else if (getSize().width > imageTiling.getWidth(currentZoom) * 2 1191 && getSize().height > imageTiling.getHeight(currentZoom) * 2) { 1192 // Don't let users make the image really small in the window 1193 this.zoom.set(currentZoom + 1); 1194 } 1017 1195 } else { 1018 rectangle.height = wFact / getSize().width; 1196 // Set the same ratio for the visible rectangle and the display area 1197 int hFact = rectangle.height * getSize().width; 1198 int wFact = rectangle.width * getSize().height; 1199 if (hFact > wFact) { 1200 rectangle.width = hFact / getSize().height; 1201 } else { 1202 rectangle.height = wFact / getSize().width; 1203 } 1019 1204 } 1020 1205 } 1021 1206 … … public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 1027 1212 public void updateVisibleRectangle() { 1028 1213 final VisRect currentVisibleRect; 1029 1214 final Image mouseImage; 1030 final IImageViewer iImageViewer; 1215 final IImageViewer currentImageViewer; 1216 final IImageEntry<?> imageEntry; 1031 1217 synchronized (this) { 1032 1218 currentVisibleRect = this.visibleRect; 1033 1219 mouseImage = this.image; 1034 iImageViewer = this.getIImageViewer(this.entry); 1220 imageEntry = this.entry; 1221 currentImageViewer = this.getIImageViewer(imageEntry); 1035 1222 } 1036 if (mouseImage != null && currentVisibleRect != null && iImageViewer != null) {1037 final Image maxImageSize = iImageViewer.getMaxImageSize(this, mouseImage);1223 if (mouseImage != null && currentVisibleRect != null && currentImageViewer != null) { 1224 final Image maxImageSize = currentImageViewer.getMaxImageSize(this, mouseImage); 1038 1225 final VisRect maxVisibleRect = new VisRect(0, 0, maxImageSize.getWidth(null), maxImageSize.getHeight(null)); 1039 1226 maxVisibleRect.setRect(currentVisibleRect); 1040 1227 ensureMaxZoom(maxVisibleRect); 1041 1228 1042 maxVisibleRect.checkRectSize( );1229 maxVisibleRect.checkRectSize(imageEntry, this.zoom.get()); 1043 1230 synchronized (this) { 1044 1231 this.visibleRect = maxVisibleRect; 1045 1232 } -
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 7ea6371f62..4170d07a04 100644
a b import static org.openstreetmap.josm.tools.I18n.tr; 6 6 import java.awt.Dimension; 7 7 import java.awt.Graphics2D; 8 8 import java.awt.Image; 9 import java.awt.Rectangle; 9 10 import java.awt.geom.AffineTransform; 10 11 import java.awt.image.BufferedImage; 11 12 import java.io.File; … … import java.net.MalformedURLException; 15 16 import java.net.URL; 16 17 import java.util.Collections; 17 18 import java.util.Objects; 19 18 20 import javax.imageio.IIOParam; 19 21 import javax.imageio.ImageReadParam; 20 22 import javax.imageio.ImageReader; … … import javax.imageio.ImageReader; 22 24 import org.openstreetmap.josm.data.ImageData; 23 25 import org.openstreetmap.josm.data.gpx.GpxImageEntry; 24 26 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 27 import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.DeepTileSet; 28 import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling; 25 29 import org.openstreetmap.josm.tools.ExifReader; 26 30 import org.openstreetmap.josm.tools.ImageProvider; 27 31 import org.openstreetmap.josm.tools.Logging; … … import org.openstreetmap.josm.tools.Utils; 31 35 * Stores info about each image, with an optional thumbnail 32 36 * @since 2662 33 37 */ 34 public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry> {38 public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry>, IImageTiling<BufferedImage> { 35 39 36 40 private Image thumbnail; 37 41 private ImageData dataSet; 42 private DeepTileSet deepTileSet; 38 43 39 44 /** 40 45 * Constructs a new {@code ImageEntry}. … … public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry> 51 56 super(other); 52 57 thumbnail = other.thumbnail; 53 58 dataSet = other.dataSet; 59 this.deepTileSet = other.deepTileSet; 54 60 } 55 61 56 62 /** … … public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry> 223 229 return applyExifRotation(image); 224 230 } 225 231 232 @Override 233 public BufferedImage getTileImage(int zoom, int tileSize, int column, int row) { 234 final Rectangle tile = IImageTiling.super.getTileDimension(zoom, column, row, tileSize); 235 if (column < 0 || row < 0 || zoom > this.getMaxZoom() || tile.getWidth() <= 0 || tile.getHeight() <= 0) { 236 return null; 237 } 238 final URL imageUrl; 239 final BufferedImage image; 240 try { 241 imageUrl = getImageUrl(); 242 Logging.info(tr("Loading {0} at {1}/{2}/{3} with size {4}", imageUrl, zoom, column, row, tileSize)); 243 image = ImageProvider.read(imageUrl, true, false, 244 r -> this.withSubsampling(r, tile, zoom)); 245 } catch (IOException e) { 246 Logging.error(e); 247 return null; 248 } 249 250 if (image == null) { 251 Logging.warn("Unable to load {0}", imageUrl); 252 } 253 // applyExifRotation not used here since it will not work with tiled images 254 // Instead, we will have to rotate the column/row, and then apply rotation here. 255 return image; 256 } 257 226 258 protected URL getImageUrl() throws MalformedURLException { 227 259 return getFile().toURI().toURL(); 228 260 } 229 261 262 private ImageReadParam withSubsampling(ImageReader reader, final Rectangle tile, int zoom) { 263 ImageReadParam param = reader.getDefaultReadParam(); 264 param.setSourceRegion(tile); 265 int subsampling = (int) Math.floor(Math.max(Math.pow(IImageTiling.super.getScale(zoom), -1), 1)); 266 param.setSourceSubsampling(subsampling, subsampling, 0, 0); 267 return param; 268 } 269 230 270 private ImageReadParam withSubsampling(ImageReader reader, Dimension target) { 231 271 try { 232 272 ImageReadParam param = reader.getDefaultReadParam(); … … public class ImageEntry extends GpxImageEntry implements IImageEntry<ImageEntry> 258 298 g.dispose(); 259 299 return rotated; 260 300 } 301 302 @Override 303 public DeepTileSet getDeepTileSet() { 304 if (this.deepTileSet == null) { 305 this.deepTileSet = new DeepTileSet(this.getMinZoom(), this.getMaxZoom(), this); 306 } 307 return this.deepTileSet; 308 } 309 310 @Override 311 public boolean isTilingEnabled() { 312 // Flipped images are going to need more work (the column/rows will need to be translated) 313 return IImageTiling.super.isTilingEnabled() && !ExifReader.orientationNeedsCorrection(getExifOrientation()); 314 } 261 315 } -
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 index f76b252344..0a5467342b 100644
a b import java.awt.image.BufferedImage; 12 12 import java.util.Collections; 13 13 import java.util.Set; 14 14 15 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 15 16 import org.openstreetmap.josm.data.imagery.street_level.Projections; 16 17 import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay; 18 import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.GeoImageTileLoader; 19 import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling; 17 20 import org.openstreetmap.josm.gui.util.GuiHelper; 18 21 import org.openstreetmap.josm.gui.util.imagery.CameraPlane; 19 22 import org.openstreetmap.josm.gui.util.imagery.Vector3D; … … import org.openstreetmap.josm.gui.util.imagery.Vector3D; 24 27 * @since 18246 25 28 */ 26 29 public class Equirectangular extends ComponentAdapter implements IImageViewer { 30 private final GeoImageTileLoader tileLoader = new GeoImageTileLoader(null, IImageTiling.IMAGE_CACHE); 27 31 private volatile CameraPlane cameraPlane; 28 32 private volatile BufferedImage offscreenImage; 29 33 … … public class Equirectangular extends ComponentAdapter implements IImageViewer { 108 112 public Image getMaxImageSize(ImageDisplay imageDisplay, Image image) { 109 113 return this.offscreenImage; 110 114 } 115 116 @Override 117 public TileLoader getTileLoader() { 118 return this.tileLoader; 119 } 111 120 } -
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 index 3c1d41e534..c9edb55163 100644
a b import java.awt.Point; 8 8 import java.awt.Rectangle; 9 9 import java.awt.event.ComponentListener; 10 10 import java.awt.image.BufferedImage; 11 import java.util.List; 11 12 import java.util.Set; 13 import java.util.Timer; 14 import java.util.TimerTask; 15 import java.util.function.Predicate; 16 import java.util.stream.Collectors; 12 17 18 import org.openstreetmap.gui.jmapviewer.Tile; 19 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob; 20 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 21 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 13 22 import org.openstreetmap.josm.data.imagery.street_level.Projections; 14 23 import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay; 24 import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.GeoImageTileLoader; 25 import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling; 26 import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.TileSet; 15 27 import org.openstreetmap.josm.gui.util.imagery.Vector3D; 16 28 17 29 /** … … import org.openstreetmap.josm.gui.util.imagery.Vector3D; 19 31 * @since 18246 20 32 */ 21 33 public interface IImageViewer extends ComponentListener { 34 /** 35 * A class for {@link IImageViewer}. Probably shouldn't be used elsewhere. 36 */ 37 class ImageTimerTask { 38 /** A timer so that we aren't doing many repaints in a short time frame */ 39 static final Timer REPAINT_TIMER = new Timer("IImageViewerTimer"); 40 /** The current timer task */ 41 static TimerTask timerTask; 42 private ImageTimerTask() { 43 // Hide constructor 44 } 45 } 46 22 47 /** 23 48 * Get the supported projections for the image viewer 24 49 * @return The projections supported. Typically, only one. … … public interface IImageViewer extends ComponentListener { 34 59 */ 35 60 void paintImage(Graphics g, BufferedImage image, Rectangle target, Rectangle visibleRect); 36 61 62 /** 63 * Paint the image tile 64 * @param g The graphics to paint on 65 * @param visibleRect The visible area 66 * @param entry The image entry (specifically, with the tile size) 67 * @param tile The tile to paint (x, y, z) 68 * @param zoom The current zoom 69 */ 70 default void paintImageTile(final Graphics g, final Rectangle visibleRect, final IImageTiling<?> entry, final Tile tile, final int zoom) { 71 final int tileSize = entry.getTileSize(); 72 final Image image; 73 int xPositionInImage = tileSize * tile.getXtile(); 74 int yPositionInImage = tileSize * tile.getYtile(); 75 if (zoom == tile.getZoom()) { 76 image = tile.getImage(); 77 } else { 78 // When the zooms are not the same, we have to scale the image appropriately 79 final double scalingFactor = Math.pow(2, (double) zoom - tile.getZoom()); 80 yPositionInImage *= scalingFactor; 81 xPositionInImage *= scalingFactor; 82 final Image tileImage = tile.getImage(); 83 image = tileImage.getScaledInstance((int) (tileImage.getWidth(null) * scalingFactor), 84 (int) (tileImage.getHeight(null) * scalingFactor), Image.SCALE_DEFAULT); 85 } 86 final int x = xPositionInImage - visibleRect.x; 87 final int y = yPositionInImage - visibleRect.y; 88 g.drawImage(image, x, y, null); 89 } 90 91 /** 92 * Paint the image 93 * @param g The graphics to paint on 94 * @param imageEntry The image to paint 95 * @param target The target area 96 * @param visibleRect The visible rectangle 97 * @param zoom The zoom level 98 * @param component The component to repaint when tiles are loaded 99 */ 100 default void paintTiledImage(Graphics g, IImageTiling<?> imageEntry, Rectangle target, Rectangle visibleRect, 101 int zoom, Component component) { 102 if (this.getTileLoader() instanceof GeoImageTileLoader) { 103 ((GeoImageTileLoader) this.getTileLoader()).listener = (tile, success) -> updateRepaintTimer(component); 104 } 105 final Predicate<Tile> paintableTile = tile -> tile.isLoaded() && !tile.hasError(); 106 final Predicate<Tile> missingTile = tile -> !tile.isLoaded() && !tile.isLoading(); 107 final List<Tile> tiles = imageEntry.getTiles(zoom, visibleRect).collect(Collectors.toList()); 108 final List<Tile> missed = tiles.stream().filter(missingTile).collect(Collectors.toList()); 109 // Attempt to paint tiles at a lower zoom that are already loaded. 110 if (zoom > imageEntry.getMinZoom()) { 111 final List<Tile> superToPaint = missed.stream().map(tile -> imageEntry.getCoveringTileRange(tile, zoom - 1)) 112 .distinct().flatMap(TileSet::allTiles).distinct().collect(Collectors.toList()); 113 // Paint lower resolution filler first 114 superToPaint.forEach(tile -> tiles.add(0, tile)); 115 // Add any unloaded lower resolution tiles to the missed tiles. We are probably going to use them as the 116 // user pans around. 117 superToPaint.stream().filter(missingTile).forEach(missed::add); 118 } 119 // Attempt to paint tiles at a higher zoom that are already loaded 120 if (zoom < imageEntry.getMaxZoom()) { 121 final List<Tile> subsetToPaint = missed.stream().map(tile -> imageEntry.getCoveringTileRange(tile, zoom + 1)) 122 .distinct().flatMap(TileSet::allTiles).distinct().collect(Collectors.toList()); 123 // Paint higher resolution filler last 124 tiles.addAll(subsetToPaint); 125 } 126 // Paint the tiles that are loaded on this layer 127 tiles.stream().filter(paintableTile).forEach(tile -> this.paintImageTile(g, visibleRect, imageEntry, tile, zoom)); 128 // Start loading tiles that have yet to be loaded. 129 missed.stream().map(this.getTileLoader()::createTileLoaderJob).forEach(TileJob::submit); 130 } 131 132 /** 133 * Update the common repaint timer 134 * @param component The component to update 135 */ 136 static void updateRepaintTimer(Component component) { 137 synchronized (ImageTimerTask.REPAINT_TIMER) { 138 if (ImageTimerTask.timerTask != null) { 139 ImageTimerTask.timerTask.cancel(); 140 } 141 ImageTimerTask.timerTask = new TimerTask() { 142 @Override 143 public void run() { 144 component.repaint(); 145 } 146 }; 147 ImageTimerTask.REPAINT_TIMER.schedule(ImageTimerTask.timerTask, 100); 148 } 149 } 150 151 /** 152 * Get the tile loader for this image 153 * @return The tile loader. 154 */ 155 TileLoader getTileLoader(); 156 37 157 /** 38 158 * Get the default visible rectangle for the projection 39 159 * @param component The component the image will be displayed in … … public interface IImageViewer extends ComponentListener { 42 162 */ 43 163 ImageDisplay.VisRect getDefaultVisibleRectangle(Component component, Image image); 44 164 165 /** 166 * Get the default visible rectangle for the projection and entry 167 * @param component The component the image will be displayed in 168 * @param image The image that will be shown 169 * @param entry The entry that will be used 170 * @return The default visible rectangle 171 */ 172 default ImageDisplay.VisRect getDefaultVisibleRectangle(Component component, Image image, IImageEntry<?> entry) { 173 return this.getDefaultVisibleRectangle(component, image); 174 } 175 45 176 /** 46 177 * Get the current rotation in the image viewer 47 178 * @return The rotation … … public interface IImageViewer extends ComponentListener { 77 208 } 78 209 } 79 210 211 /** 212 * Check and modify the visible rect size to appropriate dimensions 213 * @param visibleRect the visible rectangle to update 214 * @param entry the entry to use for checking 215 * @param image The image to use for checking 216 */ 217 default void checkAndModifyVisibleRectSize(Image image, IImageEntry<?> entry, ImageDisplay.VisRect visibleRect) { 218 if (entry instanceof IImageTiling) { 219 final IImageTiling<?> tiling = (IImageTiling<?>) entry; 220 if (visibleRect.width > tiling.getWidth()) { 221 visibleRect.width = tiling.getWidth(); 222 } 223 if (visibleRect.height > tiling.getHeight()) { 224 visibleRect.height = tiling.getHeight(); 225 } 226 if (visibleRect.x + visibleRect.width > tiling.getWidth()) { 227 visibleRect.x = tiling.getWidth() - visibleRect.width; 228 } 229 if (visibleRect.y + visibleRect.height > tiling.getHeight()) { 230 visibleRect.y = tiling.getHeight() - visibleRect.height; 231 } 232 if (visibleRect.x < 0) { 233 visibleRect.x = 0; 234 } 235 if (visibleRect.y < 0) { 236 visibleRect.y = 0; 237 } 238 } else { 239 this.checkAndModifyVisibleRectSize(image, visibleRect); 240 } 241 } 242 80 243 /** 81 244 * Get the maximum image size that can be displayed 82 245 * @param imageDisplay The image display -
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 index d570c53dde..b2a721dae1 100644
a b import java.awt.image.BufferedImage; 10 10 import java.util.EnumSet; 11 11 import java.util.Set; 12 12 13 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 14 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 13 15 import org.openstreetmap.josm.data.imagery.street_level.Projections; 14 16 import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay; 17 import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.GeoImageTileLoader; 18 import org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling.IImageTiling; 15 19 16 20 /** 17 21 * The default perspective image viewer class. … … import org.openstreetmap.josm.gui.layer.geoimage.ImageDisplay; 19 23 * @since 18246 20 24 */ 21 25 public class Perspective extends ComponentAdapter implements IImageViewer { 22 26 private final GeoImageTileLoader tileLoader = new GeoImageTileLoader(null, IImageTiling.IMAGE_CACHE); 23 27 @Override 24 28 public Set<Projections> getSupportedProjections() { 25 29 return EnumSet.of(Projections.PERSPECTIVE); … … public class Perspective extends ComponentAdapter implements IImageViewer { 32 36 r.x, r.y, r.x + r.width, r.y + r.height, null); 33 37 } 34 38 39 @Override 40 public TileLoader getTileLoader() { 41 return this.tileLoader; 42 } 43 35 44 @Override 36 45 public ImageDisplay.VisRect getDefaultVisibleRectangle(Component component, Image image) { 37 46 return new ImageDisplay.VisRect(0, 0, image.getWidth(null), image.getHeight(null)); 38 47 } 48 49 @Override 50 public ImageDisplay.VisRect getDefaultVisibleRectangle(Component component, Image image, IImageEntry<?> entry) { 51 if (entry instanceof IImageTiling) { 52 return new ImageDisplay.VisRect(0, 0, ((IImageTiling) entry).getWidth(), ((IImageTiling) entry).getHeight()); 53 } 54 return IImageViewer.super.getDefaultVisibleRectangle(component, image, entry); 55 } 39 56 } -
new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/DeepTileSet.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/DeepTileSet.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/DeepTileSet.java new file mode 100644 index 0000000000..8532f936a3
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling; 3 4 import java.util.Arrays; 5 import java.util.Collection; 6 import java.util.Iterator; 7 import java.util.List; 8 import java.util.Objects; 9 import java.util.Set; 10 import java.util.stream.Collectors; 11 import java.util.stream.Stream; 12 13 import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 14 import org.openstreetmap.gui.jmapviewer.TileXY; 15 import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 16 17 /** 18 * A collection of caching tile sets 19 * @author Taylor Smock 20 * @since xxx 21 */ 22 public class DeepTileSet implements Set<TileSet> { 23 private final TileCache memoryTileCache = new MemoryTileCache(); 24 25 private final int minZoom; 26 private final int maxZoom; 27 private final TileSet[] tileSets; 28 private final TileSet nullTileSet = new TileSet(); 29 private final IImageTiling<?> imageTiling; 30 31 public DeepTileSet(final int minZoom, final int maxZoom, final IImageTiling<?> imageTiling) { 32 this.minZoom = minZoom; 33 this.maxZoom = maxZoom; 34 if (minZoom > maxZoom) { 35 throw new IllegalArgumentException(minZoom + " > " + maxZoom); 36 } 37 this.tileSets = new TileSet[maxZoom - minZoom + 1]; 38 this.imageTiling = imageTiling; 39 } 40 41 public TileSet getTileSet(int zoom) { 42 if (zoom < minZoom) { 43 return nullTileSet; 44 } else if (zoom > maxZoom) { 45 zoom = maxZoom; 46 } 47 synchronized (tileSets) { 48 TileSet ts = tileSets[zoom-minZoom]; 49 if (ts == null) { 50 ts = new TileSet(new TileXY(0, 0), 51 new TileXY(this.imageTiling.getTileXMax(zoom), this.imageTiling.getTileYMax(zoom)), 52 zoom, this.memoryTileCache, this.imageTiling); 53 ts.allTilesCreate(); 54 tileSets[zoom-minZoom] = ts; 55 } 56 return ts; 57 } 58 } 59 60 /** 61 * Get the tile cache for this deep tile set 62 * @return The tile cache 63 */ 64 TileCache getTileCache() { 65 return this.memoryTileCache; 66 } 67 68 @Override 69 public int size() { 70 return Math.toIntExact(Stream.of(this.tileSets).filter(Objects::nonNull).count()); 71 } 72 73 @Override 74 public boolean isEmpty() { 75 return Stream.of(this.tileSets).allMatch(Objects::isNull); 76 } 77 78 @Override 79 public boolean contains(Object o) { 80 Objects.requireNonNull(o); 81 return Arrays.asList(this.tileSets).contains(o); 82 } 83 84 @Override 85 public Iterator<TileSet> iterator() { 86 return Stream.of(this.tileSets).filter(Objects::nonNull).iterator(); 87 } 88 89 @Override 90 public Object[] toArray() { 91 return Stream.of(this.tileSets).filter(Objects::nonNull).toArray(); 92 } 93 94 @Override 95 public <T> T[] toArray(T[] array) { 96 return Arrays.asList(this.tileSets).toArray(array); 97 } 98 99 @Override 100 public boolean add(TileSet tileSet) { 101 throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support add"); 102 } 103 104 @Override 105 public boolean remove(Object object) { 106 Objects.requireNonNull(object); 107 for (int i = 0; i < this.tileSets.length; i++) { 108 if (object.equals(this.tileSets[i])) { 109 this.tileSets[i] = null; 110 return true; 111 } 112 } 113 return false; 114 } 115 116 @Override 117 public boolean containsAll(Collection<?> collection) { 118 Objects.requireNonNull(collection); 119 List<TileSet> list = Arrays.asList(this.tileSets); 120 return list.containsAll(collection); 121 } 122 123 @Override 124 public boolean addAll(Collection<? extends TileSet> collection) { 125 throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support addAll"); 126 } 127 128 @Override 129 public boolean removeAll(Collection<?> collection) { 130 Objects.requireNonNull(collection); 131 final int count = this.size(); 132 collection.forEach(this::remove); 133 return count != this.size(); 134 } 135 136 @Override 137 public boolean retainAll(Collection<?> collection) { 138 Objects.requireNonNull(collection); 139 List<?> toRemove = Stream.of(this.tileSets).filter(Objects::nonNull) 140 .filter(set -> !collection.contains(set)).collect(Collectors.toList()); 141 return this.removeAll(toRemove); 142 } 143 144 @Override 145 public void clear() { 146 this.memoryTileCache.clear(); 147 Arrays.fill(this.tileSets, null); 148 } 149 } -
new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoader.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoader.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoader.java new file mode 100644 index 0000000000..a96f462b4c
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling; 3 4 import java.util.concurrent.ThreadPoolExecutor; 5 6 import org.apache.commons.jcs3.access.behavior.ICacheAccess; 7 import org.apache.commons.jcs3.engine.behavior.ICache; 8 import org.openstreetmap.gui.jmapviewer.Tile; 9 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader; 10 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob; 11 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 12 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 13 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 14 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 15 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 16 import org.openstreetmap.josm.tools.CheckParameterUtil; 17 18 /** 19 * A tile loader for geo images 20 * @author Taylor Smock 21 * @since xxx 22 */ 23 public class GeoImageTileLoader implements TileLoader, CachedTileLoader { 24 25 protected final ICacheAccess<String, BufferedImageCacheEntry> cache; 26 public TileLoaderListener listener; 27 private static final ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = TMSCachedTileLoader.getNewThreadPoolExecutor("GeoImage-tiler-%d"); 28 29 /** 30 * Constructor for the GeoImageTileLoader 31 * @param listener The listener to notify when tile loading finishes 32 * @param cache The cache to use 33 */ 34 public GeoImageTileLoader(final TileLoaderListener listener, final ICacheAccess<String, BufferedImageCacheEntry> cache) { 35 CheckParameterUtil.ensureParameterNotNull(cache); 36 this.cache = cache; 37 this.listener = listener; 38 } 39 40 @Override 41 public void clearCache(TileSource source) { 42 this.cache.remove(source.getName() + ICache.NAME_COMPONENT_DELIMITER); 43 } 44 45 @Override 46 public TileJob createTileLoaderJob(Tile tile) { 47 return new GeoImageTileLoaderJob(this.listener, tile, 48 this.cache, DEFAULT_DOWNLOAD_JOB_DISPATCHER 49 ); 50 } 51 52 @Override 53 public boolean hasOutstandingTasks() { 54 return DEFAULT_DOWNLOAD_JOB_DISPATCHER.getTaskCount() > DEFAULT_DOWNLOAD_JOB_DISPATCHER.getCompletedTaskCount(); 55 } 56 57 @Override 58 public void cancelOutstandingTasks() { 59 for (Runnable runnable : DEFAULT_DOWNLOAD_JOB_DISPATCHER.getQueue()) { 60 DEFAULT_DOWNLOAD_JOB_DISPATCHER.remove(runnable); 61 } 62 } 63 } -
new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoaderJob.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoaderJob.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/GeoImageTileLoaderJob.java new file mode 100644 index 0000000000..79945abe13
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling; 3 4 import java.awt.image.RenderedImage; 5 import java.io.ByteArrayInputStream; 6 import java.io.ByteArrayOutputStream; 7 import java.io.File; 8 import java.io.IOException; 9 import java.net.URL; 10 import java.time.Duration; 11 import java.util.Collections; 12 import java.util.concurrent.ThreadPoolExecutor; 13 14 import javax.imageio.ImageIO; 15 16 import org.apache.commons.jcs3.access.behavior.ICacheAccess; 17 import org.openstreetmap.gui.jmapviewer.Tile; 18 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob; 19 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 20 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 21 import org.openstreetmap.josm.data.cache.CacheEntry; 22 import org.openstreetmap.josm.data.cache.CacheEntryAttributes; 23 import org.openstreetmap.josm.data.cache.ICachedLoaderListener; 24 import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob; 25 import org.openstreetmap.josm.data.imagery.TileJobOptions; 26 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 27 import org.openstreetmap.josm.tools.Logging; 28 29 /** 30 * A job to load geoimage tiles 31 * @author Taylor Smock 32 * @since xxx 33 */ 34 public class GeoImageTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, 35 ICachedLoaderListener { 36 private static final TileJobOptions DEFAULT_OPTIONS = new TileJobOptions(0, 0, Collections.emptyMap(), 37 Duration.ofHours(1).getSeconds()); 38 private final Tile tile; 39 private final TileLoaderListener listener; 40 41 public GeoImageTileLoaderJob(TileLoaderListener listener, Tile tile, 42 ICacheAccess<String, BufferedImageCacheEntry> cache, ThreadPoolExecutor geoimagetileloader) { 43 super(cache, DEFAULT_OPTIONS, geoimagetileloader); 44 this.listener = listener; 45 this.tile = tile; 46 } 47 48 @Override 49 public void submit() { 50 submit(false); 51 } 52 53 @Override 54 public void submit(boolean force) { 55 try { 56 super.submit(this, force); 57 58 } catch (IllegalArgumentException | IOException e) { 59 Logging.warn(e); 60 } 61 } 62 63 @Override 64 public String getCacheKey() { 65 return tile.getKey(); 66 } 67 68 @Override 69 public URL getUrl() throws IOException { 70 if (this.tile.getTileSource() instanceof IImageEntry) { 71 return ((IImageEntry<?>) this.tile.getTileSource()).getFile().toURI().toURL(); 72 } 73 return null; 74 } 75 76 @Override 77 public void loadingFinished(CacheEntry data, CacheEntryAttributes attributes, LoadResult result) { 78 try { 79 if (data instanceof BufferedImageCacheEntry) { 80 this.tile.setImage(((BufferedImageCacheEntry) data).getImage()); 81 } else if (data != null) { 82 this.tile.loadImage(new ByteArrayInputStream(data.getContent())); 83 } 84 this.tile.finishLoading(); 85 } catch (IOException e) { 86 this.tile.setError(e); 87 } 88 this.listener.tileLoadingFinished(this.tile, LoadResult.SUCCESS == result); 89 } 90 91 @Override 92 protected BufferedImageCacheEntry createCacheEntry(byte[] content) { 93 return new BufferedImageCacheEntry(content); 94 } 95 96 @Override 97 protected byte[] loadObjectBytes(File file) throws IOException { 98 if (this.tile.getTileSource() instanceof IImageTiling) { 99 final IImageTiling<?> tileSource = (IImageTiling<?>) this.tile.getTileSource(); 100 final RenderedImage image = tileSource.getTileImage(this.tile.getZoom(), this.tile.getXtile(), this.tile.getYtile()); 101 if (image == null) { 102 throw new IOException("No image loaded for " + file.toString()); 103 } 104 final ByteArrayOutputStream output = new ByteArrayOutputStream(image.getHeight() * image.getWidth()); 105 ImageIO.write(image, "jpg", output); 106 return output.toByteArray(); 107 } 108 return super.loadObjectBytes(file); 109 } 110 } -
new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTiling.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTiling.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTiling.java new file mode 100644 index 0000000000..eb43fb09b3
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling; 3 4 import java.awt.Dimension; 5 import java.awt.Image; 6 import java.awt.Point; 7 import java.awt.Rectangle; 8 import java.awt.image.RenderedImage; 9 import java.io.IOException; 10 import java.util.List; 11 import java.util.Map; 12 import java.util.Objects; 13 import java.util.function.DoubleToIntFunction; 14 import java.util.function.IntUnaryOperator; 15 import java.util.stream.IntStream; 16 import java.util.stream.Stream; 17 18 import org.apache.commons.jcs3.access.CacheAccess; 19 import org.openstreetmap.gui.jmapviewer.Tile; 20 import org.openstreetmap.gui.jmapviewer.TileXY; 21 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 22 import org.openstreetmap.gui.jmapviewer.interfaces.IProjected; 23 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 24 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 25 import org.openstreetmap.josm.data.cache.JCSCacheManager; 26 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 27 import org.openstreetmap.josm.gui.layer.imagery.TilePosition; 28 import org.openstreetmap.josm.spi.preferences.Config; 29 30 /** 31 * An interface for tiled images. Primarily used to reduce memory usage in large images. 32 * @author Taylor Smock 33 * @param <I> The image type returned 34 * @since xxx 35 */ 36 public interface IImageTiling<I extends Image & RenderedImage> extends TileSource { 37 38 /** 39 * The default tile size for the image tiles -- each tile takes 1024 px * 1024 px * 4 bytes = 4 MiB max 40 * A 4k image (4160x3120) has (Math.ceil(4160/1024) * Math.ceil(3120/1024) = 20 tiles). Some tiles are almost empty. 41 * This gives a reasonable number of tiles for most image sizes. 42 */ 43 int DEFAULT_TILE_SIZE = 1024; 44 45 /** A good default minimum zoom (the image size is {@link #DEFAULT_TILE_SIZE} max, at 1024 it is 5) */ 46 int DEFAULT_MIN_ZOOM = (int) Math.round(Math.log(Math.sqrt(DEFAULT_TILE_SIZE))/Math.log(2)); 47 48 /** A cache for images */ 49 CacheAccess<String, BufferedImageCacheEntry> IMAGE_CACHE = 50 JCSCacheManager.getCache("iimagetiling", 100, 1_000, 51 Config.getDirs().getCacheDirectory(true).getAbsolutePath()); 52 53 /** 54 * Get the size of the image at a specified zoom level 55 * @param zoom The zoom level. Zoom 0 == 1 px for the image. Zoom 1 == 4 px for the image. 56 * @return The number of pixels (max, for a square image) 57 */ 58 static long getSizeAtZoom(final int zoom) { 59 final long dimension = 1L << zoom; 60 return dimension * dimension; 61 } 62 63 /** 64 * Get the default tile size. 65 * @return The tile size to use 66 */ 67 default int getDefaultTileSize() { 68 return DEFAULT_TILE_SIZE; 69 } 70 71 /** 72 * Get the tile size. 73 * @return The tile size to use 74 */ 75 default int getTileSize() { 76 return this.getDefaultTileSize(); 77 } 78 79 /** 80 * Get the maximum zoom that the image supports 81 * Feel free to override and cache the result for performance reasons. 82 * 83 * @return The maximum zoom of the image 84 */ 85 default int getMaxZoom() { 86 final int maxSize = Math.max(this.getWidth(), this.getHeight()); 87 return (int) Math.round(Math.ceil(Math.log(maxSize) / Math.log(2))); 88 } 89 90 /** 91 * Get the minimum zoom that the image supports or makes sense 92 * @return The minimum zoom that makes sense 93 */ 94 default int getMinZoom() { 95 final IntUnaryOperator minZoom = input -> Math.toIntExact( 96 Math.round(Math.floor(this.getMaxZoom() + Math.log((double) this.getTileSize() / input) / Math.log(2)))); 97 return Math.min(minZoom.applyAsInt(this.getWidth()), minZoom.applyAsInt(this.getHeight())); 98 } 99 100 /** 101 * Get the current scale of the image 102 * @param zoom The zoom level 103 * @return The scaling of the image at the specified level 104 */ 105 default double getScale(final int zoom) { 106 return Math.pow(2, (double) zoom - this.getMaxZoom()); 107 } 108 109 /** 110 * Get the width of the image 111 * @return The width of the image 112 */ 113 int getWidth(); 114 115 /** 116 * Get the width of the image at a specified scale 117 * @param zoom The zoom to use 118 * @return The width at the specified scale 119 */ 120 default int getWidth(final int zoom) { 121 return Math.toIntExact(Math.round(this.getScale(zoom) * this.getWidth())); 122 } 123 124 /** 125 * Get the height of the image 126 * @return The height of the image 127 */ 128 int getHeight(); 129 130 /** 131 * Get the height of the image at a specified scale 132 * @param zoom The zoom to use 133 * @return The height at the specified scale 134 */ 135 default int getHeight(final int zoom) { 136 return Math.toIntExact(Math.round(this.getScale(zoom) * this.getHeight())); 137 } 138 139 /** 140 * Get the size of the image 141 * @return The image size at the zoom level 142 */ 143 default Dimension getSize(int zoom) { 144 return new Dimension(this.getWidth(zoom), this.getHeight(zoom)); 145 } 146 147 /** 148 * Get the number of rows at a specified zoom level 149 * @param zoom The zoom level 150 * @return The number of rows 151 */ 152 default int getRows(final int zoom) { 153 return this.getRows(zoom, this.getTileSize()); 154 } 155 156 /** 157 * Get the number of rows at a specified zoom level 158 * @param zoom The zoom level 159 * @param tileSize The tile size 160 * @return The number of rows 161 */ 162 default int getRows(final int zoom, final int tileSize) { 163 final int height = this.getHeight(zoom); 164 return Math.toIntExact(Math.round(Math.ceil(height / (double) tileSize))); 165 } 166 167 /** 168 * Get the number of columns at a specified zoom level 169 * @param zoom The zoom level 170 * @return The number of columns 171 */ 172 default int getColumns(final int zoom) { 173 return this.getColumns(zoom, this.getTileSize()); 174 } 175 176 /** 177 * Get the number of columns at a specified zoom level 178 * @param zoom The zoom level 179 * @param tileSize The tile size 180 * @return The number of columns 181 */ 182 default int getColumns(final int zoom, final int tileSize) { 183 final int width = this.getWidth(zoom); 184 return Math.toIntExact(Math.round(Math.ceil(width / (double) tileSize))); 185 } 186 187 /** 188 * A DeepTileSet (to avoid creating and loading tiles over and over) 189 * @return The deap tile set 190 */ 191 DeepTileSet getDeepTileSet(); 192 193 /** 194 * Get the image to show for a specific tile location. This should be cached by the implementation in most cases. 195 * Top-left corner is 0,0 196 * @param zoom The zoom to use 197 * @param tileSize The tile size to use 198 * @param column The column to get (x) 199 * @param row The row to get (y) 200 * @return The image to display (not padded). May be {@code null}. 201 */ 202 I getTileImage(int zoom, int tileSize, int column, int row); 203 204 /** 205 * Get the image to show for a specific tile location with the default tile size 206 * Top-left corner is 0,0 207 * @param zoom The zoom to use 208 * @param column The column to get (x) 209 * @param row The row to get (y) 210 * @return The image to display (not padded). May be {@code null}. 211 */ 212 default I getTileImage(final int zoom, final int column, final int row) { 213 return this.getTileImage(zoom, this.getTileSize(), column, row); 214 } 215 216 /** 217 * Get the subsection of the image to show 218 * Top-left corner is 0,0 219 * @param zoom The zoom to use 220 * @param column The column to get (x) 221 * @param row The row to get (y) 222 * @return The subsection of the image to get 223 */ 224 default Rectangle getTileDimension(final int zoom, final int column, final int row) { 225 return this.getTileDimension(zoom, column, row, this.getTileSize()); 226 } 227 228 /** 229 * Get the subsection of the image to show 230 * Top-left corner is 0,0 231 * @param zoom The zoom to use 232 * @param column The column to get (x) 233 * @param row The row to get (y) 234 * @param tileSize the tile size to use 235 * @return The subsection of the image to get 236 */ 237 default Rectangle getTileDimension(final int zoom, final int column, final int row, final int tileSize) { 238 final double scale = this.getScale(zoom); // e.g., 1, 1/2, 1/4, etc. 239 final DoubleToIntFunction roundToInt = dbl -> Math.toIntExact(Math.round(Math.floor(dbl))); 240 final int x = roundToInt.applyAsInt(column * tileSize / scale); 241 final int y = roundToInt.applyAsInt(row * tileSize / scale); 242 final int defaultDimension = roundToInt.applyAsInt(tileSize / scale); 243 final int width = Math.min(defaultDimension, roundToInt.applyAsInt(this.getWidth() - column * tileSize / scale)); 244 final int height = Math.min(defaultDimension, roundToInt.applyAsInt(this.getHeight() - row * tileSize / scale)); 245 return new Rectangle(x, y, width, height); 246 } 247 248 /** 249 * Get the tiles for a zoom level given a visible rectangle 250 * @param zoom The zoom to get 251 * @param visibleRect The rectangle to get 252 * @return A stream of tiles to images (may be parallel) 253 */ 254 default Stream<Tile> getTiles(int zoom, Rectangle visibleRect) { 255 // We very specifically want to "overscan" -- this fixes some issues where the image isn't fully loaded 256 final int startX = Math.max(0, Math.toIntExact(Math.round(Math.floor(visibleRect.getMinX() / this.getTileSize()))) - 1); 257 final int startY = Math.max(0, Math.toIntExact(Math.round(Math.floor(visibleRect.getMinY() / this.getTileSize()))) - 1); 258 final int endX = Math.min(this.getColumns(zoom), Math.toIntExact(Math.round(Math.ceil(visibleRect.getMaxX() / this.getTileSize()))) + 1); 259 final int endY = Math.min(this.getRows(zoom), Math.toIntExact(Math.round(Math.ceil(visibleRect.getMaxY() / this.getTileSize()))) + 1); 260 final TileSet tileSet = this.getDeepTileSet().getTileSet(zoom); 261 tileSet.allTilesCreate(); 262 return IntStream.range(startX, endX).mapToObj(x -> IntStream.range(startY, endY).mapToObj(y -> new TilePosition(x, y, zoom))) 263 .flatMap(stream -> stream).map(tileSet::getTile).filter(Objects::nonNull); 264 } 265 266 /** 267 * Check if tiling is enabled for this object. 268 * 269 * @return {@code true} if tiling should be u sed 270 */ 271 default boolean isTilingEnabled() { 272 return true; 273 } 274 275 /* ************** The following are filler methods ***************** */ 276 277 @Override 278 default String getName() { 279 if (this instanceof IImageEntry) { 280 return ((IImageEntry<?>) this).getDisplayName(); 281 } 282 return this.getClass().getSimpleName(); 283 } 284 285 @Override 286 default String getId() { 287 return this.getClass().getName(); 288 } 289 290 @Override 291 default String getTileUrl(int zoom, int tilex, int tiley) throws IOException { 292 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 293 } 294 295 @Override 296 default String getTileId(int zoom, int tilex, int tiley) { 297 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 298 } 299 300 @Override 301 default double getDistance(double la1, double lo1, double la2, double lo2) { 302 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 303 } 304 305 @Override 306 default Point latLonToXY(double lat, double lon, int zoom) { 307 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 308 } 309 310 @Override 311 default Point latLonToXY(ICoordinate point, int zoom) { 312 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 313 } 314 315 @Override 316 default ICoordinate xyToLatLon(Point point, int zoom) { 317 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 318 } 319 320 @Override 321 default ICoordinate xyToLatLon(int x, int y, int zoom) { 322 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 323 } 324 325 @Override 326 default TileXY latLonToTileXY(double lat, double lon, int zoom) { 327 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 328 } 329 330 @Override 331 default TileXY latLonToTileXY(ICoordinate point, int zoom) { 332 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 333 } 334 335 @Override 336 default ICoordinate tileXYToLatLon(TileXY xy, int zoom) { 337 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 338 } 339 340 @Override 341 default ICoordinate tileXYToLatLon(Tile tile) { 342 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 343 } 344 345 @Override 346 default ICoordinate tileXYToLatLon(int x, int y, int zoom) { 347 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 348 } 349 350 @Override 351 default int getTileXMax(int zoom) { 352 return this.getColumns(zoom); 353 } 354 355 @Override 356 default int getTileXMin(int zoom) { 357 return 0; 358 } 359 360 @Override 361 default int getTileYMax(int zoom) { 362 return this.getRows(zoom); 363 } 364 365 @Override 366 default int getTileYMin(int zoom) { 367 return 0; 368 } 369 370 @Override 371 default boolean isNoTileAtZoom(Map<String, List<String>> headers, int statusCode, byte[] content) { 372 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 373 } 374 375 @Override 376 default Map<String, String> getMetadata(Map<String, List<String>> headers) { 377 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 378 } 379 380 @Override 381 default IProjected tileXYtoProjected(int x, int y, int zoom) { 382 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 383 } 384 385 @Override 386 default TileXY projectedToTileXY(IProjected p, int zoom) { 387 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 388 } 389 390 @Override 391 default boolean isInside(Tile inner, Tile outer) { 392 // If the outer zoom is greater than inner zoom, then it the inner tile cannot be inside the outer tile 393 final int zoomDifference = inner.getZoom() - outer.getZoom(); 394 // Example: outer(z13) > inner(z12), so outer covers a smaller "real" area than the inner. 395 if (zoomDifference < 0) { 396 return false; 397 } 398 // Each zoom level has 4x as many tiles, 2x in each direction 399 final double tileScale = Math.pow(2, zoomDifference); 400 final double minX = inner.getXtile() * tileScale; 401 final double minY = inner.getXtile() * tileScale; 402 final double maxX = minX + tileScale - 1; 403 final double maxY = minY + tileScale - 1; 404 return inner.getXtile() >= minX && inner.getXtile() <= maxX 405 && inner.getYtile() >= minY && inner.getYtile() <= maxY; 406 } 407 408 @Override 409 default TileSet getCoveringTileRange(Tile tile, int newZoom) { 410 final int tileZoom = tile.getZoom(); 411 final double tileScale = Math.pow(2, newZoom - tileZoom); 412 final DoubleToIntFunction clampDouble = dbl -> Math.toIntExact(Math.round(dbl)); 413 if (tileScale < 1) { 414 final TileXY superTile = new TileXY(clampDouble.applyAsInt(tile.getXtile() * tileScale), 415 clampDouble.applyAsInt(tile.getYtile() * tileScale)); 416 return new TileSet(superTile, superTile, newZoom, this.getDeepTileSet().getTileCache(), this); 417 } 418 final TileXY subTile1 = new TileXY(clampDouble.applyAsInt(tile.getXtile() * tileScale), 419 clampDouble.applyAsInt(tile.getYtile() * tileScale)); 420 final TileXY subTile2 = new TileXY(subTile1.getXIndex() + clampDouble.applyAsInt(tileScale), 421 subTile1.getY() + clampDouble.applyAsInt(tileScale)); 422 return new TileSet(subTile1, subTile2, newZoom, this.getDeepTileSet().getTileCache(), this); 423 } 424 425 @Override 426 default String getServerCRS() { 427 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 428 } 429 430 @Override 431 default boolean requiresAttribution() { 432 return false; 433 } 434 435 @Override 436 default String getAttributionText(int zoom, ICoordinate topLeft, ICoordinate botRight) { 437 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 438 } 439 440 @Override 441 default String getAttributionLinkURL() { 442 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 443 } 444 445 @Override 446 default Image getAttributionImage() { 447 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 448 } 449 450 @Override 451 default String getAttributionImageURL() { 452 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 453 } 454 455 @Override 456 default String getTermsOfUseText() { 457 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 458 } 459 460 @Override 461 default String getTermsOfUseURL() { 462 throw new UnsupportedOperationException("This method is not currently supported for " + this.getClass().getName()); 463 } 464 } -
new file src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/TileSet.java
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/TileSet.java b/src/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/TileSet.java new file mode 100644 index 0000000000..4572f73fe6
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling; 3 4 import java.util.Collection; 5 import java.util.Iterator; 6 import java.util.List; 7 import java.util.Objects; 8 import java.util.Set; 9 import java.util.function.Function; 10 import java.util.stream.Collectors; 11 import java.util.stream.IntStream; 12 import java.util.stream.Stream; 13 14 import org.openstreetmap.gui.jmapviewer.Tile; 15 import org.openstreetmap.gui.jmapviewer.TileRange; 16 import org.openstreetmap.gui.jmapviewer.TileXY; 17 import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 18 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 19 import org.openstreetmap.josm.gui.layer.imagery.TilePosition; 20 21 /** 22 * A set of tiles 23 * @author Taylor Smock 24 * @since xxx 25 */ 26 public class TileSet extends TileRange implements Set<Tile> { 27 private final TileCache tileCache; 28 private final TileSource tileSource; 29 30 /** 31 * Constructs a new {@code TileRange}. 32 * @param t1 first tile 33 * @param t2 second tile 34 * @param zoom zoom level 35 */ 36 public TileSet(final TileXY t1, final TileXY t2, final int zoom, final TileCache tileCache, final TileSource tileSource) { 37 super(t1, t2, zoom); 38 this.tileCache = tileCache; 39 this.tileSource = tileSource; 40 } 41 42 TileSet() { 43 this.tileCache = null; 44 this.tileSource = null; 45 } 46 47 /** 48 * Gets a stream of all tile positions in this set 49 * @return A stream of all positions 50 */ 51 public Stream<TilePosition> tilePositions() { 52 if (zoom == 0) { 53 return Stream.empty(); // Tileset is empty 54 } else { 55 return IntStream.rangeClosed(minX, maxX).mapToObj( 56 x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom)) 57 ).flatMap(Function.identity()); 58 } 59 } 60 61 /** 62 * Get a tile at a position 63 * @param tilePosition The position to get 64 * @return The tile (may be null) 65 */ 66 public Tile getTile(final TilePosition tilePosition) { 67 return this.tileCache.getTile(this.tileSource, tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom()); 68 } 69 70 protected List<Tile> allTilesCreate() { 71 return this.allTiles(this::createOrGetTiles).collect(Collectors.toList()); 72 } 73 74 private Tile createOrGetTiles(final TilePosition tilePosition) { 75 Tile tile = this.tileCache.getTile(this.tileSource, tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom()); 76 if (tile != null) { 77 return tile; 78 } 79 tile = new Tile(this.tileSource, tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom()); 80 this.tileCache.addTile(tile); 81 return tile; 82 } 83 84 /** 85 * Get all tiles 86 * @return All tiles in this set 87 */ 88 public Stream<Tile> allTiles() { 89 return this.allTiles(this::getTile); 90 } 91 92 private Stream<Tile> allTiles(Function<TilePosition, Tile> mapper) { 93 return tilePositions().map(mapper).filter(Objects::nonNull); 94 } 95 96 @Override 97 public boolean isEmpty() { 98 return this.allTiles(tile -> this.tileCache.getTile(this.tileSource, tile.getX(), tile.getY(), tile.getZoom())) 99 .anyMatch(Objects::nonNull); 100 } 101 102 @Override 103 public boolean contains(Object o) { 104 if (o instanceof Tile) { 105 Tile tile = (Tile) o; 106 return this.getTile(new TilePosition(tile.getXtile(), tile.getYtile(), tile.getZoom())) != null; 107 } 108 return false; 109 } 110 111 @Override 112 public Iterator<Tile> iterator() { 113 return allTiles().iterator(); 114 } 115 116 @Override 117 public Object[] toArray() { 118 return allTiles().toArray(); 119 } 120 121 @Override 122 public <T> T[] toArray(T[] array) { 123 return allTiles().collect(Collectors.toList()).toArray(array); 124 } 125 126 @Override 127 public boolean add(Tile tile) { 128 throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support add"); 129 } 130 131 @Override 132 public boolean remove(Object o) { 133 throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support remove"); 134 } 135 136 @Override 137 public boolean containsAll(Collection<?> c) { 138 return c.stream().allMatch(this::contains); 139 } 140 141 @Override 142 public boolean addAll(Collection<? extends Tile> c) { 143 throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support add"); 144 } 145 146 @Override 147 public boolean retainAll(Collection<?> c) { 148 throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support remove"); 149 } 150 151 @Override 152 public boolean removeAll(Collection<?> c) { 153 throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support remove"); 154 } 155 156 @Override 157 public void clear() { 158 throw new UnsupportedOperationException(this.getClass().getSimpleName() + " does not support remove"); 159 } 160 } -
new file test/unit/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTilingTest.java
diff --git a/test/unit/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTilingTest.java b/test/unit/org/openstreetmap/josm/gui/layer/geoimage/viewers/tiling/IImageTilingTest.java new file mode 100644 index 0000000000..a5b8d11d6e
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.geoimage.viewers.tiling; 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.assertNotEquals; 7 import static org.junit.jupiter.api.Assertions.assertNotNull; 8 import static org.junit.jupiter.api.Assertions.assertTrue; 9 10 import java.awt.Image; 11 import java.awt.image.BufferedImage; 12 import java.util.concurrent.atomic.AtomicInteger; 13 import java.util.stream.Stream; 14 15 import org.junit.jupiter.params.ParameterizedTest; 16 import org.junit.jupiter.params.provider.Arguments; 17 import org.junit.jupiter.params.provider.MethodSource; 18 import org.openstreetmap.josm.testutils.annotations.BasicPreferences; 19 20 /** 21 * Test class for {@link IImageTiling} 22 * @author Taylor Smock 23 */ 24 @BasicPreferences 25 class IImageTilingTest { 26 static Stream<Arguments> testSizeAtZoom() { 27 return Stream.of(Arguments.of(0, 1L), Arguments.of(1, 4L)); 28 } 29 30 @ParameterizedTest 31 @MethodSource 32 void testSizeAtZoom(int zoom, long expected) { 33 assertEquals(expected, IImageTiling.getSizeAtZoom(zoom)); 34 } 35 36 static Stream<Arguments> getImageTilingSamples() { 37 return Stream.of( 38 Arguments.of(new ImageTiling(new BufferedImage(5000, 2500, BufferedImage.TYPE_INT_ARGB)), 13) 39 ); 40 } 41 42 @ParameterizedTest 43 @MethodSource("getImageTilingSamples") 44 void testGetTileSizes(final ImageTiling imageTiling) { 45 // The fake class uses default methods 46 assertEquals(imageTiling.getTileSize(), imageTiling.getDefaultTileSize()); 47 } 48 49 @ParameterizedTest 50 @MethodSource("getImageTilingSamples") 51 void testGetMaxZoom(final ImageTiling imageTiling, final int maxZoom) { 52 assertEquals(maxZoom, imageTiling.getMaxZoom()); 53 } 54 55 @ParameterizedTest 56 @MethodSource("getImageTilingSamples") 57 void testGetScale(final ImageTiling imageTiling, final int maxZoom) { 58 assertEquals(1, imageTiling.getScale(maxZoom)); 59 assertEquals(0.5, imageTiling.getScale(maxZoom - 1)); 60 assertEquals(0.25, imageTiling.getScale(maxZoom - 2)); 61 } 62 63 @ParameterizedTest 64 @MethodSource("getImageTilingSamples") 65 void testGetWidth(final IImageTiling imageTiling, final int maxZoom) { 66 assertEquals(imageTiling.getWidth(), imageTiling.getWidth(maxZoom)); 67 assertEquals(imageTiling.getWidth() / 2, imageTiling.getWidth(maxZoom - 1)); 68 assertEquals(imageTiling.getWidth() / 4, imageTiling.getWidth(maxZoom - 2)); 69 } 70 71 @ParameterizedTest 72 @MethodSource("getImageTilingSamples") 73 void testGetHeight(final IImageTiling imageTiling, final int maxZoom) { 74 assertEquals(imageTiling.getHeight(), imageTiling.getHeight(maxZoom)); 75 assertEquals(imageTiling.getHeight() / 2, imageTiling.getHeight(maxZoom - 1)); 76 assertEquals(imageTiling.getHeight() / 4, imageTiling.getHeight(maxZoom - 2)); 77 } 78 79 @ParameterizedTest 80 @MethodSource("getImageTilingSamples") 81 void testGetRows(final IImageTiling imageTiling, final int maxZoom) { 82 assertEquals(3, imageTiling.getRows(maxZoom)); 83 assertEquals(2, imageTiling.getRows(maxZoom - 1)); 84 } 85 86 @ParameterizedTest 87 @MethodSource("getImageTilingSamples") 88 void testGetColumns(final IImageTiling imageTiling, final int maxZoom) { 89 assertEquals(5, imageTiling.getColumns(maxZoom)); 90 assertEquals(3, imageTiling.getColumns(maxZoom - 1)); 91 } 92 93 @ParameterizedTest 94 @MethodSource("getImageTilingSamples") 95 void testGetTileImage(final IImageTiling imageTiling, final int maxZoom) { 96 assertNotNull(imageTiling.getTileImage(maxZoom, 0, 0)); 97 final Image cornerImage = imageTiling.getTileImage(maxZoom, imageTiling.getColumns(maxZoom) - 1, imageTiling.getRows(maxZoom) - 1); 98 assertAll(() -> assertNotEquals(-1, cornerImage.getWidth(null)), 99 () -> assertNotEquals(-1, cornerImage.getHeight(null)), 100 () -> assertTrue(imageTiling.getTileSize() > cornerImage.getWidth(null)), 101 () -> assertTrue(imageTiling.getTileSize() > cornerImage.getHeight(null))); 102 } 103 104 @ParameterizedTest 105 @MethodSource("getImageTilingSamples") 106 void testGetTileDimension(final IImageTiling imageTiling) { 107 imageTiling.getTileDimension(0, 0, 0); 108 } 109 110 private static class ImageTiling implements IImageTiling { 111 private final int width; 112 private final int height; 113 private final Image image; 114 final AtomicInteger counter = new AtomicInteger(0); 115 ImageTiling(final Image image) { 116 this.image = image; 117 this.width = image.getWidth(null); 118 this.height = image.getHeight(null); 119 } 120 121 @Override 122 public int getWidth() { 123 return this.width; 124 } 125 126 @Override 127 public int getHeight() { 128 return this.height; 129 } 130 131 @Override 132 public DeepTileSet getDeepTileSet() { 133 return new DeepTileSet(this.getMinZoom(), this.getMaxZoom(), this); 134 } 135 136 @Override 137 public BufferedImage getTileImage(int zoom, int tileSize, int column, int row) { 138 this.counter.incrementAndGet(); 139 if (image instanceof BufferedImage) { 140 final BufferedImage bufferedImage = (BufferedImage) image; 141 return bufferedImage.getSubimage(column * tileSize, row * tileSize, 142 Math.min(tileSize, bufferedImage.getWidth() - column * tileSize - 1), 143 Math.min(tileSize, bufferedImage.getHeight() - row * tileSize - 1)); 144 } 145 throw new UnsupportedOperationException("The test ImageTiling class only supports BufferedImages"); 146 } 147 } 148 }