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

Last change on this file since 10826 was 10826, checked in by stoecker, 8 years ago

fix #13365 - patch my Michael Zangl

  • Property svn:eol-style set to native
File size: 71.7 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 */
1434 private List<Tile> allExistingTiles() {
1435 return allTiles(p -> getTile(p));
1436 }
1437
1438 private List<Tile> allTilesCreate() {
1439 return allTiles(p -> getOrCreateTile(p));
1440 }
1441
1442 private List<Tile> allTiles(Function<TilePosition, Tile> mapper) {
1443 return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList());
1444 }
1445
1446 @Override
1447 public Stream<TilePosition> tilePositions() {
1448 if (this.insane()) {
1449 // Tileset is either empty or too large
1450 return Stream.empty();
1451 } else {
1452 return super.tilePositions();
1453 }
1454 }
1455
1456 private List<Tile> allLoadedTiles() {
1457 return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList());
1458 }
1459
1460 /**
1461 * @return comparator, that sorts the tiles from the center to the edge of the current screen
1462 */
1463 private Comparator<Tile> getTileDistanceComparator() {
1464 final int centerX = (int) Math.ceil((minX + maxX) / 2d);
1465 final int centerY = (int) Math.ceil((minY + maxY) / 2d);
1466 return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY));
1467 }
1468
1469 private void loadAllTiles(boolean force) {
1470 if (!getDisplaySettings().isAutoLoad() && !force)
1471 return;
1472 List<Tile> allTiles = allTilesCreate();
1473 allTiles.sort(getTileDistanceComparator());
1474 for (Tile t : allTiles) {
1475 loadTile(t, force);
1476 }
1477 }
1478
1479 private void loadAllErrorTiles(boolean force) {
1480 if (!getDisplaySettings().isAutoLoad() && !force)
1481 return;
1482 for (Tile t : this.allTilesCreate()) {
1483 if (t.hasError()) {
1484 tileLoader.createTileLoaderJob(t).submit(force);
1485 }
1486 }
1487 }
1488
1489 /**
1490 * Call the given paint method for all tiles in this tile set.
1491 * <p>
1492 * Uses a parallel stream.
1493 * @param visitor A visitor to call for each tile.
1494 * @param missed a consumer to call for each missed tile.
1495 */
1496 public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) {
1497 tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed));
1498 }
1499
1500 private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) {
1501 Tile tile = getTile(tp);
1502 if (tile == null) {
1503 missed.accept(tp);
1504 } else {
1505 visitor.accept(tile);
1506 }
1507 }
1508
1509 @Override
1510 public String toString() {
1511 return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size();
1512 }
1513 }
1514
1515 /**
1516 * Create a TileSet by EastNorth bbox taking a layer shift in account
1517 * @param topLeft top-left lat/lon
1518 * @param botRight bottom-right lat/lon
1519 * @param zoom zoom level
1520 * @return the tile set
1521 * @since 10651
1522 */
1523 protected TileSet getTileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
1524 return getTileSet(getShiftedLatLon(topLeft), getShiftedLatLon(botRight), zoom);
1525 }
1526
1527 /**
1528 * Create a TileSet by known LatLon bbox without layer shift correction
1529 * @param topLeft top-left lat/lon
1530 * @param botRight bottom-right lat/lon
1531 * @param zoom zoom level
1532 * @return the tile set
1533 * @since 10651
1534 */
1535 protected TileSet getTileSet(LatLon topLeft, LatLon botRight, int zoom) {
1536 if (zoom == 0)
1537 return new TileSet();
1538
1539 TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
1540 TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
1541 return new TileSet(t1, t2, zoom);
1542 }
1543
1544 private static class TileSetInfo {
1545 public boolean hasVisibleTiles;
1546 public boolean hasOverzoomedTiles;
1547 public boolean hasLoadingTiles;
1548 }
1549
1550 private static <S extends AbstractTMSTileSource> TileSetInfo getTileSetInfo(AbstractTileSourceLayer<S>.TileSet ts) {
1551 List<Tile> allTiles = ts.allExistingTiles();
1552 TileSetInfo result = new TileSetInfo();
1553 result.hasLoadingTiles = allTiles.size() < ts.size();
1554 for (Tile t : allTiles) {
1555 if ("no-tile".equals(t.getValue("tile-info"))) {
1556 result.hasOverzoomedTiles = true;
1557 }
1558
1559 if (t.isLoaded()) {
1560 if (!t.hasError()) {
1561 result.hasVisibleTiles = true;
1562 }
1563 } else if (t.isLoading()) {
1564 result.hasLoadingTiles = true;
1565 }
1566 }
1567 return result;
1568 }
1569
1570 private class DeepTileSet {
1571 private final ProjectionBounds bounds;
1572 private final int minZoom, maxZoom;
1573 private final TileSet[] tileSets;
1574 private final TileSetInfo[] tileSetInfos;
1575
1576 @SuppressWarnings("unchecked")
1577 DeepTileSet(ProjectionBounds bounds, int minZoom, int maxZoom) {
1578 this.bounds = bounds;
1579 this.minZoom = minZoom;
1580 this.maxZoom = maxZoom;
1581 this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1];
1582 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
1583 }
1584
1585 public TileSet getTileSet(int zoom) {
1586 if (zoom < minZoom)
1587 return nullTileSet;
1588 synchronized (tileSets) {
1589 TileSet ts = tileSets[zoom-minZoom];
1590 if (ts == null) {
1591 ts = AbstractTileSourceLayer.this.getTileSet(bounds.getMin(), bounds.getMax(), zoom);
1592 tileSets[zoom-minZoom] = ts;
1593 }
1594 return ts;
1595 }
1596 }
1597
1598 public TileSetInfo getTileSetInfo(int zoom) {
1599 if (zoom < minZoom)
1600 return new TileSetInfo();
1601 synchronized (tileSetInfos) {
1602 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
1603 if (tsi == null) {
1604 tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom));
1605 tileSetInfos[zoom-minZoom] = tsi;
1606 }
1607 return tsi;
1608 }
1609 }
1610 }
1611
1612 @Override
1613 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1614 // old and unused.
1615 }
1616
1617 private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) {
1618 int zoom = currentZoomLevel;
1619 if (getDisplaySettings().isAutoZoom()) {
1620 zoom = getBestZoom();
1621 }
1622
1623 DeepTileSet dts = new DeepTileSet(pb, getMinZoomLvl(), zoom);
1624 TileSet ts = dts.getTileSet(zoom);
1625
1626 int displayZoomLevel = zoom;
1627
1628 boolean noTilesAtZoom = false;
1629 if (getDisplaySettings().isAutoZoom() && getDisplaySettings().isAutoLoad()) {
1630 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
1631 TileSetInfo tsi = dts.getTileSetInfo(zoom);
1632 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
1633 noTilesAtZoom = true;
1634 }
1635 // Find highest zoom level with at least one visible tile
1636 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
1637 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
1638 displayZoomLevel = tmpZoom;
1639 break;
1640 }
1641 }
1642 // Do binary search between currentZoomLevel and displayZoomLevel
1643 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) {
1644 zoom = (zoom + displayZoomLevel)/2;
1645 tsi = dts.getTileSetInfo(zoom);
1646 }
1647
1648 setZoomLevel(zoom);
1649
1650 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
1651 // to make sure there're really no more zoom levels
1652 // loading is done in the next if section
1653 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
1654 zoom++;
1655 tsi = dts.getTileSetInfo(zoom);
1656 }
1657 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
1658 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
1659 // loading is done in the next if section
1660 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
1661 zoom--;
1662 tsi = dts.getTileSetInfo(zoom);
1663 }
1664 ts = dts.getTileSet(zoom);
1665 } else if (getDisplaySettings().isAutoZoom()) {
1666 setZoomLevel(zoom);
1667 }
1668
1669 // Too many tiles... refuse to download
1670 if (!ts.tooLarge()) {
1671 //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
1672 ts.loadAllTiles(false);
1673 }
1674
1675 if (displayZoomLevel != zoom) {
1676 ts = dts.getTileSet(displayZoomLevel);
1677 }
1678
1679 g.setColor(Color.DARK_GRAY);
1680
1681 List<Tile> missedTiles = this.paintTileImages(g, ts);
1682 int[] otherZooms = {-1, 1, -2, 2, -3, -4, -5};
1683 for (int zoomOffset : otherZooms) {
1684 if (!getDisplaySettings().isAutoZoom()) {
1685 break;
1686 }
1687 int newzoom = displayZoomLevel + zoomOffset;
1688 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) {
1689 continue;
1690 }
1691 if (missedTiles.isEmpty()) {
1692 break;
1693 }
1694 List<Tile> newlyMissedTiles = new LinkedList<>();
1695 for (Tile missed : missedTiles) {
1696 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
1697 // Don't try to paint from higher zoom levels when tile is overzoomed
1698 newlyMissedTiles.add(missed);
1699 continue;
1700 }
1701 Tile t2 = tempCornerTile(missed);
1702 TileSet ts2 = getTileSet(
1703 getShiftedLatLon(tileSource.tileXYToLatLon(missed)),
1704 getShiftedLatLon(tileSource.tileXYToLatLon(t2)),
1705 newzoom);
1706 // Instantiating large TileSets is expensive. If there
1707 // are no loaded tiles, don't bother even trying.
1708 if (ts2.allLoadedTiles().isEmpty()) {
1709 newlyMissedTiles.add(missed);
1710 continue;
1711 }
1712 if (ts2.tooLarge()) {
1713 continue;
1714 }
1715 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
1716 }
1717 missedTiles = newlyMissedTiles;
1718 }
1719 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
1720 Main.debug("still missed "+missedTiles.size()+" in the end");
1721 }
1722 g.setColor(Color.red);
1723 g.setFont(InfoFont);
1724
1725 // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge()
1726 for (Tile t : ts.allExistingTiles()) {
1727 this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
1728 }
1729
1730 EastNorth min = pb.getMin();
1731 EastNorth max = pb.getMax();
1732 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max),
1733 displayZoomLevel, this);
1734
1735 //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
1736 g.setColor(Color.lightGray);
1737
1738 if (ts.insane()) {
1739 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
1740 } else if (ts.tooLarge()) {
1741 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
1742 } else if (!getDisplaySettings().isAutoZoom() && ts.tooSmall()) {
1743 myDrawString(g, tr("increase tiles zoom level (change resolution) to see more detail"), 120, 120);
1744 }
1745
1746 if (noTilesAtZoom) {
1747 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
1748 }
1749 if (Main.isDebugEnabled()) {
1750 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
1751 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
1752 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
1753 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
1754 myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200);
1755 if (tileLoader instanceof TMSCachedTileLoader) {
1756 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader;
1757 int offset = 200;
1758 for (String part: cachedTileLoader.getStats().split("\n")) {
1759 offset += 15;
1760 myDrawString(g, tr("Cache stats: {0}", part), 50, offset);
1761 }
1762 }
1763 }
1764 }
1765
1766 /**
1767 * Returns tile for a pixel position.<p>
1768 * This isn't very efficient, but it is only used when the user right-clicks on the map.
1769 * @param px pixel X coordinate
1770 * @param py pixel Y coordinate
1771 * @return Tile at pixel position
1772 */
1773 private Tile getTileForPixelpos(int px, int py) {
1774 if (Main.isDebugEnabled()) {
1775 Main.debug("getTileForPixelpos("+px+", "+py+')');
1776 }
1777 MapView mv = Main.map.mapView;
1778 Point clicked = new Point(px, py);
1779 EastNorth topLeft = mv.getEastNorth(0, 0);
1780 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1781 int z = currentZoomLevel;
1782 TileSet ts = getTileSet(topLeft, botRight, z);
1783
1784 if (!ts.tooLarge()) {
1785 ts.loadAllTiles(false); // make sure there are tile objects for all tiles
1786 }
1787 Stream<Tile> clickedTiles = ts.allExistingTiles().stream()
1788 .filter(t -> coordinateConverter.getRectangleForTile(t).contains(clicked));
1789 if (Main.isTraceEnabled()) {
1790 clickedTiles = clickedTiles.peek(t -> Main.trace("Clicked on tile: " + t.getXtile() + ' ' + t.getYtile() +
1791 " currentZoomLevel: " + currentZoomLevel));
1792 }
1793 return clickedTiles.findAny().orElse(null);
1794 }
1795
1796 @Override
1797 public Action[] getMenuEntries() {
1798 ArrayList<Action> actions = new ArrayList<>();
1799 actions.addAll(Arrays.asList(getLayerListEntries()));
1800 actions.addAll(Arrays.asList(getCommonEntries()));
1801 actions.add(SeparatorLayerAction.INSTANCE);
1802 actions.add(new LayerListPopup.InfoAction(this));
1803 return actions.toArray(new Action[actions.size()]);
1804 }
1805
1806 public Action[] getLayerListEntries() {
1807 return new Action[] {
1808 LayerListDialog.getInstance().createActivateLayerAction(this),
1809 LayerListDialog.getInstance().createShowHideLayerAction(),
1810 LayerListDialog.getInstance().createDeleteLayerAction(),
1811 SeparatorLayerAction.INSTANCE,
1812 // color,
1813 new OffsetAction(),
1814 new RenameLayerAction(this.getAssociatedFile(), this),
1815 SeparatorLayerAction.INSTANCE
1816 };
1817 }
1818
1819 /**
1820 * Returns the common menu entries.
1821 * @return the common menu entries
1822 */
1823 public Action[] getCommonEntries() {
1824 return new Action[] {
1825 new AutoLoadTilesAction(),
1826 new AutoZoomAction(),
1827 new ShowErrorsAction(),
1828 new IncreaseZoomAction(),
1829 new DecreaseZoomAction(),
1830 new ZoomToBestAction(),
1831 new ZoomToNativeLevelAction(),
1832 new FlushTileCacheAction(),
1833 new LoadErroneusTilesAction(),
1834 new LoadAllTilesAction()
1835 };
1836 }
1837
1838 @Override
1839 public String getToolTipText() {
1840 if (getDisplaySettings().isAutoLoad()) {
1841 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1842 } else {
1843 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1844 }
1845 }
1846
1847 @Override
1848 public void visitBoundingBox(BoundingXYVisitor v) {
1849 }
1850
1851 @Override
1852 public boolean isChanged() {
1853 // we use #invalidate()
1854 return false;
1855 }
1856
1857 /**
1858 * Task responsible for precaching imagery along the gpx track
1859 *
1860 */
1861 public class PrecacheTask implements TileLoaderListener {
1862 private final ProgressMonitor progressMonitor;
1863 private int totalCount;
1864 private final AtomicInteger processedCount = new AtomicInteger(0);
1865 private final TileLoader tileLoader;
1866
1867 /**
1868 * @param progressMonitor that will be notified about progess of the task
1869 */
1870 public PrecacheTask(ProgressMonitor progressMonitor) {
1871 this.progressMonitor = progressMonitor;
1872 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
1873 if (this.tileLoader instanceof TMSCachedTileLoader) {
1874 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
1875 TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
1876 }
1877 }
1878
1879 /**
1880 * @return true, if all is done
1881 */
1882 public boolean isFinished() {
1883 return processedCount.get() >= totalCount;
1884 }
1885
1886 /**
1887 * @return total number of tiles to download
1888 */
1889 public int getTotalCount() {
1890 return totalCount;
1891 }
1892
1893 /**
1894 * cancel the task
1895 */
1896 public void cancel() {
1897 if (tileLoader instanceof TMSCachedTileLoader) {
1898 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
1899 }
1900 }
1901
1902 @Override
1903 public void tileLoadingFinished(Tile tile, boolean success) {
1904 int processed = this.processedCount.incrementAndGet();
1905 if (success) {
1906 this.progressMonitor.worked(1);
1907 this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processed, totalCount));
1908 } else {
1909 Main.warn("Tile loading failure: " + tile + " - " + tile.getErrorMessage());
1910 }
1911 }
1912
1913 /**
1914 * @return tile loader that is used to load the tiles
1915 */
1916 public TileLoader getTileLoader() {
1917 return tileLoader;
1918 }
1919 }
1920
1921 /**
1922 * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download
1923 * all of the tiles. Buffer contains at least one tile.
1924 *
1925 * To prevent accidental clear of the queue, new download executor is created with separate queue
1926 *
1927 * @param progressMonitor progress monitor for download task
1928 * @param points lat/lon coordinates to download
1929 * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides
1930 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides
1931 * @return precache task representing download task
1932 */
1933 public AbstractTileSourceLayer<T>.PrecacheTask downloadAreaToCache(final ProgressMonitor progressMonitor, List<LatLon> points,
1934 double bufferX, double bufferY) {
1935 PrecacheTask precacheTask = new PrecacheTask(progressMonitor);
1936 final Set<Tile> requestedTiles = new ConcurrentSkipListSet<>(
1937 (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey()));
1938 for (LatLon point: points) {
1939
1940 TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel);
1941 TileXY curTile = tileSource.latLonToTileXY(point.toCoordinate(), currentZoomLevel);
1942 TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel);
1943
1944 // take at least one tile of buffer
1945 int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex());
1946 int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex());
1947 int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex());
1948 int maxX = Math.max(curTile.getXIndex() + 1, maxTile.getXIndex());
1949
1950 for (int x = minX; x <= maxX; x++) {
1951 for (int y = minY; y <= maxY; y++) {
1952 requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
1953 }
1954 }
1955 }
1956
1957 precacheTask.totalCount = requestedTiles.size();
1958 precacheTask.progressMonitor.setTicksCount(requestedTiles.size());
1959
1960 TileLoader loader = precacheTask.getTileLoader();
1961 for (Tile t: requestedTiles) {
1962 loader.createTileLoaderJob(t).submit();
1963 }
1964 return precacheTask;
1965 }
1966
1967 @Override
1968 public boolean isSavable() {
1969 return true; // With WMSLayerExporter
1970 }
1971
1972 @Override
1973 public File createAndOpenSaveFileChooser() {
1974 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
1975 }
1976
1977 @Override
1978 public void destroy() {
1979 super.destroy();
1980 adjustAction.destroy();
1981 }
1982
1983 private class TileSourcePainter extends CompatibilityModeLayerPainter {
1984 /**
1985 * The memory handle that will hold our tile source.
1986 */
1987 private MemoryHandle<?> memory;
1988
1989 @Override
1990 public void paint(MapViewGraphics graphics) {
1991 allocateCacheMemory();
1992 if (memory != null) {
1993 doPaint(graphics);
1994 }
1995 }
1996
1997 private void doPaint(MapViewGraphics graphics) {
1998 ProjectionBounds pb = graphics.getClipBounds().getProjectionBounds();
1999
2000 drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), pb);
2001 }
2002
2003 private void allocateCacheMemory() {
2004 if (memory == null) {
2005 MemoryManager manager = MemoryManager.getInstance();
2006 if (manager.isAvailable(getEstimatedCacheSize())) {
2007 try {
2008 memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new);
2009 } catch (NotEnoughMemoryException e) {
2010 Main.warn("Could not allocate tile source memory", e);
2011 }
2012 }
2013 }
2014 }
2015
2016 protected long getEstimatedCacheSize() {
2017 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize();
2018 }
2019
2020 @Override
2021 public void detachFromMapView(MapViewEvent event) {
2022 event.getMapView().removeMouseListener(adapter);
2023 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this);
2024 super.detachFromMapView(event);
2025 if (memory != null) {
2026 memory.free();
2027 }
2028 }
2029 }
2030}
Note: See TracBrowser for help on using the repository browser.