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

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