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

Last change on this file since 18926 was 18926, checked in by taylor.smock, 5 months ago

Fix #23367: Don't try to load too many tiles

In this case, we are catching an ArithmeticException when an integer overflow occurs.

Additionally, fix some lint issues and update some dependencies:

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