source: josm/trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java@ 8344

Last change on this file since 8344 was 8344, checked in by bastiK, 9 years ago

applied #10454 - Mapbox "empty" tile (imagery with zoom level > 17) (patch by wiktorn)

  • Property svn:eol-style set to native
File size: 52.7 KB
RevLine 
[3719]1// License: GPL. For details, see LICENSE file.
[3715]2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.awt.Font;
8import java.awt.Graphics;
9import java.awt.Graphics2D;
10import java.awt.Image;
11import java.awt.Point;
12import java.awt.Rectangle;
13import java.awt.Toolkit;
14import java.awt.event.ActionEvent;
15import java.awt.event.MouseAdapter;
16import java.awt.event.MouseEvent;
17import java.awt.image.ImageObserver;
[3777]18import java.io.File;
[3715]19import java.io.IOException;
[4825]20import java.io.StringReader;
21import java.net.URL;
[4985]22import java.util.ArrayList;
23import java.util.Collections;
[8168]24import java.util.HashMap;
[4985]25import java.util.LinkedList;
26import java.util.List;
27import java.util.Map;
28import java.util.Scanner;
[4825]29import java.util.concurrent.Callable;
[4531]30import java.util.regex.Matcher;
31import java.util.regex.Pattern;
[3715]32
33import javax.swing.AbstractAction;
34import javax.swing.Action;
35import javax.swing.JCheckBoxMenuItem;
36import javax.swing.JMenuItem;
[4183]37import javax.swing.JOptionPane;
[3715]38import javax.swing.JPopupMenu;
39
[4489]40import org.openstreetmap.gui.jmapviewer.AttributionSupport;
[3715]41import org.openstreetmap.gui.jmapviewer.Coordinate;
42import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
[3777]43import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
[3715]44import org.openstreetmap.gui.jmapviewer.Tile;
[6042]45import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
[8168]46import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
47import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
[3715]48import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
49import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
[3915]50import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
51import org.openstreetmap.gui.jmapviewer.tilesources.ScanexTileSource;
52import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
53import org.openstreetmap.gui.jmapviewer.tilesources.TemplatedTMSTileSource;
[3715]54import org.openstreetmap.josm.Main;
55import org.openstreetmap.josm.actions.RenameLayerAction;
56import org.openstreetmap.josm.data.Bounds;
[5898]57import org.openstreetmap.josm.data.Version;
[3715]58import org.openstreetmap.josm.data.coor.EastNorth;
59import org.openstreetmap.josm.data.coor.LatLon;
60import org.openstreetmap.josm.data.imagery.ImageryInfo;
61import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
[8168]62import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
[3715]63import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
64import org.openstreetmap.josm.data.preferences.BooleanProperty;
65import org.openstreetmap.josm.data.preferences.IntegerProperty;
[3777]66import org.openstreetmap.josm.data.preferences.StringProperty;
[4183]67import org.openstreetmap.josm.data.projection.Projection;
[5261]68import org.openstreetmap.josm.gui.MapFrame;
[3715]69import org.openstreetmap.josm.gui.MapView;
70import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
[4985]71import org.openstreetmap.josm.gui.PleaseWaitRunnable;
[3715]72import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
73import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
[4985]74import org.openstreetmap.josm.gui.progress.ProgressMonitor;
[4825]75import org.openstreetmap.josm.io.CacheCustomContent;
[4985]76import org.openstreetmap.josm.io.OsmTransferException;
[4825]77import org.openstreetmap.josm.io.UTFInputStreamReader;
[7864]78import org.openstreetmap.josm.tools.CheckParameterUtil;
[5868]79import org.openstreetmap.josm.tools.Utils;
[4825]80import org.xml.sax.InputSource;
[4985]81import org.xml.sax.SAXException;
[3715]82
83/**
84 * Class that displays a slippy map layer.
85 *
[6830]86 * @author Frederik Ramm
87 * @author LuVar <lubomir.varga@freemap.sk>
88 * @author Dave Hansen <dave@sr71.net>
89 * @author Upliner <upliner@gmail.com>
[3715]90 *
91 */
92public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener {
93 public static final String PREFERENCE_PREFIX = "imagery.tms";
94
95 public static final int MAX_ZOOM = 30;
96 public static final int MIN_ZOOM = 2;
[3773]97 public static final int DEFAULT_MAX_ZOOM = 20;
[3715]98 public static final int DEFAULT_MIN_ZOOM = 2;
99
100 public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
101 public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true);
[4301]102 public static final BooleanProperty PROP_DEFAULT_SHOWERRORS = new BooleanProperty(PREFERENCE_PREFIX + ".default_showerrors", true);
[3715]103 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", DEFAULT_MIN_ZOOM);
104 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", DEFAULT_MAX_ZOOM);
[4188]105 //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
[3715]106 public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + ".add_to_slippymap_chooser", true);
[3777]107 public static final StringProperty PROP_TILECACHE_DIR;
[7864]108
[3777]109 static {
110 String defPath = null;
111 try {
[7911]112 defPath = new File(Main.pref.getCacheDirectory(), "tms").getAbsolutePath();
[3777]113 } catch (SecurityException e) {
[6310]114 Main.warn(e);
[3777]115 }
[7911]116 PROP_TILECACHE_DIR = new StringProperty(PREFERENCE_PREFIX + ".tilecache", defPath);
[3777]117 }
118
[5779]119 public interface TileLoaderFactory {
[8168]120 TileLoader makeTileLoader(TileLoaderListener listener);
121 TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
[5779]122 }
123
[8168]124 protected TileCache tileCache;
[3715]125 protected TileSource tileSource;
[8168]126 protected TileLoader tileLoader;
[6070]127
[8168]128
[5779]129 public static TileLoaderFactory loaderFactory = new TileLoaderFactory() {
130 @Override
[8168]131 public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
132 Map<String, String> headers = new HashMap<>();
133 headers.put("User-Agent", Version.getInstance().getFullAgentString());
134 headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
135 if (inputHeaders != null)
136 headers.putAll(inputHeaders);
137
138 try {
139 return new TMSCachedTileLoader(listener, "TMS",
140 Main.pref.getInteger("socket.timeout.connect",15) * 1000,
141 Main.pref.getInteger("socket.timeout.read", 30) * 1000,
142 headers,
143 PROP_TILECACHE_DIR.get());
144 } catch (IOException e) {
145 Main.warn(e);
[5779]146 }
147 return null;
148 }
[8168]149
150 @Override
151 public TileLoader makeTileLoader(TileLoaderListener listener) {
152 return makeTileLoader(listener, null);
153 }
[5779]154 };
[6070]155
[5779]156 /**
[6310]157 * Plugins that wish to set custom tile loader should call this method
158 */
[8168]159
[5779]160 public static void setCustomTileLoaderFactory(TileLoaderFactory loaderFactory) {
161 TMSLayer.loaderFactory = loaderFactory;
162 }
[6070]163
[3715]164 @Override
[5261]165 public synchronized void tileLoadingFinished(Tile tile, boolean success) {
[3773]166 if (tile.hasError()) {
167 success = false;
168 tile.setImage(null);
[3715]169 }
[3773]170 if (sharpenLevel != 0 && success) {
171 tile.setImage(sharpenImage(tile.getImage()));
172 }
[8168]173 tile.setLoaded(success);
[3715]174 needRedraw = true;
[7329]175 if (Main.map != null) {
176 Main.map.repaint(100);
177 }
[7024]178 if (Main.isDebugEnabled()) {
[4188]179 Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success);
[7024]180 }
[3715]181 }
[6070]182
[5261]183 /**
184 * Clears the tile cache.
[6070]185 *
186 * If the current tileLoader is an instance of OsmTileLoader, a new
187 * TmsTileClearController is created and passed to the according clearCache
[5261]188 * method.
[6070]189 *
190 * @param monitor
[6897]191 * @see OsmFileCacheTileLoader#clearCache(org.openstreetmap.gui.jmapviewer.interfaces.TileSource, org.openstreetmap.gui.jmapviewer.interfaces.TileClearController)
[5261]192 */
193 void clearTileCache(ProgressMonitor monitor) {
[4529]194 tileCache.clear();
[6042]195 if (tileLoader instanceof CachedTileLoader) {
[8186]196 ((CachedTileLoader)tileLoader).clearCache(tileSource);
[4529]197 }
[3715]198 }
199
200 /**
[3774]201 * Zoomlevel at which tiles is currently downloaded.
202 * Initial zoom lvl is set to bestZoom
[3715]203 */
204 public int currentZoomLevel;
205
206 private Tile clickedTile;
207 private boolean needRedraw;
208 private JPopupMenu tileOptionMenu;
[8285]209 private JCheckBoxMenuItem autoZoomPopup;
210 private JCheckBoxMenuItem autoLoadPopup;
211 private JCheckBoxMenuItem showErrorsPopup;
212 private Tile showMetadataTile;
[4489]213 private AttributionSupport attribution = new AttributionSupport();
[3740]214 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
[3715]215
216 protected boolean autoZoom;
217 protected boolean autoLoad;
[4301]218 protected boolean showErrors;
[3715]219
[5261]220 /**
221 * Initiates a repaint of Main.map
[6070]222 *
[5261]223 * @see Main#map
[6070]224 * @see MapFrame#repaint()
[5261]225 */
226 void redraw() {
[3715]227 needRedraw = true;
228 Main.map.repaint();
229 }
230
[5261]231 static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
[3715]232 if(maxZoomLvl > MAX_ZOOM) {
233 maxZoomLvl = MAX_ZOOM;
234 }
235 if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
236 maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
237 }
238 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
239 maxZoomLvl = ts.getMaxZoom();
240 }
241 return maxZoomLvl;
242 }
243
[5261]244 public static int getMaxZoomLvl(TileSource ts) {
[3715]245 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
246 }
247
248 public static void setMaxZoomLvl(int maxZoomLvl) {
249 maxZoomLvl = checkMaxZoomLvl(maxZoomLvl, null);
250 PROP_MAX_ZOOM_LVL.put(maxZoomLvl);
251 }
252
[5261]253 static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
[3715]254 if(minZoomLvl < MIN_ZOOM) {
[4188]255 /*Main.debug("Min. zoom level should not be less than "+MIN_ZOOM+"! Setting to that.");*/
[3715]256 minZoomLvl = MIN_ZOOM;
257 }
258 if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
[4188]259 /*Main.debug("Min. zoom level should not be more than Max. zoom level! Setting to Max.");*/
[3715]260 minZoomLvl = getMaxZoomLvl(ts);
261 }
262 if (ts != null && ts.getMinZoom() > minZoomLvl) {
[4188]263 /*Main.debug("Increasing min. zoom level to match tile source");*/
[3715]264 minZoomLvl = ts.getMinZoom();
265 }
266 return minZoomLvl;
267 }
268
[5261]269 public static int getMinZoomLvl(TileSource ts) {
[3715]270 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
271 }
272
273 public static void setMinZoomLvl(int minZoomLvl) {
274 minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
275 PROP_MIN_ZOOM_LVL.put(minZoomLvl);
276 }
277
[4825]278 private static class CachedAttributionBingAerialTileSource extends BingAerialTileSource {
279
[8344]280 public CachedAttributionBingAerialTileSource(ImageryInfo info) {
281 super(info);
[7823]282 }
283
[4825]284 class BingAttributionData extends CacheCustomContent<IOException> {
285
286 public BingAttributionData() {
[5281]287 super("bing.attribution.xml", CacheCustomContent.INTERVAL_HOURLY);
[4825]288 }
289
290 @Override
291 protected byte[] updateData() throws IOException {
292 URL u = getAttributionUrl();
[7033]293 try (Scanner scanner = new Scanner(UTFInputStreamReader.create(Utils.openURL(u)))) {
294 String r = scanner.useDelimiter("\\A").next();
295 Main.info("Successfully loaded Bing attribution data.");
296 return r.getBytes("UTF-8");
297 }
[4825]298 }
299 }
300
301 @Override
302 protected Callable<List<Attribution>> getAttributionLoaderCallable() {
303 return new Callable<List<Attribution>>() {
304
305 @Override
306 public List<Attribution> call() throws Exception {
307 BingAttributionData attributionLoader = new BingAttributionData();
308 int waitTimeSec = 1;
309 while (true) {
310 try {
311 String xml = attributionLoader.updateIfRequiredString();
312 return parseAttributionText(new InputSource(new StringReader((xml))));
313 } catch (IOException ex) {
[6248]314 Main.warn("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
[4825]315 Thread.sleep(waitTimeSec * 1000L);
316 waitTimeSec *= 2;
317 }
318 }
319 }
320 };
321 }
322 }
323
[5261]324 /**
325 * Creates and returns a new TileSource instance depending on the {@link ImageryType}
326 * of the passed ImageryInfo object.
[6070]327 *
[5261]328 * If no appropriate TileSource is found, null is returned.
[6070]329 * Currently supported ImageryType are {@link ImageryType#TMS},
[5261]330 * {@link ImageryType#BING}, {@link ImageryType#SCANEX}.
[6070]331 *
[5261]332 * @param info
333 * @return a new TileSource instance or null if no TileSource for the ImageryInfo/ImageryType could be found.
[6070]334 * @throws IllegalArgumentException
[5261]335 */
[8291]336 public static TileSource getTileSource(ImageryInfo info) {
[3715]337 if (info.getImageryType() == ImageryType.TMS) {
[4531]338 checkUrl(info.getUrl());
[8344]339 TMSTileSource t = new TemplatedTMSTileSource(info);
[4432]340 info.setAttribution(t);
341 return t;
[8344]342 } else if (info.getImageryType() == ImageryType.BING) {
343 //return new CachedAttributionBingAerialTileSource(info.getId());
344 return new CachedAttributionBingAerialTileSource(info);
345 } else if (info.getImageryType() == ImageryType.SCANEX) {
346 //return new ScanexTileSource(info.getName(), info.getUrl(), info.getId(), info.getMaxZoom());
347 return new ScanexTileSource(info);
[4825]348 }
[3715]349 return null;
350 }
[4825]351
[7864]352 /**
353 * Checks validity of given URL.
354 * @param url URL to check
355 * @throws IllegalArgumentException if url is null or invalid
356 */
357 public static void checkUrl(String url) {
358 CheckParameterUtil.ensureParameterNotNull(url, "url");
359 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
360 while (m.find()) {
361 boolean isSupportedPattern = false;
362 for (String pattern : TemplatedTMSTileSource.ALL_PATTERNS) {
363 if (m.group().matches(pattern)) {
364 isSupportedPattern = true;
365 break;
[4531]366 }
367 }
[7864]368 if (!isSupportedPattern) {
369 throw new IllegalArgumentException(
370 tr("{0} is not a valid TMS argument. Please check this server URL:\n{1}", m.group(), url));
371 }
[4531]372 }
373 }
[3715]374
[5261]375 private void initTileSource(TileSource tileSource) {
[3715]376 this.tileSource = tileSource;
[4489]377 attribution.initialize(tileSource);
[3715]378
[3785]379 currentZoomLevel = getBestZoom();
[3773]380
[8168]381 Map<String, String> headers = null;
[4538]382 if (tileSource instanceof TemplatedTMSTileSource) {
[8168]383 headers = (((TemplatedTMSTileSource)tileSource).getHeaders());
[4538]384 }
[3715]385
[8168]386 // FIXME: tileCache = new MemoryTileCache();
387 tileLoader = loaderFactory.makeTileLoader(this, headers);
388 if (tileLoader instanceof TMSCachedTileLoader) {
389 tileCache = (TileCache) tileLoader;
390 } else {
391 tileCache = new MemoryTileCache();
392 }
393 if (tileLoader == null)
394 tileLoader = new OsmTileLoader(this);
[3715]395 }
396
[3785]397 /**
[8288]398 * Marks layer as needing redraw on offset change
399 */
400 @Override
401 public void setOffset(double dx, double dy) {
402 super.setOffset(dx, dy);
403 needRedraw = true;
404 }
405 /**
[3785]406 * Returns average number of screen pixels per tile pixel for current mapview
407 */
408 private double getScaleFactor(int zoom) {
[6336]409 if (!Main.isDisplayingMapView()) return 1;
[3733]410 MapView mv = Main.map.mapView;
[3785]411 LatLon topLeft = mv.getLatLon(0, 0);
412 LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
[3878]413 double x1 = tileSource.lonToTileX(topLeft.lon(), zoom);
414 double y1 = tileSource.latToTileY(topLeft.lat(), zoom);
415 double x2 = tileSource.lonToTileX(botRight.lon(), zoom);
416 double y2 = tileSource.latToTileY(botRight.lat(), zoom);
[3785]417
418 int screenPixels = mv.getWidth()*mv.getHeight();
419 double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize());
420 if (screenPixels == 0 || tilePixels == 0) return 1;
421 return screenPixels/tilePixels;
[3715]422 }
423
[6890]424 private final int getBestZoom() {
[3785]425 double factor = getScaleFactor(1);
426 double result = Math.log(factor)/Math.log(2)/2+1;
[4328]427 // In general, smaller zoom levels are more readable. We prefer big,
428 // block, pixelated (but readable) map text to small, smeared,
429 // unreadable underzoomed text. So, use .floor() instead of rounding
430 // to skew things a bit toward the lower zooms.
431 int intResult = (int)Math.floor(result);
[3785]432 if (intResult > getMaxZoomLvl())
433 return getMaxZoomLvl();
434 if (intResult < getMinZoomLvl())
435 return getMinZoomLvl();
436 return intResult;
[3715]437 }
438
439 @SuppressWarnings("serial")
440 public TMSLayer(ImageryInfo info) {
441 super(info);
442
[4183]443 if(!isProjectionSupported(Main.getProjection())) {
[4301]444 JOptionPane.showMessageDialog(Main.parent,
[8168]445 tr("TMS layers do not support the projection {0}.\n{1}\n"
446 + "Change the projection or remove the layer.",
447 Main.getProjection().toCode(), nameSupportedProjections()),
448 tr("Warning"),
449 JOptionPane.WARNING_MESSAGE);
[4183]450 }
451
[3715]452 setBackgroundLayer(true);
453 this.setVisible(true);
454
455 TileSource source = getTileSource(info);
456 if (source == null)
[4306]457 throw new IllegalStateException("Cannot create TMSLayer with non-TMS ImageryInfo");
[3715]458 initTileSource(source);
[5391]459 }
[3715]460
[5391]461 /**
462 * Adds a context menu to the mapView.
463 */
464 @Override
465 public void hookUpMapView() {
[3715]466 tileOptionMenu = new JPopupMenu();
467
468 autoZoom = PROP_DEFAULT_AUTOZOOM.get();
469 autoZoomPopup = new JCheckBoxMenuItem();
470 autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
471 @Override
472 public void actionPerformed(ActionEvent ae) {
473 autoZoom = !autoZoom;
474 }
475 });
476 autoZoomPopup.setSelected(autoZoom);
477 tileOptionMenu.add(autoZoomPopup);
478
479 autoLoad = PROP_DEFAULT_AUTOLOAD.get();
480 autoLoadPopup = new JCheckBoxMenuItem();
481 autoLoadPopup.setAction(new AbstractAction(tr("Auto load tiles")) {
482 @Override
483 public void actionPerformed(ActionEvent ae) {
484 autoLoad= !autoLoad;
485 }
486 });
487 autoLoadPopup.setSelected(autoLoad);
488 tileOptionMenu.add(autoLoadPopup);
489
[4301]490 showErrors = PROP_DEFAULT_SHOWERRORS.get();
491 showErrorsPopup = new JCheckBoxMenuItem();
492 showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) {
493 @Override
494 public void actionPerformed(ActionEvent ae) {
495 showErrors = !showErrors;
496 }
497 });
498 showErrorsPopup.setSelected(showErrors);
499 tileOptionMenu.add(showErrorsPopup);
500
[3715]501 tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
502 @Override
503 public void actionPerformed(ActionEvent ae) {
504 if (clickedTile != null) {
[4306]505 loadTile(clickedTile, true);
[3715]506 redraw();
507 }
508 }
509 }));
510
511 tileOptionMenu.add(new JMenuItem(new AbstractAction(
512 tr("Show Tile Info")) {
513 @Override
514 public void actionPerformed(ActionEvent ae) {
515 if (clickedTile != null) {
516 showMetadataTile = clickedTile;
517 redraw();
518 }
519 }
520 }));
521
522 /* FIXME
523 tileOptionMenu.add(new JMenuItem(new AbstractAction(
524 tr("Request Update")) {
525 public void actionPerformed(ActionEvent ae) {
526 if (clickedTile != null) {
527 clickedTile.requestUpdate();
528 redraw();
529 }
530 }
531 }));*/
532
533 tileOptionMenu.add(new JMenuItem(new AbstractAction(
534 tr("Load All Tiles")) {
535 @Override
536 public void actionPerformed(ActionEvent ae) {
537 loadAllTiles(true);
538 redraw();
539 }
540 }));
541
[4306]542 tileOptionMenu.add(new JMenuItem(new AbstractAction(
543 tr("Load All Error Tiles")) {
544 @Override
545 public void actionPerformed(ActionEvent ae) {
546 loadAllErrorTiles(true);
547 redraw();
548 }
549 }));
550
[3715]551 // increase and decrease commands
[5391]552 tileOptionMenu.add(new JMenuItem(new AbstractAction(
553 tr("Increase zoom")) {
554 @Override
555 public void actionPerformed(ActionEvent ae) {
556 increaseZoomLevel();
557 redraw();
558 }
559 }));
[3715]560
[5391]561 tileOptionMenu.add(new JMenuItem(new AbstractAction(
562 tr("Decrease zoom")) {
563 @Override
564 public void actionPerformed(ActionEvent ae) {
565 decreaseZoomLevel();
566 redraw();
567 }
568 }));
569
570 tileOptionMenu.add(new JMenuItem(new AbstractAction(
571 tr("Snap to tile size")) {
572 @Override
573 public void actionPerformed(ActionEvent ae) {
574 double new_factor = Math.sqrt(getScaleFactor(currentZoomLevel));
575 Main.map.mapView.zoomToFactor(new_factor);
576 redraw();
577 }
578 }));
579
580 tileOptionMenu.add(new JMenuItem(new AbstractAction(
581 tr("Flush Tile Cache")) {
582 @Override
583 public void actionPerformed(ActionEvent ae) {
584 new PleaseWaitRunnable(tr("Flush Tile Cache")) {
[3715]585 @Override
[5391]586 protected void realRun() throws SAXException, IOException,
587 OsmTransferException {
588 clearTileCache(getProgressMonitor());
[3715]589 }
590
591 @Override
[5391]592 protected void finish() {
[3715]593 }
594
595 @Override
[5391]596 protected void cancel() {
[3715]597 }
[5391]598 }.run();
599 }
600 }));
[3715]601
[5391]602 final MouseAdapter adapter = new MouseAdapter() {
[3715]603 @Override
[5391]604 public void mouseClicked(MouseEvent e) {
605 if (!isVisible()) return;
606 if (e.getButton() == MouseEvent.BUTTON3) {
607 clickedTile = getTileForPixelpos(e.getX(), e.getY());
608 tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
609 } else if (e.getButton() == MouseEvent.BUTTON1) {
610 attribution.handleAttribution(e.getPoint(), true);
611 }
612 }
613 };
614 Main.map.mapView.addMouseListener(adapter);
[3715]615
[5391]616 MapView.addLayerChangeListener(new LayerChangeListener() {
617 @Override
618 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
619 //
620 }
[3715]621
[5391]622 @Override
623 public void layerAdded(Layer newLayer) {
624 //
625 }
[3715]626
[5391]627 @Override
628 public void layerRemoved(Layer oldLayer) {
629 if (oldLayer == TMSLayer.this) {
630 Main.map.mapView.removeMouseListener(adapter);
631 MapView.removeLayerChangeListener(this);
632 }
[3715]633 }
634 });
635 }
636
[5261]637 void zoomChanged() {
[7024]638 if (Main.isDebugEnabled()) {
[4188]639 Main.debug("zoomChanged(): " + currentZoomLevel);
[7024]640 }
[3715]641 needRedraw = true;
642 }
643
[5261]644 int getMaxZoomLvl() {
[3715]645 if (info.getMaxZoom() != 0)
646 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
647 else
648 return getMaxZoomLvl(tileSource);
649 }
650
[5261]651 int getMinZoomLvl() {
[3715]652 return getMinZoomLvl(tileSource);
653 }
654
655 /**
656 * Zoom in, go closer to map.
657 *
658 * @return true, if zoom increasing was successfull, false othervise
659 */
[5261]660 public boolean zoomIncreaseAllowed() {
[3785]661 boolean zia = currentZoomLevel < this.getMaxZoomLvl();
[7024]662 if (Main.isDebugEnabled()) {
[4188]663 Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );
[7024]664 }
[3715]665 return zia;
666 }
[6070]667
[5261]668 public boolean increaseZoomLevel() {
[3715]669 if (zoomIncreaseAllowed()) {
670 currentZoomLevel++;
[7024]671 if (Main.isDebugEnabled()) {
[4188]672 Main.debug("increasing zoom level to: " + currentZoomLevel);
[7024]673 }
[3715]674 zoomChanged();
675 } else {
[4188]676 Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
677 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
[3715]678 return false;
679 }
680 return true;
681 }
682
[5261]683 public boolean setZoomLevel(int zoom) {
[3785]684 if (zoom == currentZoomLevel) return true;
[3773]685 if (zoom > this.getMaxZoomLvl()) return false;
686 if (zoom < this.getMinZoomLvl()) return false;
687 currentZoomLevel = zoom;
688 zoomChanged();
689 return true;
690 }
691
[3715]692 /**
[5311]693 * Check if zooming out is allowed
[3715]694 *
[6830]695 * @return true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
[3715]696 */
[5261]697 public boolean zoomDecreaseAllowed() {
[3715]698 return currentZoomLevel > this.getMinZoomLvl();
699 }
[6070]700
[5311]701 /**
702 * Zoom out from map.
[6070]703 *
[5311]704 * @return true, if zoom increasing was successfull, false othervise
705 */
[3715]706 public boolean decreaseZoomLevel() {
[4529]707 //int minZoom = this.getMinZoomLvl();
[3715]708 if (zoomDecreaseAllowed()) {
[7024]709 if (Main.isDebugEnabled()) {
[4188]710 Main.debug("decreasing zoom level to: " + currentZoomLevel);
[7024]711 }
[3715]712 currentZoomLevel--;
713 zoomChanged();
714 } else {
[4188]715 /*Main.debug("Current zoom level could not be decreased. Min. zoom level "+minZoom+" reached.");*/
[3715]716 return false;
717 }
718 return true;
719 }
720
721 /*
722 * We use these for quick, hackish calculations. They
723 * are temporary only and intentionally not inserted
724 * into the tileCache.
725 */
[8168]726 Tile tempCornerTile(Tile t) {
[3715]727 int x = t.getXtile() + 1;
728 int y = t.getYtile() + 1;
729 int zoom = t.getZoom();
730 Tile tile = getTile(x, y, zoom);
731 if (tile != null)
732 return tile;
733 return new Tile(tileSource, x, y, zoom);
734 }
[6070]735
[8168]736 Tile getOrCreateTile(int x, int y, int zoom) {
[3715]737 Tile tile = getTile(x, y, zoom);
738 if (tile == null) {
739 tile = new Tile(tileSource, x, y, zoom);
740 tileCache.addTile(tile);
741 tile.loadPlaceholderFromCache(tileCache);
742 }
743 return tile;
744 }
745
746 /*
747 * This can and will return null for tiles that are not
748 * already in the cache.
749 */
[8168]750 Tile getTile(int x, int y, int zoom) {
[3715]751 int max = (1 << zoom);
752 if (x < 0 || x >= max || y < 0 || y >= max)
753 return null;
[6792]754 return tileCache.getTile(tileSource, x, y, zoom);
[3715]755 }
756
[8168]757 boolean loadTile(Tile tile, boolean force) {
[3715]758 if (tile == null)
759 return false;
[8168]760 if (!force && (tile.isLoaded() || tile.hasError()))
[3715]761 return false;
762 if (tile.isLoading())
763 return false;
[8168]764 tileLoader.createTileLoaderJob(tile).submit();
[3715]765 return true;
766 }
767
768 void loadAllTiles(boolean force) {
769 MapView mv = Main.map.mapView;
770 EastNorth topLeft = mv.getEastNorth(0, 0);
771 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
772
773 TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
774
775 // if there is more than 18 tiles on screen in any direction, do not
776 // load all tiles!
777 if (ts.tooLarge()) {
[4306]778 Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
[3715]779 return;
780 }
781 ts.loadAllTiles(force);
782 }
783
[4306]784 void loadAllErrorTiles(boolean force) {
785 MapView mv = Main.map.mapView;
786 EastNorth topLeft = mv.getEastNorth(0, 0);
787 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
788
789 TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
790
791 ts.loadAllErrorTiles(force);
792 }
793
[3715]794 @Override
795 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
796 boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0);
797 needRedraw = true;
[7024]798 if (Main.isDebugEnabled()) {
[4188]799 Main.debug("imageUpdate() done: " + done + " calling repaint");
[7024]800 }
[3715]801 Main.map.repaint(done ? 0 : 100);
802 return !done;
803 }
[6070]804
[3715]805 boolean imageLoaded(Image i) {
806 if (i == null)
807 return false;
808 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
809 if ((status & ALLBITS) != 0)
810 return true;
811 return false;
812 }
[6070]813
[5261]814 /**
[6070]815 * Returns the image for the given tile if both tile and image are loaded.
[5261]816 * Otherwise returns null.
[6070]817 *
[5261]818 * @param tile the Tile for which the image should be returned
819 * @return the image of the tile or null.
820 */
821 Image getLoadedTileImage(Tile tile) {
[3715]822 if (!tile.isLoaded())
823 return null;
824 Image img = tile.getImage();
825 if (!imageLoaded(img))
826 return null;
827 return img;
828 }
829
[5261]830 LatLon tileLatLon(Tile t) {
[3715]831 int zoom = t.getZoom();
[3878]832 return new LatLon(tileSource.tileYToLat(t.getYtile(), zoom),
833 tileSource.tileXToLon(t.getXtile(), zoom));
[3715]834 }
835
[5261]836 Rectangle tileToRect(Tile t1) {
[3715]837 /*
838 * We need to get a box in which to draw, so advance by one tile in
839 * each direction to find the other corner of the box.
840 * Note: this somewhat pollutes the tile cache
841 */
842 Tile t2 = tempCornerTile(t1);
843 Rectangle rect = new Rectangle(pixelPos(t1));
844 rect.add(pixelPos(t2));
845 return rect;
846 }
847
848 // 'source' is the pixel coordinates for the area that
849 // the img is capable of filling in. However, we probably
850 // only want a portion of it.
851 //
852 // 'border' is the screen cordinates that need to be drawn.
853 // We must not draw outside of it.
[5261]854 void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) {
[3715]855 Rectangle target = source;
856
857 // If a border is specified, only draw the intersection
858 // if what we have combined with what we are supposed
859 // to draw.
860 if (border != null) {
861 target = source.intersection(border);
[7024]862 if (Main.isDebugEnabled()) {
[4188]863 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
[7024]864 }
[3715]865 }
866
867 // All of the rectangles are in screen coordinates. We need
868 // to how these correlate to the sourceImg pixels. We could
869 // avoid doing this by scaling the image up to the 'source' size,
870 // but this should be cheaper.
871 //
872 // In some projections, x any y are scaled differently enough to
873 // cause a pixel or two of fudge. Calculate them separately.
874 double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
875 double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
876
877 // How many pixels into the 'source' rectangle are we drawing?
878 int screen_x_offset = target.x - source.x;
879 int screen_y_offset = target.y - source.y;
880 // And how many pixels into the image itself does that
881 // correlate to?
[8153]882 int img_x_offset = (int)(screen_x_offset * imageXScaling + 0.5);
883 int img_y_offset = (int)(screen_y_offset * imageYScaling + 0.5);
[3715]884 // Now calculate the other corner of the image that we need
885 // by scaling the 'target' rectangle's dimensions.
[8153]886 int img_x_end = img_x_offset + (int)(target.getWidth() * imageXScaling + 0.5);
887 int img_y_end = img_y_offset + (int)(target.getHeight() * imageYScaling + 0.5);
[3715]888
[7024]889 if (Main.isDebugEnabled()) {
[4188]890 Main.debug("drawing image into target rect: " + target);
[7024]891 }
[3715]892 g.drawImage(sourceImg,
893 target.x, target.y,
894 target.x + target.width, target.y + target.height,
895 img_x_offset, img_y_offset,
896 img_x_end, img_y_end,
897 this);
898 if (PROP_FADE_AMOUNT.get() != 0) {
899 // dimm by painting opaque rect...
900 g.setColor(getFadeColorWithAlpha());
901 g.fillRect(target.x, target.y,
902 target.width, target.height);
903 }
904 }
[6070]905
[3715]906 // This function is called for several zoom levels, not just
907 // the current one. It should not trigger any tiles to be
908 // downloaded. It should also avoid polluting the tile cache
909 // with any tiles since these tiles are not mandatory.
910 //
911 // The "border" tile tells us the boundaries of where we may
912 // draw. It will not be from the zoom level that is being
[3774]913 // drawn currently. If drawing the displayZoomLevel,
[3715]914 // border is null and we draw the entire tile set.
915 List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
[3774]916 if (zoom <= 0) return Collections.emptyList();
[3715]917 Rectangle borderRect = null;
918 if (border != null) {
919 borderRect = tileToRect(border);
920 }
[7005]921 List<Tile> missedTiles = new LinkedList<>();
[4328]922 // The callers of this code *require* that we return any tiles
[4330]923 // that we do not draw in missedTiles. ts.allExistingTiles() by
924 // default will only return already-existing tiles. However, we
925 // need to return *all* tiles to the callers, so force creation
926 // here.
[4529]927 //boolean forceTileCreation = true;
[4330]928 for (Tile tile : ts.allTilesCreate()) {
[3715]929 Image img = getLoadedTileImage(tile);
[3773]930 if (img == null || tile.hasError()) {
[7024]931 if (Main.isDebugEnabled()) {
[4188]932 Main.debug("missed tile: " + tile);
[7024]933 }
[3715]934 missedTiles.add(tile);
935 continue;
936 }
937 Rectangle sourceRect = tileToRect(tile);
938 if (borderRect != null && !sourceRect.intersects(borderRect)) {
939 continue;
940 }
941 drawImageInside(g, img, sourceRect, borderRect);
[7024]942 }
[3715]943 return missedTiles;
944 }
945
[3740]946 void myDrawString(Graphics g, String text, int x, int y) {
947 Color oldColor = g.getColor();
948 g.setColor(Color.black);
949 g.drawString(text,x+1,y+1);
950 g.setColor(oldColor);
951 g.drawString(text,x,y);
952 }
953
[3715]954 void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
955 int fontHeight = g.getFontMetrics().getHeight();
956 if (tile == null)
957 return;
958 Point p = pixelPos(t);
959 int texty = p.y + 2 + fontHeight;
960
[4188]961 /*if (PROP_DRAW_DEBUG.get()) {
[3740]962 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
[3715]963 texty += 1 + fontHeight;
964 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
[3740]965 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
[3715]966 texty += 1 + fontHeight;
967 }
[7024]968 }*/
[3715]969
970 if (tile == showMetadataTile) {
971 String md = tile.toString();
972 if (md != null) {
[3740]973 myDrawString(g, md, p.x + 2, texty);
[3715]974 texty += 1 + fontHeight;
975 }
[3739]976 Map<String, String> meta = tile.getMetadata();
977 if (meta != null) {
978 for (Map.Entry<String, String> entry : meta.entrySet()) {
[3740]979 myDrawString(g, entry.getKey() + ": " + entry.getValue(), p.x + 2, texty);
[3739]980 texty += 1 + fontHeight;
981 }
982 }
[3715]983 }
984
[4529]985 /*String tileStatus = tile.getStatus();
986 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
[3740]987 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
[3715]988 texty += 1 + fontHeight;
[4188]989 }*/
[3715]990
[4301]991 if (tile.hasError() && showErrors) {
[3773]992 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty);
993 texty += 1 + fontHeight;
994 }
995
[4188]996 /*int xCursor = -1;
[3715]997 int yCursor = -1;
998 if (PROP_DRAW_DEBUG.get()) {
999 if (yCursor < t.getYtile()) {
1000 if (t.getYtile() % 32 == 31) {
1001 g.fillRect(0, p.y - 1, mv.getWidth(), 3);
1002 } else {
1003 g.drawLine(0, p.y, mv.getWidth(), p.y);
1004 }
1005 yCursor = t.getYtile();
1006 }
1007 // This draws the vertical lines for the entire
1008 // column. Only draw them for the top tile in
1009 // the column.
1010 if (xCursor < t.getXtile()) {
1011 if (t.getXtile() % 32 == 0) {
1012 // level 7 tile boundary
1013 g.fillRect(p.x - 1, 0, 3, mv.getHeight());
1014 } else {
1015 g.drawLine(p.x, 0, p.x, mv.getHeight());
1016 }
1017 xCursor = t.getXtile();
1018 }
[4188]1019 }*/
[3715]1020 }
1021
1022 private Point pixelPos(LatLon ll) {
[4126]1023 return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy()));
[3715]1024 }
[6070]1025
[3715]1026 private Point pixelPos(Tile t) {
[3878]1027 double lon = tileSource.tileXToLon(t.getXtile(), t.getZoom());
1028 LatLon tmpLL = new LatLon(tileSource.tileYToLat(t.getYtile(), t.getZoom()), lon);
[3715]1029 return pixelPos(tmpLL);
1030 }
[6070]1031
[3715]1032 private LatLon getShiftedLatLon(EastNorth en) {
[4126]1033 return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy()));
[3715]1034 }
[6070]1035
[3715]1036 private Coordinate getShiftedCoord(EastNorth en) {
1037 LatLon ll = getShiftedLatLon(en);
1038 return new Coordinate(ll.lat(),ll.lon());
1039 }
[4489]1040
[3785]1041 private final TileSet nullTileSet = new TileSet((LatLon)null, (LatLon)null, 0);
[3715]1042 private class TileSet {
[8285]1043 private int x0, x1, y0, y1;
1044 private int zoom;
1045 private int tileMax = -1;
[3715]1046
1047 /**
1048 * Create a TileSet by EastNorth bbox taking a layer shift in account
1049 */
1050 TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
1051 this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom);
1052 }
1053
1054 /**
1055 * Create a TileSet by known LatLon bbox without layer shift correction
1056 */
1057 TileSet(LatLon topLeft, LatLon botRight, int zoom) {
1058 this.zoom = zoom;
[3785]1059 if (zoom == 0)
1060 return;
[3715]1061
[3878]1062 x0 = (int)tileSource.lonToTileX(topLeft.lon(), zoom);
1063 y0 = (int)tileSource.latToTileY(topLeft.lat(), zoom);
1064 x1 = (int)tileSource.lonToTileX(botRight.lon(), zoom);
1065 y1 = (int)tileSource.latToTileY(botRight.lat(), zoom);
[3774]1066 if (x0 > x1) {
1067 int tmp = x0;
1068 x0 = x1;
1069 x1 = tmp;
[3715]1070 }
[3774]1071 if (y0 > y1) {
1072 int tmp = y0;
1073 y0 = y1;
1074 y1 = tmp;
[3715]1075 }
1076 tileMax = (int)Math.pow(2.0, zoom);
[3774]1077 if (x0 < 0) {
1078 x0 = 0;
[3715]1079 }
[3774]1080 if (y0 < 0) {
1081 y0 = 0;
[3715]1082 }
[3774]1083 if (x1 > tileMax) {
1084 x1 = tileMax;
[3715]1085 }
[3774]1086 if (y1 > tileMax) {
1087 y1 = tileMax;
[3715]1088 }
1089 }
[6070]1090
[3715]1091 boolean tooSmall() {
1092 return this.tilesSpanned() < 2.1;
1093 }
[6070]1094
[3715]1095 boolean tooLarge() {
1096 return this.tilesSpanned() > 10;
1097 }
[6070]1098
[3715]1099 boolean insane() {
1100 return this.tilesSpanned() > 100;
1101 }
[6070]1102
[3715]1103 double tilesSpanned() {
1104 return Math.sqrt(1.0 * this.size());
1105 }
1106
[3785]1107 int size() {
1108 int x_span = x1 - x0 + 1;
1109 int y_span = y1 - y0 + 1;
[3715]1110 return x_span * y_span;
1111 }
1112
1113 /*
1114 * Get all tiles represented by this TileSet that are
1115 * already in the tileCache.
1116 */
[5261]1117 List<Tile> allExistingTiles() {
[4330]1118 return this.__allTiles(false);
[3715]1119 }
[6070]1120
[5261]1121 List<Tile> allTilesCreate() {
[4330]1122 return this.__allTiles(true);
1123 }
[6070]1124
[5261]1125 private List<Tile> __allTiles(boolean create) {
[3785]1126 // Tileset is either empty or too large
1127 if (zoom == 0 || this.insane())
1128 return Collections.emptyList();
[7005]1129 List<Tile> ret = new ArrayList<>();
[3774]1130 for (int x = x0; x <= x1; x++) {
1131 for (int y = y0; y <= y1; y++) {
[3715]1132 Tile t;
1133 if (create) {
1134 t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
1135 } else {
1136 t = getTile(x % tileMax, y % tileMax, zoom);
1137 }
1138 if (t != null) {
1139 ret.add(t);
1140 }
1141 }
1142 }
1143 return ret;
1144 }
[6070]1145
[5261]1146 private List<Tile> allLoadedTiles() {
[7005]1147 List<Tile> ret = new ArrayList<>();
[4330]1148 for (Tile t : this.allExistingTiles()) {
[4328]1149 if (t.isLoaded())
1150 ret.add(t);
1151 }
1152 return ret;
1153 }
[3774]1154
[5261]1155 void loadAllTiles(boolean force) {
[3715]1156 if (!autoLoad && !force)
1157 return;
[4330]1158 for (Tile t : this.allTilesCreate()) {
[4306]1159 loadTile(t, false);
1160 }
1161 }
1162
[5261]1163 void loadAllErrorTiles(boolean force) {
[4306]1164 if (!autoLoad && !force)
1165 return;
[4330]1166 for (Tile t : this.allTilesCreate()) {
[4306]1167 if (t.hasError()) {
1168 loadTile(t, true);
[3715]1169 }
1170 }
1171 }
1172 }
1173
[3785]1174
1175 private static class TileSetInfo {
1176 public boolean hasVisibleTiles = false;
1177 public boolean hasOverzoomedTiles = false;
1178 public boolean hasLoadingTiles = false;
[3715]1179 }
[3785]1180
1181 private static TileSetInfo getTileSetInfo(TileSet ts) {
[4330]1182 List<Tile> allTiles = ts.allExistingTiles();
[3785]1183 TileSetInfo result = new TileSetInfo();
1184 result.hasLoadingTiles = allTiles.size() < ts.size();
1185 for (Tile t : allTiles) {
1186 if (t.isLoaded()) {
1187 if (!t.hasError()) {
1188 result.hasVisibleTiles = true;
1189 }
1190 if ("no-tile".equals(t.getValue("tile-info"))) {
1191 result.hasOverzoomedTiles = true;
1192 }
1193 } else {
1194 result.hasLoadingTiles = true;
1195 }
1196 }
1197 return result;
1198 }
1199
1200 private class DeepTileSet {
[8285]1201 private final EastNorth topLeft, botRight;
1202 private final int minZoom, maxZoom;
[3785]1203 private final TileSet[] tileSets;
1204 private final TileSetInfo[] tileSetInfos;
1205 public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
1206 this.topLeft = topLeft;
1207 this.botRight = botRight;
1208 this.minZoom = minZoom;
[3787]1209 this.maxZoom = maxZoom;
[3785]1210 this.tileSets = new TileSet[maxZoom - minZoom + 1];
1211 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
1212 }
1213 public TileSet getTileSet(int zoom) {
1214 if (zoom < minZoom)
1215 return nullTileSet;
[8168]1216 synchronized (tileSets) {
1217 TileSet ts = tileSets[zoom-minZoom];
1218 if (ts == null) {
1219 ts = new TileSet(topLeft, botRight, zoom);
1220 tileSets[zoom-minZoom] = ts;
1221 }
1222 return ts;
[3785]1223 }
1224 }
[8168]1225
[3785]1226 public TileSetInfo getTileSetInfo(int zoom) {
1227 if (zoom < minZoom)
1228 return new TileSetInfo();
[8168]1229 synchronized (tileSetInfos) {
1230 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
1231 if (tsi == null) {
1232 tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
1233 tileSetInfos[zoom-minZoom] = tsi;
1234 }
1235 return tsi;
[3785]1236 }
1237 }
1238 }
1239
[3715]1240 @Override
1241 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1242 EastNorth topLeft = mv.getEastNorth(0, 0);
1243 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1244
1245 if (botRight.east() == 0.0 || botRight.north() == 0) {
[4188]1246 /*Main.debug("still initializing??");*/
[3715]1247 // probably still initializing
1248 return;
1249 }
1250
1251 needRedraw = false;
1252
1253 int zoom = currentZoomLevel;
[3785]1254 if (autoZoom) {
1255 double pixelScaling = getScaleFactor(zoom);
[4328]1256 if (pixelScaling > 3 || pixelScaling < 0.7) {
[3785]1257 zoom = getBestZoom();
1258 }
1259 }
[3715]1260
[3787]1261 DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom);
[3785]1262 TileSet ts = dts.getTileSet(zoom);
1263
1264 int displayZoomLevel = zoom;
1265
1266 boolean noTilesAtZoom = false;
1267 if (autoZoom && autoLoad) {
1268 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
1269 TileSetInfo tsi = dts.getTileSetInfo(zoom);
1270 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
1271 noTilesAtZoom = true;
[3715]1272 }
[3787]1273 // Find highest zoom level with at least one visible tile
[4329]1274 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
1275 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
1276 displayZoomLevel = tmpZoom;
1277 break;
1278 }
[3787]1279 }
1280 // Do binary search between currentZoomLevel and displayZoomLevel
1281 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles){
1282 zoom = (zoom + displayZoomLevel)/2;
[3785]1283 tsi = dts.getTileSetInfo(zoom);
[3715]1284 }
[3787]1285
[3785]1286 setZoomLevel(zoom);
[3787]1287
1288 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
1289 // to make sure there're really no more zoom levels
1290 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
1291 zoom++;
1292 tsi = dts.getTileSetInfo(zoom);
1293 }
[3785]1294 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
1295 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
1296 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
1297 zoom--;
1298 tsi = dts.getTileSetInfo(zoom);
1299 }
1300 ts = dts.getTileSet(zoom);
1301 } else if (autoZoom) {
1302 setZoomLevel(zoom);
[3715]1303 }
1304
[3774]1305 // Too many tiles... refuse to download
[3715]1306 if (!ts.tooLarge()) {
[4188]1307 //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
[3715]1308 ts.loadAllTiles(false);
1309 }
1310
[3774]1311 if (displayZoomLevel != zoom) {
[3785]1312 ts = dts.getTileSet(displayZoomLevel);
[3774]1313 }
1314
[3715]1315 g.setColor(Color.DARK_GRAY);
1316
[3774]1317 List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null);
[6085]1318 int[] otherZooms = { -1, 1, -2, 2, -3, -4, -5};
[3715]1319 for (int zoomOffset : otherZooms) {
[4492]1320 if (!autoZoom) {
[3715]1321 break;
[4492]1322 }
[3774]1323 int newzoom = displayZoomLevel + zoomOffset;
[4492]1324 if (newzoom < MIN_ZOOM) {
[4328]1325 continue;
[4492]1326 }
[8318]1327 if (missedTiles.isEmpty()) {
[3715]1328 break;
1329 }
[7005]1330 List<Tile> newlyMissedTiles = new LinkedList<>();
[3715]1331 for (Tile missed : missedTiles) {
[3785]1332 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
1333 // Don't try to paint from higher zoom levels when tile is overzoomed
1334 newlyMissedTiles.add(missed);
1335 continue;
1336 }
[3715]1337 Tile t2 = tempCornerTile(missed);
1338 LatLon topLeft2 = tileLatLon(missed);
1339 LatLon botRight2 = tileLatLon(t2);
1340 TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
[4328]1341 // Instantiating large TileSets is expensive. If there
1342 // are no loaded tiles, don't bother even trying.
[6093]1343 if (ts2.allLoadedTiles().isEmpty()) {
[4328]1344 newlyMissedTiles.add(missed);
1345 continue;
1346 }
[3715]1347 if (ts2.tooLarge()) {
1348 continue;
1349 }
1350 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
1351 }
1352 missedTiles = newlyMissedTiles;
1353 }
[8318]1354 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
[4188]1355 Main.debug("still missed "+missedTiles.size()+" in the end");
[7024]1356 }
[3715]1357 g.setColor(Color.red);
[3740]1358 g.setFont(InfoFont);
[3715]1359
[4331]1360 // The current zoom tileset should have all of its tiles
1361 // due to the loadAllTiles(), unless it to tooLarge()
[4330]1362 for (Tile t : ts.allExistingTiles()) {
[3774]1363 this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
[3715]1364 }
[4506]1365
[4489]1366 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight), displayZoomLevel, this);
[3715]1367
1368 //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
[3773]1369 g.setColor(Color.lightGray);
[3774]1370 if (!autoZoom) {
1371 if (ts.insane()) {
1372 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
1373 } else if (ts.tooLarge()) {
1374 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
1375 } else if (ts.tooSmall()) {
1376 myDrawString(g, tr("increase zoom level to see more detail"), 120, 120);
1377 }
[3715]1378 }
[3785]1379 if (noTilesAtZoom) {
[3774]1380 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
1381 }
[7024]1382 if (Main.isDebugEnabled()) {
[3774]1383 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
1384 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
[3785]1385 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
1386 myDrawString(g, tr("Best zoom: {0}", Math.log(getScaleFactor(1))/Math.log(2)/2+1), 50, 185);
[8168]1387 if(tileLoader instanceof TMSCachedTileLoader) {
1388 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader)tileLoader;
1389 int offset = 185;
1390 for(String part: cachedTileLoader.getStats().split("\n")) {
1391 myDrawString(g, tr("Cache stats: {0}", part), 50, offset+=15);
1392 }
1393
1394 }
[7024]1395 }
[4492]1396 }
[3715]1397
1398 /**
1399 * This isn't very efficient, but it is only used when the
1400 * user right-clicks on the map.
1401 */
1402 Tile getTileForPixelpos(int px, int py) {
[7024]1403 if (Main.isDebugEnabled()) {
[4188]1404 Main.debug("getTileForPixelpos("+px+", "+py+")");
[7024]1405 }
[3715]1406 MapView mv = Main.map.mapView;
1407 Point clicked = new Point(px, py);
1408 EastNorth topLeft = mv.getEastNorth(0, 0);
1409 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1410 int z = currentZoomLevel;
1411 TileSet ts = new TileSet(topLeft, botRight, z);
1412
1413 if (!ts.tooLarge()) {
1414 ts.loadAllTiles(false); // make sure there are tile objects for all tiles
1415 }
1416 Tile clickedTile = null;
[4330]1417 for (Tile t1 : ts.allExistingTiles()) {
[3715]1418 Tile t2 = tempCornerTile(t1);
1419 Rectangle r = new Rectangle(pixelPos(t1));
1420 r.add(pixelPos(t2));
[7024]1421 if (Main.isDebugEnabled()) {
[4188]1422 Main.debug("r: " + r + " clicked: " + clicked);
[7024]1423 }
[3715]1424 if (!r.contains(clicked)) {
1425 continue;
1426 }
1427 clickedTile = t1;
1428 break;
1429 }
1430 if (clickedTile == null)
1431 return null;
[4188]1432 /*Main.debug("Clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
1433 " currentZoomLevel: " + currentZoomLevel);*/
[3715]1434 return clickedTile;
1435 }
1436
1437 @Override
1438 public Action[] getMenuEntries() {
1439 return new Action[] {
1440 LayerListDialog.getInstance().createShowHideLayerAction(),
1441 LayerListDialog.getInstance().createDeleteLayerAction(),
1442 SeparatorLayerAction.INSTANCE,
1443 // color,
1444 new OffsetAction(),
1445 new RenameLayerAction(this.getAssociatedFile(), this),
1446 SeparatorLayerAction.INSTANCE,
1447 new LayerListPopup.InfoAction(this) };
1448 }
1449
1450 @Override
1451 public String getToolTipText() {
[5963]1452 return tr("TMS layer ({0}), downloading in zoom {1}", getName(), currentZoomLevel);
[3715]1453 }
1454
1455 @Override
1456 public void visitBoundingBox(BoundingXYVisitor v) {
1457 }
1458
1459 @Override
1460 public boolean isChanged() {
1461 return needRedraw;
1462 }
[4183]1463
1464 @Override
[6890]1465 public final boolean isProjectionSupported(Projection proj) {
[5017]1466 return "EPSG:3857".equals(proj.toCode()) || "EPSG:4326".equals(proj.toCode());
[4183]1467 }
1468
1469 @Override
[6890]1470 public final String nameSupportedProjections() {
[4183]1471 return tr("EPSG:4326 and Mercator projection are supported");
1472 }
[3715]1473}
Note: See TracBrowser for help on using the repository browser.