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

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

fix #16730 - make sure we try to display popup menus only if their parent is visible on screen

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