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

Last change on this file since 11709 was 11637, checked in by stoecker, 7 years ago

fix #13535 - fix FindBugs issue

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