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