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

Last change on this file since 11785 was 11785, checked in by bastiK, 7 years ago

Imagery: remove fade setting (superseded by layer opacity), see #7427

  • Property svn:eol-style set to native
File size: 71.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.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 }
1113
1114 private List<Tile> paintTileImages(Graphics g, TileSet ts) {
1115 Object paintMutex = new Object();
1116 List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>());
1117 ts.visitTiles(tile -> {
1118 Image img = getLoadedTileImage(tile);
1119 if (img == null) {
1120 missed.add(new TilePosition(tile));
1121 return;
1122 }
1123 img = applyImageProcessors((BufferedImage) img);
1124 Rectangle2D sourceRect = coordinateConverter.getRectangleForTile(tile);
1125 synchronized (paintMutex) {
1126 //cannot paint in parallel
1127 drawImageInside(g, img, sourceRect, null);
1128 }
1129 }, missed::add);
1130
1131 return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList());
1132 }
1133
1134 // This function is called for several zoom levels, not just the current one.
1135 // It should not trigger any tiles to be downloaded.
1136 // It should also avoid polluting the tile cache with any tiles since these tiles are not mandatory.
1137 //
1138 // The "border" tile tells us the boundaries of where we may drawn.
1139 // It will not be from the zoom level that is being drawn currently.
1140 // If drawing the displayZoomLevel, border is null and we draw the entire tile set.
1141 private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
1142 if (zoom <= 0) return Collections.emptyList();
1143 Rectangle2D borderRect = coordinateConverter.getRectangleForTile(border);
1144 List<Tile> missedTiles = new LinkedList<>();
1145 // The callers of this code *require* that we return any tiles that we do not draw in missedTiles.
1146 // ts.allExistingTiles() by default will only return already-existing tiles.
1147 // However, we need to return *all* tiles to the callers, so force creation here.
1148 for (Tile tile : ts.allTilesCreate()) {
1149 Image img = getLoadedTileImage(tile);
1150 if (img == null || tile.hasError()) {
1151 if (Main.isDebugEnabled()) {
1152 Main.debug("missed tile: " + tile);
1153 }
1154 missedTiles.add(tile);
1155 continue;
1156 }
1157
1158 // applying all filters to this layer
1159 img = applyImageProcessors((BufferedImage) img);
1160
1161 Rectangle2D sourceRect = coordinateConverter.getRectangleForTile(tile);
1162 if (!sourceRect.intersects(borderRect)) {
1163 continue;
1164 }
1165 drawImageInside(g, img, sourceRect, borderRect);
1166 }
1167 return missedTiles;
1168 }
1169
1170 private void myDrawString(Graphics g, String text, int x, int y) {
1171 Color oldColor = g.getColor();
1172 String textToDraw = text;
1173 if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) {
1174 // text longer than tile size, split it
1175 StringBuilder line = new StringBuilder();
1176 StringBuilder ret = new StringBuilder();
1177 for (String s: text.split(" ")) {
1178 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) {
1179 ret.append(line).append('\n');
1180 line.setLength(0);
1181 }
1182 line.append(s).append(' ');
1183 }
1184 ret.append(line);
1185 textToDraw = ret.toString();
1186 }
1187 int offset = 0;
1188 for (String s: textToDraw.split("\n")) {
1189 g.setColor(Color.black);
1190 g.drawString(s, x + 1, y + offset + 1);
1191 g.setColor(oldColor);
1192 g.drawString(s, x, y + offset);
1193 offset += g.getFontMetrics().getHeight() + 3;
1194 }
1195 }
1196
1197 private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
1198 if (tile == null) {
1199 return;
1200 }
1201 Point2D p = coordinateConverter.getPixelForTile(t);
1202 int fontHeight = g.getFontMetrics().getHeight();
1203 int x = (int) p.getX();
1204 int y = (int) p.getY();
1205 int texty = y + 2 + fontHeight;
1206
1207 /*if (PROP_DRAW_DEBUG.get()) {
1208 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
1209 texty += 1 + fontHeight;
1210 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
1211 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
1212 texty += 1 + fontHeight;
1213 }
1214 }
1215
1216 String tileStatus = tile.getStatus();
1217 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
1218 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
1219 texty += 1 + fontHeight;
1220 }*/
1221
1222 if (tile.hasError() && getDisplaySettings().isShowErrors()) {
1223 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), x + 2, texty);
1224 //texty += 1 + fontHeight;
1225 }
1226
1227 int xCursor = -1;
1228 int yCursor = -1;
1229 if (Main.isDebugEnabled()) {
1230 if (yCursor < t.getYtile()) {
1231 if (Math.abs(t.getYtile() % 32) == 31) {
1232 g.fillRect(0, y - 1, mv.getWidth(), 3);
1233 } else {
1234 g.drawLine(0, y, mv.getWidth(), y);
1235 }
1236 //yCursor = t.getYtile();
1237 }
1238 // This draws the vertical lines for the entire column. Only draw them for the top tile in the column.
1239 if (xCursor < t.getXtile()) {
1240 if (t.getXtile() % 32 == 0) {
1241 // level 7 tile boundary
1242 g.fillRect(x - 1, 0, 3, mv.getHeight());
1243 } else {
1244 g.drawLine(x, 0, x, mv.getHeight());
1245 }
1246 //xCursor = t.getXtile();
1247 }
1248 }
1249 }
1250
1251 private LatLon getShiftedLatLon(EastNorth en) {
1252 return coordinateConverter.getProjecting().eastNorth2latlonClamped(en);
1253 }
1254
1255 private ICoordinate getShiftedCoord(EastNorth en) {
1256 return getShiftedLatLon(en).toCoordinate();
1257 }
1258
1259 private LatLon getShiftedLatLon(ICoordinate latLon) {
1260 return getShiftedLatLon(Main.getProjection().latlon2eastNorth(new LatLon(latLon)));
1261 }
1262
1263 private final TileSet nullTileSet = new TileSet();
1264
1265 private class TileSet extends TileRange {
1266
1267 protected TileSet(TileXY t1, TileXY t2, int zoom) {
1268 super(t1, t2, zoom);
1269 sanitize();
1270 }
1271
1272 /**
1273 * null tile set
1274 */
1275 private TileSet() {
1276 // default
1277 }
1278
1279 protected void sanitize() {
1280 if (minX < tileSource.getTileXMin(zoom)) {
1281 minX = tileSource.getTileXMin(zoom);
1282 }
1283 if (minY < tileSource.getTileYMin(zoom)) {
1284 minY = tileSource.getTileYMin(zoom);
1285 }
1286 if (maxX > tileSource.getTileXMax(zoom)) {
1287 maxX = tileSource.getTileXMax(zoom);
1288 }
1289 if (maxY > tileSource.getTileYMax(zoom)) {
1290 maxY = tileSource.getTileYMax(zoom);
1291 }
1292 }
1293
1294 private boolean tooSmall() {
1295 return this.tilesSpanned() < 2.1;
1296 }
1297
1298 private boolean tooLarge() {
1299 return insane() || this.tilesSpanned() > 20;
1300 }
1301
1302 private boolean insane() {
1303 return tileCache == null || size() > tileCache.getCacheSize();
1304 }
1305
1306 /**
1307 * Get all tiles represented by this TileSet that are already in the tileCache.
1308 * @return all tiles represented by this TileSet that are already in the tileCache
1309 */
1310 private List<Tile> allExistingTiles() {
1311 return allTiles(AbstractTileSourceLayer.this::getTile);
1312 }
1313
1314 private List<Tile> allTilesCreate() {
1315 return allTiles(AbstractTileSourceLayer.this::getOrCreateTile);
1316 }
1317
1318 private List<Tile> allTiles(Function<TilePosition, Tile> mapper) {
1319 return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList());
1320 }
1321
1322 @Override
1323 public Stream<TilePosition> tilePositions() {
1324 if (this.insane()) {
1325 return Stream.empty(); // Tileset is either empty or too large
1326 } else {
1327 return super.tilePositions();
1328 }
1329 }
1330
1331 private List<Tile> allLoadedTiles() {
1332 return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList());
1333 }
1334
1335 /**
1336 * @return comparator, that sorts the tiles from the center to the edge of the current screen
1337 */
1338 private Comparator<Tile> getTileDistanceComparator() {
1339 final int centerX = (int) Math.ceil((minX + maxX) / 2d);
1340 final int centerY = (int) Math.ceil((minY + maxY) / 2d);
1341 return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY));
1342 }
1343
1344 private void loadAllTiles(boolean force) {
1345 if (!getDisplaySettings().isAutoLoad() && !force)
1346 return;
1347 List<Tile> allTiles = allTilesCreate();
1348 allTiles.sort(getTileDistanceComparator());
1349 for (Tile t : allTiles) {
1350 loadTile(t, force);
1351 }
1352 }
1353
1354 private void loadAllErrorTiles(boolean force) {
1355 if (!getDisplaySettings().isAutoLoad() && !force)
1356 return;
1357 for (Tile t : this.allTilesCreate()) {
1358 if (t.hasError()) {
1359 tileLoader.createTileLoaderJob(t).submit(force);
1360 }
1361 }
1362 }
1363
1364 /**
1365 * Call the given paint method for all tiles in this tile set.<p>
1366 * Uses a parallel stream.
1367 * @param visitor A visitor to call for each tile.
1368 * @param missed a consumer to call for each missed tile.
1369 */
1370 public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) {
1371 tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed));
1372 }
1373
1374 private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) {
1375 Tile tile = getTile(tp);
1376 if (tile == null) {
1377 missed.accept(tp);
1378 } else {
1379 visitor.accept(tile);
1380 }
1381 }
1382
1383 @Override
1384 public String toString() {
1385 return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size();
1386 }
1387 }
1388
1389 /**
1390 * Create a TileSet by EastNorth bbox taking a layer shift in account
1391 * @param topLeft top-left lat/lon
1392 * @param botRight bottom-right lat/lon
1393 * @param zoom zoom level
1394 * @return the tile set
1395 * @since 10651
1396 */
1397 protected TileSet getTileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
1398 return getTileSet(getShiftedLatLon(topLeft), getShiftedLatLon(botRight), zoom);
1399 }
1400
1401 /**
1402 * Create a TileSet by known LatLon bbox without layer shift correction
1403 * @param topLeft top-left lat/lon
1404 * @param botRight bottom-right lat/lon
1405 * @param zoom zoom level
1406 * @return the tile set
1407 * @since 10651
1408 */
1409 protected TileSet getTileSet(LatLon topLeft, LatLon botRight, int zoom) {
1410 if (zoom == 0)
1411 return new TileSet();
1412
1413 TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
1414 TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
1415 return new TileSet(t1, t2, zoom);
1416 }
1417
1418 private static class TileSetInfo {
1419 boolean hasVisibleTiles;
1420 boolean hasOverzoomedTiles;
1421 boolean hasLoadingTiles;
1422 boolean hasAllLoadedTiles;
1423 }
1424
1425 private static <S extends AbstractTMSTileSource> TileSetInfo getTileSetInfo(AbstractTileSourceLayer<S>.TileSet ts) {
1426 List<Tile> allTiles = ts.allExistingTiles();
1427 TileSetInfo result = new TileSetInfo();
1428 result.hasLoadingTiles = allTiles.size() < ts.size();
1429 result.hasAllLoadedTiles = true;
1430 for (Tile t : allTiles) {
1431 if ("no-tile".equals(t.getValue("tile-info"))) {
1432 result.hasOverzoomedTiles = true;
1433 }
1434 if (t.isLoaded()) {
1435 if (!t.hasError()) {
1436 result.hasVisibleTiles = true;
1437 }
1438 } else {
1439 result.hasAllLoadedTiles = false;
1440 if (t.isLoading()) {
1441 result.hasLoadingTiles = true;
1442 }
1443 }
1444 }
1445 return result;
1446 }
1447
1448 private class DeepTileSet {
1449 private final ProjectionBounds bounds;
1450 private final int minZoom, maxZoom;
1451 private final TileSet[] tileSets;
1452 private final TileSetInfo[] tileSetInfos;
1453
1454 @SuppressWarnings("unchecked")
1455 DeepTileSet(ProjectionBounds bounds, int minZoom, int maxZoom) {
1456 this.bounds = bounds;
1457 this.minZoom = minZoom;
1458 this.maxZoom = maxZoom;
1459 this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1];
1460 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
1461 }
1462
1463 public TileSet getTileSet(int zoom) {
1464 if (zoom < minZoom)
1465 return nullTileSet;
1466 synchronized (tileSets) {
1467 TileSet ts = tileSets[zoom-minZoom];
1468 if (ts == null) {
1469 ts = AbstractTileSourceLayer.this.getTileSet(bounds.getMin(), bounds.getMax(), zoom);
1470 tileSets[zoom-minZoom] = ts;
1471 }
1472 return ts;
1473 }
1474 }
1475
1476 public TileSetInfo getTileSetInfo(int zoom) {
1477 if (zoom < minZoom)
1478 return new TileSetInfo();
1479 synchronized (tileSetInfos) {
1480 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
1481 if (tsi == null) {
1482 tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom));
1483 tileSetInfos[zoom-minZoom] = tsi;
1484 }
1485 return tsi;
1486 }
1487 }
1488 }
1489
1490 @Override
1491 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1492 // old and unused.
1493 }
1494
1495 private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) {
1496 int zoom = currentZoomLevel;
1497 if (getDisplaySettings().isAutoZoom()) {
1498 zoom = getBestZoom();
1499 }
1500
1501 DeepTileSet dts = new DeepTileSet(pb, getMinZoomLvl(), zoom);
1502 TileSet ts = dts.getTileSet(zoom);
1503
1504 int displayZoomLevel = zoom;
1505
1506 boolean noTilesAtZoom = false;
1507 if (getDisplaySettings().isAutoZoom() && getDisplaySettings().isAutoLoad()) {
1508 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
1509 TileSetInfo tsi = dts.getTileSetInfo(zoom);
1510 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
1511 noTilesAtZoom = true;
1512 }
1513 // Find highest zoom level with at least one visible tile
1514 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
1515 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
1516 displayZoomLevel = tmpZoom;
1517 break;
1518 }
1519 }
1520 // Do binary search between currentZoomLevel and displayZoomLevel
1521 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) {
1522 zoom = (zoom + displayZoomLevel)/2;
1523 tsi = dts.getTileSetInfo(zoom);
1524 }
1525
1526 setZoomLevel(zoom);
1527
1528 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
1529 // to make sure there're really no more zoom levels
1530 // loading is done in the next if section
1531 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
1532 zoom++;
1533 tsi = dts.getTileSetInfo(zoom);
1534 }
1535 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
1536 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
1537 // loading is done in the next if section
1538 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
1539 zoom--;
1540 tsi = dts.getTileSetInfo(zoom);
1541 }
1542 ts = dts.getTileSet(zoom);
1543 } else if (getDisplaySettings().isAutoZoom()) {
1544 setZoomLevel(zoom);
1545 }
1546
1547 // Too many tiles... refuse to download
1548 if (!ts.tooLarge()) {
1549 // try to load tiles from desired zoom level, no matter what we will show (for example, tiles from previous zoom level
1550 // on zoom in)
1551 ts.loadAllTiles(false);
1552 }
1553
1554 if (displayZoomLevel != zoom) {
1555 ts = dts.getTileSet(displayZoomLevel);
1556 if (!dts.getTileSetInfo(displayZoomLevel).hasAllLoadedTiles && displayZoomLevel < zoom) {
1557 // if we are showing tiles from lower zoom level, ensure that all tiles are loaded as they are few,
1558 // and should not trash the tile cache
1559 // This is especially needed when dts.getTileSet(zoom).tooLarge() is true and we are not loading tiles
1560 ts.loadAllTiles(false);
1561 }
1562 }
1563
1564 g.setColor(Color.DARK_GRAY);
1565
1566 List<Tile> missedTiles = this.paintTileImages(g, ts);
1567 int[] otherZooms = {-1, 1, -2, 2, -3, -4, -5};
1568 for (int zoomOffset : otherZooms) {
1569 if (!getDisplaySettings().isAutoZoom()) {
1570 break;
1571 }
1572 int newzoom = displayZoomLevel + zoomOffset;
1573 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) {
1574 continue;
1575 }
1576 if (missedTiles.isEmpty()) {
1577 break;
1578 }
1579 List<Tile> newlyMissedTiles = new LinkedList<>();
1580 for (Tile missed : missedTiles) {
1581 if (zoomOffset > 0 && "no-tile".equals(missed.getValue("tile-info"))) {
1582 // Don't try to paint from higher zoom levels when tile is overzoomed
1583 newlyMissedTiles.add(missed);
1584 continue;
1585 }
1586 Tile t2 = tempCornerTile(missed);
1587 TileSet ts2 = getTileSet(getShiftedLatLon(tileSource.tileXYToLatLon(missed)),
1588 getShiftedLatLon(tileSource.tileXYToLatLon(t2)), newzoom);
1589 // Instantiating large TileSets is expensive. If there are no loaded tiles, don't bother even trying.
1590 if (ts2.allLoadedTiles().isEmpty()) {
1591 newlyMissedTiles.add(missed);
1592 continue;
1593 }
1594 if (ts2.tooLarge()) {
1595 continue;
1596 }
1597 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
1598 }
1599 missedTiles = newlyMissedTiles;
1600 }
1601 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
1602 Main.debug("still missed "+missedTiles.size()+" in the end");
1603 }
1604 g.setColor(Color.red);
1605 g.setFont(InfoFont);
1606
1607 // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge()
1608 for (Tile t : ts.allExistingTiles()) {
1609 this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
1610 }
1611
1612 EastNorth min = pb.getMin();
1613 EastNorth max = pb.getMax();
1614 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max),
1615 displayZoomLevel, this);
1616
1617 g.setColor(Color.lightGray);
1618
1619 if (ts.insane()) {
1620 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
1621 } else if (ts.tooLarge()) {
1622 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
1623 } else if (!getDisplaySettings().isAutoZoom() && ts.tooSmall()) {
1624 myDrawString(g, tr("increase tiles zoom level (change resolution) to see more detail"), 120, 120);
1625 }
1626 if (noTilesAtZoom) {
1627 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
1628 }
1629 if (Main.isDebugEnabled()) {
1630 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
1631 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
1632 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
1633 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
1634 myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200);
1635 if (tileLoader instanceof TMSCachedTileLoader) {
1636 int offset = 200;
1637 for (String part: ((TMSCachedTileLoader) tileLoader).getStats().split("\n")) {
1638 offset += 15;
1639 myDrawString(g, tr("Cache stats: {0}", part), 50, offset);
1640 }
1641 }
1642 }
1643 }
1644
1645 /**
1646 * Returns tile for a pixel position.<p>
1647 * This isn't very efficient, but it is only used when the user right-clicks on the map.
1648 * @param px pixel X coordinate
1649 * @param py pixel Y coordinate
1650 * @return Tile at pixel position
1651 */
1652 private Tile getTileForPixelpos(int px, int py) {
1653 if (Main.isDebugEnabled()) {
1654 Main.debug("getTileForPixelpos("+px+", "+py+')');
1655 }
1656 MapView mv = Main.map.mapView;
1657 Point clicked = new Point(px, py);
1658 EastNorth topLeft = mv.getEastNorth(0, 0);
1659 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1660 TileSet ts = getTileSet(topLeft, botRight, currentZoomLevel);
1661
1662 if (!ts.tooLarge()) {
1663 ts.loadAllTiles(false); // make sure there are tile objects for all tiles
1664 }
1665 Stream<Tile> clickedTiles = ts.allExistingTiles().stream()
1666 .filter(t -> coordinateConverter.getRectangleForTile(t).contains(clicked));
1667 if (Main.isTraceEnabled()) {
1668 clickedTiles = clickedTiles.peek(t -> Main.trace("Clicked on tile: " + t.getXtile() + ' ' + t.getYtile() +
1669 " currentZoomLevel: " + currentZoomLevel));
1670 }
1671 return clickedTiles.findAny().orElse(null);
1672 }
1673
1674 /**
1675 * Class to store a menu action and the class it belongs to.
1676 */
1677 private static class MenuAddition {
1678 final Action addition;
1679 @SuppressWarnings("rawtypes")
1680 final Class<? extends AbstractTileSourceLayer> clazz;
1681
1682 @SuppressWarnings("rawtypes")
1683 MenuAddition(Action addition, Class<? extends AbstractTileSourceLayer> clazz) {
1684 this.addition = addition;
1685 this.clazz = clazz;
1686 }
1687 }
1688
1689 /**
1690 * Register an additional layer context menu entry.
1691 *
1692 * @param addition additional menu action
1693 * @since 11197
1694 */
1695 public static void registerMenuAddition(Action addition) {
1696 menuAdditions.add(new MenuAddition(addition, AbstractTileSourceLayer.class));
1697 }
1698
1699 /**
1700 * Register an additional layer context menu entry for a imagery layer
1701 * class. The menu entry is valid for the specified class and subclasses
1702 * thereof only.
1703 * <p>
1704 * Example:
1705 * <pre>
1706 * TMSLayer.registerMenuAddition(new TMSSpecificAction(), TMSLayer.class);
1707 * </pre>
1708 *
1709 * @param addition additional menu action
1710 * @param clazz class the menu action is registered for
1711 * @since 11197
1712 */
1713 public static void registerMenuAddition(Action addition,
1714 Class<? extends AbstractTileSourceLayer<?>> clazz) {
1715 menuAdditions.add(new MenuAddition(addition, clazz));
1716 }
1717
1718 /**
1719 * Prepare list of additional layer context menu entries. The list is
1720 * empty if there are no additional menu entries.
1721 *
1722 * @return list of additional layer context menu entries
1723 */
1724 private List<Action> getMenuAdditions() {
1725 final LinkedList<Action> menuAdds = new LinkedList<>();
1726 for (MenuAddition menuAdd: menuAdditions) {
1727 if (menuAdd.clazz.isInstance(this)) {
1728 menuAdds.add(menuAdd.addition);
1729 }
1730 }
1731 if (!menuAdds.isEmpty()) {
1732 menuAdds.addFirst(SeparatorLayerAction.INSTANCE);
1733 }
1734 return menuAdds;
1735 }
1736
1737 @Override
1738 public Action[] getMenuEntries() {
1739 ArrayList<Action> actions = new ArrayList<>();
1740 actions.addAll(Arrays.asList(getLayerListEntries()));
1741 actions.addAll(Arrays.asList(getCommonEntries()));
1742 actions.addAll(getMenuAdditions());
1743 actions.add(SeparatorLayerAction.INSTANCE);
1744 actions.add(new LayerListPopup.InfoAction(this));
1745 return actions.toArray(new Action[actions.size()]);
1746 }
1747
1748 /**
1749 * Returns the contextual menu entries in layer list dialog.
1750 * @return the contextual menu entries in layer list dialog
1751 */
1752 public Action[] getLayerListEntries() {
1753 return new Action[] {
1754 LayerListDialog.getInstance().createActivateLayerAction(this),
1755 LayerListDialog.getInstance().createShowHideLayerAction(),
1756 LayerListDialog.getInstance().createDeleteLayerAction(),
1757 SeparatorLayerAction.INSTANCE,
1758 // color,
1759 new OffsetAction(),
1760 new RenameLayerAction(this.getAssociatedFile(), this),
1761 SeparatorLayerAction.INSTANCE
1762 };
1763 }
1764
1765 /**
1766 * Returns the common menu entries.
1767 * @return the common menu entries
1768 */
1769 public Action[] getCommonEntries() {
1770 return new Action[] {
1771 new AutoLoadTilesAction(),
1772 new AutoZoomAction(),
1773 new ShowErrorsAction(),
1774 new IncreaseZoomAction(),
1775 new DecreaseZoomAction(),
1776 new ZoomToBestAction(),
1777 new ZoomToNativeLevelAction(),
1778 new FlushTileCacheAction(),
1779 new LoadErroneusTilesAction(),
1780 new LoadAllTilesAction()
1781 };
1782 }
1783
1784 @Override
1785 public String getToolTipText() {
1786 if (getDisplaySettings().isAutoLoad()) {
1787 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1788 } else {
1789 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1790 }
1791 }
1792
1793 @Override
1794 public void visitBoundingBox(BoundingXYVisitor v) {
1795 }
1796
1797 @Override
1798 public boolean isChanged() {
1799 return false; // we use #invalidate()
1800 }
1801
1802 /**
1803 * Task responsible for precaching imagery along the gpx track
1804 *
1805 */
1806 public class PrecacheTask implements TileLoaderListener {
1807 private final ProgressMonitor progressMonitor;
1808 private int totalCount;
1809 private final AtomicInteger processedCount = new AtomicInteger(0);
1810 private final TileLoader tileLoader;
1811
1812 /**
1813 * @param progressMonitor that will be notified about progess of the task
1814 */
1815 public PrecacheTask(ProgressMonitor progressMonitor) {
1816 this.progressMonitor = progressMonitor;
1817 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
1818 if (this.tileLoader instanceof TMSCachedTileLoader) {
1819 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
1820 TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
1821 }
1822 }
1823
1824 /**
1825 * @return true, if all is done
1826 */
1827 public boolean isFinished() {
1828 return processedCount.get() >= totalCount;
1829 }
1830
1831 /**
1832 * @return total number of tiles to download
1833 */
1834 public int getTotalCount() {
1835 return totalCount;
1836 }
1837
1838 /**
1839 * cancel the task
1840 */
1841 public void cancel() {
1842 if (tileLoader instanceof TMSCachedTileLoader) {
1843 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
1844 }
1845 }
1846
1847 @Override
1848 public void tileLoadingFinished(Tile tile, boolean success) {
1849 int processed = this.processedCount.incrementAndGet();
1850 if (success) {
1851 this.progressMonitor.worked(1);
1852 this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processed, totalCount));
1853 } else {
1854 Main.warn("Tile loading failure: " + tile + " - " + tile.getErrorMessage());
1855 }
1856 }
1857
1858 /**
1859 * @return tile loader that is used to load the tiles
1860 */
1861 public TileLoader getTileLoader() {
1862 return tileLoader;
1863 }
1864 }
1865
1866 /**
1867 * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download
1868 * all of the tiles. Buffer contains at least one tile.
1869 *
1870 * To prevent accidental clear of the queue, new download executor is created with separate queue
1871 *
1872 * @param progressMonitor progress monitor for download task
1873 * @param points lat/lon coordinates to download
1874 * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides
1875 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides
1876 * @return precache task representing download task
1877 */
1878 public AbstractTileSourceLayer<T>.PrecacheTask downloadAreaToCache(final ProgressMonitor progressMonitor, List<LatLon> points,
1879 double bufferX, double bufferY) {
1880 PrecacheTask precacheTask = new PrecacheTask(progressMonitor);
1881 final Set<Tile> requestedTiles = new ConcurrentSkipListSet<>(
1882 (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey()));
1883 for (LatLon point: points) {
1884 TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel);
1885 TileXY curTile = tileSource.latLonToTileXY(point.toCoordinate(), currentZoomLevel);
1886 TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel);
1887
1888 // take at least one tile of buffer
1889 int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex());
1890 int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex());
1891 int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex());
1892 int maxX = Math.max(curTile.getXIndex() + 1, maxTile.getXIndex());
1893
1894 for (int x = minX; x <= maxX; x++) {
1895 for (int y = minY; y <= maxY; y++) {
1896 requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
1897 }
1898 }
1899 }
1900
1901 precacheTask.totalCount = requestedTiles.size();
1902 precacheTask.progressMonitor.setTicksCount(requestedTiles.size());
1903
1904 TileLoader loader = precacheTask.getTileLoader();
1905 for (Tile t: requestedTiles) {
1906 loader.createTileLoaderJob(t).submit();
1907 }
1908 return precacheTask;
1909 }
1910
1911 @Override
1912 public boolean isSavable() {
1913 return true; // With WMSLayerExporter
1914 }
1915
1916 @Override
1917 public File createAndOpenSaveFileChooser() {
1918 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
1919 }
1920
1921 @Override
1922 public void destroy() {
1923 super.destroy();
1924 adjustAction.destroy();
1925 }
1926
1927 private class TileSourcePainter extends CompatibilityModeLayerPainter {
1928 /** The memory handle that will hold our tile source. */
1929 private MemoryHandle<?> memory;
1930
1931 @Override
1932 public void paint(MapViewGraphics graphics) {
1933 allocateCacheMemory();
1934 if (memory != null) {
1935 doPaint(graphics);
1936 }
1937 }
1938
1939 private void doPaint(MapViewGraphics graphics) {
1940 drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getClipBounds().getProjectionBounds());
1941 }
1942
1943 private void allocateCacheMemory() {
1944 if (memory == null) {
1945 MemoryManager manager = MemoryManager.getInstance();
1946 if (manager.isAvailable(getEstimatedCacheSize())) {
1947 try {
1948 memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new);
1949 } catch (NotEnoughMemoryException e) {
1950 Main.warn("Could not allocate tile source memory", e);
1951 }
1952 }
1953 }
1954 }
1955
1956 protected long getEstimatedCacheSize() {
1957 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize();
1958 }
1959
1960 @Override
1961 public void detachFromMapView(MapViewEvent event) {
1962 event.getMapView().removeMouseListener(adapter);
1963 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this);
1964 super.detachFromMapView(event);
1965 if (memory != null) {
1966 memory.free();
1967 }
1968 }
1969 }
1970}
Note: See TracBrowser for help on using the repository browser.