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

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

see #16824 - remove unneeded null-check

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