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

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

sonar - squid:S138 - Methods should not have too many lines

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