001 // License: GPL. See LICENSE file for details. 002 package org.openstreetmap.josm.gui.bbox; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 006 import java.awt.Color; 007 import java.awt.Dimension; 008 import java.awt.Graphics; 009 import java.awt.Graphics2D; 010 import java.awt.Image; 011 import java.awt.Point; 012 import java.awt.Rectangle; 013 import java.io.File; 014 import java.io.IOException; 015 import java.util.ArrayList; 016 import java.util.Arrays; 017 import java.util.Collections; 018 import java.util.HashSet; 019 import java.util.List; 020 import java.util.Vector; 021 import java.util.concurrent.CopyOnWriteArrayList; 022 023 import javax.swing.JOptionPane; 024 025 import org.openstreetmap.gui.jmapviewer.Coordinate; 026 import org.openstreetmap.gui.jmapviewer.JMapViewer; 027 import org.openstreetmap.gui.jmapviewer.MapMarkerDot; 028 import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 029 import org.openstreetmap.gui.jmapviewer.OsmFileCacheTileLoader; 030 import org.openstreetmap.gui.jmapviewer.OsmMercator; 031 import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 032 import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; 033 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 034 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 035 import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource; 036 import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource; 037 import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource; 038 import org.openstreetmap.josm.Main; 039 import org.openstreetmap.josm.data.Bounds; 040 import org.openstreetmap.josm.data.coor.LatLon; 041 import org.openstreetmap.josm.data.imagery.ImageryInfo; 042 import org.openstreetmap.josm.data.imagery.ImageryLayerInfo; 043 import org.openstreetmap.josm.data.preferences.StringProperty; 044 import org.openstreetmap.josm.gui.layer.TMSLayer; 045 046 public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser{ 047 048 public interface TileSourceProvider { 049 List<TileSource> getTileSources(); 050 } 051 052 public static class RenamedSourceDecorator implements TileSource { 053 054 private final TileSource source; 055 private final String name; 056 057 public RenamedSourceDecorator(TileSource source, String name) { 058 this.source = source; 059 this.name = name; 060 } 061 062 @Override public String getName() { 063 return name; 064 } 065 066 @Override public int getMaxZoom() { return source.getMaxZoom(); } 067 068 @Override public int getMinZoom() { return source.getMinZoom(); } 069 070 @Override public int getTileSize() { return source.getTileSize(); } 071 072 @Override public String getTileType() { return source.getTileType(); } 073 074 @Override public TileUpdate getTileUpdate() { return source.getTileUpdate(); } 075 076 @Override public String getTileUrl(int zoom, int tilex, int tiley) throws IOException { return source.getTileUrl(zoom, tilex, tiley); } 077 078 @Override public boolean requiresAttribution() { return source.requiresAttribution(); } 079 080 @Override public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) { return source.getAttributionText(zoom, topLeft, botRight); } 081 082 @Override public String getAttributionLinkURL() { return source.getAttributionLinkURL(); } 083 084 @Override public Image getAttributionImage() { return source.getAttributionImage(); } 085 086 @Override public String getAttributionImageURL() { return source.getAttributionImageURL(); } 087 088 @Override public String getTermsOfUseText() { return source.getTermsOfUseText(); } 089 090 @Override public String getTermsOfUseURL() { return source.getTermsOfUseURL(); } 091 092 @Override public double latToTileY(double lat, int zoom) { return source.latToTileY(lat,zoom); } 093 094 @Override public double lonToTileX(double lon, int zoom) { return source.lonToTileX(lon,zoom); } 095 096 @Override public double tileYToLat(int y, int zoom) { return source.tileYToLat(y, zoom); } 097 098 @Override public double tileXToLon(int x, int zoom) { return source.tileXToLon(x, zoom); } 099 } 100 101 /** 102 * TMS TileSource provider for the slippymap chooser 103 */ 104 public static class TMSTileSourceProvider implements TileSourceProvider { 105 static final HashSet<String> existingSlippyMapUrls = new HashSet<String>(); 106 static { 107 // Urls that already exist in the slippymap chooser and shouldn't be copied from TMS layer list 108 existingSlippyMapUrls.add("http://tile.openstreetmap.org/{zoom}/{x}/{y}.png"); // Mapnik 109 existingSlippyMapUrls.add("http://tile.opencyclemap.org/cycle/{zoom}/{x}/{y}.png"); // Cyclemap 110 existingSlippyMapUrls.add("http://otile{switch:1,2,3,4}.mqcdn.com/tiles/1.0.0/osm/{zoom}/{x}/{y}.png"); // MapQuest-OSM 111 existingSlippyMapUrls.add("http://oatile{switch:1,2,3,4}.mqcdn.com/tiles/1.0.0/sat/{zoom}/{x}/{y}.png"); // MapQuest Open Aerial 112 } 113 114 @Override 115 public List<TileSource> getTileSources() { 116 if (!TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get()) return Collections.<TileSource>emptyList(); 117 List<TileSource> sources = new ArrayList<TileSource>(); 118 for (ImageryInfo info : ImageryLayerInfo.instance.getLayers()) { 119 if (existingSlippyMapUrls.contains(info.getUrl())) { 120 continue; 121 } 122 try { 123 TileSource source = TMSLayer.getTileSource(info); 124 if (source != null) { 125 sources.add(source); 126 } 127 } catch (IllegalArgumentException ex) { 128 if (ex.getMessage() != null && !ex.getMessage().isEmpty()) { 129 JOptionPane.showMessageDialog(Main.parent, 130 ex.getMessage(), tr("Warning"), 131 JOptionPane.WARNING_MESSAGE); 132 } 133 } 134 } 135 return sources; 136 } 137 138 public static void addExistingSlippyMapUrl(String url) { 139 existingSlippyMapUrls.add(url); 140 } 141 } 142 143 144 /** 145 * Plugins that wish to add custom tile sources to slippy map choose should call this method 146 * @param tileSourceProvider 147 */ 148 public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) { 149 providers.addIfAbsent(tileSourceProvider); 150 } 151 152 private static CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<TileSourceProvider>(); 153 154 static { 155 addTileSourceProvider(new TileSourceProvider() { 156 @Override 157 public List<TileSource> getTileSources() { 158 return Arrays.<TileSource>asList( 159 new RenamedSourceDecorator(new OsmTileSource.Mapnik(), "Mapnik"), 160 new RenamedSourceDecorator(new OsmTileSource.CycleMap(), "Cyclemap"), 161 new RenamedSourceDecorator(new MapQuestOsmTileSource(), "MapQuest-OSM"), 162 new RenamedSourceDecorator(new MapQuestOpenAerialTileSource(), "MapQuest Open Aerial") 163 ); 164 } 165 }); 166 addTileSourceProvider(new TMSTileSourceProvider()); 167 } 168 169 private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik"); 170 public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize"; 171 172 private TileLoader cachedLoader; 173 private TileLoader uncachedLoader; 174 175 private final SizeButton iSizeButton = new SizeButton(); 176 private final SourceButton iSourceButton; 177 private Bounds bbox; 178 179 // upper left and lower right corners of the selection rectangle (x/y on 180 // ZOOM_MAX) 181 Point iSelectionRectStart; 182 Point iSelectionRectEnd; 183 184 public SlippyMapBBoxChooser() { 185 super(); 186 TMSLayer.setMaxWorkers(); 187 cachedLoader = null; 188 String cachePath = TMSLayer.PROP_TILECACHE_DIR.get(); 189 if (cachePath != null && !cachePath.isEmpty()) { 190 try { 191 cachedLoader = new OsmFileCacheTileLoader(this, new File(cachePath)); 192 } catch (IOException e) { 193 } 194 } 195 196 uncachedLoader = new OsmTileLoader(this); 197 setZoomContolsVisible(Main.pref.getBoolean("slippy_map_chooser.zoomcontrols",false)); 198 setMapMarkerVisible(false); 199 setMinimumSize(new Dimension(350, 350 / 2)); 200 // We need to set an initial size - this prevents a wrong zoom selection 201 // for 202 // the area before the component has been displayed the first time 203 setBounds(new Rectangle(getMinimumSize())); 204 if (cachedLoader == null) { 205 setFileCacheEnabled(false); 206 } else { 207 setFileCacheEnabled(Main.pref.getBoolean("slippy_map_chooser.file_cache", true)); 208 } 209 setMaxTilesInMemory(Main.pref.getInteger("slippy_map_chooser.max_tiles", 1000)); 210 211 List<TileSource> tileSources = new ArrayList<TileSource>(); 212 for (TileSourceProvider provider: providers) { 213 tileSources.addAll(provider.getTileSources()); 214 } 215 216 iSourceButton = new SourceButton(tileSources); 217 218 String mapStyle = PROP_MAPSTYLE.get(); 219 boolean foundSource = false; 220 for (TileSource source: tileSources) { 221 if (source.getName().equals(mapStyle)) { 222 this.setTileSource(source); 223 iSourceButton.setCurrentMap(source); 224 foundSource = true; 225 break; 226 } 227 } 228 if (!foundSource) { 229 setTileSource(tileSources.get(0)); 230 iSourceButton.setCurrentMap(tileSources.get(0)); 231 } 232 233 new SlippyMapControler(this, this, iSizeButton, iSourceButton); 234 } 235 236 public boolean handleAttribution(Point p, boolean click) { 237 return attribution.handleAttribution(p, click); 238 } 239 240 protected Point getTopLeftCoordinates() { 241 return new Point(center.x - (getWidth() / 2), center.y - (getHeight() / 2)); 242 } 243 244 /** 245 * Draw the map. 246 */ 247 @Override 248 public void paint(Graphics g) { 249 try { 250 super.paint(g); 251 252 // draw selection rectangle 253 if (iSelectionRectStart != null && iSelectionRectEnd != null) { 254 255 int zoomDiff = MAX_ZOOM - zoom; 256 Point tlc = getTopLeftCoordinates(); 257 int x_min = (iSelectionRectStart.x >> zoomDiff) - tlc.x; 258 int y_min = (iSelectionRectStart.y >> zoomDiff) - tlc.y; 259 int x_max = (iSelectionRectEnd.x >> zoomDiff) - tlc.x; 260 int y_max = (iSelectionRectEnd.y >> zoomDiff) - tlc.y; 261 262 int w = x_max - x_min; 263 int h = y_max - y_min; 264 g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f)); 265 g.fillRect(x_min, y_min, w, h); 266 267 g.setColor(Color.BLACK); 268 g.drawRect(x_min, y_min, w, h); 269 } 270 271 iSizeButton.paint(g); 272 iSourceButton.paint((Graphics2D)g); 273 } catch (Exception e) { 274 e.printStackTrace(); 275 } 276 } 277 278 public void setFileCacheEnabled(boolean enabled) { 279 if (enabled) { 280 setTileLoader(cachedLoader); 281 } else { 282 setTileLoader(uncachedLoader); 283 } 284 } 285 286 public void setMaxTilesInMemory(int tiles) { 287 ((MemoryTileCache) getTileCache()).setCacheSize(tiles); 288 } 289 290 291 /** 292 * Callback for the OsmMapControl. (Re-)Sets the start and end point of the 293 * selection rectangle. 294 * 295 * @param aStart 296 * @param aEnd 297 */ 298 public void setSelection(Point aStart, Point aEnd) { 299 if (aStart == null || aEnd == null || aStart.x == aEnd.x || aStart.y == aEnd.y) 300 return; 301 302 Point p_max = new Point(Math.max(aEnd.x, aStart.x), Math.max(aEnd.y, aStart.y)); 303 Point p_min = new Point(Math.min(aEnd.x, aStart.x), Math.min(aEnd.y, aStart.y)); 304 305 Point tlc = getTopLeftCoordinates(); 306 int zoomDiff = MAX_ZOOM - zoom; 307 Point pEnd = new Point(p_max.x + tlc.x, p_max.y + tlc.y); 308 Point pStart = new Point(p_min.x + tlc.x, p_min.y + tlc.y); 309 310 pEnd.x <<= zoomDiff; 311 pEnd.y <<= zoomDiff; 312 pStart.x <<= zoomDiff; 313 pStart.y <<= zoomDiff; 314 315 iSelectionRectStart = pStart; 316 iSelectionRectEnd = pEnd; 317 318 Coordinate l1 = getPosition(p_max); // lon may be outside [-180,180] 319 Coordinate l2 = getPosition(p_min); // lon may be outside [-180,180] 320 Bounds b = new Bounds( 321 new LatLon( 322 Math.min(l2.getLat(), l1.getLat()), 323 LatLon.toIntervalLon(Math.min(l1.getLon(), l2.getLon())) 324 ), 325 new LatLon( 326 Math.max(l2.getLat(), l1.getLat()), 327 LatLon.toIntervalLon(Math.max(l1.getLon(), l2.getLon()))) 328 ); 329 Bounds oldValue = this.bbox; 330 this.bbox = b; 331 repaint(); 332 firePropertyChange(BBOX_PROP, oldValue, this.bbox); 333 } 334 335 /** 336 * Performs resizing of the DownloadDialog in order to enlarge or shrink the 337 * map. 338 */ 339 public void resizeSlippyMap() { 340 boolean large = iSizeButton.isEnlarged(); 341 firePropertyChange(RESIZE_PROP, !large, large); 342 } 343 344 public void toggleMapSource(TileSource tileSource) { 345 this.tileController.setTileCache(new MemoryTileCache()); 346 this.setTileSource(tileSource); 347 PROP_MAPSTYLE.put(tileSource.getName()); // TODO Is name really unique? 348 } 349 350 public Bounds getBoundingBox() { 351 return bbox; 352 } 353 354 /** 355 * Sets the current bounding box in this bbox chooser without 356 * emiting a property change event. 357 * 358 * @param bbox the bounding box. null to reset the bounding box 359 */ 360 public void setBoundingBox(Bounds bbox) { 361 if (bbox == null || (bbox.getMin().lat() == 0.0 && bbox.getMin().lon() == 0.0 362 && bbox.getMax().lat() == 0.0 && bbox.getMax().lon() == 0.0)) { 363 this.bbox = null; 364 iSelectionRectStart = null; 365 iSelectionRectEnd = null; 366 repaint(); 367 return; 368 } 369 370 this.bbox = bbox; 371 double minLon = bbox.getMin().lon(); 372 double maxLon = bbox.getMax().lon(); 373 374 if (bbox.crosses180thMeridian()) { 375 minLon -= 360.0; 376 } 377 378 int y1 = OsmMercator.LatToY(bbox.getMin().lat(), MAX_ZOOM); 379 int y2 = OsmMercator.LatToY(bbox.getMax().lat(), MAX_ZOOM); 380 int x1 = OsmMercator.LonToX(minLon, MAX_ZOOM); 381 int x2 = OsmMercator.LonToX(maxLon, MAX_ZOOM); 382 383 iSelectionRectStart = new Point(Math.min(x1, x2), Math.min(y1, y2)); 384 iSelectionRectEnd = new Point(Math.max(x1, x2), Math.max(y1, y2)); 385 386 // calc the screen coordinates for the new selection rectangle 387 MapMarkerDot xmin_ymin = new MapMarkerDot(bbox.getMin().lat(), bbox.getMin().lon()); 388 MapMarkerDot xmax_ymax = new MapMarkerDot(bbox.getMax().lat(), bbox.getMax().lon()); 389 390 Vector<MapMarker> marker = new Vector<MapMarker>(2); 391 marker.add(xmin_ymin); 392 marker.add(xmax_ymax); 393 setMapMarkerList(marker); 394 setDisplayToFitMapMarkers(); 395 zoomOut(); 396 repaint(); 397 } 398 }