001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Component; 008import java.awt.Dimension; 009import java.awt.Font; 010import java.awt.Graphics; 011import java.awt.Graphics2D; 012import java.awt.GridBagLayout; 013import java.awt.Image; 014import java.awt.Shape; 015import java.awt.Toolkit; 016import java.awt.event.ActionEvent; 017import java.awt.event.MouseAdapter; 018import java.awt.event.MouseEvent; 019import java.awt.geom.AffineTransform; 020import java.awt.geom.Point2D; 021import java.awt.geom.Rectangle2D; 022import java.awt.image.BufferedImage; 023import java.awt.image.ImageObserver; 024import java.io.File; 025import java.io.IOException; 026import java.net.MalformedURLException; 027import java.net.URL; 028import java.time.Instant; 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.Collection; 032import java.util.Collections; 033import java.util.Comparator; 034import java.util.LinkedList; 035import java.util.List; 036import java.util.Map; 037import java.util.Map.Entry; 038import java.util.Objects; 039import java.util.Set; 040import java.util.TreeSet; 041import java.util.concurrent.ConcurrentSkipListSet; 042import java.util.concurrent.atomic.AtomicInteger; 043import java.util.function.Consumer; 044import java.util.function.Function; 045import java.util.stream.Collectors; 046import java.util.stream.IntStream; 047import java.util.stream.Stream; 048 049import javax.swing.AbstractAction; 050import javax.swing.Action; 051import javax.swing.JLabel; 052import javax.swing.JMenu; 053import javax.swing.JMenuItem; 054import javax.swing.JOptionPane; 055import javax.swing.JPanel; 056import javax.swing.JPopupMenu; 057import javax.swing.JSeparator; 058import javax.swing.Timer; 059 060import org.openstreetmap.gui.jmapviewer.AttributionSupport; 061import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 062import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 063import org.openstreetmap.gui.jmapviewer.Tile; 064import org.openstreetmap.gui.jmapviewer.TileRange; 065import org.openstreetmap.gui.jmapviewer.TileXY; 066import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader; 067import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 068import org.openstreetmap.gui.jmapviewer.interfaces.IProjected; 069import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource; 070import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 071import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 072import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 073import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 074import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource; 075import org.openstreetmap.josm.actions.AutoScaleAction; 076import org.openstreetmap.josm.actions.ExpertToggleAction; 077import org.openstreetmap.josm.actions.ImageryAdjustAction; 078import org.openstreetmap.josm.actions.RenameLayerAction; 079import org.openstreetmap.josm.actions.SaveActionBase; 080import org.openstreetmap.josm.data.Bounds; 081import org.openstreetmap.josm.data.ProjectionBounds; 082import org.openstreetmap.josm.data.coor.EastNorth; 083import org.openstreetmap.josm.data.coor.LatLon; 084import org.openstreetmap.josm.data.imagery.CoordinateConversion; 085import org.openstreetmap.josm.data.imagery.ImageryInfo; 086import org.openstreetmap.josm.data.imagery.OffsetBookmark; 087import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 088import org.openstreetmap.josm.data.imagery.TileLoaderFactory; 089import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 090import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 091import org.openstreetmap.josm.data.preferences.BooleanProperty; 092import org.openstreetmap.josm.data.preferences.IntegerProperty; 093import org.openstreetmap.josm.data.projection.Projection; 094import org.openstreetmap.josm.data.projection.ProjectionRegistry; 095import org.openstreetmap.josm.data.projection.Projections; 096import org.openstreetmap.josm.gui.ExtendedDialog; 097import org.openstreetmap.josm.gui.MainApplication; 098import org.openstreetmap.josm.gui.MapView; 099import org.openstreetmap.josm.gui.NavigatableComponent; 100import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener; 101import org.openstreetmap.josm.gui.Notification; 102import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 103import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 104import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter; 105import org.openstreetmap.josm.gui.layer.imagery.AutoLoadTilesAction; 106import org.openstreetmap.josm.gui.layer.imagery.AutoZoomAction; 107import org.openstreetmap.josm.gui.layer.imagery.DecreaseZoomAction; 108import org.openstreetmap.josm.gui.layer.imagery.FlushTileCacheAction; 109import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener; 110import org.openstreetmap.josm.gui.layer.imagery.IncreaseZoomAction; 111import org.openstreetmap.josm.gui.layer.imagery.LoadAllTilesAction; 112import org.openstreetmap.josm.gui.layer.imagery.LoadErroneousTilesAction; 113import org.openstreetmap.josm.gui.layer.imagery.MVTLayer; 114import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile; 115import org.openstreetmap.josm.gui.layer.imagery.ShowErrorsAction; 116import org.openstreetmap.josm.gui.layer.imagery.TileAnchor; 117import org.openstreetmap.josm.gui.layer.imagery.TileCoordinateConverter; 118import org.openstreetmap.josm.gui.layer.imagery.TilePosition; 119import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings; 120import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent; 121import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener; 122import org.openstreetmap.josm.gui.layer.imagery.ZoomToBestAction; 123import org.openstreetmap.josm.gui.layer.imagery.ZoomToNativeLevelAction; 124import org.openstreetmap.josm.gui.progress.ProgressMonitor; 125import org.openstreetmap.josm.gui.util.GuiHelper; 126import org.openstreetmap.josm.tools.GBC; 127import org.openstreetmap.josm.tools.HttpClient; 128import org.openstreetmap.josm.tools.Logging; 129import org.openstreetmap.josm.tools.MemoryManager; 130import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle; 131import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException; 132import org.openstreetmap.josm.tools.Utils; 133import org.openstreetmap.josm.tools.bugreport.BugReport; 134 135/** 136 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS 137 * It implements all standard functions of tilesource based layers: autozoom, tile reloads, layer saving, loading,etc. 138 * 139 * @author Upliner 140 * @author Wiktor Niesiobędzki 141 * @param <T> Tile Source class used for this layer 142 * @since 3715 143 * @since 8526 (copied from TMSLayer) 144 */ 145public abstract class AbstractTileSourceLayer<T extends AbstractTMSTileSource> extends ImageryLayer 146implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeListener, DisplaySettingsChangeListener { 147 private static final String PREFERENCE_PREFIX = "imagery.generic"; 148 private static final int MAX_TILES_SPANNED = 40; 149 static { // Registers all setting properties 150 new TileSourceDisplaySettings(); 151 } 152 153 /** maximum zoom level supported */ 154 public static final int MAX_ZOOM = 30; 155 /** minimum zoom level supported */ 156 public static final int MIN_ZOOM = 2; 157 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13); 158 159 /** additional layer menu actions */ 160 private static final List<MenuAddition> menuAdditions = new LinkedList<>(); 161 162 /** minimum zoom level to show to user */ 163 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2); 164 /** maximum zoom level to show to user */ 165 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20); 166 167 //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false); 168 /** Zoomlevel at which tiles is currently downloaded. Initial zoom lvl is set to bestZoom */ 169 private int currentZoomLevel; 170 171 private final AttributionSupport attribution = new AttributionSupport(); 172 173 /** 174 * Offset between calculated zoom level and zoom level used to download and show tiles. Negative values will result in 175 * lower resolution of imagery useful in "retina" displays, positive values will result in higher resolution 176 */ 177 public static final IntegerProperty ZOOM_OFFSET = new IntegerProperty(PREFERENCE_PREFIX + ".zoom_offset", 0); 178 179 private static final BooleanProperty POPUP_MENU_ENABLED = new BooleanProperty(PREFERENCE_PREFIX + ".popupmenu", true); 180 181 /* 182 * use MemoryTileCache instead of tileLoader JCS cache, as tileLoader caches only content (byte[] of image) 183 * and MemoryTileCache caches whole Tile. This gives huge performance improvement when a lot of tiles are visible 184 * in MapView (for example - when limiting min zoom in imagery) 185 * 186 * Use per-layer tileCache instance, as the more layers there are, the more tiles needs to be cached 187 */ 188 protected TileCache tileCache; // initialized together with tileSource 189 protected T tileSource; 190 protected TileLoader tileLoader; 191 192 /** A timer that is used to delay invalidation events if required. */ 193 private final Timer invalidateLaterTimer = new Timer(100, e -> this.invalidate()); 194 195 private final MouseAdapter adapter = new MouseAdapter() { 196 @Override 197 public void mouseClicked(MouseEvent e) { 198 if (!isVisible()) return; 199 if (e.getButton() == MouseEvent.BUTTON3) { 200 Component component = e.getComponent(); 201 if (POPUP_MENU_ENABLED.get() && component.isShowing()) { 202 new TileSourceLayerPopup(e.getX(), e.getY()).show(component, e.getX(), e.getY()); 203 } 204 } else if (e.getButton() == MouseEvent.BUTTON1) { 205 attribution.handleAttribution(e.getPoint(), true); 206 } 207 } 208 }; 209 210 private final TileSourceDisplaySettings displaySettings = createDisplaySettings(); 211 212 private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this); 213 // prepared to be moved to the painter 214 protected TileCoordinateConverter coordinateConverter; 215 private final long minimumTileExpire; 216 217 /** 218 * Creates Tile Source based Imagery Layer based on Imagery Info 219 * @param info imagery info 220 */ 221 protected AbstractTileSourceLayer(ImageryInfo info) { 222 super(info); 223 setBackgroundLayer(true); 224 this.setVisible(true); 225 getFilterSettings().addFilterChangeListener(this); 226 getDisplaySettings().addSettingsChangeListener(this); 227 this.minimumTileExpire = info.getMinimumTileExpire(); 228 } 229 230 /** 231 * This method creates the {@link TileSourceDisplaySettings} object. Subclasses may implement it to e.g. change the prefix. 232 * @return The object. 233 * @since 10568 234 */ 235 protected TileSourceDisplaySettings createDisplaySettings() { 236 return new TileSourceDisplaySettings(); 237 } 238 239 /** 240 * Gets the {@link TileSourceDisplaySettings} instance associated with this tile source. 241 * @return The tile source display settings 242 * @since 10568 243 */ 244 public TileSourceDisplaySettings getDisplaySettings() { 245 return displaySettings; 246 } 247 248 @Override 249 public void filterChanged() { 250 invalidate(); 251 } 252 253 protected abstract TileLoaderFactory getTileLoaderFactory(); 254 255 /** 256 * Get projections this imagery layer supports natively. 257 * <p/> 258 * For example projection of tiles that are downloaded from a server. Layer may support even more 259 * projections (by reprojecting the tiles), but with a certain loss in image quality and performance. 260 * @return projections this imagery layer supports natively; null if layer is projection agnostic. 261 */ 262 public abstract Collection<String> getNativeProjections(); 263 264 /** 265 * Creates and returns a new {@link TileSource} instance depending on {@link #info} specified in the constructor. 266 * 267 * @return TileSource for specified ImageryInfo 268 * @throws IllegalArgumentException when Imagery is not supported by layer 269 */ 270 protected abstract T getTileSource(); 271 272 protected Map<String, String> getHeaders(T tileSource) { 273 if (tileSource instanceof TemplatedTileSource) { 274 return ((TemplatedTileSource) tileSource).getHeaders(); 275 } 276 return null; 277 } 278 279 protected void initTileSource(T tileSource) { 280 coordinateConverter = new TileCoordinateConverter(MainApplication.getMap().mapView, tileSource, getDisplaySettings()); 281 attribution.initialize(tileSource); 282 283 currentZoomLevel = getBestZoom(); 284 285 Map<String, String> headers = getHeaders(tileSource); 286 287 tileLoader = getTileLoaderFactory().makeTileLoader(this, headers, minimumTileExpire); 288 289 try { 290 if ("file".equalsIgnoreCase(new URL(tileSource.getBaseUrl()).getProtocol())) { 291 tileLoader = new OsmTileLoader(this); 292 } 293 } catch (MalformedURLException e) { 294 // ignore, assume that this is not a file 295 Logging.log(Logging.LEVEL_DEBUG, e); 296 } 297 298 if (tileLoader == null) 299 tileLoader = new OsmTileLoader(this, headers); 300 301 tileCache = new MemoryTileCache(estimateTileCacheSize()); 302 } 303 304 @Override 305 public synchronized void tileLoadingFinished(Tile tile, boolean success) { 306 if (tile.hasError()) { 307 success = false; 308 tile.setImage(null); 309 } 310 invalidateLater(); 311 Logging.debug("tileLoadingFinished() tile: {0} success: {1}", tile, success); 312 } 313 314 /** 315 * Clears the tile cache. 316 */ 317 public void clearTileCache() { 318 if (tileLoader instanceof CachedTileLoader) { 319 ((CachedTileLoader) tileLoader).clearCache(tileSource); 320 } 321 tileCache.clear(); 322 } 323 324 @Override 325 public Object getInfoComponent() { 326 JPanel panel = (JPanel) super.getInfoComponent(); 327 List<List<String>> content = new ArrayList<>(); 328 Collection<String> nativeProjections = getNativeProjections(); 329 if (nativeProjections != null) { 330 content.add(Arrays.asList(tr("Native projections"), String.join(", ", getNativeProjections()))); 331 } 332 EastNorth offset = getDisplaySettings().getDisplacement(); 333 if (offset.distanceSq(0, 0) > 1e-10) { 334 content.add(Arrays.asList(tr("Offset"), offset.east() + ";" + offset.north())); 335 } 336 if (coordinateConverter.requiresReprojection()) { 337 content.add(Arrays.asList(tr("Tile download projection"), tileSource.getServerCRS())); 338 content.add(Arrays.asList(tr("Tile display projection"), ProjectionRegistry.getProjection().toCode())); 339 } 340 content.add(Arrays.asList(tr("Current zoom"), Integer.toString(currentZoomLevel))); 341 for (List<String> entry: content) { 342 panel.add(new JLabel(entry.get(0) + ':'), GBC.std()); 343 panel.add(GBC.glue(5, 0), GBC.std()); 344 panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL)); 345 } 346 return panel; 347 } 348 349 @Override 350 protected Action getAdjustAction() { 351 return adjustAction; 352 } 353 354 /** 355 * Returns average number of screen pixels per tile pixel for current mapview 356 * @param zoom zoom level 357 * @return average number of screen pixels per tile pixel 358 */ 359 public double getScaleFactor(int zoom) { 360 if (coordinateConverter != null) { 361 return coordinateConverter.getScaleFactor(zoom); 362 } else { 363 return 1; 364 } 365 } 366 367 /** 368 * Returns best zoom level. 369 * @return best zoom level 370 */ 371 public int getBestZoom() { 372 double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view 373 double result = Math.log(factor)/Math.log(2)/2; 374 /* 375 * Math.log(factor)/Math.log(2) - gives log base 2 of factor 376 * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2 377 * 378 * ZOOM_OFFSET controls, whether we work with overzoomed or underzoomed tiles. Positive ZOOM_OFFSET 379 * is for working with underzoomed tiles (higher quality when working with aerial imagery), negative ZOOM_OFFSET 380 * is for working with overzoomed tiles (big, pixelated), which is good when working with high-dpi screens and/or 381 * maps as a imagery layer 382 */ 383 int intResult = (int) Math.round(result + 1 + ZOOM_OFFSET.get() / 1.9); 384 int minZoom = getMinZoomLvl(); 385 int maxZoom = getMaxZoomLvl(); 386 if (minZoom <= maxZoom) { 387 intResult = Utils.clamp(intResult, minZoom, maxZoom); 388 } else if (intResult > maxZoom) { 389 intResult = maxZoom; 390 } 391 return intResult; 392 } 393 394 /** 395 * Default implementation of {@link org.openstreetmap.josm.gui.layer.Layer.LayerAction#supportLayers(List)}. 396 * @param layers layers 397 * @return {@code true} is layers contains only a {@code TMSLayer} 398 */ 399 public static boolean actionSupportLayers(List<Layer> layers) { 400 return layers.size() == 1 && layers.get(0) instanceof TMSLayer; 401 } 402 403 private abstract static class AbstractTileAction extends AbstractAction { 404 405 protected final AbstractTileSourceLayer<?> layer; 406 protected final Tile tile; 407 408 AbstractTileAction(String name, AbstractTileSourceLayer<?> layer, Tile tile) { 409 super(name); 410 this.layer = layer; 411 this.tile = tile; 412 } 413 } 414 415 private static final class ShowTileInfoAction extends AbstractTileAction { 416 417 private ShowTileInfoAction(AbstractTileSourceLayer<?> layer, Tile tile) { 418 super(tr("Show tile info"), layer, tile); 419 setEnabled(tile != null); 420 } 421 422 private static String getSizeString(int size) { 423 return new StringBuilder().append(size).append('x').append(size).toString(); 424 } 425 426 @Override 427 public void actionPerformed(ActionEvent ae) { 428 if (tile != null) { 429 ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Tile Info"), tr("OK")); 430 JPanel panel = new JPanel(new GridBagLayout()); 431 Rectangle2D displaySize = layer.coordinateConverter.getRectangleForTile(tile); 432 String url = ""; 433 try { 434 url = tile.getUrl(); 435 } catch (IOException e) { 436 // silence exceptions 437 Logging.trace(e); 438 } 439 440 List<List<String>> content = new ArrayList<>(); 441 content.add(Arrays.asList(tr("Tile name"), tile.getKey())); 442 content.add(Arrays.asList(tr("Tile URL"), url)); 443 if (tile.getTileSource() instanceof TemplatedTileSource) { 444 Map<String, String> headers = ((TemplatedTileSource) tile.getTileSource()).getHeaders(); 445 for (String key: new TreeSet<>(headers.keySet())) { 446 // iterate over sorted keys 447 content.add(Arrays.asList(tr("Custom header: {0}", key), headers.get(key))); 448 } 449 } 450 content.add(Arrays.asList(tr("Tile size"), 451 getSizeString(tile.getTileSource().getTileSize()))); 452 content.add(Arrays.asList(tr("Tile display size"), 453 new StringBuilder().append(displaySize.getWidth()) 454 .append('x') 455 .append(displaySize.getHeight()).toString())); 456 if (layer.coordinateConverter.requiresReprojection()) { 457 content.add(Arrays.asList(tr("Reprojection"), 458 tile.getTileSource().getServerCRS() + 459 " -> " + ProjectionRegistry.getProjection().toCode())); 460 BufferedImage img = tile.getImage(); 461 if (img != null) { 462 content.add(Arrays.asList(tr("Reprojected tile size"), 463 img.getWidth() + "x" + img.getHeight())); 464 465 } 466 } 467 content.add(Arrays.asList(tr("Status"), tr(tile.getStatus()))); 468 content.add(Arrays.asList(tr("Loaded"), tr(Boolean.toString(tile.isLoaded())))); 469 content.add(Arrays.asList(tr("Loading"), tr(Boolean.toString(tile.isLoading())))); 470 content.add(Arrays.asList(tr("Error"), tr(Boolean.toString(tile.hasError())))); 471 for (List<String> entry: content) { 472 panel.add(new JLabel(entry.get(0) + ':'), GBC.std()); 473 panel.add(GBC.glue(5, 0), GBC.std()); 474 panel.add(layer.createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL)); 475 } 476 477 for (Entry<String, String> e: tile.getMetadata().entrySet()) { 478 panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ':'), GBC.std()); 479 panel.add(GBC.glue(5, 0), GBC.std()); 480 String value = e.getValue(); 481 if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) { 482 value = Instant.ofEpochMilli(Long.parseLong(value)).toString(); 483 } 484 panel.add(layer.createTextField(value), GBC.eol().fill(GBC.HORIZONTAL)); 485 486 } 487 ed.setIcon(JOptionPane.INFORMATION_MESSAGE); 488 ed.setContent(panel); 489 ed.showDialog(); 490 } 491 } 492 } 493 494 private static final class LoadTileAction extends AbstractTileAction { 495 496 private LoadTileAction(AbstractTileSourceLayer<?> layer, Tile tile) { 497 super(tr("Load tile"), layer, tile); 498 setEnabled(tile != null); 499 } 500 501 @Override 502 public void actionPerformed(ActionEvent ae) { 503 if (tile != null) { 504 layer.loadTile(tile, true); 505 layer.invalidate(); 506 } 507 } 508 } 509 510 private static void sendOsmTileRequest(Tile tile, String request) { 511 if (tile != null) { 512 try { 513 new Notification(HttpClient.create(new URL(tile.getUrl() + '/' + request)) 514 .connect().fetchContent()).show(); 515 } catch (IOException ex) { 516 Logging.error(ex); 517 } 518 } 519 } 520 521 private static final class GetOsmTileStatusAction extends AbstractTileAction { 522 private GetOsmTileStatusAction(AbstractTileSourceLayer<?> layer, Tile tile) { 523 super(tr("Get tile status"), layer, tile); 524 setEnabled(tile != null); 525 } 526 527 @Override 528 public void actionPerformed(ActionEvent e) { 529 sendOsmTileRequest(tile, "status"); 530 } 531 } 532 533 private static final class MarkOsmTileDirtyAction extends AbstractTileAction { 534 private MarkOsmTileDirtyAction(AbstractTileSourceLayer<?> layer, Tile tile) { 535 super(tr("Force tile rendering"), layer, tile); 536 setEnabled(tile != null); 537 } 538 539 @Override 540 public void actionPerformed(ActionEvent e) { 541 sendOsmTileRequest(tile, "dirty"); 542 } 543 } 544 545 /** 546 * Creates popup menu items and binds to mouse actions 547 */ 548 @Override 549 public void hookUpMapView() { 550 // this needs to be here and not in constructor to allow empty TileSource class construction using SessionWriter 551 initializeIfRequired(); 552 super.hookUpMapView(); 553 } 554 555 @Override 556 public LayerPainter attachToMapView(MapViewEvent event) { 557 initializeIfRequired(); 558 559 event.getMapView().addMouseListener(adapter); 560 MapView.addZoomChangeListener(this); 561 562 if (this instanceof NativeScaleLayer && NavigatableComponent.PROP_ZOOM_SCALE_FOLLOW_NATIVE_RES_AT_LOAD.get()) { 563 event.getMapView().setNativeScaleLayer((NativeScaleLayer) this); 564 } 565 566 // FIXME: why do we need this? Without this, if you add a WMS layer and do not move the mouse, sometimes, tiles do not start loading. 567 // FIXME: Check if this is still required. 568 event.getMapView().repaint(500); 569 570 return super.attachToMapView(event); 571 } 572 573 private void initializeIfRequired() { 574 if (tileSource == null) { 575 tileSource = getTileSource(); 576 if (tileSource == null) { 577 throw new IllegalArgumentException(tr("Failed to create tile source")); 578 } 579 // check if projection is supported 580 projectionChanged(null, ProjectionRegistry.getProjection()); 581 initTileSource(this.tileSource); 582 } 583 } 584 585 @Override 586 protected LayerPainter createMapViewPainter(MapViewEvent event) { 587 return new TileSourcePainter(); 588 } 589 590 /** 591 * Tile source layer popup menu. 592 */ 593 public class TileSourceLayerPopup extends JPopupMenu { 594 /** 595 * Constructs a new {@code TileSourceLayerPopup}. 596 * @param x horizontal dimension where user clicked 597 * @param y vertical dimension where user clicked 598 */ 599 public TileSourceLayerPopup(int x, int y) { 600 List<JMenu> submenus = new ArrayList<>(); 601 MainApplication.getLayerManager().getVisibleLayersInZOrder().stream() 602 .filter(AbstractTileSourceLayer.class::isInstance) 603 .map(AbstractTileSourceLayer.class::cast) 604 .forEachOrdered(layer -> { 605 JMenu submenu = new JMenu(layer.getName()); 606 for (Action a : layer.getCommonEntries()) { 607 if (a instanceof LayerAction) { 608 submenu.add(((LayerAction) a).createMenuComponent()); 609 } else { 610 submenu.add(new JMenuItem(a)); 611 } 612 } 613 submenu.add(new JSeparator()); 614 Tile tile = layer.getTileForPixelpos(x, y); 615 submenu.add(new JMenuItem(new LoadTileAction(layer, tile))); 616 submenu.add(new JMenuItem(new ShowTileInfoAction(layer, tile))); 617 if (ExpertToggleAction.isExpert() && tileSource != null && tileSource.isModTileFeatures()) { 618 submenu.add(new JMenuItem(new GetOsmTileStatusAction(layer, tile))); 619 submenu.add(new JMenuItem(new MarkOsmTileDirtyAction(layer, tile))); 620 } 621 submenus.add(submenu); 622 }); 623 624 if (submenus.size() == 1) { 625 JMenu menu = submenus.get(0); 626 Arrays.stream(menu.getMenuComponents()).forEachOrdered(this::add); 627 } else if (submenus.size() > 1) { 628 submenus.stream().forEachOrdered(this::add); 629 } 630 } 631 } 632 633 protected int estimateTileCacheSize() { 634 Dimension screenSize = GuiHelper.getMaximumScreenSize(); 635 int height = screenSize.height; 636 int width = screenSize.width; 637 int tileSize = 256; // default tile size 638 if (tileSource != null) { 639 tileSize = tileSource.getTileSize(); 640 } 641 /** 642 * As we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that 643 */ 644 int maxYtiles = (int) Math.ceil((double) height / tileSize + 1); 645 int maxXtiles = (int) Math.ceil((double) width / tileSize + 1); 646 int visibleTiles = maxXtiles * maxYtiles; 647 /** 648 * Take into account ZOOM_OFFSET to calculate real number of tiles and multiply by 7, to cover all tiles, that might be 649 * accessed when looking for tiles outside current zoom level. 650 * 651 * Currently we use otherZooms = {1, 2, -1, -2, -3, -4, -5} 652 * 653 * The value should be sum(2^x for x in (-5 to 2)) - 1 654 * -1 to exclude current zoom level 655 * 656 * Check call to tryLoadFromDifferentZoom 657 * @see #tryLoadFromDifferentZoom(Graphics2D, int, List<Tile>,int) 658 * @see #drawInViewArea((Graphics2D, MapView, ProjectionBounds) 659 * 660 * Add +2 to maxYtiles / maxXtiles to add space in cache for extra tiles in current zoom level that are 661 * download by overloadTiles(). This is not added in computation of visibleTiles as this unnecessarily grow the cache size 662 * @see #overloadTiles() 663 */ 664 int ret = (int) Math.ceil( 665 Math.pow(2d, ZOOM_OFFSET.get()) * // use offset to decide, how many tiles are visible 666 visibleTiles * 7 + // 7 to cover tiles from other zooms as described above 667 ((maxYtiles + 2) * (maxXtiles +2))); // to add as many tiles as they will be accessed on current zoom level 668 Logging.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibleTiles, ret); 669 return ret; 670 } 671 672 @Override 673 public void displaySettingsChanged(DisplaySettingsChangeEvent e) { 674 if (tileSource == null) { 675 return; 676 } 677 switch (e.getChangedSetting()) { 678 case TileSourceDisplaySettings.AUTO_ZOOM: 679 if (getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel) { 680 setZoomLevel(getBestZoom()); 681 invalidate(); 682 } 683 break; 684 case TileSourceDisplaySettings.AUTO_LOAD: 685 if (getDisplaySettings().isAutoLoad()) { 686 invalidate(); 687 } 688 break; 689 default: 690 // e.g. displacement 691 // trigger a redraw in every case 692 invalidate(); 693 } 694 } 695 696 /** 697 * Checks zoom level against settings 698 * @param maxZoomLvl zoom level to check 699 * @param ts tile source to crosscheck with 700 * @return maximum zoom level, not higher than supported by tilesource nor set by the user 701 */ 702 public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) { 703 if (maxZoomLvl > MAX_ZOOM) { 704 maxZoomLvl = MAX_ZOOM; 705 } 706 if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) { 707 maxZoomLvl = PROP_MIN_ZOOM_LVL.get(); 708 } 709 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) { 710 maxZoomLvl = ts.getMaxZoom(); 711 } 712 return maxZoomLvl; 713 } 714 715 /** 716 * Checks zoom level against settings 717 * @param minZoomLvl zoom level to check 718 * @param ts tile source to crosscheck with 719 * @return minimum zoom level, not higher than supported by tilesource nor set by the user 720 */ 721 public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) { 722 if (minZoomLvl < MIN_ZOOM) { 723 minZoomLvl = MIN_ZOOM; 724 } 725 if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) { 726 minZoomLvl = getMaxZoomLvl(ts); 727 } 728 if (ts != null && ts.getMinZoom() > minZoomLvl) { 729 minZoomLvl = ts.getMinZoom(); 730 } 731 return minZoomLvl; 732 } 733 734 /** 735 * Returns maximum max zoom level, that will be shown on layer. 736 * @param ts TileSource for which we want to know maximum zoom level 737 * @return maximum max zoom level, that will be shown on layer 738 */ 739 public static int getMaxZoomLvl(TileSource ts) { 740 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts); 741 } 742 743 /** 744 * Returns minimum zoom level, that will be shown on layer. 745 * @param ts TileSource for which we want to know minimum zoom level 746 * @return minimum zoom level, that will be shown on layer 747 */ 748 public static int getMinZoomLvl(TileSource ts) { 749 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts); 750 } 751 752 /** 753 * Sets maximum zoom level, that layer will attempt show 754 * @param maxZoomLvl maximum zoom level 755 */ 756 public static void setMaxZoomLvl(int maxZoomLvl) { 757 PROP_MAX_ZOOM_LVL.put(checkMaxZoomLvl(maxZoomLvl, null)); 758 } 759 760 /** 761 * Sets minimum zoom level, that layer will attempt show 762 * @param minZoomLvl minimum zoom level 763 */ 764 public static void setMinZoomLvl(int minZoomLvl) { 765 PROP_MIN_ZOOM_LVL.put(checkMinZoomLvl(minZoomLvl, null)); 766 } 767 768 /** 769 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all 770 * changes to visible map (panning/zooming) 771 */ 772 @Override 773 public void zoomChanged() { 774 zoomChanged(true); 775 } 776 777 private void zoomChanged(boolean invalidate) { 778 Logging.debug("zoomChanged(): {0}", currentZoomLevel); 779 if (tileLoader instanceof TMSCachedTileLoader) { 780 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks(); 781 } 782 if (invalidate) { 783 invalidate(); 784 } 785 } 786 787 protected int getMaxZoomLvl() { 788 if (info.getMaxZoom() != 0) 789 return checkMaxZoomLvl(info.getMaxZoom(), tileSource); 790 else 791 return getMaxZoomLvl(tileSource); 792 } 793 794 protected int getMinZoomLvl() { 795 if (info.getMinZoom() != 0) 796 return checkMinZoomLvl(info.getMinZoom(), tileSource); 797 else 798 return getMinZoomLvl(tileSource); 799 } 800 801 /** 802 * Determines if it is allowed to zoom in. 803 * @return if it is allowed to zoom in 804 */ 805 public boolean zoomIncreaseAllowed() { 806 boolean zia = currentZoomLevel < this.getMaxZoomLvl(); 807 Logging.debug("zoomIncreaseAllowed(): {0} {1} vs. {2}", zia, currentZoomLevel, this.getMaxZoomLvl()); 808 return zia; 809 } 810 811 /** 812 * Zoom in, go closer to map. 813 * 814 * @return true, if zoom increasing was successful, false otherwise 815 */ 816 public boolean increaseZoomLevel() { 817 if (zoomIncreaseAllowed()) { 818 currentZoomLevel++; 819 Logging.debug("increasing zoom level to: {0}", currentZoomLevel); 820 zoomChanged(); 821 } else { 822 Logging.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+ 823 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached."); 824 return false; 825 } 826 return true; 827 } 828 829 /** 830 * Get the current zoom level of the layer 831 * @return the current zoom level 832 * @since 12603 833 */ 834 public int getZoomLevel() { 835 return currentZoomLevel; 836 } 837 838 /** 839 * Sets the zoom level of the layer 840 * @param zoom zoom level 841 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels 842 */ 843 public boolean setZoomLevel(int zoom) { 844 return setZoomLevel(zoom, true); 845 } 846 847 private boolean setZoomLevel(int zoom, boolean invalidate) { 848 if (zoom == currentZoomLevel) return true; 849 if (zoom > this.getMaxZoomLvl()) return false; 850 if (zoom < this.getMinZoomLvl()) return false; 851 currentZoomLevel = zoom; 852 zoomChanged(invalidate); 853 return true; 854 } 855 856 /** 857 * Check if zooming out is allowed 858 * 859 * @return true, if zooming out is allowed (currentZoomLevel > minZoomLevel) 860 */ 861 public boolean zoomDecreaseAllowed() { 862 boolean zda = currentZoomLevel > this.getMinZoomLvl(); 863 Logging.debug("zoomDecreaseAllowed(): {0} {1} vs. {2}", zda, currentZoomLevel, this.getMinZoomLvl()); 864 return zda; 865 } 866 867 /** 868 * Zoom out from map. 869 * 870 * @return true, if zoom increasing was successful, false otherwise 871 */ 872 public boolean decreaseZoomLevel() { 873 if (zoomDecreaseAllowed()) { 874 Logging.debug("decreasing zoom level to: {0}", currentZoomLevel); 875 currentZoomLevel--; 876 zoomChanged(); 877 } else { 878 return false; 879 } 880 return true; 881 } 882 883 private Tile getOrCreateTile(TilePosition tilePosition) { 884 return getOrCreateTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom()); 885 } 886 887 private Tile getOrCreateTile(int x, int y, int zoom) { 888 Tile tile = getTile(x, y, zoom); 889 if (tile == null) { 890 if (coordinateConverter.requiresReprojection()) { 891 tile = new ReprojectionTile(createTile(tileSource, x, y, zoom)); 892 } else { 893 tile = createTile(tileSource, x, y, zoom); 894 } 895 tileCache.addTile(tile); 896 } 897 return tile; 898 } 899 900 private Tile getTile(TilePosition tilePosition) { 901 return getTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom()); 902 } 903 904 /** 905 * Returns tile at given position. 906 * This can and will return null for tiles that are not already in the cache. 907 * @param x tile number on the x axis of the tile to be retrieved 908 * @param y tile number on the y axis of the tile to be retrieved 909 * @param zoom zoom level of the tile to be retrieved 910 * @return tile at given position 911 */ 912 private Tile getTile(int x, int y, int zoom) { 913 if (x < tileSource.getTileXMin(zoom) || x > tileSource.getTileXMax(zoom) 914 || y < tileSource.getTileYMin(zoom) || y > tileSource.getTileYMax(zoom)) 915 return null; 916 return tileCache.getTile(tileSource, x, y, zoom); 917 } 918 919 private boolean loadTile(Tile tile, boolean force) { 920 if (tile == null) 921 return false; 922 if (!force && tile.isLoaded()) 923 return false; 924 if (tile.isLoading()) 925 return false; 926 tileLoader.createTileLoaderJob(tile).submit(force); 927 return true; 928 } 929 930 private TileSet getVisibleTileSet() { 931 if (!MainApplication.isDisplayingMapView()) 932 return new TileSet(); 933 ProjectionBounds bounds = MainApplication.getMap().mapView.getProjectionBounds(); 934 return getTileSet(bounds, currentZoomLevel); 935 } 936 937 /** 938 * Load all visible tiles. 939 * @param force {@code true} to force loading if auto-load is disabled 940 * @since 11950 941 */ 942 public void loadAllTiles(boolean force) { 943 TileSet ts = getVisibleTileSet(); 944 ts.loadAllTiles(force); 945 invalidate(); 946 } 947 948 /** 949 * Load all visible tiles in error. 950 * @param force {@code true} to force loading if auto-load is disabled 951 * @since 11950 952 */ 953 public void loadAllErrorTiles(boolean force) { 954 TileSet ts = getVisibleTileSet(); 955 ts.loadAllErrorTiles(force); 956 invalidate(); 957 } 958 959 @Override 960 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { 961 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0; 962 Logging.debug("imageUpdate() done: {0} calling repaint", done); 963 964 if (done) { 965 invalidate(); 966 } else { 967 invalidateLater(); 968 } 969 return !done; 970 } 971 972 /** 973 * Invalidate the layer at a time in the future so that the user still sees the interface responsive. 974 */ 975 private void invalidateLater() { 976 GuiHelper.runInEDT(() -> { 977 if (!invalidateLaterTimer.isRunning()) { 978 invalidateLaterTimer.setRepeats(false); 979 invalidateLaterTimer.start(); 980 } 981 }); 982 } 983 984 private boolean imageLoaded(Image i) { 985 if (i == null) 986 return false; 987 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this); 988 return (status & ALLBITS) != 0; 989 } 990 991 /** 992 * Returns the image for the given tile image is loaded. 993 * Otherwise returns null. 994 * 995 * @param tile the Tile for which the image should be returned 996 * @return the image of the tile or null. 997 */ 998 private BufferedImage getLoadedTileImage(Tile tile) { 999 BufferedImage img = tile.getImage(); 1000 if (!imageLoaded(img)) 1001 return null; 1002 return img; 1003 } 1004 1005 /** 1006 * Draw a tile image on screen. 1007 * @param g the Graphics2D 1008 * @param toDrawImg tile image 1009 * @param anchorImage tile anchor in image coordinates 1010 * @param anchorScreen tile anchor in screen coordinates 1011 * @param clip clipping region in screen coordinates (can be null) 1012 */ 1013 private void drawImageInside(Graphics2D g, BufferedImage toDrawImg, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) { 1014 AffineTransform imageToScreen = anchorImage.convert(anchorScreen); 1015 Point2D screen0 = imageToScreen.transform(new Point2D.Double(0, 0), null); 1016 Point2D screen1 = imageToScreen.transform(new Point2D.Double( 1017 toDrawImg.getWidth(), toDrawImg.getHeight()), null); 1018 1019 Shape oldClip = null; 1020 if (clip != null) { 1021 oldClip = g.getClip(); 1022 g.clip(clip); 1023 } 1024 g.drawImage(toDrawImg, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()), 1025 (int) Math.round(screen1.getX()) - (int) Math.round(screen0.getX()), 1026 (int) Math.round(screen1.getY()) - (int) Math.round(screen0.getY()), this); 1027 if (clip != null) { 1028 g.setClip(oldClip); 1029 } 1030 } 1031 1032 private List<Tile> paintTileImages(Graphics2D g, TileSet ts) { 1033 Object paintMutex = new Object(); 1034 List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>()); 1035 ts.visitTiles(tile -> { 1036 boolean miss = false; 1037 BufferedImage img = null; 1038 TileAnchor anchorImage = null; 1039 if (!tile.isLoaded() || tile.hasError()) { 1040 miss = true; 1041 } else { 1042 synchronized (tile) { 1043 img = getLoadedTileImage(tile); 1044 anchorImage = getAnchor(tile, img); 1045 } 1046 if (img == null || anchorImage == null || (tile instanceof VectorTile && !tile.isLoaded())) { 1047 miss = true; 1048 } 1049 } 1050 if (miss) { 1051 missed.add(new TilePosition(tile)); 1052 return; 1053 } 1054 1055 if (img != null) { 1056 img = applyImageProcessors(img); 1057 } 1058 1059 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile); 1060 synchronized (paintMutex) { 1061 //cannot paint in parallel 1062 drawImageInside(g, img, anchorImage, anchorScreen, null); 1063 } 1064 MapView mapView = MainApplication.getMap().mapView; 1065 if (tile instanceof ReprojectionTile && ((ReprojectionTile) tile).needsUpdate(mapView.getScale())) { 1066 // This means we have a reprojected tile in memory cache, but not at 1067 // current scale. Generally, the positioning of the tile will still 1068 // be correct, but for best image quality, the tile should be 1069 // reprojected to the target scale. The original tile image should 1070 // still be in disk cache, so this is fairly cheap. 1071 ((ReprojectionTile) tile).invalidate(); 1072 loadTile(tile, false); 1073 } 1074 1075 }, missed::add); 1076 1077 return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList()); 1078 } 1079 1080 // This function is called for several zoom levels, not just the current one. 1081 // It should not trigger any tiles to be downloaded. 1082 // It should also avoid polluting the tile cache with any tiles since these tiles are not mandatory. 1083 // 1084 // The "border" tile tells us the boundaries of where we may drawn. 1085 // It will not be from the zoom level that is being drawn currently. 1086 // If drawing the displayZoomLevel, border is null and we draw the entire tile set. 1087 private List<Tile> paintTileImages(Graphics2D g, TileSet ts, int zoom, Tile border) { 1088 if (zoom <= 0) return Collections.emptyList(); 1089 Shape borderClip = coordinateConverter.getTileShapeScreen(border); 1090 List<Tile> missedTiles = new LinkedList<>(); 1091 // The callers of this code *require* that we return any tiles that we do not draw in missedTiles. 1092 // ts.allExistingTiles() by default will only return already-existing tiles. 1093 // However, we need to return *all* tiles to the callers, so force creation here. 1094 for (Tile tile : ts.allTilesCreate()) { 1095 boolean miss = false; 1096 BufferedImage img = null; 1097 TileAnchor anchorImage = null; 1098 if (!tile.isLoaded() || tile.hasError()) { 1099 miss = true; 1100 } else { 1101 synchronized (tile) { 1102 img = getLoadedTileImage(tile); 1103 anchorImage = getAnchor(tile, img); 1104 } 1105 1106 if (img == null || anchorImage == null) { 1107 miss = true; 1108 } 1109 } 1110 if (miss) { 1111 missedTiles.add(tile); 1112 continue; 1113 } 1114 1115 // applying all filters to this layer 1116 img = applyImageProcessors(img); 1117 1118 Shape clip; 1119 if (tileSource.isInside(tile, border)) { 1120 clip = null; 1121 } else if (tileSource.isInside(border, tile)) { 1122 clip = borderClip; 1123 } else { 1124 continue; 1125 } 1126 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile); 1127 drawImageInside(g, img, anchorImage, anchorScreen, clip); 1128 } 1129 return Collections.unmodifiableList(missedTiles); 1130 } 1131 1132 private static TileAnchor getAnchor(Tile tile, BufferedImage image) { 1133 if (tile instanceof ReprojectionTile) { 1134 return ((ReprojectionTile) tile).getAnchor(); 1135 } else if (image != null) { 1136 return new TileAnchor(new Point2D.Double(0, 0), new Point2D.Double(image.getWidth(), image.getHeight())); 1137 } else { 1138 return null; 1139 } 1140 } 1141 1142 private void myDrawString(Graphics g, String text, int x, int y) { 1143 Color oldColor = g.getColor(); 1144 String textToDraw = text; 1145 if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) { 1146 // text longer than tile size, split it 1147 StringBuilder line = new StringBuilder(); 1148 StringBuilder ret = new StringBuilder(); 1149 for (String s: text.split(" ", -1)) { 1150 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) { 1151 ret.append(line).append('\n'); 1152 line.setLength(0); 1153 } 1154 line.append(s).append(' '); 1155 } 1156 ret.append(line); 1157 textToDraw = ret.toString(); 1158 } 1159 int offset = 0; 1160 for (String s: textToDraw.split("\n", -1)) { 1161 g.setColor(Color.black); 1162 g.drawString(s, x + 1, y + offset + 1); 1163 g.setColor(oldColor); 1164 g.drawString(s, x, y + offset); 1165 offset += g.getFontMetrics().getHeight() + 3; 1166 } 1167 } 1168 1169 private void paintTileText(Tile tile, Graphics2D g) { 1170 if (tile == null) { 1171 return; 1172 } 1173 Point2D p = coordinateConverter.getPixelForTile(tile); 1174 int fontHeight = g.getFontMetrics().getHeight(); 1175 int x = (int) p.getX(); 1176 int y = (int) p.getY(); 1177 int texty = y + 2 + fontHeight; 1178 1179 /*if (PROP_DRAW_DEBUG.get()) { 1180 myDrawString(g, "x=" + tile.getXtile() + " y=" + tile.getYtile() + " z=" + tile.getZoom() + "", x + 2, texty); 1181 texty += 1 + fontHeight; 1182 if ((tile.getXtile() % 32 == 0) && (tile.getYtile() % 32 == 0)) { 1183 myDrawString(g, "x=" + tile.getXtile() / 32 + " y=" + tile.getYtile() / 32 + " z=7", x + 2, texty); 1184 texty += 1 + fontHeight; 1185 } 1186 } 1187 1188 String tileStatus = tile.getStatus(); 1189 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) { 1190 myDrawString(g, tr("image " + tileStatus), x, texty); 1191 texty += 1 + fontHeight; 1192 }*/ 1193 1194 if (tile.hasError() && getDisplaySettings().isShowErrors()) { 1195 String errorMessage = tile.getErrorMessage(); 1196 if (errorMessage != null) { 1197 try { 1198 errorMessage = tr(tile.getErrorMessage()); 1199 } catch (IllegalArgumentException e) { 1200 Logging.debug(e); 1201 } 1202 if (!errorMessage.startsWith("Error") && !errorMessage.startsWith(tr("Error"))) { 1203 errorMessage = tr("Error") + ": " + errorMessage; 1204 } 1205 myDrawString(g, errorMessage, x + 2, texty); 1206 } 1207 //texty += 1 + fontHeight; 1208 } 1209 1210 if (Logging.isDebugEnabled()) { 1211 // draw tile outline in semi-transparent red 1212 g.setColor(new Color(255, 0, 0, 50)); 1213 g.draw(coordinateConverter.getTileShapeScreen(tile)); 1214 } 1215 } 1216 1217 private LatLon getShiftedLatLon(EastNorth en) { 1218 return coordinateConverter.getProjecting().eastNorth2latlonClamped(en); 1219 } 1220 1221 private ICoordinate getShiftedCoord(EastNorth en) { 1222 return CoordinateConversion.llToCoor(getShiftedLatLon(en)); 1223 } 1224 1225 private final TileSet nullTileSet = new TileSet(); 1226 1227 protected class TileSet extends TileRange { 1228 1229 private volatile TileSetInfo info; 1230 1231 protected TileSet(TileXY t1, TileXY t2, int zoom) { 1232 super(t1, t2, zoom); 1233 sanitize(); 1234 } 1235 1236 protected TileSet(TileRange range) { 1237 super(range); 1238 sanitize(); 1239 } 1240 1241 /** 1242 * null tile set 1243 */ 1244 private TileSet() { 1245 // default 1246 } 1247 1248 protected void sanitize() { 1249 minX = Utils.clamp(minX, tileSource.getTileXMin(zoom), tileSource.getTileXMax(zoom)); 1250 maxX = Utils.clamp(maxX, tileSource.getTileXMin(zoom), tileSource.getTileXMax(zoom)); 1251 minY = Utils.clamp(minY, tileSource.getTileYMin(zoom), tileSource.getTileYMax(zoom)); 1252 maxY = Utils.clamp(maxY, tileSource.getTileYMin(zoom), tileSource.getTileYMax(zoom)); 1253 } 1254 1255 private boolean tooSmall() { 1256 return this.tilesSpanned() < 2.1; 1257 } 1258 1259 private boolean tooLarge() { 1260 return tileCache == null || size() > tileCache.getCacheSize(); 1261 } 1262 1263 /** 1264 * Get all tiles represented by this TileSet that are already in the tileCache. 1265 * @return all tiles represented by this TileSet that are already in the tileCache 1266 */ 1267 private List<Tile> allExistingTiles() { 1268 return allTiles(AbstractTileSourceLayer.this::getTile); 1269 } 1270 1271 private List<Tile> allTilesCreate() { 1272 return allTiles(AbstractTileSourceLayer.this::getOrCreateTile); 1273 } 1274 1275 private List<Tile> allTiles(Function<TilePosition, Tile> mapper) { 1276 return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList()); 1277 } 1278 1279 /** 1280 * Gets a stream of all tile positions in this set 1281 * @return A stream of all positions 1282 */ 1283 public Stream<TilePosition> tilePositions() { 1284 if (zoom == 0 || this.tooLarge()) { 1285 return Stream.empty(); // Tileset is either empty or too large 1286 } else { 1287 return IntStream.rangeClosed(minX, maxX).mapToObj( 1288 x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom)) 1289 ).flatMap(Function.identity()); 1290 } 1291 } 1292 1293 private List<Tile> allLoadedTiles() { 1294 return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList()); 1295 } 1296 1297 /** 1298 * @return comparator, that sorts the tiles from the center to the edge of the current screen 1299 */ 1300 private Comparator<Tile> getTileDistanceComparator() { 1301 final int centerX = (int) Math.ceil((minX + maxX) / 2d); 1302 final int centerY = (int) Math.ceil((minY + maxY) / 2d); 1303 return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY)); 1304 } 1305 1306 private void loadAllTiles(boolean force) { 1307 if (!getDisplaySettings().isAutoLoad() && !force) { 1308 return; 1309 } 1310 if (tooLarge()) { 1311 // Too many tiles... refuse to download 1312 Logging.warn("Not downloading all tiles because there is more than {0} tiles on an axis!", MAX_TILES_SPANNED); 1313 return; 1314 } 1315 List<Tile> allTiles = allTilesCreate(); 1316 allTiles.sort(getTileDistanceComparator()); 1317 for (Tile t : allTiles) { 1318 loadTile(t, force); 1319 } 1320 } 1321 1322 /** 1323 * Extend tile loading corridor, so that no flickering happens when panning 1324 */ 1325 private void overloadTiles() { 1326 /** 1327 * consult calculation in estimateTileCacheSize() before changing values here. 1328 * 1329 * @see #estimateTileCacheSize() 1330 */ 1331 int overload = 1; 1332 1333 int minXo = Utils.clamp(minX-overload, tileSource.getTileXMin(zoom), tileSource.getTileXMax(zoom)); 1334 int maxXo = Utils.clamp(maxX+overload, tileSource.getTileXMin(zoom), tileSource.getTileXMax(zoom)); 1335 int minYo = Utils.clamp(minY-overload, tileSource.getTileYMin(zoom), tileSource.getTileYMax(zoom)); 1336 int maxYo = Utils.clamp(maxY+overload, tileSource.getTileYMin(zoom), tileSource.getTileYMax(zoom)); 1337 1338 TileSet ts = new TileSet(new TileXY(minXo, minYo), new TileXY(maxXo, maxYo), zoom); 1339 ts.loadAllTiles(false); 1340 } 1341 1342 private void loadAllErrorTiles(boolean force) { 1343 if (!getDisplaySettings().isAutoLoad() && !force) 1344 return; 1345 for (Tile t : this.allTilesCreate()) { 1346 if (t.hasError()) { 1347 tileLoader.createTileLoaderJob(t).submit(force); 1348 } 1349 } 1350 } 1351 1352 /** 1353 * Call the given paint method for all tiles in this tile set.<p> 1354 * Uses a parallel stream. 1355 * @param visitor A visitor to call for each tile. 1356 * @param missed a consumer to call for each missed tile. 1357 */ 1358 public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) { 1359 tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed)); 1360 } 1361 1362 private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) { 1363 Tile tile = getTile(tp); 1364 if (tile == null) { 1365 missed.accept(tp); 1366 } else { 1367 visitor.accept(tile); 1368 } 1369 } 1370 1371 /** 1372 * Check if there is any tile fully loaded without error. 1373 * @return true if there is any tile fully loaded without error 1374 */ 1375 public boolean hasVisibleTiles() { 1376 return getTileSetInfo().hasVisibleTiles; 1377 } 1378 1379 /** 1380 * Check if there there is a tile that is overzoomed. 1381 * <p> 1382 * I.e. the server response for one tile was "there is no tile here". 1383 * This usually happens when zoomed in too much. The limit depends on 1384 * the region, so at the edge of such a region, some tiles may be 1385 * available and some not. 1386 * @return true if there there is a tile that is overzoomed 1387 */ 1388 public boolean hasOverzoomedTiles() { 1389 return getTileSetInfo().hasOverzoomedTiles; 1390 } 1391 1392 /** 1393 * Check if there are tiles still loading. 1394 * <p> 1395 * This is the case if there is a tile not yet in the cache, or in the 1396 * cache but marked as loading ({@link Tile#isLoading()}. 1397 * @return true if there are tiles still loading 1398 */ 1399 public boolean hasLoadingTiles() { 1400 return getTileSetInfo().hasLoadingTiles; 1401 } 1402 1403 /** 1404 * Check if all tiles in the range are fully loaded. 1405 * <p> 1406 * A tile is considered to be fully loaded even if the result of loading 1407 * the tile was an error. 1408 * @return true if all tiles in the range are fully loaded 1409 */ 1410 public boolean hasAllLoadedTiles() { 1411 return getTileSetInfo().hasAllLoadedTiles; 1412 } 1413 1414 private TileSetInfo getTileSetInfo() { 1415 if (info == null) { 1416 synchronized (this) { 1417 if (info == null) { 1418 List<Tile> allTiles = this.allExistingTiles(); 1419 TileSetInfo newInfo = new TileSetInfo(); 1420 newInfo.hasLoadingTiles = allTiles.size() < this.size(); 1421 newInfo.hasAllLoadedTiles = true; 1422 for (Tile t : allTiles) { 1423 if ("no-tile".equals(t.getValue("tile-info"))) { 1424 newInfo.hasOverzoomedTiles = true; 1425 } 1426 if (t.isLoaded()) { 1427 if (!t.hasError()) { 1428 newInfo.hasVisibleTiles = true; 1429 } 1430 } else { 1431 newInfo.hasAllLoadedTiles = false; 1432 if (t.isLoading()) { 1433 newInfo.hasLoadingTiles = true; 1434 } 1435 } 1436 } 1437 info = newInfo; 1438 } 1439 } 1440 } 1441 return info; 1442 } 1443 1444 @Override 1445 public String toString() { 1446 return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size(); 1447 } 1448 } 1449 1450 /** 1451 * Data container to hold information about a {@code TileSet} class. 1452 */ 1453 private static class TileSetInfo { 1454 boolean hasVisibleTiles; 1455 boolean hasOverzoomedTiles; 1456 boolean hasLoadingTiles; 1457 boolean hasAllLoadedTiles; 1458 } 1459 1460 /** 1461 * Create a TileSet by EastNorth bbox taking a layer shift in account 1462 * @param bounds the EastNorth bounds 1463 * @param zoom zoom level 1464 * @return the tile set 1465 */ 1466 protected TileSet getTileSet(ProjectionBounds bounds, int zoom) { 1467 if (zoom == 0) 1468 return new TileSet(); 1469 TileXY t1, t2; 1470 IProjected topLeftUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMin()); 1471 IProjected botRightUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMax()); 1472 if (coordinateConverter.requiresReprojection()) { 1473 Projection projServer = Projections.getProjectionByCode(tileSource.getServerCRS()); 1474 if (projServer == null) { 1475 throw new IllegalStateException(tileSource.toString()); 1476 } 1477 ProjectionBounds projBounds = new ProjectionBounds( 1478 CoordinateConversion.projToEn(topLeftUnshifted), 1479 CoordinateConversion.projToEn(botRightUnshifted)); 1480 ProjectionBounds bbox = projServer.getEastNorthBoundsBox(projBounds, ProjectionRegistry.getProjection()); 1481 t1 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(bbox.getMin()), zoom); 1482 t2 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(bbox.getMax()), zoom); 1483 } else { 1484 t1 = tileSource.projectedToTileXY(topLeftUnshifted, zoom); 1485 t2 = tileSource.projectedToTileXY(botRightUnshifted, zoom); 1486 } 1487 return new TileSet(t1, t2, zoom); 1488 } 1489 1490 private class DeepTileSet { 1491 private final ProjectionBounds bounds; 1492 private final int minZoom, maxZoom; 1493 private final TileSet[] tileSets; 1494 1495 @SuppressWarnings("unchecked") 1496 DeepTileSet(ProjectionBounds bounds, int minZoom, int maxZoom) { 1497 this.bounds = bounds; 1498 this.minZoom = minZoom; 1499 this.maxZoom = maxZoom; 1500 if (minZoom > maxZoom) { 1501 throw new IllegalArgumentException(minZoom + " > " + maxZoom); 1502 } 1503 this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1]; 1504 } 1505 1506 public TileSet getTileSet(int zoom) { 1507 if (zoom < minZoom) 1508 return nullTileSet; 1509 synchronized (tileSets) { 1510 TileSet ts = tileSets[zoom-minZoom]; 1511 if (ts == null) { 1512 ts = AbstractTileSourceLayer.this.getTileSet(bounds, zoom); 1513 tileSets[zoom-minZoom] = ts; 1514 } 1515 return ts; 1516 } 1517 } 1518 } 1519 1520 @Override 1521 public void paint(Graphics2D g, MapView mv, Bounds bounds) { 1522 // old and unused. 1523 } 1524 1525 private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) { 1526 int zoom = currentZoomLevel; 1527 if (getDisplaySettings().isAutoZoom()) { 1528 zoom = getBestZoom(); 1529 } 1530 1531 DeepTileSet dts = new DeepTileSet(pb, getMinZoomLvl(), zoom); 1532 1533 int displayZoomLevel = zoom; 1534 1535 boolean noTilesAtZoom = false; 1536 if (getDisplaySettings().isAutoZoom() && getDisplaySettings().isAutoLoad()) { 1537 // Auto-detection of tilesource maxzoom (currently fully works only for Bing) 1538 TileSet ts0 = dts.getTileSet(zoom); 1539 if (!ts0.hasVisibleTiles() && (!ts0.hasLoadingTiles() || ts0.hasOverzoomedTiles())) { 1540 noTilesAtZoom = true; 1541 } 1542 // Find highest zoom level with at least one visible tile 1543 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) { 1544 if (dts.getTileSet(tmpZoom).hasVisibleTiles()) { 1545 displayZoomLevel = tmpZoom; 1546 break; 1547 } 1548 } 1549 // Do binary search between currentZoomLevel and displayZoomLevel 1550 while (zoom > displayZoomLevel && !ts0.hasVisibleTiles() && ts0.hasOverzoomedTiles()) { 1551 zoom = (zoom + displayZoomLevel)/2; 1552 ts0 = dts.getTileSet(zoom); 1553 } 1554 1555 setZoomLevel(zoom, false); 1556 1557 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level 1558 // to make sure there're really no more zoom levels 1559 // loading is done in the next if section 1560 if (zoom == displayZoomLevel && !ts0.hasLoadingTiles() && zoom < dts.maxZoom) { 1561 zoom++; 1562 ts0 = dts.getTileSet(zoom); 1563 } 1564 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded, 1565 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded. 1566 // loading is done in the next if section 1567 while (zoom > dts.minZoom && ts0.hasOverzoomedTiles() && !ts0.hasLoadingTiles()) { 1568 zoom--; 1569 ts0 = dts.getTileSet(zoom); 1570 } 1571 } else if (getDisplaySettings().isAutoZoom()) { 1572 setZoomLevel(zoom, false); 1573 } 1574 TileSet ts = dts.getTileSet(zoom); 1575 1576 // try to load tiles from desired zoom level, no matter what we will show (for example, tiles from previous zoom level 1577 // on zoom in) 1578 ts.loadAllTiles(false); 1579 1580 if (displayZoomLevel != zoom) { 1581 ts = dts.getTileSet(displayZoomLevel); 1582 if (!dts.getTileSet(displayZoomLevel).hasAllLoadedTiles() && displayZoomLevel < zoom) { 1583 // if we are showing tiles from lower zoom level, ensure that all tiles are loaded as they are few, 1584 // and should not trash the tile cache 1585 // This is especially needed when dts.getTileSet(zoom).tooLarge() is true and we are not loading tiles 1586 ts.loadAllTiles(false); 1587 } 1588 } 1589 1590 g.setColor(Color.DARK_GRAY); 1591 1592 List<Tile> missedTiles = this.paintTileImages(g, ts); 1593 if (getDisplaySettings().isAutoLoad()) { 1594 ts.overloadTiles(); 1595 } 1596 if (getDisplaySettings().isAutoZoom()) { 1597 /** 1598 * consult calculation in estimateTileCacheSize() before changing values here. 1599 * 1600 * @see #estimateTileCacheSize() 1601 */ 1602 int[] otherZooms = {1, 2, -1, -2, -3, -4, -5}; 1603 1604 for (int otherZoom: otherZooms) { 1605 missedTiles = tryLoadFromDifferentZoom(g, displayZoomLevel, missedTiles, otherZoom); 1606 if (missedTiles.isEmpty()) { 1607 break; 1608 } 1609 } 1610 } 1611 1612 if (Logging.isDebugEnabled() && !missedTiles.isEmpty()) { 1613 Logging.debug("still missed {0} in the end", missedTiles.size()); 1614 } 1615 g.setColor(Color.red); 1616 g.setFont(InfoFont); 1617 1618 // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge() 1619 for (Tile t : ts.allExistingTiles()) { 1620 this.paintTileText(t, g); 1621 } 1622 1623 EastNorth min = pb.getMin(); 1624 EastNorth max = pb.getMax(); 1625 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max), 1626 displayZoomLevel, this); 1627 1628 g.setColor(Color.lightGray); 1629 1630 if (ts.tooLarge()) { 1631 myDrawString(g, tr("zoom in to load more tiles"), 120, 120); 1632 } else if (!getDisplaySettings().isAutoZoom() && ts.tooSmall()) { 1633 myDrawString(g, tr("increase tiles zoom level (change resolution) to see more detail"), 120, 120); 1634 } 1635 if (noTilesAtZoom) { 1636 myDrawString(g, tr("No tiles at this zoom level"), 120, 120); 1637 } 1638 if (Logging.isDebugEnabled()) { 1639 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140); 1640 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155); 1641 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170); 1642 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185); 1643 myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200); 1644 if (tileLoader instanceof TMSCachedTileLoader) { 1645 int offset = 200; 1646 for (String part: ((TMSCachedTileLoader) tileLoader).getStats().split("\n", -1)) { 1647 offset += 15; 1648 myDrawString(g, tr("Cache stats: {0}", part), 50, offset); 1649 } 1650 } 1651 } 1652 } 1653 1654 private List<Tile> tryLoadFromDifferentZoom(Graphics2D g, int displayZoomLevel, List<Tile> missedTiles, 1655 int zoomOffset) { 1656 1657 int newzoom = displayZoomLevel + zoomOffset; 1658 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) { 1659 return missedTiles; 1660 } 1661 1662 List<Tile> newlyMissedTiles = new LinkedList<>(); 1663 for (Tile missed : missedTiles) { 1664 if (zoomOffset > 0 && "no-tile".equals(missed.getValue("tile-info"))) { 1665 // Don't try to paint from higher zoom levels when tile is overzoomed 1666 newlyMissedTiles.add(missed); 1667 continue; 1668 } 1669 TileSet ts2 = new TileSet(tileSource.getCoveringTileRange(missed, newzoom)); 1670 // Instantiating large TileSets is expensive. If there are no loaded tiles, don't bother even trying. 1671 if (ts2.allLoadedTiles().isEmpty()) { 1672 if (zoomOffset > 0) { 1673 newlyMissedTiles.add(missed); 1674 continue; 1675 } else { 1676 /* 1677 * We have negative zoom offset. Try to load tiles from lower zoom levels, as they may be not present 1678 * in tile cache (e.g. when user panned the map or opened layer above zoom level, for which tiles are present. 1679 * This will ensure, that tileCache is populated with tiles from lower zoom levels so it will be possible to 1680 * use them to paint overzoomed tiles. 1681 * See: #14562 1682 */ 1683 ts2.loadAllTiles(false); 1684 } 1685 } 1686 if (ts2.tooLarge()) { 1687 continue; 1688 } 1689 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed)); 1690 } 1691 return newlyMissedTiles; 1692 } 1693 1694 /** 1695 * Returns tile for a pixel position.<p> 1696 * This isn't very efficient, but it is only used when the user right-clicks on the map. 1697 * @param px pixel X coordinate 1698 * @param py pixel Y coordinate 1699 * @return Tile at pixel position 1700 */ 1701 private Tile getTileForPixelpos(int px, int py) { 1702 Logging.debug("getTileForPixelpos({0}, {1})", px, py); 1703 TileXY xy = coordinateConverter.getTileforPixel(px, py, currentZoomLevel); 1704 return getTile(xy.getXIndex(), xy.getYIndex(), currentZoomLevel); 1705 } 1706 1707 /** 1708 * Class to store a menu action and the class it belongs to. 1709 */ 1710 private static class MenuAddition { 1711 final Action addition; 1712 @SuppressWarnings("rawtypes") 1713 final Class<? extends AbstractTileSourceLayer> clazz; 1714 1715 @SuppressWarnings("rawtypes") 1716 MenuAddition(Action addition, Class<? extends AbstractTileSourceLayer> clazz) { 1717 this.addition = addition; 1718 this.clazz = clazz; 1719 } 1720 } 1721 1722 /** 1723 * Register an additional layer context menu entry. 1724 * 1725 * @param addition additional menu action 1726 * @since 11197 1727 */ 1728 public static void registerMenuAddition(Action addition) { 1729 menuAdditions.add(new MenuAddition(addition, AbstractTileSourceLayer.class)); 1730 } 1731 1732 /** 1733 * Register an additional layer context menu entry for a imagery layer 1734 * class. The menu entry is valid for the specified class and subclasses 1735 * thereof only. 1736 * <p> 1737 * Example: 1738 * <pre> 1739 * TMSLayer.registerMenuAddition(new TMSSpecificAction(), TMSLayer.class); 1740 * </pre> 1741 * 1742 * @param addition additional menu action 1743 * @param clazz class the menu action is registered for 1744 * @since 11197 1745 */ 1746 public static void registerMenuAddition(Action addition, 1747 Class<? extends AbstractTileSourceLayer<?>> clazz) { 1748 menuAdditions.add(new MenuAddition(addition, clazz)); 1749 } 1750 1751 /** 1752 * Prepare list of additional layer context menu entries. The list is 1753 * empty if there are no additional menu entries. 1754 * 1755 * @return list of additional layer context menu entries 1756 */ 1757 private List<Action> getMenuAdditions() { 1758 final LinkedList<Action> menuAdds = menuAdditions.stream() 1759 .filter(menuAdd -> menuAdd.clazz.isInstance(this)) 1760 .map(menuAdd -> menuAdd.addition) 1761 .collect(Collectors.toCollection(LinkedList::new)); 1762 if (!menuAdds.isEmpty()) { 1763 menuAdds.addFirst(SeparatorLayerAction.INSTANCE); 1764 } 1765 return menuAdds; 1766 } 1767 1768 @Override 1769 public Action[] getMenuEntries() { 1770 ArrayList<Action> actions = new ArrayList<>(); 1771 actions.addAll(Arrays.asList(getLayerListEntries())); 1772 actions.addAll(Arrays.asList(getCommonEntries())); 1773 actions.addAll(getMenuAdditions()); 1774 actions.add(SeparatorLayerAction.INSTANCE); 1775 actions.add(new LayerListPopup.InfoAction(this)); 1776 return actions.toArray(new Action[0]); 1777 } 1778 1779 /** 1780 * Returns the contextual menu entries in layer list dialog. 1781 * @return the contextual menu entries in layer list dialog 1782 */ 1783 public Action[] getLayerListEntries() { 1784 return new Action[] { 1785 LayerListDialog.getInstance().createActivateLayerAction(this), 1786 LayerListDialog.getInstance().createShowHideLayerAction(), 1787 MainApplication.getMenu().autoScaleActions.get(AutoScaleAction.AutoScaleMode.LAYER), 1788 LayerListDialog.getInstance().createDeleteLayerAction(), 1789 SeparatorLayerAction.INSTANCE, 1790 // color, 1791 new OffsetAction(), 1792 new RenameLayerAction(this.getAssociatedFile(), this), 1793 SeparatorLayerAction.INSTANCE 1794 }; 1795 } 1796 1797 /** 1798 * Returns the common menu entries. 1799 * @return the common menu entries 1800 */ 1801 public Action[] getCommonEntries() { 1802 return new Action[] { 1803 new AutoLoadTilesAction(this), 1804 new AutoZoomAction(this), 1805 new ShowErrorsAction(this), 1806 new IncreaseZoomAction(this), 1807 new DecreaseZoomAction(this), 1808 new ZoomToBestAction(this), 1809 new ZoomToNativeLevelAction(this), 1810 new FlushTileCacheAction(this), 1811 new LoadErroneousTilesAction(this), 1812 new LoadAllTilesAction(this) 1813 }; 1814 } 1815 1816 @Override 1817 public String getToolTipText() { 1818 if (getDisplaySettings().isAutoLoad()) { 1819 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); 1820 } else { 1821 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); 1822 } 1823 } 1824 1825 @Override 1826 public void visitBoundingBox(BoundingXYVisitor v) { 1827 } 1828 1829 /** 1830 * Task responsible for precaching imagery along the gpx track 1831 * @since 8526 1832 */ 1833 public class PrecacheTask implements TileLoaderListener { 1834 private final ProgressMonitor progressMonitor; 1835 private final int totalCount; 1836 private final AtomicInteger processedCount = new AtomicInteger(0); 1837 private final TileLoader tileLoader; 1838 private final Set<Tile> requestedTiles; 1839 1840 /** 1841 * Constructs a new {@code PrecacheTask}. 1842 * @param progressMonitor that will be notified about progess of the task 1843 * @param bufferY buffer Y in degrees around which to download tiles 1844 * @param bufferX buffer X in degrees around which to download tiles 1845 * @param points list of points along which to download 1846 */ 1847 public PrecacheTask(ProgressMonitor progressMonitor, List<LatLon> points, double bufferX, double bufferY) { 1848 this.progressMonitor = progressMonitor; 1849 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource), minimumTileExpire); 1850 if (this.tileLoader instanceof TMSCachedTileLoader) { 1851 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor( 1852 TMSCachedTileLoader.getNewThreadPoolExecutor("precache-downloader-%d")); 1853 } 1854 requestedTiles = new ConcurrentSkipListSet<>( 1855 (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey())); 1856 for (LatLon point: points) { 1857 TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel); 1858 TileXY curTile = tileSource.latLonToTileXY(CoordinateConversion.llToCoor(point), currentZoomLevel); 1859 TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel); 1860 1861 // take at least one tile of buffer 1862 int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex()); 1863 int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex()); 1864 int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex()); 1865 int maxX = Math.max(curTile.getXIndex() + 1, maxTile.getXIndex()); 1866 1867 for (int x = minX; x <= maxX; x++) { 1868 for (int y = minY; y <= maxY; y++) { 1869 requestedTiles.add(createTile(tileSource, x, y, currentZoomLevel)); 1870 } 1871 } 1872 } 1873 1874 this.totalCount = requestedTiles.size(); 1875 this.progressMonitor.setTicksCount(requestedTiles.size()); 1876 } 1877 1878 /** 1879 * Determines if the task is finished. 1880 * @return true, if all is done 1881 */ 1882 public boolean isFinished() { 1883 return processedCount.get() >= totalCount; 1884 } 1885 1886 /** 1887 * Returns total number of tiles to download. 1888 * @return total number of tiles to download 1889 */ 1890 public int getTotalCount() { 1891 return totalCount; 1892 } 1893 1894 /** 1895 * cancel the task 1896 */ 1897 public void cancel() { 1898 shutdownTmsTileLoader(); 1899 } 1900 1901 @Override 1902 public void tileLoadingFinished(Tile tile, boolean success) { 1903 int processed = this.processedCount.incrementAndGet(); 1904 if (success) { 1905 synchronized (progressMonitor) { 1906 if (!this.progressMonitor.isCanceled()) { 1907 this.progressMonitor.worked(1); 1908 this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processed, totalCount)); 1909 } 1910 } 1911 } else { 1912 Logging.warn("Tile loading failure: " + tile + " - " + tile.getErrorMessage()); 1913 } 1914 if (isFinished()) { 1915 shutdownTmsTileLoader(); 1916 } 1917 } 1918 1919 private void shutdownTmsTileLoader() { 1920 if (tileLoader instanceof TMSCachedTileLoader) { 1921 ((TMSCachedTileLoader) tileLoader).shutdown(); 1922 } 1923 } 1924 1925 /** 1926 * Execute the download 1927 */ 1928 public void run() { 1929 for (Tile t: requestedTiles) { 1930 if (!progressMonitor.isCanceled()) { 1931 tileLoader.createTileLoaderJob(t).submit(); 1932 } 1933 } 1934 } 1935 } 1936 1937 /** 1938 * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download 1939 * all of the tiles. Buffer contains at least one tile. 1940 * 1941 * To prevent accidental clear of the queue, new download executor is created with separate queue 1942 * 1943 * @param progressMonitor progress monitor for download task 1944 * @param points lat/lon coordinates to download 1945 * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides 1946 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides 1947 * @return precache task representing download task 1948 */ 1949 public AbstractTileSourceLayer<T>.PrecacheTask getDownloadAreaToCacheTask(final ProgressMonitor progressMonitor, List<LatLon> points, 1950 double bufferX, double bufferY) { 1951 return new PrecacheTask(progressMonitor, points, bufferX, bufferY); 1952 } 1953 1954 @Override 1955 public boolean isSavable() { 1956 return true; // With WMSLayerExporter 1957 } 1958 1959 @Override 1960 public File createAndOpenSaveFileChooser() { 1961 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 1962 } 1963 1964 /** 1965 * Create a new tile. Added to allow use of custom {@link Tile} objects. 1966 * 1967 * @param source Tile source 1968 * @param x X coordinate 1969 * @param y Y coordinate 1970 * @param zoom Zoom level 1971 * @return The new {@link Tile} 1972 * @since 17862 1973 */ 1974 public Tile createTile(T source, int x, int y, int zoom) { 1975 return new Tile(source, x, y, zoom); 1976 } 1977 1978 @Override 1979 public synchronized void destroy() { 1980 super.destroy(); 1981 MapView.removeZoomChangeListener(this); 1982 adjustAction.destroy(); 1983 if (tileLoader instanceof TMSCachedTileLoader) { 1984 ((TMSCachedTileLoader) tileLoader).shutdown(); 1985 } 1986 } 1987 1988 private class TileSourcePainter extends CompatibilityModeLayerPainter { 1989 /** The memory handle that will hold our tile source. */ 1990 private MemoryHandle<?> memory; 1991 1992 @Override 1993 public void paint(MapViewGraphics graphics) { 1994 allocateCacheMemory(); 1995 if (memory != null) { 1996 doPaint(graphics); 1997 if (AbstractTileSourceLayer.this instanceof MVTLayer) { 1998 AbstractTileSourceLayer.this.paint(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getMapView() 1999 .getRealBounds()); 2000 } 2001 } else { 2002 Graphics g = graphics.getDefaultGraphics(); 2003 Color oldColor = g.getColor(); 2004 g.setColor(Color.BLACK); 2005 g.drawString("Not enough memory to draw layer: " + getName(), 10, 120); 2006 g.setColor(Color.RED); 2007 g.drawString("Not enough memory to draw layer: " + getName(), 11, 121); 2008 g.setColor(oldColor); 2009 } 2010 } 2011 2012 private void doPaint(MapViewGraphics graphics) { 2013 try { 2014 drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getClipBounds().getProjectionBounds()); 2015 } catch (IllegalArgumentException | IllegalStateException e) { 2016 throw BugReport.intercept(e) 2017 .put("graphics", graphics).put("tileSource", tileSource).put("currentZoomLevel", currentZoomLevel); 2018 } 2019 } 2020 2021 private void allocateCacheMemory() { 2022 if (memory == null) { 2023 MemoryManager manager = MemoryManager.getInstance(); 2024 if (manager.isAvailable(getEstimatedCacheSize())) { 2025 try { 2026 memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new); 2027 } catch (NotEnoughMemoryException e) { 2028 Logging.warn("Could not allocate tile source memory", e); 2029 } 2030 } 2031 } 2032 } 2033 2034 protected long getEstimatedCacheSize() { 2035 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize(); 2036 } 2037 2038 @Override 2039 public void detachFromMapView(MapViewEvent event) { 2040 event.getMapView().removeMouseListener(adapter); 2041 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this); 2042 super.detachFromMapView(event); 2043 if (memory != null) { 2044 memory.free(); 2045 } 2046 } 2047 } 2048 2049 @Override 2050 public void projectionChanged(Projection oldValue, Projection newValue) { 2051 super.projectionChanged(oldValue, newValue); 2052 displaySettings.setOffsetBookmark(displaySettings.getOffsetBookmark()); 2053 if (tileCache != null) { 2054 tileCache.clear(); 2055 } 2056 } 2057 2058 @Override 2059 protected List<OffsetMenuEntry> getOffsetMenuEntries() { 2060 return OffsetBookmark.getBookmarks() 2061 .stream() 2062 .filter(b -> b.isUsable(this)) 2063 .map(OffsetMenuBookmarkEntry::new) 2064 .collect(Collectors.toList()); 2065 } 2066 2067 /** 2068 * An entry for a bookmark in the offset menu. 2069 * @author Michael Zangl 2070 */ 2071 private class OffsetMenuBookmarkEntry implements OffsetMenuEntry { 2072 private final OffsetBookmark bookmark; 2073 2074 OffsetMenuBookmarkEntry(OffsetBookmark bookmark) { 2075 this.bookmark = bookmark; 2076 2077 } 2078 2079 @Override 2080 public String getLabel() { 2081 return bookmark.getName(); 2082 } 2083 2084 @Override 2085 public boolean isActive() { 2086 EastNorth offset = bookmark.getDisplacement(ProjectionRegistry.getProjection()); 2087 EastNorth active = getDisplaySettings().getDisplacement(); 2088 return Utils.equalsEpsilon(offset.east(), active.east()) && Utils.equalsEpsilon(offset.north(), active.north()); 2089 } 2090 2091 @Override 2092 public void actionPerformed() { 2093 getDisplaySettings().setOffsetBookmark(bookmark); 2094 } 2095 } 2096}