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

Last change on this file since 11835 was 11835, checked in by bastiK, 7 years ago

see #7427 - alignment to the pixelgrid for precise tile placement

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