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

Last change on this file since 9430 was 9430, checked in by Don-vip, 8 years ago

add basic unit tests for tile source layers

  • Property svn:eol-style set to native
File size: 63.8 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);
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 &gt; 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}
Note: See TracBrowser for help on using the repository browser.