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