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

Last change on this file since 11188 was 11167, checked in by simon04, 8 years ago

AbstractTileSourceLayer#getTileSource: drop method parameter, use field instead

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