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

Last change on this file since 17402 was 17402, checked in by wiktorn, 3 years ago

Refuse to download tiles if the tileset is too large

Use TileSet in overloadTiles and move decision whether to load the tileset or not to loadAllTiles.

Closes: #20207

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