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

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

fix #12752 - Add more image filters (patch by michael2402, modified)

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