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.Point; 015import java.awt.Rectangle; 016import java.awt.Toolkit; 017import java.awt.event.ActionEvent; 018import java.awt.event.MouseAdapter; 019import java.awt.event.MouseEvent; 020import java.awt.image.BufferedImage; 021import java.awt.image.ImageObserver; 022import java.io.File; 023import java.io.IOException; 024import java.net.MalformedURLException; 025import java.net.URL; 026import java.text.SimpleDateFormat; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collections; 030import java.util.Comparator; 031import java.util.Date; 032import java.util.LinkedList; 033import java.util.List; 034import java.util.Map; 035import java.util.Map.Entry; 036import java.util.Set; 037import java.util.concurrent.ConcurrentSkipListSet; 038import java.util.concurrent.atomic.AtomicInteger; 039 040import javax.swing.AbstractAction; 041import javax.swing.Action; 042import javax.swing.BorderFactory; 043import javax.swing.JCheckBoxMenuItem; 044import javax.swing.JLabel; 045import javax.swing.JMenuItem; 046import javax.swing.JOptionPane; 047import javax.swing.JPanel; 048import javax.swing.JPopupMenu; 049import javax.swing.JSeparator; 050import javax.swing.JTextField; 051 052import org.openstreetmap.gui.jmapviewer.AttributionSupport; 053import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 054import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 055import org.openstreetmap.gui.jmapviewer.Tile; 056import org.openstreetmap.gui.jmapviewer.TileXY; 057import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader; 058import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 059import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource; 060import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 061import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 062import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 063import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 064import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource; 065import org.openstreetmap.josm.Main; 066import org.openstreetmap.josm.actions.RenameLayerAction; 067import org.openstreetmap.josm.actions.SaveActionBase; 068import org.openstreetmap.josm.data.Bounds; 069import org.openstreetmap.josm.data.coor.EastNorth; 070import org.openstreetmap.josm.data.coor.LatLon; 071import org.openstreetmap.josm.data.imagery.ImageryInfo; 072import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 073import org.openstreetmap.josm.data.imagery.TileLoaderFactory; 074import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 075import org.openstreetmap.josm.data.preferences.BooleanProperty; 076import org.openstreetmap.josm.data.preferences.IntegerProperty; 077import org.openstreetmap.josm.gui.ExtendedDialog; 078import org.openstreetmap.josm.gui.MapFrame; 079import org.openstreetmap.josm.gui.MapView; 080import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener; 081import org.openstreetmap.josm.gui.PleaseWaitRunnable; 082import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 083import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 084import org.openstreetmap.josm.gui.progress.ProgressMonitor; 085import org.openstreetmap.josm.gui.util.GuiHelper; 086import org.openstreetmap.josm.io.WMSLayerImporter; 087import org.openstreetmap.josm.tools.GBC; 088 089/** 090 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS 091 * 092 * It implements all standard functions of tilesource based layers: autozoom, tile reloads, layer saving, loading,etc. 093 * 094 * @author Upliner 095 * @author Wiktor Niesiobędzki 096 * @param <T> Tile Source class used for this layer 097 * @since 3715 098 * @since 8526 (copied from TMSLayer) 099 */ 100public abstract class AbstractTileSourceLayer<T extends AbstractTMSTileSource> extends ImageryLayer 101implements ImageObserver, TileLoaderListener, ZoomChangeListener { 102 private static final String PREFERENCE_PREFIX = "imagery.generic"; 103 104 /** maximum zoom level supported */ 105 public static final int MAX_ZOOM = 30; 106 /** minium zoom level supported */ 107 public static final int MIN_ZOOM = 2; 108 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13); 109 110 /** do set autozoom when creating a new layer */ 111 public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true); 112 /** do set autoload when creating a new layer */ 113 public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true); 114 /** do show errors per default */ 115 public static final BooleanProperty PROP_DEFAULT_SHOWERRORS = new BooleanProperty(PREFERENCE_PREFIX + ".default_showerrors", true); 116 /** minimum zoom level to show to user */ 117 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2); 118 /** maximum zoom level to show to user */ 119 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20); 120 121 //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false); 122 /** 123 * Zoomlevel at which tiles is currently downloaded. 124 * Initial zoom lvl is set to bestZoom 125 */ 126 public int currentZoomLevel; 127 private boolean needRedraw; 128 129 private final AttributionSupport attribution = new AttributionSupport(); 130 private final TileHolder clickedTileHolder = new TileHolder(); 131 132 // needed public access for session exporter 133 /** if layers changes automatically, when user zooms in */ 134 public boolean autoZoom = PROP_DEFAULT_AUTOZOOM.get(); 135 /** if layer automatically loads new tiles */ 136 public boolean autoLoad = PROP_DEFAULT_AUTOLOAD.get(); 137 /** if layer should show errors on tiles */ 138 public boolean showErrors = PROP_DEFAULT_SHOWERRORS.get(); 139 140 /** 141 * Offset between calculated zoom level and zoom level used to download and show tiles. Negative values will result in 142 * lower resolution of imagery useful in "retina" displays, positive values will result in higher resolution 143 */ 144 public static final IntegerProperty ZOOM_OFFSET = new IntegerProperty(PREFERENCE_PREFIX + ".zoom_offset", 0); 145 146 /* 147 * use MemoryTileCache instead of tileLoader JCS cache, as tileLoader caches only content (byte[] of image) 148 * and MemoryTileCache caches whole Tile. This gives huge performance improvement when a lot of tiles are visible 149 * in MapView (for example - when limiting min zoom in imagery) 150 * 151 * Use per-layer tileCache instance, as the more layers there are, the more tiles needs to be cached 152 */ 153 protected TileCache tileCache; // initialized together with tileSource 154 protected T tileSource; 155 protected TileLoader tileLoader; 156 157 private final MouseAdapter adapter = new MouseAdapter() { 158 @Override 159 public void mouseClicked(MouseEvent e) { 160 if (!isVisible()) return; 161 if (e.getButton() == MouseEvent.BUTTON3) { 162 clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY())); 163 new TileSourceLayerPopup().show(e.getComponent(), e.getX(), e.getY()); 164 } else if (e.getButton() == MouseEvent.BUTTON1) { 165 attribution.handleAttribution(e.getPoint(), true); 166 } 167 } 168 }; 169 /** 170 * Creates Tile Source based Imagery Layer based on Imagery Info 171 * @param info imagery info 172 */ 173 public AbstractTileSourceLayer(ImageryInfo info) { 174 super(info); 175 setBackgroundLayer(true); 176 this.setVisible(true); 177 } 178 179 protected abstract TileLoaderFactory getTileLoaderFactory(); 180 181 /** 182 * 183 * @param info imagery info 184 * @return TileSource for specified ImageryInfo 185 * @throws IllegalArgumentException when Imagery is not supported by layer 186 */ 187 protected abstract T getTileSource(ImageryInfo info); 188 189 protected Map<String, String> getHeaders(T tileSource) { 190 if (tileSource instanceof TemplatedTileSource) { 191 return ((TemplatedTileSource) tileSource).getHeaders(); 192 } 193 return null; 194 } 195 196 protected void initTileSource(T tileSource) { 197 attribution.initialize(tileSource); 198 199 currentZoomLevel = getBestZoom(); 200 201 Map<String, String> headers = getHeaders(tileSource); 202 203 tileLoader = getTileLoaderFactory().makeTileLoader(this, headers); 204 205 try { 206 if ("file".equalsIgnoreCase(new URL(tileSource.getBaseUrl()).getProtocol())) { 207 tileLoader = new OsmTileLoader(this); 208 } 209 } catch (MalformedURLException e) { 210 // ignore, assume that this is not a file 211 if (Main.isDebugEnabled()) { 212 Main.debug(e.getMessage()); 213 } 214 } 215 216 if (tileLoader == null) 217 tileLoader = new OsmTileLoader(this, headers); 218 219 tileCache = new MemoryTileCache(estimateTileCacheSize()); 220 } 221 222 @Override 223 public synchronized void tileLoadingFinished(Tile tile, boolean success) { 224 if (tile.hasError()) { 225 success = false; 226 tile.setImage(null); 227 } 228 tile.setLoaded(success); 229 needRedraw = true; 230 if (Main.map != null) { 231 Main.map.repaint(100); 232 } 233 if (Main.isDebugEnabled()) { 234 Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success); 235 } 236 } 237 238 /** 239 * Clears the tile cache. 240 * 241 * If the current tileLoader is an instance of OsmTileLoader, a new 242 * TmsTileClearController is created and passed to the according clearCache 243 * method. 244 * 245 * @param monitor not used in this implementation - as cache clear is instaneus 246 */ 247 public void clearTileCache(ProgressMonitor monitor) { 248 if (tileLoader instanceof CachedTileLoader) { 249 ((CachedTileLoader) tileLoader).clearCache(tileSource); 250 } 251 tileCache.clear(); 252 } 253 254 /** 255 * Initiates a repaint of Main.map 256 * 257 * @see Main#map 258 * @see MapFrame#repaint() 259 */ 260 protected void redraw() { 261 needRedraw = true; 262 if (isVisible()) Main.map.repaint(); 263 } 264 265 @Override 266 public void setGamma(double gamma) { 267 super.setGamma(gamma); 268 redraw(); 269 } 270 271 @Override 272 public void setSharpenLevel(double sharpenLevel) { 273 super.setSharpenLevel(sharpenLevel); 274 redraw(); 275 } 276 277 @Override 278 public void setColorfulness(double colorfulness) { 279 super.setColorfulness(colorfulness); 280 redraw(); 281 } 282 283 /** 284 * Marks layer as needing redraw on offset change 285 */ 286 @Override 287 public void setOffset(double dx, double dy) { 288 super.setOffset(dx, dy); 289 needRedraw = true; 290 } 291 292 293 /** 294 * Returns average number of screen pixels per tile pixel for current mapview 295 * @param zoom zoom level 296 * @return average number of screen pixels per tile pixel 297 */ 298 private double getScaleFactor(int zoom) { 299 if (!Main.isDisplayingMapView()) return 1; 300 MapView mv = Main.map.mapView; 301 LatLon topLeft = mv.getLatLon(0, 0); 302 LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight()); 303 TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom); 304 TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom); 305 306 int screenPixels = mv.getWidth()*mv.getHeight(); 307 double tilePixels = Math.abs((t2.getY()-t1.getY())*(t2.getX()-t1.getX())*tileSource.getTileSize()*tileSource.getTileSize()); 308 if (screenPixels == 0 || tilePixels == 0) return 1; 309 return screenPixels/tilePixels; 310 } 311 312 protected int getBestZoom() { 313 double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view 314 double result = Math.log(factor)/Math.log(2)/2; 315 /* 316 * Math.log(factor)/Math.log(2) - gives log base 2 of factor 317 * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2 318 * 319 * ZOOM_OFFSET controls, whether we work with overzoomed or underzoomed tiles. Positive ZOOM_OFFSET 320 * is for working with underzoomed tiles (higher quality when working with aerial imagery), negative ZOOM_OFFSET 321 * is for working with overzoomed tiles (big, pixelated), which is good when working with high-dpi screens and/or 322 * maps as a imagery layer 323 */ 324 325 int intResult = (int) Math.round(result + 1 + ZOOM_OFFSET.get() / 1.9); 326 327 intResult = Math.min(intResult, getMaxZoomLvl()); 328 intResult = Math.max(intResult, getMinZoomLvl()); 329 return intResult; 330 } 331 332 private static boolean actionSupportLayers(List<Layer> layers) { 333 return layers.size() == 1 && layers.get(0) instanceof TMSLayer; 334 } 335 336 private final class ShowTileInfoAction extends AbstractAction { 337 338 private ShowTileInfoAction() { 339 super(tr("Show tile info")); 340 } 341 342 private String getSizeString(int size) { 343 StringBuilder ret = new StringBuilder(); 344 return ret.append(size).append('x').append(size).toString(); 345 } 346 347 private JTextField createTextField(String text) { 348 JTextField ret = new JTextField(text); 349 ret.setEditable(false); 350 ret.setBorder(BorderFactory.createEmptyBorder()); 351 return ret; 352 } 353 354 @Override 355 public void actionPerformed(ActionEvent ae) { 356 Tile clickedTile = clickedTileHolder.getTile(); 357 if (clickedTile != null) { 358 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")}); 359 JPanel panel = new JPanel(new GridBagLayout()); 360 Rectangle displaySize = tileToRect(clickedTile); 361 String url = ""; 362 try { 363 url = clickedTile.getUrl(); 364 } catch (IOException e) { 365 // silence exceptions 366 if (Main.isTraceEnabled()) { 367 Main.trace(e.getMessage()); 368 } 369 } 370 371 String[][] content = { 372 {"Tile name", clickedTile.getKey()}, 373 {"Tile url", url}, 374 {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) }, 375 {"Tile display size", new StringBuilder().append(displaySize.width).append('x').append(displaySize.height).toString()}, 376 }; 377 378 for (String[] entry: content) { 379 panel.add(new JLabel(tr(entry[0]) + ':'), GBC.std()); 380 panel.add(GBC.glue(5, 0), GBC.std()); 381 panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL)); 382 } 383 384 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) { 385 panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ':'), GBC.std()); 386 panel.add(GBC.glue(5, 0), GBC.std()); 387 String value = e.getValue(); 388 if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) { 389 value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value))); 390 } 391 panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL)); 392 393 } 394 ed.setIcon(JOptionPane.INFORMATION_MESSAGE); 395 ed.setContent(panel); 396 ed.showDialog(); 397 } 398 } 399 } 400 401 private final class LoadTileAction extends AbstractAction { 402 403 private LoadTileAction() { 404 super(tr("Load tile")); 405 } 406 407 @Override 408 public void actionPerformed(ActionEvent ae) { 409 Tile clickedTile = clickedTileHolder.getTile(); 410 if (clickedTile != null) { 411 loadTile(clickedTile, true); 412 redraw(); 413 } 414 } 415 } 416 417 private class AutoZoomAction extends AbstractAction implements LayerAction { 418 AutoZoomAction() { 419 super(tr("Auto zoom")); 420 } 421 422 @Override 423 public void actionPerformed(ActionEvent ae) { 424 autoZoom = !autoZoom; 425 if (autoZoom && getBestZoom() != currentZoomLevel) { 426 setZoomLevel(getBestZoom()); 427 redraw(); 428 } 429 } 430 431 @Override 432 public Component createMenuComponent() { 433 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 434 item.setSelected(autoZoom); 435 return item; 436 } 437 438 @Override 439 public boolean supportLayers(List<Layer> layers) { 440 return actionSupportLayers(layers); 441 } 442 } 443 444 private class AutoLoadTilesAction extends AbstractAction implements LayerAction { 445 AutoLoadTilesAction() { 446 super(tr("Auto load tiles")); 447 } 448 449 @Override 450 public void actionPerformed(ActionEvent ae) { 451 autoLoad = !autoLoad; 452 if (autoLoad) redraw(); 453 } 454 455 @Override 456 public Component createMenuComponent() { 457 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 458 item.setSelected(autoLoad); 459 return item; 460 } 461 462 @Override 463 public boolean supportLayers(List<Layer> layers) { 464 return actionSupportLayers(layers); 465 } 466 } 467 468 private class ShowErrorsAction extends AbstractAction implements LayerAction { 469 ShowErrorsAction() { 470 super(tr("Show errors")); 471 } 472 473 @Override 474 public void actionPerformed(ActionEvent ae) { 475 showErrors = !showErrors; 476 redraw(); 477 } 478 479 @Override 480 public Component createMenuComponent() { 481 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 482 item.setSelected(showErrors); 483 return item; 484 } 485 486 @Override 487 public boolean supportLayers(List<Layer> layers) { 488 return actionSupportLayers(layers); 489 } 490 } 491 492 private class LoadAllTilesAction extends AbstractAction { 493 LoadAllTilesAction() { 494 super(tr("Load all tiles")); 495 } 496 497 @Override 498 public void actionPerformed(ActionEvent ae) { 499 loadAllTiles(true); 500 redraw(); 501 } 502 } 503 504 private class LoadErroneusTilesAction extends AbstractAction { 505 LoadErroneusTilesAction() { 506 super(tr("Load all error tiles")); 507 } 508 509 @Override 510 public void actionPerformed(ActionEvent ae) { 511 loadAllErrorTiles(true); 512 redraw(); 513 } 514 } 515 516 private class ZoomToNativeLevelAction extends AbstractAction { 517 ZoomToNativeLevelAction() { 518 super(tr("Zoom to native resolution")); 519 } 520 521 @Override 522 public void actionPerformed(ActionEvent ae) { 523 double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel)); 524 Main.map.mapView.zoomToFactor(newFactor); 525 redraw(); 526 } 527 } 528 529 private class ZoomToBestAction extends AbstractAction { 530 ZoomToBestAction() { 531 super(tr("Change resolution")); 532 setEnabled(!autoZoom && getBestZoom() != currentZoomLevel); 533 } 534 535 @Override 536 public void actionPerformed(ActionEvent ae) { 537 setZoomLevel(getBestZoom()); 538 redraw(); 539 } 540 } 541 542 private class IncreaseZoomAction extends AbstractAction { 543 IncreaseZoomAction() { 544 super(tr("Increase zoom")); 545 setEnabled(!autoZoom && zoomIncreaseAllowed()); 546 } 547 548 @Override 549 public void actionPerformed(ActionEvent ae) { 550 increaseZoomLevel(); 551 redraw(); 552 } 553 } 554 555 private class DecreaseZoomAction extends AbstractAction { 556 DecreaseZoomAction() { 557 super(tr("Decrease zoom")); 558 setEnabled(!autoZoom && zoomDecreaseAllowed()); 559 } 560 561 @Override 562 public void actionPerformed(ActionEvent ae) { 563 decreaseZoomLevel(); 564 redraw(); 565 } 566 } 567 568 private class FlushTileCacheAction extends AbstractAction { 569 FlushTileCacheAction() { 570 super(tr("Flush tile cache")); 571 setEnabled(tileLoader instanceof CachedTileLoader); 572 } 573 574 @Override 575 public void actionPerformed(ActionEvent ae) { 576 new PleaseWaitRunnable(tr("Flush tile cache")) { 577 @Override 578 protected void realRun() { 579 clearTileCache(getProgressMonitor()); 580 } 581 582 @Override 583 protected void finish() { 584 // empty - flush is instaneus 585 } 586 587 @Override 588 protected void cancel() { 589 // empty - flush is instaneus 590 } 591 }.run(); 592 } 593 } 594 595 /** 596 * Simple class to keep clickedTile within hookUpMapView 597 */ 598 private static final class TileHolder { 599 private Tile t; 600 601 public Tile getTile() { 602 return t; 603 } 604 605 public void setTile(Tile t) { 606 this.t = t; 607 } 608 } 609 610 /** 611 * Creates popup menu items and binds to mouse actions 612 */ 613 @Override 614 public void hookUpMapView() { 615 // this needs to be here and not in constructor to allow empty TileSource class construction 616 // using SessionWriter 617 initializeIfRequired(); 618 619 super.hookUpMapView(); 620 } 621 622 @Override 623 public LayerPainter attachToMapView(MapViewEvent event) { 624 initializeIfRequired(); 625 626 event.getMapView().addMouseListener(adapter); 627 MapView.addZoomChangeListener(AbstractTileSourceLayer.this); 628 629 if (this instanceof NativeScaleLayer) { 630 event.getMapView().setNativeScaleLayer((NativeScaleLayer) this); 631 } 632 633 // FIXME: why do we need this? Without this, if you add a WMS layer and do not move the mouse, sometimes, tiles do not 634 // start loading. 635 // FIXME: Check if this is still required. 636 event.getMapView().repaint(500); 637 638 return super.attachToMapView(event); 639 } 640 641 private void initializeIfRequired() { 642 if (tileSource == null) { 643 tileSource = getTileSource(info); 644 if (tileSource == null) { 645 throw new IllegalArgumentException(tr("Failed to create tile source")); 646 } 647 checkLayerMemoryDoesNotExceedMaximum(); 648 // check if projection is supported 649 projectionChanged(null, Main.getProjection()); 650 initTileSource(this.tileSource); 651 } 652 } 653 654 @Override 655 protected LayerPainter createMapViewPainter(MapViewEvent event) { 656 return new CompatibilityModeLayerPainter() { 657 @Override 658 public void detachFromMapView(MapViewEvent event) { 659 event.getMapView().removeMouseListener(adapter); 660 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this); 661 super.detachFromMapView(event); 662 } 663 }; 664 } 665 666 /** 667 * Tile source layer popup menu. 668 */ 669 public class TileSourceLayerPopup extends JPopupMenu { 670 /** 671 * Constructs a new {@code TileSourceLayerPopup}. 672 */ 673 public TileSourceLayerPopup() { 674 for (Action a : getCommonEntries()) { 675 if (a instanceof LayerAction) { 676 add(((LayerAction) a).createMenuComponent()); 677 } else { 678 add(new JMenuItem(a)); 679 } 680 } 681 add(new JSeparator()); 682 add(new JMenuItem(new LoadTileAction())); 683 add(new JMenuItem(new ShowTileInfoAction())); 684 } 685 } 686 687 @Override 688 protected long estimateMemoryUsage() { 689 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize(); 690 } 691 692 protected int estimateTileCacheSize() { 693 Dimension screenSize = GuiHelper.getMaximumScreenSize(); 694 int height = screenSize.height; 695 int width = screenSize.width; 696 int tileSize = 256; // default tile size 697 if (tileSource != null) { 698 tileSize = tileSource.getTileSize(); 699 } 700 // as we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that 701 int visibileTiles = (int) (Math.ceil((double) height / tileSize + 1) * Math.ceil((double) width / tileSize + 1)); 702 // add 10% for tiles from different zoom levels 703 int ret = (int) Math.ceil( 704 Math.pow(2d, ZOOM_OFFSET.get()) * visibileTiles // use offset to decide, how many tiles are visible 705 * 2); 706 Main.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibileTiles, ret); 707 return ret; 708 } 709 710 /** 711 * Checks zoom level against settings 712 * @param maxZoomLvl zoom level to check 713 * @param ts tile source to crosscheck with 714 * @return maximum zoom level, not higher than supported by tilesource nor set by the user 715 */ 716 public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) { 717 if (maxZoomLvl > MAX_ZOOM) { 718 maxZoomLvl = MAX_ZOOM; 719 } 720 if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) { 721 maxZoomLvl = PROP_MIN_ZOOM_LVL.get(); 722 } 723 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) { 724 maxZoomLvl = ts.getMaxZoom(); 725 } 726 return maxZoomLvl; 727 } 728 729 /** 730 * Checks zoom level against settings 731 * @param minZoomLvl zoom level to check 732 * @param ts tile source to crosscheck with 733 * @return minimum zoom level, not higher than supported by tilesource nor set by the user 734 */ 735 public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) { 736 if (minZoomLvl < MIN_ZOOM) { 737 minZoomLvl = MIN_ZOOM; 738 } 739 if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) { 740 minZoomLvl = getMaxZoomLvl(ts); 741 } 742 if (ts != null && ts.getMinZoom() > minZoomLvl) { 743 minZoomLvl = ts.getMinZoom(); 744 } 745 return minZoomLvl; 746 } 747 748 /** 749 * @param ts TileSource for which we want to know maximum zoom level 750 * @return maximum max zoom level, that will be shown on layer 751 */ 752 public static int getMaxZoomLvl(TileSource ts) { 753 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts); 754 } 755 756 /** 757 * @param ts TileSource for which we want to know minimum zoom level 758 * @return minimum zoom level, that will be shown on layer 759 */ 760 public static int getMinZoomLvl(TileSource ts) { 761 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts); 762 } 763 764 /** 765 * Sets maximum zoom level, that layer will attempt show 766 * @param maxZoomLvl maximum zoom level 767 */ 768 public static void setMaxZoomLvl(int maxZoomLvl) { 769 PROP_MAX_ZOOM_LVL.put(checkMaxZoomLvl(maxZoomLvl, null)); 770 } 771 772 /** 773 * Sets minimum zoom level, that layer will attempt show 774 * @param minZoomLvl minimum zoom level 775 */ 776 public static void setMinZoomLvl(int minZoomLvl) { 777 PROP_MIN_ZOOM_LVL.put(checkMinZoomLvl(minZoomLvl, null)); 778 } 779 780 /** 781 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all 782 * changes to visible map (panning/zooming) 783 */ 784 @Override 785 public void zoomChanged() { 786 if (Main.isDebugEnabled()) { 787 Main.debug("zoomChanged(): " + currentZoomLevel); 788 } 789 if (tileLoader instanceof TMSCachedTileLoader) { 790 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks(); 791 } 792 needRedraw = true; 793 } 794 795 protected int getMaxZoomLvl() { 796 if (info.getMaxZoom() != 0) 797 return checkMaxZoomLvl(info.getMaxZoom(), tileSource); 798 else 799 return getMaxZoomLvl(tileSource); 800 } 801 802 protected int getMinZoomLvl() { 803 if (info.getMinZoom() != 0) 804 return checkMinZoomLvl(info.getMinZoom(), tileSource); 805 else 806 return getMinZoomLvl(tileSource); 807 } 808 809 /** 810 * 811 * @return if its allowed to zoom in 812 */ 813 public boolean zoomIncreaseAllowed() { 814 boolean zia = currentZoomLevel < this.getMaxZoomLvl(); 815 if (Main.isDebugEnabled()) { 816 Main.debug("zoomIncreaseAllowed(): " + zia + ' ' + currentZoomLevel + " vs. " + this.getMaxZoomLvl()); 817 } 818 return zia; 819 } 820 821 /** 822 * Zoom in, go closer to map. 823 * 824 * @return true, if zoom increasing was successful, false otherwise 825 */ 826 public boolean increaseZoomLevel() { 827 if (zoomIncreaseAllowed()) { 828 currentZoomLevel++; 829 if (Main.isDebugEnabled()) { 830 Main.debug("increasing zoom level to: " + currentZoomLevel); 831 } 832 zoomChanged(); 833 } else { 834 Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+ 835 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached."); 836 return false; 837 } 838 return true; 839 } 840 841 /** 842 * Sets the zoom level of the layer 843 * @param zoom zoom level 844 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels 845 */ 846 public boolean setZoomLevel(int zoom) { 847 if (zoom == currentZoomLevel) return true; 848 if (zoom > this.getMaxZoomLvl()) return false; 849 if (zoom < this.getMinZoomLvl()) return false; 850 currentZoomLevel = zoom; 851 zoomChanged(); 852 return true; 853 } 854 855 /** 856 * Check if zooming out is allowed 857 * 858 * @return true, if zooming out is allowed (currentZoomLevel > minZoomLevel) 859 */ 860 public boolean zoomDecreaseAllowed() { 861 boolean zda = currentZoomLevel > this.getMinZoomLvl(); 862 if (Main.isDebugEnabled()) { 863 Main.debug("zoomDecreaseAllowed(): " + zda + ' ' + currentZoomLevel + " vs. " + this.getMinZoomLvl()); 864 } 865 return zda; 866 } 867 868 /** 869 * Zoom out from map. 870 * 871 * @return true, if zoom increasing was successfull, false othervise 872 */ 873 public boolean decreaseZoomLevel() { 874 if (zoomDecreaseAllowed()) { 875 if (Main.isDebugEnabled()) { 876 Main.debug("decreasing zoom level to: " + currentZoomLevel); 877 } 878 currentZoomLevel--; 879 zoomChanged(); 880 } else { 881 return false; 882 } 883 return true; 884 } 885 886 /* 887 * We use these for quick, hackish calculations. They 888 * are temporary only and intentionally not inserted 889 * into the tileCache. 890 */ 891 private Tile tempCornerTile(Tile t) { 892 int x = t.getXtile() + 1; 893 int y = t.getYtile() + 1; 894 int zoom = t.getZoom(); 895 Tile tile = getTile(x, y, zoom); 896 if (tile != null) 897 return tile; 898 return new Tile(tileSource, x, y, zoom); 899 } 900 901 private Tile getOrCreateTile(int x, int y, int zoom) { 902 Tile tile = getTile(x, y, zoom); 903 if (tile == null) { 904 tile = new Tile(tileSource, x, y, zoom); 905 tileCache.addTile(tile); 906 tile.loadPlaceholderFromCache(tileCache); 907 } 908 return tile; 909 } 910 911 /** 912 * Returns tile at given position. 913 * This can and will return null for tiles that are not already in the cache. 914 * @param x tile number on the x axis of the tile to be retrieved 915 * @param y tile number on the y axis of the tile to be retrieved 916 * @param zoom zoom level of the tile to be retrieved 917 * @return tile at given position 918 */ 919 private Tile getTile(int x, int y, int zoom) { 920 if (x < tileSource.getTileXMin(zoom) || x > tileSource.getTileXMax(zoom) 921 || y < tileSource.getTileYMin(zoom) || y > tileSource.getTileYMax(zoom)) 922 return null; 923 return tileCache.getTile(tileSource, x, y, zoom); 924 } 925 926 private boolean loadTile(Tile tile, boolean force) { 927 if (tile == null) 928 return false; 929 if (!force && (tile.isLoaded() || tile.hasError())) 930 return false; 931 if (tile.isLoading()) 932 return false; 933 tileLoader.createTileLoaderJob(tile).submit(force); 934 return true; 935 } 936 937 private TileSet getVisibleTileSet() { 938 MapView mv = Main.map.mapView; 939 EastNorth topLeft = mv.getEastNorth(0, 0); 940 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight()); 941 return new TileSet(topLeft, botRight, currentZoomLevel); 942 } 943 944 protected void loadAllTiles(boolean force) { 945 TileSet ts = getVisibleTileSet(); 946 947 // if there is more than 18 tiles on screen in any direction, do not load all tiles! 948 if (ts.tooLarge()) { 949 Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!"); 950 return; 951 } 952 ts.loadAllTiles(force); 953 } 954 955 protected void loadAllErrorTiles(boolean force) { 956 TileSet ts = getVisibleTileSet(); 957 ts.loadAllErrorTiles(force); 958 } 959 960 @Override 961 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { 962 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0; 963 needRedraw = true; 964 if (Main.isDebugEnabled()) { 965 Main.debug("imageUpdate() done: " + done + " calling repaint"); 966 } 967 Main.map.repaint(done ? 0 : 100); 968 return !done; 969 } 970 971 private boolean imageLoaded(Image i) { 972 if (i == null) 973 return false; 974 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this); 975 if ((status & ALLBITS) != 0) 976 return true; 977 return false; 978 } 979 980 /** 981 * Returns the image for the given tile image is loaded. 982 * Otherwise returns null. 983 * 984 * @param tile the Tile for which the image should be returned 985 * @return the image of the tile or null. 986 */ 987 private Image getLoadedTileImage(Tile tile) { 988 Image img = tile.getImage(); 989 if (!imageLoaded(img)) 990 return null; 991 return img; 992 } 993 994 private Rectangle tileToRect(Tile t1) { 995 /* 996 * We need to get a box in which to draw, so advance by one tile in 997 * each direction to find the other corner of the box. 998 * Note: this somewhat pollutes the tile cache 999 */ 1000 Tile t2 = tempCornerTile(t1); 1001 Rectangle rect = new Rectangle(pixelPos(t1)); 1002 rect.add(pixelPos(t2)); 1003 return rect; 1004 } 1005 1006 // 'source' is the pixel coordinates for the area that 1007 // the img is capable of filling in. However, we probably 1008 // only want a portion of it. 1009 // 1010 // 'border' is the screen cordinates that need to be drawn. 1011 // We must not draw outside of it. 1012 private void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) { 1013 Rectangle target = source; 1014 1015 // If a border is specified, only draw the intersection 1016 // if what we have combined with what we are supposed to draw. 1017 if (border != null) { 1018 target = source.intersection(border); 1019 if (Main.isDebugEnabled()) { 1020 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target); 1021 } 1022 } 1023 1024 // All of the rectangles are in screen coordinates. We need 1025 // to how these correlate to the sourceImg pixels. We could 1026 // avoid doing this by scaling the image up to the 'source' size, 1027 // but this should be cheaper. 1028 // 1029 // In some projections, x any y are scaled differently enough to 1030 // cause a pixel or two of fudge. Calculate them separately. 1031 double imageYScaling = sourceImg.getHeight(this) / source.getHeight(); 1032 double imageXScaling = sourceImg.getWidth(this) / source.getWidth(); 1033 1034 // How many pixels into the 'source' rectangle are we drawing? 1035 int screenXoffset = target.x - source.x; 1036 int screenYoffset = target.y - source.y; 1037 // And how many pixels into the image itself does that correlate to? 1038 int imgXoffset = (int) (screenXoffset * imageXScaling + 0.5); 1039 int imgYoffset = (int) (screenYoffset * imageYScaling + 0.5); 1040 // Now calculate the other corner of the image that we need 1041 // by scaling the 'target' rectangle's dimensions. 1042 int imgXend = imgXoffset + (int) (target.getWidth() * imageXScaling + 0.5); 1043 int imgYend = imgYoffset + (int) (target.getHeight() * imageYScaling + 0.5); 1044 1045 if (Main.isDebugEnabled()) { 1046 Main.debug("drawing image into target rect: " + target); 1047 } 1048 g.drawImage(sourceImg, 1049 target.x, target.y, 1050 target.x + target.width, target.y + target.height, 1051 imgXoffset, imgYoffset, 1052 imgXend, imgYend, 1053 this); 1054 if (PROP_FADE_AMOUNT.get() != 0) { 1055 // dimm by painting opaque rect... 1056 g.setColor(getFadeColorWithAlpha()); 1057 g.fillRect(target.x, target.y, 1058 target.width, target.height); 1059 } 1060 } 1061 1062 // This function is called for several zoom levels, not just 1063 // the current one. It should not trigger any tiles to be 1064 // downloaded. It should also avoid polluting the tile cache 1065 // with any tiles since these tiles are not mandatory. 1066 // 1067 // The "border" tile tells us the boundaries of where we may 1068 // draw. It will not be from the zoom level that is being 1069 // drawn currently. If drawing the displayZoomLevel, 1070 // border is null and we draw the entire tile set. 1071 private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) { 1072 if (zoom <= 0) return Collections.emptyList(); 1073 Rectangle borderRect = null; 1074 if (border != null) { 1075 borderRect = tileToRect(border); 1076 } 1077 List<Tile> missedTiles = new LinkedList<>(); 1078 // The callers of this code *require* that we return any tiles 1079 // that we do not draw in missedTiles. ts.allExistingTiles() by 1080 // default will only return already-existing tiles. However, we 1081 // need to return *all* tiles to the callers, so force creation here. 1082 for (Tile tile : ts.allTilesCreate()) { 1083 Image img = getLoadedTileImage(tile); 1084 if (img == null || tile.hasError()) { 1085 if (Main.isDebugEnabled()) { 1086 Main.debug("missed tile: " + tile); 1087 } 1088 missedTiles.add(tile); 1089 continue; 1090 } 1091 1092 // applying all filters to this layer 1093 img = applyImageProcessors((BufferedImage) img); 1094 1095 Rectangle sourceRect = tileToRect(tile); 1096 if (borderRect != null && !sourceRect.intersects(borderRect)) { 1097 continue; 1098 } 1099 drawImageInside(g, img, sourceRect, borderRect); 1100 } 1101 return missedTiles; 1102 } 1103 1104 private void myDrawString(Graphics g, String text, int x, int y) { 1105 Color oldColor = g.getColor(); 1106 String textToDraw = text; 1107 if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) { 1108 // text longer than tile size, split it 1109 StringBuilder line = new StringBuilder(); 1110 StringBuilder ret = new StringBuilder(); 1111 for (String s: text.split(" ")) { 1112 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) { 1113 ret.append(line).append('\n'); 1114 line.setLength(0); 1115 } 1116 line.append(s).append(' '); 1117 } 1118 ret.append(line); 1119 textToDraw = ret.toString(); 1120 } 1121 int offset = 0; 1122 for (String s: textToDraw.split("\n")) { 1123 g.setColor(Color.black); 1124 g.drawString(s, x + 1, y + offset + 1); 1125 g.setColor(oldColor); 1126 g.drawString(s, x, y + offset); 1127 offset += g.getFontMetrics().getHeight() + 3; 1128 } 1129 } 1130 1131 private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) { 1132 int fontHeight = g.getFontMetrics().getHeight(); 1133 if (tile == null) 1134 return; 1135 Point p = pixelPos(t); 1136 int texty = p.y + 2 + fontHeight; 1137 1138 /*if (PROP_DRAW_DEBUG.get()) { 1139 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty); 1140 texty += 1 + fontHeight; 1141 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) { 1142 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty); 1143 texty += 1 + fontHeight; 1144 } 1145 }*/ 1146 1147 /*String tileStatus = tile.getStatus(); 1148 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) { 1149 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty); 1150 texty += 1 + fontHeight; 1151 }*/ 1152 1153 if (tile.hasError() && showErrors) { 1154 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty); 1155 //texty += 1 + fontHeight; 1156 } 1157 1158 int xCursor = -1; 1159 int yCursor = -1; 1160 if (Main.isDebugEnabled()) { 1161 if (yCursor < t.getYtile()) { 1162 if (t.getYtile() % 32 == 31) { 1163 g.fillRect(0, p.y - 1, mv.getWidth(), 3); 1164 } else { 1165 g.drawLine(0, p.y, mv.getWidth(), p.y); 1166 } 1167 //yCursor = t.getYtile(); 1168 } 1169 // This draws the vertical lines for the entire column. Only draw them for the top tile in the column. 1170 if (xCursor < t.getXtile()) { 1171 if (t.getXtile() % 32 == 0) { 1172 // level 7 tile boundary 1173 g.fillRect(p.x - 1, 0, 3, mv.getHeight()); 1174 } else { 1175 g.drawLine(p.x, 0, p.x, mv.getHeight()); 1176 } 1177 //xCursor = t.getXtile(); 1178 } 1179 } 1180 } 1181 1182 private Point pixelPos(LatLon ll) { 1183 return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy())); 1184 } 1185 1186 private Point pixelPos(Tile t) { 1187 ICoordinate coord = tileSource.tileXYToLatLon(t); 1188 return pixelPos(new LatLon(coord)); 1189 } 1190 1191 private LatLon getShiftedLatLon(EastNorth en) { 1192 return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy())); 1193 } 1194 1195 private ICoordinate getShiftedCoord(EastNorth en) { 1196 return getShiftedLatLon(en).toCoordinate(); 1197 } 1198 1199 private final TileSet nullTileSet = new TileSet((LatLon) null, (LatLon) null, 0); 1200 1201 private final class TileSet { 1202 int x0, x1, y0, y1; 1203 int zoom; 1204 1205 /** 1206 * Create a TileSet by EastNorth bbox taking a layer shift in account 1207 * @param topLeft top-left lat/lon 1208 * @param botRight bottom-right lat/lon 1209 * @param zoom zoom level 1210 */ 1211 private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) { 1212 this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight), zoom); 1213 } 1214 1215 /** 1216 * Create a TileSet by known LatLon bbox without layer shift correction 1217 * @param topLeft top-left lat/lon 1218 * @param botRight bottom-right lat/lon 1219 * @param zoom zoom level 1220 */ 1221 private TileSet(LatLon topLeft, LatLon botRight, int zoom) { 1222 this.zoom = zoom; 1223 if (zoom == 0) 1224 return; 1225 1226 TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom); 1227 TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom); 1228 1229 x0 = t1.getXIndex(); 1230 y0 = t1.getYIndex(); 1231 x1 = t2.getXIndex(); 1232 y1 = t2.getYIndex(); 1233 double centerLon = getShiftedLatLon(Main.map.mapView.getCenter()).lon(); 1234 1235 if (topLeft.lon() > centerLon) { 1236 x0 = tileSource.getTileXMin(zoom); 1237 } 1238 if (botRight.lon() < centerLon) { 1239 x1 = tileSource.getTileXMax(zoom); 1240 } 1241 1242 if (x0 > x1) { 1243 int tmp = x0; 1244 x0 = x1; 1245 x1 = tmp; 1246 } 1247 if (y0 > y1) { 1248 int tmp = y0; 1249 y0 = y1; 1250 y1 = tmp; 1251 } 1252 1253 if (x0 < tileSource.getTileXMin(zoom)) { 1254 x0 = tileSource.getTileXMin(zoom); 1255 } 1256 if (y0 < tileSource.getTileYMin(zoom)) { 1257 y0 = tileSource.getTileYMin(zoom); 1258 } 1259 if (x1 > tileSource.getTileXMax(zoom)) { 1260 x1 = tileSource.getTileXMax(zoom); 1261 } 1262 if (y1 > tileSource.getTileYMax(zoom)) { 1263 y1 = tileSource.getTileYMax(zoom); 1264 } 1265 } 1266 1267 private boolean tooSmall() { 1268 return this.tilesSpanned() < 2.1; 1269 } 1270 1271 private boolean tooLarge() { 1272 return insane() || this.tilesSpanned() > 20; 1273 } 1274 1275 private boolean insane() { 1276 return tileCache == null || size() > tileCache.getCacheSize(); 1277 } 1278 1279 private double tilesSpanned() { 1280 return Math.sqrt(1.0 * this.size()); 1281 } 1282 1283 private int size() { 1284 int xSpan = x1 - x0 + 1; 1285 int ySpan = y1 - y0 + 1; 1286 return xSpan * ySpan; 1287 } 1288 1289 /* 1290 * Get all tiles represented by this TileSet that are 1291 * already in the tileCache. 1292 */ 1293 private List<Tile> allExistingTiles() { 1294 return this.__allTiles(false); 1295 } 1296 1297 private List<Tile> allTilesCreate() { 1298 return this.__allTiles(true); 1299 } 1300 1301 private List<Tile> __allTiles(boolean create) { 1302 // Tileset is either empty or too large 1303 if (zoom == 0 || this.insane()) 1304 return Collections.emptyList(); 1305 List<Tile> ret = new ArrayList<>(); 1306 for (int x = x0; x <= x1; x++) { 1307 for (int y = y0; y <= y1; y++) { 1308 Tile t; 1309 if (create) { 1310 t = getOrCreateTile(x, y, zoom); 1311 } else { 1312 t = getTile(x, y, zoom); 1313 } 1314 if (t != null) { 1315 ret.add(t); 1316 } 1317 } 1318 } 1319 return ret; 1320 } 1321 1322 private List<Tile> allLoadedTiles() { 1323 List<Tile> ret = new ArrayList<>(); 1324 for (Tile t : this.allExistingTiles()) { 1325 if (t.isLoaded()) 1326 ret.add(t); 1327 } 1328 return ret; 1329 } 1330 1331 /** 1332 * @return comparator, that sorts the tiles from the center to the edge of the current screen 1333 */ 1334 private Comparator<Tile> getTileDistanceComparator() { 1335 final int centerX = (int) Math.ceil((x0 + x1) / 2d); 1336 final int centerY = (int) Math.ceil((y0 + y1) / 2d); 1337 return new Comparator<Tile>() { 1338 private int getDistance(Tile t) { 1339 return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY); 1340 } 1341 1342 @Override 1343 public int compare(Tile o1, Tile o2) { 1344 int distance1 = getDistance(o1); 1345 int distance2 = getDistance(o2); 1346 return Integer.compare(distance1, distance2); 1347 } 1348 }; 1349 } 1350 1351 private void loadAllTiles(boolean force) { 1352 if (!autoLoad && !force) 1353 return; 1354 List<Tile> allTiles = allTilesCreate(); 1355 Collections.sort(allTiles, getTileDistanceComparator()); 1356 for (Tile t : allTiles) { 1357 loadTile(t, force); 1358 } 1359 } 1360 1361 private void loadAllErrorTiles(boolean force) { 1362 if (!autoLoad && !force) 1363 return; 1364 for (Tile t : this.allTilesCreate()) { 1365 if (t.hasError()) { 1366 tileLoader.createTileLoaderJob(t).submit(force); 1367 } 1368 } 1369 } 1370 } 1371 1372 private static class TileSetInfo { 1373 public boolean hasVisibleTiles; 1374 public boolean hasOverzoomedTiles; 1375 public boolean hasLoadingTiles; 1376 } 1377 1378 private static <S extends AbstractTMSTileSource> TileSetInfo getTileSetInfo(AbstractTileSourceLayer<S>.TileSet ts) { 1379 List<Tile> allTiles = ts.allExistingTiles(); 1380 TileSetInfo result = new TileSetInfo(); 1381 result.hasLoadingTiles = allTiles.size() < ts.size(); 1382 for (Tile t : allTiles) { 1383 if ("no-tile".equals(t.getValue("tile-info"))) { 1384 result.hasOverzoomedTiles = true; 1385 } 1386 1387 if (t.isLoaded()) { 1388 if (!t.hasError()) { 1389 result.hasVisibleTiles = true; 1390 } 1391 } else if (t.isLoading()) { 1392 result.hasLoadingTiles = true; 1393 } 1394 } 1395 return result; 1396 } 1397 1398 private class DeepTileSet { 1399 private final EastNorth topLeft, botRight; 1400 private final int minZoom, maxZoom; 1401 private final TileSet[] tileSets; 1402 private final TileSetInfo[] tileSetInfos; 1403 1404 @SuppressWarnings("unchecked") 1405 DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) { 1406 this.topLeft = topLeft; 1407 this.botRight = botRight; 1408 this.minZoom = minZoom; 1409 this.maxZoom = maxZoom; 1410 this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1]; 1411 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1]; 1412 } 1413 1414 public TileSet getTileSet(int zoom) { 1415 if (zoom < minZoom) 1416 return nullTileSet; 1417 synchronized (tileSets) { 1418 TileSet ts = tileSets[zoom-minZoom]; 1419 if (ts == null) { 1420 ts = new TileSet(topLeft, botRight, zoom); 1421 tileSets[zoom-minZoom] = ts; 1422 } 1423 return ts; 1424 } 1425 } 1426 1427 public TileSetInfo getTileSetInfo(int zoom) { 1428 if (zoom < minZoom) 1429 return new TileSetInfo(); 1430 synchronized (tileSetInfos) { 1431 TileSetInfo tsi = tileSetInfos[zoom-minZoom]; 1432 if (tsi == null) { 1433 tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom)); 1434 tileSetInfos[zoom-minZoom] = tsi; 1435 } 1436 return tsi; 1437 } 1438 } 1439 } 1440 1441 @Override 1442 public void paint(Graphics2D g, MapView mv, Bounds bounds) { 1443 EastNorth topLeft = mv.getEastNorth(0, 0); 1444 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight()); 1445 1446 if (botRight.east() == 0 || botRight.north() == 0) { 1447 /*Main.debug("still initializing??");*/ 1448 // probably still initializing 1449 return; 1450 } 1451 1452 needRedraw = false; 1453 1454 int zoom = currentZoomLevel; 1455 if (autoZoom) { 1456 zoom = getBestZoom(); 1457 } 1458 1459 DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom); 1460 TileSet ts = dts.getTileSet(zoom); 1461 1462 int displayZoomLevel = zoom; 1463 1464 boolean noTilesAtZoom = false; 1465 if (autoZoom && autoLoad) { 1466 // Auto-detection of tilesource maxzoom (currently fully works only for Bing) 1467 TileSetInfo tsi = dts.getTileSetInfo(zoom); 1468 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) { 1469 noTilesAtZoom = true; 1470 } 1471 // Find highest zoom level with at least one visible tile 1472 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) { 1473 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) { 1474 displayZoomLevel = tmpZoom; 1475 break; 1476 } 1477 } 1478 // Do binary search between currentZoomLevel and displayZoomLevel 1479 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) { 1480 zoom = (zoom + displayZoomLevel)/2; 1481 tsi = dts.getTileSetInfo(zoom); 1482 } 1483 1484 setZoomLevel(zoom); 1485 1486 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level 1487 // to make sure there're really no more zoom levels 1488 // loading is done in the next if section 1489 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) { 1490 zoom++; 1491 tsi = dts.getTileSetInfo(zoom); 1492 } 1493 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded, 1494 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded. 1495 // loading is done in the next if section 1496 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) { 1497 zoom--; 1498 tsi = dts.getTileSetInfo(zoom); 1499 } 1500 ts = dts.getTileSet(zoom); 1501 } else if (autoZoom) { 1502 setZoomLevel(zoom); 1503 } 1504 1505 // Too many tiles... refuse to download 1506 if (!ts.tooLarge()) { 1507 //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned()); 1508 ts.loadAllTiles(false); 1509 } 1510 1511 if (displayZoomLevel != zoom) { 1512 ts = dts.getTileSet(displayZoomLevel); 1513 } 1514 1515 g.setColor(Color.DARK_GRAY); 1516 1517 List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null); 1518 int[] otherZooms = {-1, 1, -2, 2, -3, -4, -5}; 1519 for (int zoomOffset : otherZooms) { 1520 if (!autoZoom) { 1521 break; 1522 } 1523 int newzoom = displayZoomLevel + zoomOffset; 1524 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) { 1525 continue; 1526 } 1527 if (missedTiles.isEmpty()) { 1528 break; 1529 } 1530 List<Tile> newlyMissedTiles = new LinkedList<>(); 1531 for (Tile missed : missedTiles) { 1532 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) { 1533 // Don't try to paint from higher zoom levels when tile is overzoomed 1534 newlyMissedTiles.add(missed); 1535 continue; 1536 } 1537 Tile t2 = tempCornerTile(missed); 1538 LatLon topLeft2 = new LatLon(tileSource.tileXYToLatLon(missed)); 1539 LatLon botRight2 = new LatLon(tileSource.tileXYToLatLon(t2)); 1540 TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom); 1541 // Instantiating large TileSets is expensive. If there 1542 // are no loaded tiles, don't bother even trying. 1543 if (ts2.allLoadedTiles().isEmpty()) { 1544 newlyMissedTiles.add(missed); 1545 continue; 1546 } 1547 if (ts2.tooLarge()) { 1548 continue; 1549 } 1550 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed)); 1551 } 1552 missedTiles = newlyMissedTiles; 1553 } 1554 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) { 1555 Main.debug("still missed "+missedTiles.size()+" in the end"); 1556 } 1557 g.setColor(Color.red); 1558 g.setFont(InfoFont); 1559 1560 // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge() 1561 for (Tile t : ts.allExistingTiles()) { 1562 this.paintTileText(ts, t, g, mv, displayZoomLevel, t); 1563 } 1564 1565 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight), 1566 displayZoomLevel, this); 1567 1568 //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120); 1569 g.setColor(Color.lightGray); 1570 1571 if (ts.insane()) { 1572 myDrawString(g, tr("zoom in to load any tiles"), 120, 120); 1573 } else if (ts.tooLarge()) { 1574 myDrawString(g, tr("zoom in to load more tiles"), 120, 120); 1575 } else if (!autoZoom && ts.tooSmall()) { 1576 myDrawString(g, tr("increase tiles zoom level (change resolution) to see more detail"), 120, 120); 1577 } 1578 1579 if (noTilesAtZoom) { 1580 myDrawString(g, tr("No tiles at this zoom level"), 120, 120); 1581 } 1582 if (Main.isDebugEnabled()) { 1583 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140); 1584 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155); 1585 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170); 1586 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185); 1587 myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200); 1588 if (tileLoader instanceof TMSCachedTileLoader) { 1589 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader; 1590 int offset = 200; 1591 for (String part: cachedTileLoader.getStats().split("\n")) { 1592 offset += 15; 1593 myDrawString(g, tr("Cache stats: {0}", part), 50, offset); 1594 } 1595 } 1596 } 1597 } 1598 1599 /** 1600 * Returns tile for a pixel position.<p> 1601 * This isn't very efficient, but it is only used when the user right-clicks on the map. 1602 * @param px pixel X coordinate 1603 * @param py pixel Y coordinate 1604 * @return Tile at pixel position 1605 */ 1606 private Tile getTileForPixelpos(int px, int py) { 1607 if (Main.isDebugEnabled()) { 1608 Main.debug("getTileForPixelpos("+px+", "+py+')'); 1609 } 1610 MapView mv = Main.map.mapView; 1611 Point clicked = new Point(px, py); 1612 EastNorth topLeft = mv.getEastNorth(0, 0); 1613 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight()); 1614 int z = currentZoomLevel; 1615 TileSet ts = new TileSet(topLeft, botRight, z); 1616 1617 if (!ts.tooLarge()) { 1618 ts.loadAllTiles(false); // make sure there are tile objects for all tiles 1619 } 1620 Tile clickedTile = null; 1621 for (Tile t1 : ts.allExistingTiles()) { 1622 Tile t2 = tempCornerTile(t1); 1623 Rectangle r = new Rectangle(pixelPos(t1)); 1624 r.add(pixelPos(t2)); 1625 if (Main.isDebugEnabled()) { 1626 Main.debug("r: " + r + " clicked: " + clicked); 1627 } 1628 if (!r.contains(clicked)) { 1629 continue; 1630 } 1631 clickedTile = t1; 1632 break; 1633 } 1634 if (clickedTile == null) 1635 return null; 1636 if (Main.isTraceEnabled()) { 1637 Main.trace("Clicked on tile: " + clickedTile.getXtile() + ' ' + clickedTile.getYtile() + 1638 " currentZoomLevel: " + currentZoomLevel); 1639 } 1640 return clickedTile; 1641 } 1642 1643 @Override 1644 public Action[] getMenuEntries() { 1645 ArrayList<Action> actions = new ArrayList<>(); 1646 actions.addAll(Arrays.asList(getLayerListEntries())); 1647 actions.addAll(Arrays.asList(getCommonEntries())); 1648 actions.add(SeparatorLayerAction.INSTANCE); 1649 actions.add(new LayerListPopup.InfoAction(this)); 1650 return actions.toArray(new Action[actions.size()]); 1651 } 1652 1653 public Action[] getLayerListEntries() { 1654 return new Action[] { 1655 LayerListDialog.getInstance().createActivateLayerAction(this), 1656 LayerListDialog.getInstance().createShowHideLayerAction(), 1657 LayerListDialog.getInstance().createDeleteLayerAction(), 1658 SeparatorLayerAction.INSTANCE, 1659 // color, 1660 new OffsetAction(), 1661 new RenameLayerAction(this.getAssociatedFile(), this), 1662 SeparatorLayerAction.INSTANCE 1663 }; 1664 } 1665 1666 /** 1667 * Returns the common menu entries. 1668 * @return the common menu entries 1669 */ 1670 public Action[] getCommonEntries() { 1671 return new Action[] { 1672 new AutoLoadTilesAction(), 1673 new AutoZoomAction(), 1674 new ShowErrorsAction(), 1675 new IncreaseZoomAction(), 1676 new DecreaseZoomAction(), 1677 new ZoomToBestAction(), 1678 new ZoomToNativeLevelAction(), 1679 new FlushTileCacheAction(), 1680 new LoadErroneusTilesAction(), 1681 new LoadAllTilesAction() 1682 }; 1683 } 1684 1685 @Override 1686 public String getToolTipText() { 1687 if (autoLoad) { 1688 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); 1689 } else { 1690 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); 1691 } 1692 } 1693 1694 @Override 1695 public void visitBoundingBox(BoundingXYVisitor v) { 1696 } 1697 1698 @Override 1699 public boolean isChanged() { 1700 return needRedraw; 1701 } 1702 1703 /** 1704 * Task responsible for precaching imagery along the gpx track 1705 * 1706 */ 1707 public class PrecacheTask implements TileLoaderListener { 1708 private final ProgressMonitor progressMonitor; 1709 private int totalCount; 1710 private final AtomicInteger processedCount = new AtomicInteger(0); 1711 private final TileLoader tileLoader; 1712 1713 /** 1714 * @param progressMonitor that will be notified about progess of the task 1715 */ 1716 public PrecacheTask(ProgressMonitor progressMonitor) { 1717 this.progressMonitor = progressMonitor; 1718 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource)); 1719 if (this.tileLoader instanceof TMSCachedTileLoader) { 1720 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor( 1721 TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader")); 1722 } 1723 } 1724 1725 /** 1726 * @return true, if all is done 1727 */ 1728 public boolean isFinished() { 1729 return processedCount.get() >= totalCount; 1730 } 1731 1732 /** 1733 * @return total number of tiles to download 1734 */ 1735 public int getTotalCount() { 1736 return totalCount; 1737 } 1738 1739 /** 1740 * cancel the task 1741 */ 1742 public void cancel() { 1743 if (tileLoader instanceof TMSCachedTileLoader) { 1744 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks(); 1745 } 1746 } 1747 1748 @Override 1749 public void tileLoadingFinished(Tile tile, boolean success) { 1750 int processed = this.processedCount.incrementAndGet(); 1751 if (success) { 1752 this.progressMonitor.worked(1); 1753 this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processed, totalCount)); 1754 } else { 1755 Main.warn("Tile loading failure: " + tile + " - " + tile.getErrorMessage()); 1756 } 1757 } 1758 1759 /** 1760 * @return tile loader that is used to load the tiles 1761 */ 1762 public TileLoader getTileLoader() { 1763 return tileLoader; 1764 } 1765 } 1766 1767 /** 1768 * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download 1769 * all of the tiles. Buffer contains at least one tile. 1770 * 1771 * To prevent accidental clear of the queue, new download executor is created with separate queue 1772 * 1773 * @param progressMonitor progress monitor for download task 1774 * @param points lat/lon coordinates to download 1775 * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides 1776 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides 1777 * @return precache task representing download task 1778 */ 1779 public AbstractTileSourceLayer<T>.PrecacheTask downloadAreaToCache(final ProgressMonitor progressMonitor, List<LatLon> points, 1780 double bufferX, double bufferY) { 1781 PrecacheTask precacheTask = new PrecacheTask(progressMonitor); 1782 final Set<Tile> requestedTiles = new ConcurrentSkipListSet<>(new Comparator<Tile>() { 1783 @Override 1784 public int compare(Tile o1, Tile o2) { 1785 return String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey()); 1786 } 1787 }); 1788 for (LatLon point: points) { 1789 1790 TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel); 1791 TileXY curTile = tileSource.latLonToTileXY(point.toCoordinate(), currentZoomLevel); 1792 TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel); 1793 1794 // take at least one tile of buffer 1795 int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex()); 1796 int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex()); 1797 int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex()); 1798 int maxX = Math.min(curTile.getXIndex() + 1, minTile.getXIndex()); 1799 1800 for (int x = minX; x <= maxX; x++) { 1801 for (int y = minY; y <= maxY; y++) { 1802 requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel)); 1803 } 1804 } 1805 } 1806 1807 precacheTask.totalCount = requestedTiles.size(); 1808 precacheTask.progressMonitor.setTicksCount(requestedTiles.size()); 1809 1810 TileLoader loader = precacheTask.getTileLoader(); 1811 for (Tile t: requestedTiles) { 1812 loader.createTileLoaderJob(t).submit(); 1813 } 1814 return precacheTask; 1815 } 1816 1817 @Override 1818 public boolean isSavable() { 1819 return true; // With WMSLayerExporter 1820 } 1821 1822 @Override 1823 public File createAndOpenSaveFileChooser() { 1824 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 1825 } 1826}