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

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

fix #13029 - Replace hookUpMapView by attachToMapView (patch by michael2402, modified) - gsoc-core

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