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

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

see #15673 - fix NPE seen in unit test

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