source: josm/trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java@ 9176

Last change on this file since 9176 was 9176, checked in by bastiK, 8 years ago

restore old reference point for wms tiling (see #12186)
use negative tile indices for tiles left to this point

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