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

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

see #7427 - actually use disk cache on scale change and do not force redownload

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