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

Last change on this file since 3740 was 3740, checked in by Upliner, 14 years ago

Make TMS info text more clearly visible

  • Property svn:eol-style set to native
File size: 46.1 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.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.font.TextAttribute;
18import java.awt.geom.Rectangle2D;
19import java.awt.image.BufferedImage;
20import java.awt.image.ImageObserver;
21import java.io.IOException;
22import java.net.URI;
23import java.net.URISyntaxException;
24import java.util.ArrayList;
25import java.util.HashMap;
26import java.util.HashSet;
27import java.util.LinkedList;
28import java.util.List;
29import java.util.Map;
30
31import javax.swing.AbstractAction;
32import javax.swing.Action;
33import javax.swing.JCheckBoxMenuItem;
34import javax.swing.JMenuItem;
35import javax.swing.JPopupMenu;
36import javax.swing.SwingUtilities;
37
38import org.openstreetmap.gui.jmapviewer.BingAerialTileSource;
39import org.openstreetmap.gui.jmapviewer.Coordinate;
40import org.openstreetmap.gui.jmapviewer.JobDispatcher;
41import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
42import org.openstreetmap.gui.jmapviewer.OsmFileCacheTileLoader;
43import org.openstreetmap.gui.jmapviewer.TMSTileSource;
44import org.openstreetmap.gui.jmapviewer.TemplatedTMSTileSource;
45import org.openstreetmap.gui.jmapviewer.Tile;
46import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
47import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
48import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
49import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
50import org.openstreetmap.josm.Main;
51import org.openstreetmap.josm.actions.RenameLayerAction;
52import org.openstreetmap.josm.data.Bounds;
53import org.openstreetmap.josm.data.coor.EastNorth;
54import org.openstreetmap.josm.data.coor.LatLon;
55import org.openstreetmap.josm.data.imagery.ImageryInfo;
56import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
57import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
58import org.openstreetmap.josm.data.preferences.BooleanProperty;
59import org.openstreetmap.josm.data.preferences.IntegerProperty;
60import org.openstreetmap.josm.gui.MapView;
61import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
62import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
63import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
64
65/**
66 * Class that displays a slippy map layer.
67 *
68 * @author Frederik Ramm <frederik@remote.org>
69 * @author LuVar <lubomir.varga@freemap.sk>
70 * @author Dave Hansen <dave@sr71.net>
71 *
72 */
73public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener {
74 public static final String PREFERENCE_PREFIX = "imagery.tms";
75
76 public static final int MAX_ZOOM = 30;
77 public static final int MIN_ZOOM = 2;
78 public static final int DEFAULT_MAX_ZOOM = 18;
79 public static final int DEFAULT_MIN_ZOOM = 2;
80
81 public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
82 public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true);
83 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", DEFAULT_MIN_ZOOM);
84 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", DEFAULT_MAX_ZOOM);
85 public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
86 public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + ".add_to_slippymap_chooser", true);
87
88 boolean debug = false;
89 void out(String s)
90 {
91 Main.debug(s);
92 }
93
94 protected MemoryTileCache tileCache;
95 protected TileSource tileSource;
96 protected TileLoader tileLoader;
97 JobDispatcher jobDispatcher = JobDispatcher.getInstance();
98
99 HashSet<Tile> tileRequestsOutstanding = new HashSet<Tile>();
100 @Override
101 public synchronized void tileLoadingFinished(Tile tile, boolean success)
102 {
103 if (!success) {
104 BufferedImage img = new BufferedImage(tileSource.getTileSize(),tileSource.getTileSize(), BufferedImage.TYPE_INT_RGB);
105 drawErrorTile(img);
106 tile.setImage(img);
107 }
108 tile.setLoaded(true);
109 needRedraw = true;
110 Main.map.repaint(100);
111 tileRequestsOutstanding.remove(tile);
112 if (sharpenLevel != 0 && success) {
113 tile.setImage(sharpenImage(tile.getImage()));
114 }
115 if (debug) {
116 out("tileLoadingFinished() tile: " + tile + " success: " + success);
117 }
118 }
119 @Override
120 public TileCache getTileCache()
121 {
122 return tileCache;
123 }
124 void clearTileCache()
125 {
126 if (debug) {
127 out("clearing tile storage");
128 }
129 tileCache = new MemoryTileCache();
130 tileCache.setCacheSize(200);
131 }
132
133 /**
134 * Actual zoom lvl. Initial zoom lvl is set to
135 */
136 public int currentZoomLevel;
137
138 private Tile clickedTile;
139 private boolean needRedraw;
140 private JPopupMenu tileOptionMenu;
141 JCheckBoxMenuItem autoZoomPopup;
142 JCheckBoxMenuItem autoLoadPopup;
143 Tile showMetadataTile;
144 private Image attrImage;
145 private String attrTermsUrl;
146 private Rectangle attrImageBounds, attrToUBounds;
147 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
148 private static final Font ATTR_FONT = new Font("Arial", Font.PLAIN, 10);
149 private static final Font ATTR_LINK_FONT;
150 static {
151 HashMap<TextAttribute, Integer> aUnderline = new HashMap<TextAttribute, Integer>();
152 aUnderline.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
153 ATTR_LINK_FONT = ATTR_FONT.deriveFont(aUnderline);
154 }
155
156 protected boolean autoZoom;
157 protected boolean autoLoad;
158
159 void redraw()
160 {
161 needRedraw = true;
162 Main.map.repaint();
163 }
164
165 static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts)
166 {
167 if(maxZoomLvl > MAX_ZOOM) {
168 System.err.println("MaxZoomLvl shouldnt be more than 30! Setting to 30.");
169 maxZoomLvl = MAX_ZOOM;
170 }
171 if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
172 System.err.println("maxZoomLvl shouldnt be more than minZoomLvl! Setting to minZoomLvl.");
173 maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
174 }
175 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
176 maxZoomLvl = ts.getMaxZoom();
177 }
178 return maxZoomLvl;
179 }
180
181 public static int getMaxZoomLvl(TileSource ts)
182 {
183 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
184 }
185
186 public static void setMaxZoomLvl(int maxZoomLvl) {
187 maxZoomLvl = checkMaxZoomLvl(maxZoomLvl, null);
188 PROP_MAX_ZOOM_LVL.put(maxZoomLvl);
189 }
190
191 static int checkMinZoomLvl(int minZoomLvl, TileSource ts)
192 {
193 if(minZoomLvl < MIN_ZOOM) {
194 System.err.println("minZoomLvl shouldnt be lees than "+MIN_ZOOM+"! Setting to that.");
195 minZoomLvl = MIN_ZOOM;
196 }
197 if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
198 System.err.println("minZoomLvl shouldnt be more than maxZoomLvl! Setting to maxZoomLvl.");
199 minZoomLvl = getMaxZoomLvl(ts);
200 }
201 if (ts != null && ts.getMinZoom() > minZoomLvl) {
202 System.err.println("increasomg minZoomLvl to match tile source");
203 minZoomLvl = ts.getMinZoom();
204 }
205 return minZoomLvl;
206 }
207
208 public static int getMinZoomLvl(TileSource ts)
209 {
210 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
211 }
212
213 public static void setMinZoomLvl(int minZoomLvl) {
214 minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
215 PROP_MIN_ZOOM_LVL.put(minZoomLvl);
216 }
217
218 public static TileSource getTileSource(ImageryInfo info) {
219 if (info.getImageryType() == ImageryType.TMS) {
220 if(ImageryInfo.isUrlWithPatterns(info.getURL()))
221 return new TemplatedTMSTileSource(info.getName(), info.getURL(), info.getMaxZoom());
222 else
223 return new TMSTileSource(info.getName(),info.getURL(), info.getMaxZoom());
224 } else if (info.getImageryType() == ImageryType.BING)
225 return new BingAerialTileSource();
226 return null;
227 }
228
229 private void initTileSource(TileSource tileSource)
230 {
231 this.tileSource = tileSource;
232 boolean requireAttr = tileSource.requiresAttribution();
233 if(requireAttr) {
234 attrImage = tileSource.getAttributionImage();
235 if(attrImage == null) {
236 System.out.println("Attribution image was null.");
237 } else {
238 System.out.println("Got an attribution image " + attrImage.getHeight(this) + "x" + attrImage.getWidth(this));
239 }
240
241 attrTermsUrl = tileSource.getTermsOfUseURL();
242 }
243
244 currentZoomLevel = getBestZoom();
245 if (currentZoomLevel > getMaxZoomLvl()) {
246 currentZoomLevel = getMaxZoomLvl();
247 }
248 if (currentZoomLevel < getMinZoomLvl()) {
249 currentZoomLevel = getMinZoomLvl();
250 }
251 clearTileCache();
252 //tileloader = new OsmTileLoader(this);
253 tileLoader = new OsmFileCacheTileLoader(this);
254 }
255
256 @Override
257 public void setOffset(double dx, double dy) {
258 super.setOffset(dx, dy);
259 needRedraw = true;
260 }
261
262 private double getPPDeg() {
263 MapView mv = Main.map.mapView;
264 return mv.getWidth()/(mv.getLatLon(mv.getWidth(), mv.getHeight()/2).lon()-mv.getLatLon(0, mv.getHeight()/2).lon());
265 }
266
267 private int getBestZoom() {
268 if (Main.map == null || Main.map.mapView == null) return 3;
269 double ret = Math.log(getPPDeg()*360/tileSource.getTileSize())/Math.log(2);
270 return (int)Math.round(ret);
271 }
272
273 @SuppressWarnings("serial")
274 public TMSLayer(ImageryInfo info) {
275 super(info);
276
277 setBackgroundLayer(true);
278 this.setVisible(true);
279
280 TileSource source = getTileSource(info);
281 if (source == null)
282 throw new IllegalStateException("cannot create TMSLayer with non-TMS ImageryInfo");
283 initTileSource(source);
284
285 tileOptionMenu = new JPopupMenu();
286
287 autoZoom = PROP_DEFAULT_AUTOZOOM.get();
288 autoZoomPopup = new JCheckBoxMenuItem();
289 autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
290 @Override
291 public void actionPerformed(ActionEvent ae) {
292 autoZoom = !autoZoom;
293 }
294 });
295 autoZoomPopup.setSelected(autoZoom);
296 tileOptionMenu.add(autoZoomPopup);
297
298 autoLoad = PROP_DEFAULT_AUTOLOAD.get();
299 autoLoadPopup = new JCheckBoxMenuItem();
300 autoLoadPopup.setAction(new AbstractAction(tr("Auto load tiles")) {
301 @Override
302 public void actionPerformed(ActionEvent ae) {
303 autoLoad= !autoLoad;
304 }
305 });
306 autoLoadPopup.setSelected(autoLoad);
307 tileOptionMenu.add(autoLoadPopup);
308
309 tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
310 @Override
311 public void actionPerformed(ActionEvent ae) {
312 if (clickedTile != null) {
313 loadTile(clickedTile);
314 redraw();
315 }
316 }
317 }));
318
319 tileOptionMenu.add(new JMenuItem(new AbstractAction(
320 tr("Show Tile Info")) {
321 @Override
322 public void actionPerformed(ActionEvent ae) {
323 out("info tile: " + clickedTile);
324 if (clickedTile != null) {
325 showMetadataTile = clickedTile;
326 redraw();
327 }
328 }
329 }));
330
331 /* FIXME
332 tileOptionMenu.add(new JMenuItem(new AbstractAction(
333 tr("Request Update")) {
334 public void actionPerformed(ActionEvent ae) {
335 if (clickedTile != null) {
336 clickedTile.requestUpdate();
337 redraw();
338 }
339 }
340 }));*/
341
342 tileOptionMenu.add(new JMenuItem(new AbstractAction(
343 tr("Load All Tiles")) {
344 @Override
345 public void actionPerformed(ActionEvent ae) {
346 loadAllTiles(true);
347 redraw();
348 }
349 }));
350
351 // increase and decrease commands
352 tileOptionMenu.add(new JMenuItem(
353 new AbstractAction(tr("Increase zoom")) {
354 @Override
355 public void actionPerformed(ActionEvent ae) {
356 increaseZoomLevel();
357 redraw();
358 }
359 }));
360
361 tileOptionMenu.add(new JMenuItem(
362 new AbstractAction(tr("Decrease zoom")) {
363 @Override
364 public void actionPerformed(ActionEvent ae) {
365 decreaseZoomLevel();
366 redraw();
367 }
368 }));
369
370 // FIXME: currently ran in errors
371
372 tileOptionMenu.add(new JMenuItem(
373 new AbstractAction(tr("Snap to tile size")) {
374 @Override
375 public void actionPerformed(ActionEvent ae) {
376 if (lastImageScale == null) {
377 out("please wait for a tile to be loaded before snapping");
378 return;
379 }
380 double new_factor = Math.sqrt(lastImageScale);
381 if (debug) {
382 out("tile snap: scale was: " + lastImageScale + ", new factor: " + new_factor);
383 }
384 Main.map.mapView.zoomToFactor(new_factor);
385 redraw();
386 }
387 }));
388 // end of adding menu commands
389
390 tileOptionMenu.add(new JMenuItem(
391 new AbstractAction(tr("Flush Tile Cache")) {
392 @Override
393 public void actionPerformed(ActionEvent ae) {
394 System.out.print("flushing all tiles...");
395 clearTileCache();
396 System.out.println("done");
397 }
398 }));
399 // end of adding menu commands
400
401 SwingUtilities.invokeLater(new Runnable() {
402 @Override
403 public void run() {
404 Main.map.mapView.addMouseListener(new MouseAdapter() {
405 @Override
406 public void mouseClicked(MouseEvent e) {
407 if (e.getButton() == MouseEvent.BUTTON3) {
408 clickedTile = getTileForPixelpos(e.getX(), e.getY());
409 tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
410 } else if (e.getButton() == MouseEvent.BUTTON1) {
411 if(!tileSource.requiresAttribution())
412 return;
413
414 if(attrImageBounds.contains(e.getPoint())) {
415 try {
416 java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
417 desktop.browse(new URI(tileSource.getAttributionLinkURL()));
418 } catch (IOException e1) {
419 e1.printStackTrace();
420 } catch (URISyntaxException e1) {
421 e1.printStackTrace();
422 }
423 } else if(attrToUBounds.contains(e.getPoint())) {
424 try {
425 java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
426 desktop.browse(new URI(tileSource.getTermsOfUseURL()));
427 } catch (IOException e1) {
428 e1.printStackTrace();
429 } catch (URISyntaxException e1) {
430 e1.printStackTrace();
431 }
432 }
433 }
434 }
435 });
436
437 MapView.addLayerChangeListener(new LayerChangeListener() {
438 @Override
439 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
440 //
441 }
442
443 @Override
444 public void layerAdded(Layer newLayer) {
445 //
446 }
447
448 @Override
449 public void layerRemoved(Layer oldLayer) {
450 MapView.removeLayerChangeListener(this);
451 }
452 });
453 }
454 });
455 }
456
457 void zoomChanged()
458 {
459 if (debug) {
460 out("zoomChanged(): " + currentZoomLevel);
461 }
462 needRedraw = true;
463 jobDispatcher.cancelOutstandingJobs();
464 tileRequestsOutstanding.clear();
465 }
466
467 int getMaxZoomLvl()
468 {
469 if (info.getMaxZoom() != 0)
470 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
471 else
472 return getMaxZoomLvl(tileSource);
473 }
474
475 int getMinZoomLvl()
476 {
477 return getMinZoomLvl(tileSource);
478 }
479
480 /**
481 * Zoom in, go closer to map.
482 *
483 * @return true, if zoom increasing was successfull, false othervise
484 */
485 public boolean zoomIncreaseAllowed()
486 {
487 boolean zia = currentZoomLevel < this.getMaxZoomLvl();
488 if (debug) {
489 out("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );
490 }
491 return zia;
492 }
493 public boolean increaseZoomLevel()
494 {
495 lastImageScale = null;
496 if (zoomIncreaseAllowed()) {
497 currentZoomLevel++;
498 if (debug) {
499 out("increasing zoom level to: " + currentZoomLevel);
500 }
501 zoomChanged();
502 } else {
503 System.err.println("current zoom lvl ("+currentZoomLevel+") couldnt be increased. "+
504 "MaxZoomLvl ("+this.getMaxZoomLvl()+") reached.");
505 return false;
506 }
507 return true;
508 }
509
510 /**
511 * Zoom out from map.
512 *
513 * @return true, if zoom increasing was successfull, false othervise
514 */
515 public boolean zoomDecreaseAllowed()
516 {
517 return currentZoomLevel > this.getMinZoomLvl();
518 }
519 public boolean decreaseZoomLevel() {
520 int minZoom = this.getMinZoomLvl();
521 lastImageScale = null;
522 if (zoomDecreaseAllowed()) {
523 if (debug) {
524 out("decreasing zoom level to: " + currentZoomLevel);
525 }
526 currentZoomLevel--;
527 zoomChanged();
528 } else {
529 System.err.println("current zoom lvl couldnt be decreased. MinZoomLvl("+minZoom+") reached.");
530 return false;
531 }
532 return true;
533 }
534
535 /*
536 * We use these for quick, hackish calculations. They
537 * are temporary only and intentionally not inserted
538 * into the tileCache.
539 */
540 synchronized Tile tempCornerTile(Tile t) {
541 int x = t.getXtile() + 1;
542 int y = t.getYtile() + 1;
543 int zoom = t.getZoom();
544 Tile tile = getTile(x, y, zoom);
545 if (tile != null)
546 return tile;
547 return new Tile(tileSource, x, y, zoom);
548 }
549 synchronized Tile getOrCreateTile(int x, int y, int zoom) {
550 Tile tile = getTile(x, y, zoom);
551 if (tile == null) {
552 tile = new Tile(tileSource, x, y, zoom);
553 tileCache.addTile(tile);
554 tile.loadPlaceholderFromCache(tileCache);
555 }
556 return tile;
557 }
558
559 /*
560 * This can and will return null for tiles that are not
561 * already in the cache.
562 */
563 synchronized Tile getTile(int x, int y, int zoom) {
564 int max = (1 << zoom);
565 if (x < 0 || x >= max || y < 0 || y >= max)
566 return null;
567 Tile tile = tileCache.getTile(tileSource, x, y, zoom);
568 return tile;
569 }
570
571 synchronized boolean loadTile(Tile tile)
572 {
573 if (tile == null)
574 return false;
575 if (tile.hasError())
576 return false;
577 if (tile.isLoaded())
578 return false;
579 if (tile.isLoading())
580 return false;
581 if (tileRequestsOutstanding.contains(tile))
582 return false;
583 tileRequestsOutstanding.add(tile);
584 jobDispatcher.addJob(tileLoader.createTileLoaderJob(tileSource,
585 tile.getXtile(), tile.getYtile(), tile.getZoom()));
586 return true;
587 }
588
589 void loadAllTiles(boolean force) {
590 MapView mv = Main.map.mapView;
591 EastNorth topLeft = mv.getEastNorth(0, 0);
592 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
593
594 TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
595
596 // if there is more than 18 tiles on screen in any direction, do not
597 // load all tiles!
598 if (ts.tooLarge()) {
599 System.out.println("Not downloading all tiles because there is more than 18 tiles on an axis!");
600 return;
601 }
602 ts.loadAllTiles(force);
603 }
604
605 /*
606 * Attempt to approximate how much the image is being scaled. For instance,
607 * a 100x100 image being scaled to 50x50 would return 0.25.
608 */
609 Image lastScaledImage = null;
610 @Override
611 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
612 boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0);
613 needRedraw = true;
614 if (debug) {
615 out("imageUpdate() done: " + done + " calling repaint");
616 }
617 Main.map.repaint(done ? 0 : 100);
618 return !done;
619 }
620 boolean imageLoaded(Image i) {
621 if (i == null)
622 return false;
623 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
624 if ((status & ALLBITS) != 0)
625 return true;
626 return false;
627 }
628 Image getLoadedTileImage(Tile tile)
629 {
630 if (!tile.isLoaded())
631 return null;
632 Image img = tile.getImage();
633 if (!imageLoaded(img))
634 return null;
635 return img;
636 }
637
638 double getImageScaling(Image img, Rectangle r) {
639 int realWidth = -1;
640 int realHeight = -1;
641 if (img != null) {
642 realWidth = img.getHeight(this);
643 realWidth = img.getWidth(this);
644 }
645 if (realWidth == -1 || realHeight == -1) {
646 /*
647 * We need a good image against which to work. If
648 * the current one isn't loaded, then try the last one.
649 * Should be good enough. If we've never seen one, then
650 * guess.
651 */
652 if (lastScaledImage != null)
653 return getImageScaling(lastScaledImage, r);
654 realWidth = 256;
655 realHeight = 256;
656 } else {
657 lastScaledImage = img;
658 }
659 /*
660 * If the zoom scale gets really, really off, these can get into
661 * the millions, so make this a double to prevent integer
662 * overflows.
663 */
664 double drawWidth = r.width;
665 double drawHeight = r.height;
666 // stem.out.println("drawWidth: " + drawWidth + " drawHeight: " +
667 // drawHeight);
668
669 double drawArea = drawWidth * drawHeight;
670 double realArea = realWidth * realHeight;
671
672 return drawArea / realArea;
673 }
674
675 LatLon tileLatLon(Tile t)
676 {
677 int zoom = t.getZoom();
678 return new LatLon(tileYToLat(t.getYtile(), zoom),
679 tileXToLon(t.getXtile(), zoom));
680 }
681
682 int paintFromOtherZooms(Graphics g, Tile topLeftTile, Tile botRightTile)
683 {
684 LatLon topLeft = tileLatLon(topLeftTile);
685 LatLon botRight = tileLatLon(botRightTile);
686
687
688 /*
689 * Go looking for tiles in zoom levels *other* than the current
690 * one. Even if they might look bad, they look better than a
691 * blank tile.
692 *
693 * Make darn sure that the tilesCache can either hold all of
694 * these "fake" tiles or that they don't get inserted in it to
695 * begin with.
696 */
697 //int otherZooms[] = {-5, -4, -3, 2, -2, 1, -1};
698 int otherZooms[] = { -1, 1, -2, 2, -3, -4, -5};
699 int painted = 0;
700 debug = true;
701 for (int zoomOff : otherZooms) {
702 int zoom = currentZoomLevel + zoomOff;
703 if ((zoom < this.getMinZoomLvl()) ||
704 (zoom > this.getMaxZoomLvl())) {
705 continue;
706 }
707 TileSet ts = new TileSet(topLeft, botRight, zoom);
708 int zoom_painted = 0;
709 this.paintTileImages(g, ts, zoom, null);
710 if (debug && zoom_painted > 0) {
711 out("painted " + zoom_painted + "/"+ ts.size() +
712 " tiles from zoom("+zoomOff+"): " + zoom);
713 }
714 painted += zoom_painted;
715 if (zoom_painted >= ts.size()) {
716 if (debug) {
717 out("broke after drawing " + zoom_painted + "/"+ ts.size() + " at zoomOff: " + zoomOff);
718 }
719 break;
720 }
721 }
722 debug = false;
723 return painted;
724 }
725 Rectangle tileToRect(Tile t1)
726 {
727 /*
728 * We need to get a box in which to draw, so advance by one tile in
729 * each direction to find the other corner of the box.
730 * Note: this somewhat pollutes the tile cache
731 */
732 Tile t2 = tempCornerTile(t1);
733 Rectangle rect = new Rectangle(pixelPos(t1));
734 rect.add(pixelPos(t2));
735 return rect;
736 }
737
738 // 'source' is the pixel coordinates for the area that
739 // the img is capable of filling in. However, we probably
740 // only want a portion of it.
741 //
742 // 'border' is the screen cordinates that need to be drawn.
743 // We must not draw outside of it.
744 void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border)
745 {
746 Rectangle target = source;
747
748 // If a border is specified, only draw the intersection
749 // if what we have combined with what we are supposed
750 // to draw.
751 if (border != null) {
752 target = source.intersection(border);
753 if (debug) {
754 out("source: " + source + "\nborder: " + border + "\nintersection: " + target);
755 }
756 }
757
758 // All of the rectangles are in screen coordinates. We need
759 // to how these correlate to the sourceImg pixels. We could
760 // avoid doing this by scaling the image up to the 'source' size,
761 // but this should be cheaper.
762 //
763 // In some projections, x any y are scaled differently enough to
764 // cause a pixel or two of fudge. Calculate them separately.
765 double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
766 double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
767
768 // How many pixels into the 'source' rectangle are we drawing?
769 int screen_x_offset = target.x - source.x;
770 int screen_y_offset = target.y - source.y;
771 // And how many pixels into the image itself does that
772 // correlate to?
773 int img_x_offset = (int)(screen_x_offset * imageXScaling);
774 int img_y_offset = (int)(screen_y_offset * imageYScaling);
775 // Now calculate the other corner of the image that we need
776 // by scaling the 'target' rectangle's dimensions.
777 int img_x_end = img_x_offset + (int)(target.getWidth() * imageXScaling);
778 int img_y_end = img_y_offset + (int)(target.getHeight() * imageYScaling);
779
780 if (debug) {
781 out("drawing image into target rect: " + target);
782 }
783 g.drawImage(sourceImg,
784 target.x, target.y,
785 target.x + target.width, target.y + target.height,
786 img_x_offset, img_y_offset,
787 img_x_end, img_y_end,
788 this);
789 if (PROP_FADE_AMOUNT.get() != 0) {
790 // dimm by painting opaque rect...
791 g.setColor(getFadeColorWithAlpha());
792 g.fillRect(target.x, target.y,
793 target.width, target.height);
794 }
795 }
796 Double lastImageScale = null;
797 // This function is called for several zoom levels, not just
798 // the current one. It should not trigger any tiles to be
799 // downloaded. It should also avoid polluting the tile cache
800 // with any tiles since these tiles are not mandatory.
801 //
802 // The "border" tile tells us the boundaries of where we may
803 // draw. It will not be from the zoom level that is being
804 // drawn currently. If drawing the currentZoomLevel,
805 // border is null and we draw the entire tile set.
806 List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
807 Rectangle borderRect = null;
808 if (border != null) {
809 borderRect = tileToRect(border);
810 }
811 List<Tile> missedTiles = new LinkedList<Tile>();
812 boolean imageScaleRecorded = false;
813 for (Tile tile : ts.allTiles()) {
814 Image img = getLoadedTileImage(tile);
815 if (img == null) {
816 if (debug) {
817 out("missed tile: " + tile);
818 }
819 missedTiles.add(tile);
820 continue;
821 }
822 Rectangle sourceRect = tileToRect(tile);
823 if (borderRect != null && !sourceRect.intersects(borderRect)) {
824 continue;
825 }
826 drawImageInside(g, img, sourceRect, borderRect);
827 if (!imageScaleRecorded && zoom == currentZoomLevel) {
828 lastImageScale = new Double(getImageScaling(img, sourceRect));
829 imageScaleRecorded = true;
830 }
831 }// end of for
832 return missedTiles;
833 }
834
835 void myDrawString(Graphics g, String text, int x, int y) {
836 Color oldColor = g.getColor();
837 g.setColor(Color.black);
838 g.drawString(text,x+1,y+1);
839 g.setColor(oldColor);
840 g.drawString(text,x,y);
841 }
842
843 void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
844 int fontHeight = g.getFontMetrics().getHeight();
845 if (tile == null)
846 return;
847 Point p = pixelPos(t);
848 int texty = p.y + 2 + fontHeight;
849
850 if (PROP_DRAW_DEBUG.get()) {
851 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
852 texty += 1 + fontHeight;
853 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
854 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
855 texty += 1 + fontHeight;
856 }
857 }// end of if draw debug
858
859 if (tile == showMetadataTile) {
860 String md = tile.toString();
861 if (md != null) {
862 myDrawString(g, md, p.x + 2, texty);
863 texty += 1 + fontHeight;
864 }
865 Map<String, String> meta = tile.getMetadata();
866 if (meta != null) {
867 for (Map.Entry<String, String> entry : meta.entrySet()) {
868 myDrawString(g, entry.getKey() + ": " + entry.getValue(), p.x + 2, texty);
869 texty += 1 + fontHeight;
870 }
871 }
872 }
873
874 String tileStatus = tile.getStatus();
875 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
876 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
877 texty += 1 + fontHeight;
878 }
879
880 int xCursor = -1;
881 int yCursor = -1;
882 if (PROP_DRAW_DEBUG.get()) {
883 if (yCursor < t.getYtile()) {
884 if (t.getYtile() % 32 == 31) {
885 g.fillRect(0, p.y - 1, mv.getWidth(), 3);
886 } else {
887 g.drawLine(0, p.y, mv.getWidth(), p.y);
888 }
889 yCursor = t.getYtile();
890 }
891 // This draws the vertical lines for the entire
892 // column. Only draw them for the top tile in
893 // the column.
894 if (xCursor < t.getXtile()) {
895 if (t.getXtile() % 32 == 0) {
896 // level 7 tile boundary
897 g.fillRect(p.x - 1, 0, 3, mv.getHeight());
898 } else {
899 g.drawLine(p.x, 0, p.x, mv.getHeight());
900 }
901 xCursor = t.getXtile();
902 }
903 }
904 }
905
906 private Point pixelPos(LatLon ll) {
907 return Main.map.mapView.getPoint(Main.proj.latlon2eastNorth(ll).add(getDx(), getDy()));
908 }
909 private Point pixelPos(Tile t) {
910 double lon = tileXToLon(t.getXtile(), t.getZoom());
911 LatLon tmpLL = new LatLon(tileYToLat(t.getYtile(), t.getZoom()), lon);
912 return pixelPos(tmpLL);
913 }
914 private LatLon getShiftedLatLon(EastNorth en) {
915 return Main.proj.eastNorth2latlon(en.add(-getDx(), -getDy()));
916 }
917 private Coordinate getShiftedCoord(EastNorth en) {
918 LatLon ll = getShiftedLatLon(en);
919 return new Coordinate(ll.lat(),ll.lon());
920 }
921 private class TileSet {
922 int z12x0, z12x1, z12y0, z12y1;
923 int zoom;
924 int tileMax = -1;
925
926 /**
927 * Create a TileSet by EastNorth bbox taking a layer shift in account
928 */
929 TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
930 this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom);
931 }
932
933 /**
934 * Create a TileSet by known LatLon bbox without layer shift correction
935 */
936 TileSet(LatLon topLeft, LatLon botRight, int zoom) {
937 this.zoom = zoom;
938
939 z12x0 = lonToTileX(topLeft.lon(), zoom);
940 z12y0 = latToTileY(topLeft.lat(), zoom);
941 z12x1 = lonToTileX(botRight.lon(), zoom);
942 z12y1 = latToTileY(botRight.lat(), zoom);
943 if (z12x0 > z12x1) {
944 int tmp = z12x0;
945 z12x0 = z12x1;
946 z12x1 = tmp;
947 }
948 if (z12y0 > z12y1) {
949 int tmp = z12y0;
950 z12y0 = z12y1;
951 z12y1 = tmp;
952 }
953 tileMax = (int)Math.pow(2.0, zoom);
954 if (z12x0 < 0) {
955 z12x0 = 0;
956 }
957 if (z12y0 < 0) {
958 z12y0 = 0;
959 }
960 if (z12x1 > tileMax) {
961 z12x1 = tileMax;
962 }
963 if (z12y1 > tileMax) {
964 z12y1 = tileMax;
965 }
966 }
967 boolean tooSmall() {
968 return this.tilesSpanned() < 2.1;
969 }
970 boolean tooLarge() {
971 return this.tilesSpanned() > 10;
972 }
973 boolean insane() {
974 return this.tilesSpanned() > 100;
975 }
976 double tilesSpanned() {
977 return Math.sqrt(1.0 * this.size());
978 }
979
980 double size() {
981 double x_span = z12x1 - z12x0 + 1.0;
982 double y_span = z12y1 - z12y0 + 1.0;
983 return x_span * y_span;
984 }
985
986 /*
987 * Get all tiles represented by this TileSet that are
988 * already in the tileCache.
989 */
990 List<Tile> allTiles()
991 {
992 return this.allTiles(false);
993 }
994 private List<Tile> allTiles(boolean create)
995 {
996 List<Tile> ret = new ArrayList<Tile>();
997 // Don't even try to iterate over the set.
998 // Someone created a crazy number of them
999 if (this.insane())
1000 return ret;
1001 for (int x = z12x0; x <= z12x1; x++) {
1002 for (int y = z12y0; y <= z12y1; y++) {
1003 Tile t;
1004 if (create) {
1005 t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
1006 } else {
1007 t = getTile(x % tileMax, y % tileMax, zoom);
1008 }
1009 if (t != null) {
1010 ret.add(t);
1011 }
1012 }
1013 }
1014 return ret;
1015 }
1016 void loadAllTiles(boolean force)
1017 {
1018 List<Tile> tiles = this.allTiles(true);
1019 if (!autoLoad && !force)
1020 return;
1021 int nr_queued = 0;
1022 for (Tile t : tiles) {
1023 if (loadTile(t)) {
1024 nr_queued++;
1025 }
1026 }
1027 if (debug)
1028 if (nr_queued > 0) {
1029 out("queued to load: " + nr_queued + "/" + tiles.size() + " tiles at zoom: " + zoom);
1030 }
1031 }
1032 }
1033
1034 boolean az_disable = false;
1035 boolean autoZoomEnabled()
1036 {
1037 if (az_disable)
1038 return false;
1039 return autoZoom;
1040 }
1041 /**
1042 */
1043 @Override
1044 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1045 //long start = System.currentTimeMillis();
1046 EastNorth topLeft = mv.getEastNorth(0, 0);
1047 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1048
1049 if (botRight.east() == 0.0 || botRight.north() == 0) {
1050 Main.debug("still initializing??");
1051 // probably still initializing
1052 return;
1053 }
1054
1055 needRedraw = false;
1056
1057 int zoom = currentZoomLevel;
1058 TileSet ts = new TileSet(topLeft, botRight, zoom);
1059
1060 if (autoZoomEnabled()) {
1061 if (zoomDecreaseAllowed() && ts.tooLarge()) {
1062 if (debug) {
1063 out("too many tiles, decreasing zoom from " + currentZoomLevel);
1064 }
1065 if (decreaseZoomLevel()) {
1066 this.paint(g, mv, bounds);
1067 }
1068 return;
1069 }
1070 if (zoomIncreaseAllowed() && ts.tooSmall()) {
1071 if (debug) {
1072 out("too zoomed in, (" + ts.tilesSpanned()
1073 + "), increasing zoom from " + currentZoomLevel);
1074 }
1075 // This is a hack. ts.tooSmall() is proabably a bad thing, and this works
1076 // around it. If we have a very small window, the tileSet may be well
1077 // less than 1 real tile wide, but that's expected. But, this sees the
1078 // tile set as too small and zooms in. The code below that checks for
1079 // pixel stretching disagrees and tries to zoom out. Both calls recurse,
1080 // hillarity ensues, and the stack overflows.
1081 //
1082 // This really needs to get fixed properly. We probably shouldn't even
1083 // have the tooSmall() check on tileSets. But, this also helps the zoom
1084 // converge to the correct place much faster.
1085 boolean tmp = az_disable;
1086 az_disable = true;
1087 if (increaseZoomLevel()) {
1088 this.paint(g, mv, bounds);
1089 }
1090 az_disable = tmp;
1091 return;
1092 }
1093 }
1094
1095 // Too many tiles... refuse to draw
1096 if (!ts.tooLarge()) {
1097 //out("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
1098 ts.loadAllTiles(false);
1099 }
1100
1101 g.setColor(Color.DARK_GRAY);
1102
1103 List<Tile> missedTiles = this.paintTileImages(g, ts, currentZoomLevel, null);
1104 int otherZooms[] = { -1, 1, -2, 2, -3, -4, -5};
1105 for (int zoomOffset : otherZooms) {
1106 if (!autoZoomEnabled()) {
1107 break;
1108 }
1109 if (!autoLoad) {
1110 break;
1111 }
1112 int newzoom = currentZoomLevel + zoomOffset;
1113 if (missedTiles.size() <= 0) {
1114 break;
1115 }
1116 List<Tile> newlyMissedTiles = new LinkedList<Tile>();
1117 for (Tile missed : missedTiles) {
1118 Tile t2 = tempCornerTile(missed);
1119 LatLon topLeft2 = tileLatLon(missed);
1120 LatLon botRight2 = tileLatLon(t2);
1121 TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
1122 if (ts2.tooLarge()) {
1123 continue;
1124 }
1125 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
1126 }
1127 missedTiles = newlyMissedTiles;
1128 }
1129 if (debug && missedTiles.size() > 0) {
1130 out("still missed "+missedTiles.size()+" in the end");
1131 }
1132 g.setColor(Color.red);
1133 g.setFont(InfoFont);
1134
1135 // The current zoom tileset is guaranteed to have all of
1136 // its tiles
1137 for (Tile t : ts.allTiles()) {
1138 this.paintTileText(ts, t, g, mv, currentZoomLevel, t);
1139 }
1140
1141 if (tileSource.requiresAttribution()) {
1142 // Draw attribution
1143 Font font = g.getFont();
1144 g.setFont(ATTR_LINK_FONT);
1145 g.setColor(Color.white);
1146
1147 // Draw terms of use text
1148 Rectangle2D termsStringBounds = g.getFontMetrics().getStringBounds("Background Terms of Use", g);
1149 int textHeight = (int) termsStringBounds.getHeight() - 5;
1150 int textWidth = (int) termsStringBounds.getWidth();
1151 int termsTextY = mv.getHeight() - textHeight;
1152 if(attrTermsUrl != null) {
1153 int x = 2;
1154 int y = mv.getHeight() - textHeight;
1155 attrToUBounds = new Rectangle(x, y, textWidth, textHeight);
1156 myDrawString(g, "Background Terms of Use", x, y);
1157 }
1158
1159 // Draw attribution logo
1160 int imgWidth = attrImage.getWidth(this);
1161 if(attrImage != null) {
1162 int x = 2;
1163 int height = attrImage.getHeight(this);
1164 int y = termsTextY - height - textHeight - 5;
1165 attrImageBounds = new Rectangle(x, y, imgWidth, height);
1166 g.drawImage(attrImage, x, y, this);
1167 }
1168
1169 g.setFont(ATTR_FONT);
1170 String attributionText = tileSource.getAttributionText(currentZoomLevel,
1171 getShiftedCoord(topLeft), getShiftedCoord(botRight));
1172 Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g);
1173 {
1174 int x = mv.getWidth() - (int) stringBounds.getWidth();
1175 int y = mv.getHeight() - textHeight;
1176 myDrawString(g, attributionText, x, y);
1177 }
1178
1179 g.setFont(font);
1180 }
1181
1182 if (autoZoomEnabled() && lastImageScale != null) {
1183 // If each source image pixel is being stretched into > 3
1184 // drawn pixels, zoom in... getting too pixelated
1185 if (lastImageScale > 3 && zoomIncreaseAllowed()) {
1186 if (debug) {
1187 out("autozoom increase: scale: " + lastImageScale);
1188 }
1189 increaseZoomLevel();
1190 this.paint(g, mv, bounds);
1191 // If each source image pixel is being squished into > 0.32
1192 // of a drawn pixels, zoom out.
1193 } else if ((lastImageScale < 0.45) && (lastImageScale > 0) && zoomDecreaseAllowed()) {
1194 if (debug) {
1195 out("autozoom decrease: scale: " + lastImageScale);
1196 }
1197 decreaseZoomLevel();
1198 this.paint(g, mv, bounds);
1199 }
1200 }
1201 //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
1202 g.setColor(Color.black);
1203 if (ts.insane()) {
1204 myDrawString(g, "zoom in to load any tiles", 120, 120);
1205 } else if (ts.tooLarge()) {
1206 myDrawString(g, "zoom in to load more tiles", 120, 120);
1207 } else if (ts.tooSmall()) {
1208 myDrawString(g, "increase zoom level to see more detail", 120, 120);
1209 }
1210 }// end of paint method
1211
1212 /**
1213 * This isn't very efficient, but it is only used when the
1214 * user right-clicks on the map.
1215 */
1216 Tile getTileForPixelpos(int px, int py) {
1217 if (debug) {
1218 out("getTileForPixelpos("+px+", "+py+")");
1219 }
1220 MapView mv = Main.map.mapView;
1221 Point clicked = new Point(px, py);
1222 EastNorth topLeft = mv.getEastNorth(0, 0);
1223 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1224 int z = currentZoomLevel;
1225 TileSet ts = new TileSet(topLeft, botRight, z);
1226
1227 if (!ts.tooLarge()) {
1228 ts.loadAllTiles(false); // make sure there are tile objects for all tiles
1229 }
1230 Tile clickedTile = null;
1231 for (Tile t1 : ts.allTiles()) {
1232 Tile t2 = tempCornerTile(t1);
1233 Rectangle r = new Rectangle(pixelPos(t1));
1234 r.add(pixelPos(t2));
1235 if (debug) {
1236 out("r: " + r + " clicked: " + clicked);
1237 }
1238 if (!r.contains(clicked)) {
1239 continue;
1240 }
1241 clickedTile = t1;
1242 break;
1243 }
1244 if (clickedTile == null)
1245 return null;
1246 System.out.println("clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
1247 " scale: " + lastImageScale + " currentZoomLevel: " + currentZoomLevel);
1248 return clickedTile;
1249 }
1250
1251 @Override
1252 public Action[] getMenuEntries() {
1253 return new Action[] {
1254 LayerListDialog.getInstance().createShowHideLayerAction(),
1255 LayerListDialog.getInstance().createDeleteLayerAction(),
1256 SeparatorLayerAction.INSTANCE,
1257 // color,
1258 new OffsetAction(),
1259 new RenameLayerAction(this.getAssociatedFile(), this),
1260 SeparatorLayerAction.INSTANCE,
1261 new LayerListPopup.InfoAction(this) };
1262 }
1263
1264 @Override
1265 public String getToolTipText() {
1266 return null;
1267 }
1268
1269 @Override
1270 public void visitBoundingBox(BoundingXYVisitor v) {
1271 }
1272
1273 @Override
1274 public boolean isChanged() {
1275 return needRedraw;
1276 }
1277
1278 private int latToTileY(double lat, int zoom) {
1279 double l = lat / 180 * Math.PI;
1280 double pf = Math.log(Math.tan(l) + (1 / Math.cos(l)));
1281 return (int) (Math.pow(2.0, zoom - 1) * (Math.PI - pf) / Math.PI);
1282 }
1283
1284 private int lonToTileX(double lon, int zoom) {
1285 return (int) (Math.pow(2.0, zoom - 3) * (lon + 180.0) / 45.0);
1286 }
1287
1288 private double tileYToLat(int y, int zoom) {
1289 return Math.atan(Math.sinh(Math.PI
1290 - (Math.PI * y / Math.pow(2.0, zoom - 1))))
1291 * 180 / Math.PI;
1292 }
1293
1294 private double tileXToLon(int x, int zoom) {
1295 return x * 45.0 / Math.pow(2.0, zoom - 3) - 180.0;
1296 }
1297}
Note: See TracBrowser for help on using the repository browser.