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

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

sonar - squid:S2197 - Modulus results should not be checked for direct equality

  • 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 * @param monitor not used in this implementation - as cache clear is instaneus
283 */
284 public void clearTileCache(ProgressMonitor monitor) {
285 if (tileLoader instanceof CachedTileLoader) {
286 ((CachedTileLoader) tileLoader).clearCache(tileSource);
287 }
288 tileCache.clear();
289 }
290
291 /**
292 * Initiates a repaint of Main.map
293 *
294 * @see Main#map
295 * @see MapFrame#repaint()
296 * @see #invalidate() To trigger a repaint of all places where the layer is displayed.
297 */
298 protected void redraw() {
299 invalidate();
300 }
301
302 /**
303 * {@inheritDoc}
304 * @deprecated Use {@link TileSourceDisplaySettings#getDx()}
305 */
306 @Override
307 @Deprecated
308 public double getDx() {
309 return getDisplaySettings().getDx();
310 }
311
312 /**
313 * {@inheritDoc}
314 * @deprecated Use {@link TileSourceDisplaySettings#getDy()}
315 */
316 @Override
317 @Deprecated
318 public double getDy() {
319 return getDisplaySettings().getDy();
320 }
321
322 /**
323 * {@inheritDoc}
324 * @deprecated Use {@link TileSourceDisplaySettings}
325 */
326 @Override
327 @Deprecated
328 public void displace(double dx, double dy) {
329 getDisplaySettings().addDisplacement(new EastNorth(dx, dy));
330 }
331
332 /**
333 * {@inheritDoc}
334 * @deprecated Use {@link TileSourceDisplaySettings}
335 */
336 @Override
337 @Deprecated
338 public void setOffset(double dx, double dy) {
339 getDisplaySettings().setDisplacement(new EastNorth(dx, dy));
340 }
341
342 @Override
343 public Object getInfoComponent() {
344 JPanel panel = (JPanel) super.getInfoComponent();
345 EastNorth offset = getDisplaySettings().getDisplacement();
346 if (offset.distanceSq(0, 0) > 1e-10) {
347 panel.add(new JLabel(tr("Offset: ") + offset.east() + ';' + offset.north()), GBC.eol().insets(0, 5, 10, 0));
348 }
349 return panel;
350 }
351
352 @Override
353 protected Action getAdjustAction() {
354 return adjustAction;
355 }
356
357 /**
358 * Returns average number of screen pixels per tile pixel for current mapview
359 * @param zoom zoom level
360 * @return average number of screen pixels per tile pixel
361 */
362 private double getScaleFactor(int zoom) {
363 if (coordinateConverter != null) {
364 return coordinateConverter.getScaleFactor(zoom);
365 } else {
366 return 1;
367 }
368 }
369
370 protected int getBestZoom() {
371 double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
372 double result = Math.log(factor)/Math.log(2)/2;
373 /*
374 * Math.log(factor)/Math.log(2) - gives log base 2 of factor
375 * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2
376 *
377 * ZOOM_OFFSET controls, whether we work with overzoomed or underzoomed tiles. Positive ZOOM_OFFSET
378 * is for working with underzoomed tiles (higher quality when working with aerial imagery), negative ZOOM_OFFSET
379 * is for working with overzoomed tiles (big, pixelated), which is good when working with high-dpi screens and/or
380 * maps as a imagery layer
381 */
382 int intResult = (int) Math.round(result + 1 + ZOOM_OFFSET.get() / 1.9);
383
384 intResult = Math.min(intResult, getMaxZoomLvl());
385 intResult = Math.max(intResult, getMinZoomLvl());
386 return intResult;
387 }
388
389 private static boolean actionSupportLayers(List<Layer> layers) {
390 return layers.size() == 1 && layers.get(0) instanceof TMSLayer;
391 }
392
393 private final class ShowTileInfoAction extends AbstractAction {
394
395 private ShowTileInfoAction() {
396 super(tr("Show tile info"));
397 }
398
399 private String getSizeString(int size) {
400 return new StringBuilder().append(size).append('x').append(size).toString();
401 }
402
403 private JTextField createTextField(String text) {
404 JTextField ret = new JTextField(text);
405 ret.setEditable(false);
406 ret.setBorder(BorderFactory.createEmptyBorder());
407 return ret;
408 }
409
410 @Override
411 public void actionPerformed(ActionEvent ae) {
412 Tile clickedTile = clickedTileHolder.getTile();
413 if (clickedTile != null) {
414 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
415 JPanel panel = new JPanel(new GridBagLayout());
416 Rectangle2D displaySize = coordinateConverter.getRectangleForTile(clickedTile);
417 String url = "";
418 try {
419 url = clickedTile.getUrl();
420 } catch (IOException e) {
421 // silence exceptions
422 Main.trace(e);
423 }
424
425 String[][] content = {
426 {"Tile name", clickedTile.getKey()},
427 {"Tile url", url},
428 {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
429 {"Tile display size", new StringBuilder().append(displaySize.getWidth())
430 .append('x')
431 .append(displaySize.getHeight()).toString()},
432 };
433
434 for (String[] entry: content) {
435 panel.add(new JLabel(tr(entry[0]) + ':'), GBC.std());
436 panel.add(GBC.glue(5, 0), GBC.std());
437 panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
438 }
439
440 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
441 panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ':'), GBC.std());
442 panel.add(GBC.glue(5, 0), GBC.std());
443 String value = e.getValue();
444 if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
445 value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
446 }
447 panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
448
449 }
450 ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
451 ed.setContent(panel);
452 ed.showDialog();
453 }
454 }
455 }
456
457 private final class LoadTileAction extends AbstractAction {
458
459 private LoadTileAction() {
460 super(tr("Load tile"));
461 }
462
463 @Override
464 public void actionPerformed(ActionEvent ae) {
465 Tile clickedTile = clickedTileHolder.getTile();
466 if (clickedTile != null) {
467 loadTile(clickedTile, true);
468 invalidate();
469 }
470 }
471 }
472
473 private class AutoZoomAction extends AbstractAction implements LayerAction {
474 AutoZoomAction() {
475 super(tr("Auto zoom"));
476 }
477
478 @Override
479 public void actionPerformed(ActionEvent ae) {
480 getDisplaySettings().setAutoZoom(!getDisplaySettings().isAutoZoom());
481 }
482
483 @Override
484 public Component createMenuComponent() {
485 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
486 item.setSelected(getDisplaySettings().isAutoZoom());
487 return item;
488 }
489
490 @Override
491 public boolean supportLayers(List<Layer> layers) {
492 return actionSupportLayers(layers);
493 }
494 }
495
496 private class AutoLoadTilesAction extends AbstractAction implements LayerAction {
497 AutoLoadTilesAction() {
498 super(tr("Auto load tiles"));
499 }
500
501 @Override
502 public void actionPerformed(ActionEvent ae) {
503 getDisplaySettings().setAutoLoad(!getDisplaySettings().isAutoLoad());
504 }
505
506 @Override
507 public Component createMenuComponent() {
508 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
509 item.setSelected(getDisplaySettings().isAutoLoad());
510 return item;
511 }
512
513 @Override
514 public boolean supportLayers(List<Layer> layers) {
515 return actionSupportLayers(layers);
516 }
517 }
518
519 private class ShowErrorsAction extends AbstractAction implements LayerAction {
520 ShowErrorsAction() {
521 super(tr("Show errors"));
522 }
523
524 @Override
525 public void actionPerformed(ActionEvent ae) {
526 getDisplaySettings().setShowErrors(!getDisplaySettings().isShowErrors());
527 }
528
529 @Override
530 public Component createMenuComponent() {
531 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
532 item.setSelected(getDisplaySettings().isShowErrors());
533 return item;
534 }
535
536 @Override
537 public boolean supportLayers(List<Layer> layers) {
538 return actionSupportLayers(layers);
539 }
540 }
541
542 private class LoadAllTilesAction extends AbstractAction {
543 LoadAllTilesAction() {
544 super(tr("Load all tiles"));
545 }
546
547 @Override
548 public void actionPerformed(ActionEvent ae) {
549 loadAllTiles(true);
550 }
551 }
552
553 private class LoadErroneusTilesAction extends AbstractAction {
554 LoadErroneusTilesAction() {
555 super(tr("Load all error tiles"));
556 }
557
558 @Override
559 public void actionPerformed(ActionEvent ae) {
560 loadAllErrorTiles(true);
561 }
562 }
563
564 private class ZoomToNativeLevelAction extends AbstractAction {
565 ZoomToNativeLevelAction() {
566 super(tr("Zoom to native resolution"));
567 }
568
569 @Override
570 public void actionPerformed(ActionEvent ae) {
571 double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));
572 Main.map.mapView.zoomToFactor(newFactor);
573 redraw();
574 }
575 }
576
577 private class ZoomToBestAction extends AbstractAction {
578 ZoomToBestAction() {
579 super(tr("Change resolution"));
580 setEnabled(!getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel);
581 }
582
583 @Override
584 public void actionPerformed(ActionEvent ae) {
585 setZoomLevel(getBestZoom());
586 }
587 }
588
589 private class IncreaseZoomAction extends AbstractAction {
590 IncreaseZoomAction() {
591 super(tr("Increase zoom"));
592 setEnabled(!getDisplaySettings().isAutoZoom() && zoomIncreaseAllowed());
593 }
594
595 @Override
596 public void actionPerformed(ActionEvent ae) {
597 increaseZoomLevel();
598 }
599 }
600
601 private class DecreaseZoomAction extends AbstractAction {
602 DecreaseZoomAction() {
603 super(tr("Decrease zoom"));
604 setEnabled(!getDisplaySettings().isAutoZoom() && zoomDecreaseAllowed());
605 }
606
607 @Override
608 public void actionPerformed(ActionEvent ae) {
609 decreaseZoomLevel();
610 }
611 }
612
613 private class FlushTileCacheAction extends AbstractAction {
614 FlushTileCacheAction() {
615 super(tr("Flush tile cache"));
616 }
617
618 @Override
619 public void actionPerformed(ActionEvent ae) {
620 new PleaseWaitRunnable(tr("Flush tile cache")) {
621 @Override
622 protected void realRun() {
623 clearTileCache(getProgressMonitor());
624 }
625
626 @Override
627 protected void finish() {
628 // empty - flush is instaneus
629 }
630
631 @Override
632 protected void cancel() {
633 // empty - flush is instaneus
634 }
635 }.run();
636 }
637 }
638
639 /**
640 * Simple class to keep clickedTile within hookUpMapView
641 */
642 private static final class TileHolder {
643 private Tile t;
644
645 public Tile getTile() {
646 return t;
647 }
648
649 public void setTile(Tile t) {
650 this.t = t;
651 }
652 }
653
654 /**
655 * Creates popup menu items and binds to mouse actions
656 */
657 @Override
658 public void hookUpMapView() {
659 // this needs to be here and not in constructor to allow empty TileSource class construction using SessionWriter
660 initializeIfRequired();
661
662 super.hookUpMapView();
663 }
664
665 @Override
666 public LayerPainter attachToMapView(MapViewEvent event) {
667 initializeIfRequired();
668
669 event.getMapView().addMouseListener(adapter);
670 MapView.addZoomChangeListener(this);
671
672 if (this instanceof NativeScaleLayer) {
673 event.getMapView().setNativeScaleLayer((NativeScaleLayer) this);
674 }
675
676 // 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.
677 // FIXME: Check if this is still required.
678 event.getMapView().repaint(500);
679
680 return super.attachToMapView(event);
681 }
682
683 private void initializeIfRequired() {
684 if (tileSource == null) {
685 tileSource = getTileSource();
686 if (tileSource == null) {
687 throw new IllegalArgumentException(tr("Failed to create tile source"));
688 }
689 // check if projection is supported
690 projectionChanged(null, Main.getProjection());
691 initTileSource(this.tileSource);
692 }
693 }
694
695 @Override
696 protected LayerPainter createMapViewPainter(MapViewEvent event) {
697 return new TileSourcePainter();
698 }
699
700 /**
701 * Tile source layer popup menu.
702 */
703 public class TileSourceLayerPopup extends JPopupMenu {
704 /**
705 * Constructs a new {@code TileSourceLayerPopup}.
706 */
707 public TileSourceLayerPopup() {
708 for (Action a : getCommonEntries()) {
709 if (a instanceof LayerAction) {
710 add(((LayerAction) a).createMenuComponent());
711 } else {
712 add(new JMenuItem(a));
713 }
714 }
715 add(new JSeparator());
716 add(new JMenuItem(new LoadTileAction()));
717 add(new JMenuItem(new ShowTileInfoAction()));
718 }
719 }
720
721 protected int estimateTileCacheSize() {
722 Dimension screenSize = GuiHelper.getMaximumScreenSize();
723 int height = screenSize.height;
724 int width = screenSize.width;
725 int tileSize = 256; // default tile size
726 if (tileSource != null) {
727 tileSize = tileSource.getTileSize();
728 }
729 // as we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that
730 int visibileTiles = (int) (Math.ceil((double) height / tileSize + 1) * Math.ceil((double) width / tileSize + 1));
731 // add 10% for tiles from different zoom levels
732 int ret = (int) Math.ceil(
733 Math.pow(2d, ZOOM_OFFSET.get()) * visibileTiles // use offset to decide, how many tiles are visible
734 * 4);
735 Main.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibileTiles, ret);
736 return ret;
737 }
738
739 @Override
740 public void displaySettingsChanged(DisplaySettingsChangeEvent e) {
741 if (tileSource == null) {
742 return;
743 }
744 switch (e.getChangedSetting()) {
745 case TileSourceDisplaySettings.AUTO_ZOOM:
746 if (getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel) {
747 setZoomLevel(getBestZoom());
748 invalidate();
749 }
750 break;
751 case TileSourceDisplaySettings.AUTO_LOAD:
752 if (getDisplaySettings().isAutoLoad()) {
753 invalidate();
754 }
755 break;
756 default:
757 // trigger a redraw just to be sure.
758 invalidate();
759 }
760 }
761
762 /**
763 * Checks zoom level against settings
764 * @param maxZoomLvl zoom level to check
765 * @param ts tile source to crosscheck with
766 * @return maximum zoom level, not higher than supported by tilesource nor set by the user
767 */
768 public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
769 if (maxZoomLvl > MAX_ZOOM) {
770 maxZoomLvl = MAX_ZOOM;
771 }
772 if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
773 maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
774 }
775 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
776 maxZoomLvl = ts.getMaxZoom();
777 }
778 return maxZoomLvl;
779 }
780
781 /**
782 * Checks zoom level against settings
783 * @param minZoomLvl zoom level to check
784 * @param ts tile source to crosscheck with
785 * @return minimum zoom level, not higher than supported by tilesource nor set by the user
786 */
787 public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
788 if (minZoomLvl < MIN_ZOOM) {
789 minZoomLvl = MIN_ZOOM;
790 }
791 if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
792 minZoomLvl = getMaxZoomLvl(ts);
793 }
794 if (ts != null && ts.getMinZoom() > minZoomLvl) {
795 minZoomLvl = ts.getMinZoom();
796 }
797 return minZoomLvl;
798 }
799
800 /**
801 * @param ts TileSource for which we want to know maximum zoom level
802 * @return maximum max zoom level, that will be shown on layer
803 */
804 public static int getMaxZoomLvl(TileSource ts) {
805 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
806 }
807
808 /**
809 * @param ts TileSource for which we want to know minimum zoom level
810 * @return minimum zoom level, that will be shown on layer
811 */
812 public static int getMinZoomLvl(TileSource ts) {
813 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
814 }
815
816 /**
817 * Sets maximum zoom level, that layer will attempt show
818 * @param maxZoomLvl maximum zoom level
819 */
820 public static void setMaxZoomLvl(int maxZoomLvl) {
821 PROP_MAX_ZOOM_LVL.put(checkMaxZoomLvl(maxZoomLvl, null));
822 }
823
824 /**
825 * Sets minimum zoom level, that layer will attempt show
826 * @param minZoomLvl minimum zoom level
827 */
828 public static void setMinZoomLvl(int minZoomLvl) {
829 PROP_MIN_ZOOM_LVL.put(checkMinZoomLvl(minZoomLvl, null));
830 }
831
832 /**
833 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
834 * changes to visible map (panning/zooming)
835 */
836 @Override
837 public void zoomChanged() {
838 if (Main.isDebugEnabled()) {
839 Main.debug("zoomChanged(): " + currentZoomLevel);
840 }
841 if (tileLoader instanceof TMSCachedTileLoader) {
842 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
843 }
844 invalidate();
845 }
846
847 protected int getMaxZoomLvl() {
848 if (info.getMaxZoom() != 0)
849 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
850 else
851 return getMaxZoomLvl(tileSource);
852 }
853
854 protected int getMinZoomLvl() {
855 if (info.getMinZoom() != 0)
856 return checkMinZoomLvl(info.getMinZoom(), tileSource);
857 else
858 return getMinZoomLvl(tileSource);
859 }
860
861 /**
862 *
863 * @return if its allowed to zoom in
864 */
865 public boolean zoomIncreaseAllowed() {
866 boolean zia = currentZoomLevel < this.getMaxZoomLvl();
867 if (Main.isDebugEnabled()) {
868 Main.debug("zoomIncreaseAllowed(): " + zia + ' ' + currentZoomLevel + " vs. " + this.getMaxZoomLvl());
869 }
870 return zia;
871 }
872
873 /**
874 * Zoom in, go closer to map.
875 *
876 * @return true, if zoom increasing was successful, false otherwise
877 */
878 public boolean increaseZoomLevel() {
879 if (zoomIncreaseAllowed()) {
880 currentZoomLevel++;
881 if (Main.isDebugEnabled()) {
882 Main.debug("increasing zoom level to: " + currentZoomLevel);
883 }
884 zoomChanged();
885 } else {
886 Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
887 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
888 return false;
889 }
890 return true;
891 }
892
893 /**
894 * Sets the zoom level of the layer
895 * @param zoom zoom level
896 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels
897 */
898 public boolean setZoomLevel(int zoom) {
899 if (zoom == currentZoomLevel) return true;
900 if (zoom > this.getMaxZoomLvl()) return false;
901 if (zoom < this.getMinZoomLvl()) return false;
902 currentZoomLevel = zoom;
903 zoomChanged();
904 return true;
905 }
906
907 /**
908 * Check if zooming out is allowed
909 *
910 * @return true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
911 */
912 public boolean zoomDecreaseAllowed() {
913 boolean zda = currentZoomLevel > this.getMinZoomLvl();
914 if (Main.isDebugEnabled()) {
915 Main.debug("zoomDecreaseAllowed(): " + zda + ' ' + currentZoomLevel + " vs. " + this.getMinZoomLvl());
916 }
917 return zda;
918 }
919
920 /**
921 * Zoom out from map.
922 *
923 * @return true, if zoom increasing was successfull, false othervise
924 */
925 public boolean decreaseZoomLevel() {
926 if (zoomDecreaseAllowed()) {
927 if (Main.isDebugEnabled()) {
928 Main.debug("decreasing zoom level to: " + currentZoomLevel);
929 }
930 currentZoomLevel--;
931 zoomChanged();
932 } else {
933 return false;
934 }
935 return true;
936 }
937
938 /*
939 * We use these for quick, hackish calculations. They are temporary only and intentionally not inserted into the tileCache.
940 */
941 private Tile tempCornerTile(Tile t) {
942 int x = t.getXtile() + 1;
943 int y = t.getYtile() + 1;
944 int zoom = t.getZoom();
945 Tile tile = getTile(x, y, zoom);
946 if (tile != null)
947 return tile;
948 return new Tile(tileSource, x, y, zoom);
949 }
950
951 private Tile getOrCreateTile(TilePosition tilePosition) {
952 return getOrCreateTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
953 }
954
955 private Tile getOrCreateTile(int x, int y, int zoom) {
956 Tile tile = getTile(x, y, zoom);
957 if (tile == null) {
958 tile = new Tile(tileSource, x, y, zoom);
959 tileCache.addTile(tile);
960 }
961
962 if (!tile.isLoaded()) {
963 tile.loadPlaceholderFromCache(tileCache);
964 }
965 return tile;
966 }
967
968 private Tile getTile(TilePosition tilePosition) {
969 return getTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
970 }
971
972 /**
973 * Returns tile at given position.
974 * This can and will return null for tiles that are not already in the cache.
975 * @param x tile number on the x axis of the tile to be retrieved
976 * @param y tile number on the y axis of the tile to be retrieved
977 * @param zoom zoom level of the tile to be retrieved
978 * @return tile at given position
979 */
980 private Tile getTile(int x, int y, int zoom) {
981 if (x < tileSource.getTileXMin(zoom) || x > tileSource.getTileXMax(zoom)
982 || y < tileSource.getTileYMin(zoom) || y > tileSource.getTileYMax(zoom))
983 return null;
984 return tileCache.getTile(tileSource, x, y, zoom);
985 }
986
987 private boolean loadTile(Tile tile, boolean force) {
988 if (tile == null)
989 return false;
990 if (!force && (tile.isLoaded() || tile.hasError()))
991 return false;
992 if (tile.isLoading())
993 return false;
994 tileLoader.createTileLoaderJob(tile).submit(force);
995 return true;
996 }
997
998 private TileSet getVisibleTileSet() {
999 ProjectionBounds bounds = Main.map.mapView.getState().getViewArea().getProjectionBounds();
1000 return getTileSet(bounds.getMin(), bounds.getMax(), currentZoomLevel);
1001 }
1002
1003 protected void loadAllTiles(boolean force) {
1004 TileSet ts = getVisibleTileSet();
1005
1006 // if there is more than 18 tiles on screen in any direction, do not load all tiles!
1007 if (ts.tooLarge()) {
1008 Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
1009 return;
1010 }
1011 ts.loadAllTiles(force);
1012 invalidate();
1013 }
1014
1015 protected void loadAllErrorTiles(boolean force) {
1016 TileSet ts = getVisibleTileSet();
1017 ts.loadAllErrorTiles(force);
1018 invalidate();
1019 }
1020
1021 @Override
1022 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
1023 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
1024 if (Main.isDebugEnabled()) {
1025 Main.debug("imageUpdate() done: " + done + " calling repaint");
1026 }
1027
1028 if (done) {
1029 invalidate();
1030 } else {
1031 invalidateLater();
1032 }
1033 return !done;
1034 }
1035
1036 /**
1037 * Invalidate the layer at a time in the future so taht the user still sees the interface responsive.
1038 */
1039 private void invalidateLater() {
1040 GuiHelper.runInEDT(() -> {
1041 if (!invalidateLaterTimer.isRunning()) {
1042 invalidateLaterTimer.setRepeats(false);
1043 invalidateLaterTimer.start();
1044 }
1045 });
1046 }
1047
1048 private boolean imageLoaded(Image i) {
1049 if (i == null)
1050 return false;
1051 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
1052 if ((status & ALLBITS) != 0)
1053 return true;
1054 return false;
1055 }
1056
1057 /**
1058 * Returns the image for the given tile image is loaded.
1059 * Otherwise returns null.
1060 *
1061 * @param tile the Tile for which the image should be returned
1062 * @return the image of the tile or null.
1063 */
1064 private Image getLoadedTileImage(Tile tile) {
1065 Image img = tile.getImage();
1066 if (!imageLoaded(img))
1067 return null;
1068 return img;
1069 }
1070
1071 // 'source' is the pixel coordinates for the area that the img is capable of filling in.
1072 // However, we probably only want a portion of it.
1073 //
1074 // 'border' is the screen cordinates that need to be drawn. We must not draw outside of it.
1075 private void drawImageInside(Graphics g, Image sourceImg, Rectangle2D source, Rectangle2D border) {
1076 Rectangle2D target = source;
1077
1078 // If a border is specified, only draw the intersection if what we have combined with what we are supposed to draw.
1079 if (border != null) {
1080 target = source.createIntersection(border);
1081 if (Main.isDebugEnabled()) {
1082 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
1083 }
1084 }
1085
1086 // All of the rectangles are in screen coordinates. We need to how these correlate to the sourceImg pixels.
1087 // We could avoid doing this by scaling the image up to the 'source' size, but this should be cheaper.
1088 //
1089 // In some projections, x any y are scaled differently enough to
1090 // cause a pixel or two of fudge. Calculate them separately.
1091 double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
1092 double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
1093
1094 // How many pixels into the 'source' rectangle are we drawing?
1095 double screenXoffset = target.getX() - source.getX();
1096 double screenYoffset = target.getY() - source.getY();
1097 // And how many pixels into the image itself does that correlate to?
1098 int imgXoffset = (int) (screenXoffset * imageXScaling + 0.5);
1099 int imgYoffset = (int) (screenYoffset * imageYScaling + 0.5);
1100 // Now calculate the other corner of the image that we need
1101 // by scaling the 'target' rectangle's dimensions.
1102 int imgXend = imgXoffset + (int) (target.getWidth() * imageXScaling + 0.5);
1103 int imgYend = imgYoffset + (int) (target.getHeight() * imageYScaling + 0.5);
1104
1105 if (Main.isDebugEnabled()) {
1106 Main.debug("drawing image into target rect: " + target);
1107 }
1108 g.drawImage(sourceImg,
1109 (int) target.getX(), (int) target.getY(),
1110 (int) target.getMaxX(), (int) target.getMaxY(),
1111 imgXoffset, imgYoffset,
1112 imgXend, imgYend,
1113 this);
1114 if (PROP_FADE_AMOUNT.get() != 0) {
1115 // dimm by painting opaque rect...
1116 g.setColor(getFadeColorWithAlpha());
1117 ((Graphics2D) g).fill(target);
1118 }
1119 }
1120
1121 private List<Tile> paintTileImages(Graphics g, TileSet ts) {
1122 Object paintMutex = new Object();
1123 List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>());
1124 ts.visitTiles(tile -> {
1125 Image img = getLoadedTileImage(tile);
1126 if (img == null) {
1127 missed.add(new TilePosition(tile));
1128 return;
1129 }
1130 img = applyImageProcessors((BufferedImage) img);
1131 Rectangle2D sourceRect = coordinateConverter.getRectangleForTile(tile);
1132 synchronized (paintMutex) {
1133 //cannot paint in parallel
1134 drawImageInside(g, img, sourceRect, null);
1135 }
1136 }, missed::add);
1137
1138 return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList());
1139 }
1140
1141 // This function is called for several zoom levels, not just the current one.
1142 // It should not trigger any tiles to be downloaded.
1143 // It should also avoid polluting the tile cache with any tiles since these tiles are not mandatory.
1144 //
1145 // The "border" tile tells us the boundaries of where we may drawn.
1146 // It will not be from the zoom level that is being drawn currently.
1147 // If drawing the displayZoomLevel, border is null and we draw the entire tile set.
1148 private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
1149 if (zoom <= 0) return Collections.emptyList();
1150 Rectangle2D borderRect = coordinateConverter.getRectangleForTile(border);
1151 List<Tile> missedTiles = new LinkedList<>();
1152 // The callers of this code *require* that we return any tiles that we do not draw in missedTiles.
1153 // ts.allExistingTiles() by default will only return already-existing tiles.
1154 // However, we need to return *all* tiles to the callers, so force creation here.
1155 for (Tile tile : ts.allTilesCreate()) {
1156 Image img = getLoadedTileImage(tile);
1157 if (img == null || tile.hasError()) {
1158 if (Main.isDebugEnabled()) {
1159 Main.debug("missed tile: " + tile);
1160 }
1161 missedTiles.add(tile);
1162 continue;
1163 }
1164
1165 // applying all filters to this layer
1166 img = applyImageProcessors((BufferedImage) img);
1167
1168 Rectangle2D sourceRect = coordinateConverter.getRectangleForTile(tile);
1169 if (!sourceRect.intersects(borderRect)) {
1170 continue;
1171 }
1172 drawImageInside(g, img, sourceRect, borderRect);
1173 }
1174 return missedTiles;
1175 }
1176
1177 private void myDrawString(Graphics g, String text, int x, int y) {
1178 Color oldColor = g.getColor();
1179 String textToDraw = text;
1180 if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) {
1181 // text longer than tile size, split it
1182 StringBuilder line = new StringBuilder();
1183 StringBuilder ret = new StringBuilder();
1184 for (String s: text.split(" ")) {
1185 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) {
1186 ret.append(line).append('\n');
1187 line.setLength(0);
1188 }
1189 line.append(s).append(' ');
1190 }
1191 ret.append(line);
1192 textToDraw = ret.toString();
1193 }
1194 int offset = 0;
1195 for (String s: textToDraw.split("\n")) {
1196 g.setColor(Color.black);
1197 g.drawString(s, x + 1, y + offset + 1);
1198 g.setColor(oldColor);
1199 g.drawString(s, x, y + offset);
1200 offset += g.getFontMetrics().getHeight() + 3;
1201 }
1202 }
1203
1204 private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
1205 if (tile == null) {
1206 return;
1207 }
1208 Point2D p = coordinateConverter.getPixelForTile(t);
1209 int fontHeight = g.getFontMetrics().getHeight();
1210 int x = (int) p.getX();
1211 int y = (int) p.getY();
1212 int texty = y + 2 + fontHeight;
1213
1214 /*if (PROP_DRAW_DEBUG.get()) {
1215 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
1216 texty += 1 + fontHeight;
1217 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
1218 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
1219 texty += 1 + fontHeight;
1220 }
1221 }
1222
1223 String tileStatus = tile.getStatus();
1224 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
1225 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
1226 texty += 1 + fontHeight;
1227 }*/
1228
1229 if (tile.hasError() && getDisplaySettings().isShowErrors()) {
1230 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), x + 2, texty);
1231 //texty += 1 + fontHeight;
1232 }
1233
1234 int xCursor = -1;
1235 int yCursor = -1;
1236 if (Main.isDebugEnabled()) {
1237 if (yCursor < t.getYtile()) {
1238 if (Math.abs(t.getYtile() % 32) == 31) {
1239 g.fillRect(0, y - 1, mv.getWidth(), 3);
1240 } else {
1241 g.drawLine(0, y, mv.getWidth(), y);
1242 }
1243 //yCursor = t.getYtile();
1244 }
1245 // This draws the vertical lines for the entire column. Only draw them for the top tile in the column.
1246 if (xCursor < t.getXtile()) {
1247 if (t.getXtile() % 32 == 0) {
1248 // level 7 tile boundary
1249 g.fillRect(x - 1, 0, 3, mv.getHeight());
1250 } else {
1251 g.drawLine(x, 0, x, mv.getHeight());
1252 }
1253 //xCursor = t.getXtile();
1254 }
1255 }
1256 }
1257
1258 private LatLon getShiftedLatLon(EastNorth en) {
1259 return coordinateConverter.getProjecting().eastNorth2latlonClamped(en);
1260 }
1261
1262 private ICoordinate getShiftedCoord(EastNorth en) {
1263 return getShiftedLatLon(en).toCoordinate();
1264 }
1265
1266 private LatLon getShiftedLatLon(ICoordinate latLon) {
1267 return getShiftedLatLon(Main.getProjection().latlon2eastNorth(new LatLon(latLon)));
1268 }
1269
1270 private final TileSet nullTileSet = new TileSet();
1271
1272 private class TileSet extends TileRange {
1273
1274 protected TileSet(TileXY t1, TileXY t2, int zoom) {
1275 super(t1, t2, zoom);
1276 sanitize();
1277 }
1278
1279 /**
1280 * null tile set
1281 */
1282 private TileSet() {
1283 // default
1284 }
1285
1286 protected void sanitize() {
1287 if (minX < tileSource.getTileXMin(zoom)) {
1288 minX = tileSource.getTileXMin(zoom);
1289 }
1290 if (minY < tileSource.getTileYMin(zoom)) {
1291 minY = tileSource.getTileYMin(zoom);
1292 }
1293 if (maxX > tileSource.getTileXMax(zoom)) {
1294 maxX = tileSource.getTileXMax(zoom);
1295 }
1296 if (maxY > tileSource.getTileYMax(zoom)) {
1297 maxY = tileSource.getTileYMax(zoom);
1298 }
1299 }
1300
1301 private boolean tooSmall() {
1302 return this.tilesSpanned() < 2.1;
1303 }
1304
1305 private boolean tooLarge() {
1306 return insane() || this.tilesSpanned() > 20;
1307 }
1308
1309 private boolean insane() {
1310 return tileCache == null || size() > tileCache.getCacheSize();
1311 }
1312
1313 /**
1314 * Get all tiles represented by this TileSet that are already in the tileCache.
1315 * @return all tiles represented by this TileSet that are already in the tileCache
1316 */
1317 private List<Tile> allExistingTiles() {
1318 return allTiles(AbstractTileSourceLayer.this::getTile);
1319 }
1320
1321 private List<Tile> allTilesCreate() {
1322 return allTiles(AbstractTileSourceLayer.this::getOrCreateTile);
1323 }
1324
1325 private List<Tile> allTiles(Function<TilePosition, Tile> mapper) {
1326 return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList());
1327 }
1328
1329 @Override
1330 public Stream<TilePosition> tilePositions() {
1331 if (this.insane()) {
1332 return Stream.empty(); // Tileset is either empty or too large
1333 } else {
1334 return super.tilePositions();
1335 }
1336 }
1337
1338 private List<Tile> allLoadedTiles() {
1339 return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList());
1340 }
1341
1342 /**
1343 * @return comparator, that sorts the tiles from the center to the edge of the current screen
1344 */
1345 private Comparator<Tile> getTileDistanceComparator() {
1346 final int centerX = (int) Math.ceil((minX + maxX) / 2d);
1347 final int centerY = (int) Math.ceil((minY + maxY) / 2d);
1348 return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY));
1349 }
1350
1351 private void loadAllTiles(boolean force) {
1352 if (!getDisplaySettings().isAutoLoad() && !force)
1353 return;
1354 List<Tile> allTiles = allTilesCreate();
1355 allTiles.sort(getTileDistanceComparator());
1356 for (Tile t : allTiles) {
1357 loadTile(t, force);
1358 }
1359 }
1360
1361 private void loadAllErrorTiles(boolean force) {
1362 if (!getDisplaySettings().isAutoLoad() && !force)
1363 return;
1364 for (Tile t : this.allTilesCreate()) {
1365 if (t.hasError()) {
1366 tileLoader.createTileLoaderJob(t).submit(force);
1367 }
1368 }
1369 }
1370
1371 /**
1372 * Call the given paint method for all tiles in this tile set.<p>
1373 * Uses a parallel stream.
1374 * @param visitor A visitor to call for each tile.
1375 * @param missed a consumer to call for each missed tile.
1376 */
1377 public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) {
1378 tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed));
1379 }
1380
1381 private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) {
1382 Tile tile = getTile(tp);
1383 if (tile == null) {
1384 missed.accept(tp);
1385 } else {
1386 visitor.accept(tile);
1387 }
1388 }
1389
1390 @Override
1391 public String toString() {
1392 return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size();
1393 }
1394 }
1395
1396 /**
1397 * Create a TileSet by EastNorth bbox taking a layer shift in account
1398 * @param topLeft top-left lat/lon
1399 * @param botRight bottom-right lat/lon
1400 * @param zoom zoom level
1401 * @return the tile set
1402 * @since 10651
1403 */
1404 protected TileSet getTileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
1405 return getTileSet(getShiftedLatLon(topLeft), getShiftedLatLon(botRight), zoom);
1406 }
1407
1408 /**
1409 * Create a TileSet by known LatLon bbox without layer shift correction
1410 * @param topLeft top-left lat/lon
1411 * @param botRight bottom-right lat/lon
1412 * @param zoom zoom level
1413 * @return the tile set
1414 * @since 10651
1415 */
1416 protected TileSet getTileSet(LatLon topLeft, LatLon botRight, int zoom) {
1417 if (zoom == 0)
1418 return new TileSet();
1419
1420 TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
1421 TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
1422 return new TileSet(t1, t2, zoom);
1423 }
1424
1425 private static class TileSetInfo {
1426 boolean hasVisibleTiles;
1427 boolean hasOverzoomedTiles;
1428 boolean hasLoadingTiles;
1429 boolean hasAllLoadedTiles;
1430 }
1431
1432 private static <S extends AbstractTMSTileSource> TileSetInfo getTileSetInfo(AbstractTileSourceLayer<S>.TileSet ts) {
1433 List<Tile> allTiles = ts.allExistingTiles();
1434 TileSetInfo result = new TileSetInfo();
1435 result.hasLoadingTiles = allTiles.size() < ts.size();
1436 for (Tile t : allTiles) {
1437 if ("no-tile".equals(t.getValue("tile-info"))) {
1438 result.hasOverzoomedTiles = true;
1439 }
1440 result.hasAllLoadedTiles &= t.isLoaded();
1441
1442 if (t.isLoaded()) {
1443 if (!t.hasError()) {
1444 result.hasVisibleTiles = true;
1445 }
1446 } else if (t.isLoading()) {
1447 result.hasLoadingTiles = true;
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.