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

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

fix #14120 - remove deprecated methods ImageryLayer.getDx()/getDy()/setOffset()

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