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

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

fix #14642 - IAE at AbstractTileSourceLayer.getBestZoom

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