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

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

fix #13210 - Start extracting coordinate conversion out of tile source (patch by michael2402, modified) - gsoc-core

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