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

Last change on this file since 17862 was 17862, checked in by simon04, 3 years ago

fix #17177 - Add support for Mapbox Vector Tile (patch by taylor.smock)

Signed-off-by: Taylor Smock <tsmock@…>

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