001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.bbox; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Dimension; 008import java.awt.Graphics; 009import java.awt.Point; 010import java.awt.Rectangle; 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.Collections; 014import java.util.HashMap; 015import java.util.HashSet; 016import java.util.List; 017import java.util.Map; 018import java.util.Set; 019import java.util.concurrent.CopyOnWriteArrayList; 020 021import javax.swing.JOptionPane; 022import javax.swing.SpringLayout; 023 024import org.openstreetmap.gui.jmapviewer.Coordinate; 025import org.openstreetmap.gui.jmapviewer.JMapViewer; 026import org.openstreetmap.gui.jmapviewer.MapMarkerDot; 027import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 028import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 029import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 030import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; 031import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 032import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 033import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource; 034import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource; 035import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource; 036import org.openstreetmap.josm.Main; 037import org.openstreetmap.josm.data.Bounds; 038import org.openstreetmap.josm.data.Version; 039import org.openstreetmap.josm.data.coor.LatLon; 040import org.openstreetmap.josm.data.imagery.ImageryInfo; 041import org.openstreetmap.josm.data.imagery.ImageryLayerInfo; 042import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 043import org.openstreetmap.josm.data.preferences.StringProperty; 044import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer; 045import org.openstreetmap.josm.gui.layer.TMSLayer; 046 047public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser { 048 049 public interface TileSourceProvider { 050 List<TileSource> getTileSources(); 051 } 052 053 /** 054 * TMS TileSource provider for the slippymap chooser 055 */ 056 public static class TMSTileSourceProvider implements TileSourceProvider { 057 private static final Set<String> existingSlippyMapUrls = new HashSet<>(); 058 static { 059 // Urls that already exist in the slippymap chooser and shouldn't be copied from TMS layer list 060 existingSlippyMapUrls.add("https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png"); // Mapnik 061 existingSlippyMapUrls.add("http://tile.opencyclemap.org/cycle/{zoom}/{x}/{y}.png"); // Cyclemap 062 existingSlippyMapUrls.add("http://otile{switch:1,2,3,4}.mqcdn.com/tiles/1.0.0/osm/{zoom}/{x}/{y}.png"); // MapQuest-OSM 063 existingSlippyMapUrls.add("http://oatile{switch:1,2,3,4}.mqcdn.com/tiles/1.0.0/sat/{zoom}/{x}/{y}.png"); // MapQuest Open Aerial 064 } 065 066 @Override 067 public List<TileSource> getTileSources() { 068 if (!TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get()) return Collections.<TileSource>emptyList(); 069 List<TileSource> sources = new ArrayList<>(); 070 for (ImageryInfo info : ImageryLayerInfo.instance.getLayers()) { 071 if (existingSlippyMapUrls.contains(info.getUrl())) { 072 continue; 073 } 074 try { 075 TileSource source = TMSLayer.getTileSourceStatic(info); 076 if (source != null) { 077 sources.add(source); 078 } 079 } catch (IllegalArgumentException ex) { 080 if (ex.getMessage() != null && !ex.getMessage().isEmpty()) { 081 JOptionPane.showMessageDialog(Main.parent, 082 ex.getMessage(), tr("Warning"), 083 JOptionPane.WARNING_MESSAGE); 084 } 085 } 086 } 087 return sources; 088 } 089 090 public static void addExistingSlippyMapUrl(String url) { 091 existingSlippyMapUrls.add(url); 092 } 093 } 094 095 /** 096 * Plugins that wish to add custom tile sources to slippy map choose should call this method 097 * @param tileSourceProvider new tile source provider 098 */ 099 public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) { 100 providers.addIfAbsent(tileSourceProvider); 101 } 102 103 private static CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<>(); 104 static { 105 addTileSourceProvider(new TileSourceProvider() { 106 @Override 107 public List<TileSource> getTileSources() { 108 return Arrays.<TileSource>asList( 109 new OsmTileSource.Mapnik(), 110 new OsmTileSource.CycleMap(), 111 new MapQuestOsmTileSource(), 112 new MapQuestOpenAerialTileSource()); 113 } 114 }); 115 addTileSourceProvider(new TMSTileSourceProvider()); 116 } 117 118 private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik"); 119 public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize"; 120 121 private final transient TileLoader cachedLoader; 122 private final transient OsmTileLoader uncachedLoader; 123 124 private final SizeButton iSizeButton; 125 private final SourceButton iSourceButton; 126 private transient Bounds bbox; 127 128 // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX) 129 private transient ICoordinate iSelectionRectStart; 130 private transient ICoordinate iSelectionRectEnd; 131 132 /** 133 * Constructs a new {@code SlippyMapBBoxChooser}. 134 */ 135 public SlippyMapBBoxChooser() { 136 debug = Main.isDebugEnabled(); 137 SpringLayout springLayout = new SpringLayout(); 138 setLayout(springLayout); 139 140 Map<String, String> headers = new HashMap<>(); 141 headers.put("User-Agent", Version.getInstance().getFullAgentString()); 142 143 cachedLoader = AbstractCachedTileSourceLayer.getTileLoaderFactory("TMS", TMSCachedTileLoader.class).makeTileLoader(this, headers); 144 145 uncachedLoader = new OsmTileLoader(this); 146 uncachedLoader.headers.putAll(headers); 147 setZoomContolsVisible(Main.pref.getBoolean("slippy_map_chooser.zoomcontrols", false)); 148 setMapMarkerVisible(false); 149 setMinimumSize(new Dimension(350, 350 / 2)); 150 // We need to set an initial size - this prevents a wrong zoom selection 151 // for the area before the component has been displayed the first time 152 setBounds(new Rectangle(getMinimumSize())); 153 if (cachedLoader == null) { 154 setFileCacheEnabled(false); 155 } else { 156 setFileCacheEnabled(Main.pref.getBoolean("slippy_map_chooser.file_cache", true)); 157 } 158 setMaxTilesInMemory(Main.pref.getInteger("slippy_map_chooser.max_tiles", 1000)); 159 160 List<TileSource> tileSources = getAllTileSources(); 161 162 iSourceButton = new SourceButton(this, tileSources); 163 add(iSourceButton); 164 springLayout.putConstraint(SpringLayout.EAST, iSourceButton, 0, SpringLayout.EAST, this); 165 springLayout.putConstraint(SpringLayout.NORTH, iSourceButton, 30, SpringLayout.NORTH, this); 166 167 iSizeButton = new SizeButton(this); 168 add(iSizeButton); 169 170 String mapStyle = PROP_MAPSTYLE.get(); 171 boolean foundSource = false; 172 for (TileSource source: tileSources) { 173 if (source.getName().equals(mapStyle)) { 174 this.setTileSource(source); 175 iSourceButton.setCurrentMap(source); 176 foundSource = true; 177 break; 178 } 179 } 180 if (!foundSource) { 181 setTileSource(tileSources.get(0)); 182 iSourceButton.setCurrentMap(tileSources.get(0)); 183 } 184 185 new SlippyMapControler(this, this); 186 } 187 188 private List<TileSource> getAllTileSources() { 189 List<TileSource> tileSources = new ArrayList<>(); 190 for (TileSourceProvider provider: providers) { 191 tileSources.addAll(provider.getTileSources()); 192 } 193 return tileSources; 194 } 195 196 public boolean handleAttribution(Point p, boolean click) { 197 return attribution.handleAttribution(p, click); 198 } 199 200 /** 201 * Draw the map. 202 */ 203 @Override 204 public void paint(Graphics g) { 205 try { 206 super.paint(g); 207 208 // draw selection rectangle 209 if (iSelectionRectStart != null && iSelectionRectEnd != null) { 210 Rectangle box = new Rectangle(getMapPosition(iSelectionRectStart, false)); 211 box.add(getMapPosition(iSelectionRectEnd, false)); 212 213 g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f)); 214 g.fillRect(box.x, box.y, box.width, box.height); 215 216 g.setColor(Color.BLACK); 217 g.drawRect(box.x, box.y, box.width, box.height); 218 } 219 } catch (Exception e) { 220 Main.error(e); 221 } 222 } 223 224 public final void setFileCacheEnabled(boolean enabled) { 225 if (enabled) { 226 setTileLoader(cachedLoader); 227 } else { 228 setTileLoader(uncachedLoader); 229 } 230 } 231 232 public final void setMaxTilesInMemory(int tiles) { 233 ((MemoryTileCache) getTileCache()).setCacheSize(tiles); 234 } 235 236 /** 237 * Callback for the OsmMapControl. (Re-)Sets the start and end point of the selection rectangle. 238 * 239 * @param aStart selection start 240 * @param aEnd selection end 241 */ 242 public void setSelection(Point aStart, Point aEnd) { 243 if (aStart == null || aEnd == null || aStart.x == aEnd.x || aStart.y == aEnd.y) 244 return; 245 246 Point p_max = new Point(Math.max(aEnd.x, aStart.x), Math.max(aEnd.y, aStart.y)); 247 Point p_min = new Point(Math.min(aEnd.x, aStart.x), Math.min(aEnd.y, aStart.y)); 248 249 iSelectionRectStart = getPosition(p_min); 250 iSelectionRectEnd = getPosition(p_max); 251 252 Bounds b = new Bounds( 253 new LatLon( 254 Math.min(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()), 255 LatLon.toIntervalLon(Math.min(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon())) 256 ), 257 new LatLon( 258 Math.max(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()), 259 LatLon.toIntervalLon(Math.max(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon()))) 260 ); 261 Bounds oldValue = this.bbox; 262 this.bbox = b; 263 repaint(); 264 firePropertyChange(BBOX_PROP, oldValue, this.bbox); 265 } 266 267 /** 268 * Performs resizing of the DownloadDialog in order to enlarge or shrink the 269 * map. 270 */ 271 public void resizeSlippyMap() { 272 boolean large = iSizeButton.isEnlarged(); 273 firePropertyChange(RESIZE_PROP, !large, large); 274 } 275 276 public void toggleMapSource(TileSource tileSource) { 277 this.tileController.setTileCache(new MemoryTileCache()); 278 this.setTileSource(tileSource); 279 PROP_MAPSTYLE.put(tileSource.getName()); // TODO Is name really unique? 280 } 281 282 @Override 283 public Bounds getBoundingBox() { 284 return bbox; 285 } 286 287 /** 288 * Sets the current bounding box in this bbox chooser without 289 * emiting a property change event. 290 * 291 * @param bbox the bounding box. null to reset the bounding box 292 */ 293 @Override 294 public void setBoundingBox(Bounds bbox) { 295 if (bbox == null || (bbox.getMinLat() == 0 && bbox.getMinLon() == 0 296 && bbox.getMaxLat() == 0 && bbox.getMaxLon() == 0)) { 297 this.bbox = null; 298 iSelectionRectStart = null; 299 iSelectionRectEnd = null; 300 repaint(); 301 return; 302 } 303 304 this.bbox = bbox; 305 iSelectionRectStart = new Coordinate(bbox.getMinLat(), bbox.getMinLon()); 306 iSelectionRectEnd = new Coordinate(bbox.getMaxLat(), bbox.getMaxLon()); 307 308 // calc the screen coordinates for the new selection rectangle 309 MapMarkerDot min = new MapMarkerDot(bbox.getMinLat(), bbox.getMinLon()); 310 MapMarkerDot max = new MapMarkerDot(bbox.getMaxLat(), bbox.getMaxLon()); 311 312 List<MapMarker> marker = new ArrayList<>(2); 313 marker.add(min); 314 marker.add(max); 315 setMapMarkerList(marker); 316 setDisplayToFitMapMarkers(); 317 zoomOut(); 318 repaint(); 319 } 320 321 /** 322 * Enables or disables painting of the shrink/enlarge button 323 * 324 * @param visible {@code true} to enable painting of the shrink/enlarge button 325 */ 326 public void setSizeButtonVisible(boolean visible) { 327 iSizeButton.setVisible(visible); 328 } 329 330 /** 331 * Refreshes the tile sources 332 * @since 6364 333 */ 334 public final void refreshTileSources() { 335 iSourceButton.setSources(getAllTileSources()); 336 } 337}