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

Last change on this file since 8860 was 8860, checked in by wiktorn, 9 years ago

Make MemoryTileCache instiated per-layer instead static.

Static MemoryTIleCache put quite a lot of pressure on the cache, esp. when
working with multiple layers. Instead of growing MemoryTileCache, small
instances per layer are implemented.

As MemoryTileCache might also put a lot pressure on memory, now size of
MemoryTileCache is calculated based on screen resolution, assuming, that user
will work in full screen mode. Implementation also checks, if all added layers
will not require more memory that is allocated for JOSM, to prevent unexpected
OutOfMemoryException.

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