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 091 /** 092 * Plugins that wish to add custom tile sources to slippy map choose should call this method 093 * @param tileSourceProvider new tile source provider 094 */ 095 public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) { 096 providers.addIfAbsent(tileSourceProvider); 097 } 098 099 private static CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<>(); 100 static { 101 addTileSourceProvider(new TileSourceProvider() { 102 @Override 103 public List<TileSource> getTileSources() { 104 return Arrays.<TileSource>asList( 105 new OsmTileSource.Mapnik(), 106 new OsmTileSource.CycleMap(), 107 new MapQuestOsmTileSource(), 108 new MapQuestOpenAerialTileSource()); 109 } 110 }); 111 addTileSourceProvider(new TMSTileSourceProvider()); 112 } 113 114 private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik"); 115 public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize"; 116 117 private final transient TileLoader cachedLoader; 118 private final transient OsmTileLoader uncachedLoader; 119 120 private final SizeButton iSizeButton; 121 private final SourceButton iSourceButton; 122 private transient Bounds bbox; 123 124 // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX) 125 private transient ICoordinate iSelectionRectStart; 126 private transient ICoordinate iSelectionRectEnd; 127 128 /** 129 * Constructs a new {@code SlippyMapBBoxChooser}. 130 */ 131 public SlippyMapBBoxChooser() { 132 debug = Main.isDebugEnabled(); 133 SpringLayout springLayout = new SpringLayout(); 134 setLayout(springLayout); 135 136 Map<String, String> headers = new HashMap<>(); 137 headers.put("User-Agent", Version.getInstance().getFullAgentString()); 138 139 cachedLoader = AbstractCachedTileSourceLayer.getTileLoaderFactory("TMS", TMSCachedTileLoader.class).makeTileLoader(this, headers); 140 141 uncachedLoader = new OsmTileLoader(this); 142 uncachedLoader.headers.putAll(headers); 143 setZoomContolsVisible(Main.pref.getBoolean("slippy_map_chooser.zoomcontrols", false)); 144 setMapMarkerVisible(false); 145 setMinimumSize(new Dimension(350, 350 / 2)); 146 // We need to set an initial size - this prevents a wrong zoom selection 147 // for the area before the component has been displayed the first time 148 setBounds(new Rectangle(getMinimumSize())); 149 if (cachedLoader == null) { 150 setFileCacheEnabled(false); 151 } else { 152 setFileCacheEnabled(Main.pref.getBoolean("slippy_map_chooser.file_cache", true)); 153 } 154 setMaxTilesInMemory(Main.pref.getInteger("slippy_map_chooser.max_tiles", 1000)); 155 156 List<TileSource> tileSources = getAllTileSources(); 157 158 iSourceButton = new SourceButton(this, tileSources); 159 add(iSourceButton); 160 springLayout.putConstraint(SpringLayout.EAST, iSourceButton, 0, SpringLayout.EAST, this); 161 springLayout.putConstraint(SpringLayout.NORTH, iSourceButton, 30, SpringLayout.NORTH, this); 162 163 iSizeButton = new SizeButton(this); 164 add(iSizeButton); 165 166 String mapStyle = PROP_MAPSTYLE.get(); 167 boolean foundSource = false; 168 for (TileSource source: tileSources) { 169 if (source.getName().equals(mapStyle)) { 170 this.setTileSource(source); 171 iSourceButton.setCurrentMap(source); 172 foundSource = true; 173 break; 174 } 175 } 176 if (!foundSource) { 177 setTileSource(tileSources.get(0)); 178 iSourceButton.setCurrentMap(tileSources.get(0)); 179 } 180 181 new SlippyMapControler(this, this); 182 } 183 184 private List<TileSource> getAllTileSources() { 185 List<TileSource> tileSources = new ArrayList<>(); 186 for (TileSourceProvider provider: providers) { 187 tileSources.addAll(provider.getTileSources()); 188 } 189 return tileSources; 190 } 191 192 public boolean handleAttribution(Point p, boolean click) { 193 return attribution.handleAttribution(p, click); 194 } 195 196 /** 197 * Draw the map. 198 */ 199 @Override 200 public void paint(Graphics g) { 201 super.paint(g); 202 203 // draw selection rectangle 204 if (iSelectionRectStart != null && iSelectionRectEnd != null) { 205 Rectangle box = new Rectangle(getMapPosition(iSelectionRectStart, false)); 206 box.add(getMapPosition(iSelectionRectEnd, false)); 207 208 g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f)); 209 g.fillRect(box.x, box.y, box.width, box.height); 210 211 g.setColor(Color.BLACK); 212 g.drawRect(box.x, box.y, box.width, box.height); 213 } 214 } 215 216 public final void setFileCacheEnabled(boolean enabled) { 217 if (enabled) { 218 setTileLoader(cachedLoader); 219 } else { 220 setTileLoader(uncachedLoader); 221 } 222 } 223 224 public final void setMaxTilesInMemory(int tiles) { 225 ((MemoryTileCache) getTileCache()).setCacheSize(tiles); 226 } 227 228 /** 229 * Callback for the OsmMapControl. (Re-)Sets the start and end point of the selection rectangle. 230 * 231 * @param aStart selection start 232 * @param aEnd selection end 233 */ 234 public void setSelection(Point aStart, Point aEnd) { 235 if (aStart == null || aEnd == null || aStart.x == aEnd.x || aStart.y == aEnd.y) 236 return; 237 238 Point pMax = new Point(Math.max(aEnd.x, aStart.x), Math.max(aEnd.y, aStart.y)); 239 Point pMin = new Point(Math.min(aEnd.x, aStart.x), Math.min(aEnd.y, aStart.y)); 240 241 iSelectionRectStart = getPosition(pMin); 242 iSelectionRectEnd = getPosition(pMax); 243 244 Bounds b = new Bounds( 245 new LatLon( 246 Math.min(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()), 247 LatLon.toIntervalLon(Math.min(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon())) 248 ), 249 new LatLon( 250 Math.max(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()), 251 LatLon.toIntervalLon(Math.max(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon()))) 252 ); 253 Bounds oldValue = this.bbox; 254 this.bbox = b; 255 repaint(); 256 firePropertyChange(BBOX_PROP, oldValue, this.bbox); 257 } 258 259 /** 260 * Performs resizing of the DownloadDialog in order to enlarge or shrink the 261 * map. 262 */ 263 public void resizeSlippyMap() { 264 boolean large = iSizeButton.isEnlarged(); 265 firePropertyChange(RESIZE_PROP, !large, large); 266 } 267 268 public void toggleMapSource(TileSource tileSource) { 269 this.tileController.setTileCache(new MemoryTileCache()); 270 this.setTileSource(tileSource); 271 PROP_MAPSTYLE.put(tileSource.getName()); // TODO Is name really unique? 272 } 273 274 @Override 275 public Bounds getBoundingBox() { 276 return bbox; 277 } 278 279 /** 280 * Sets the current bounding box in this bbox chooser without 281 * emiting a property change event. 282 * 283 * @param bbox the bounding box. null to reset the bounding box 284 */ 285 @Override 286 public void setBoundingBox(Bounds bbox) { 287 if (bbox == null || (bbox.getMinLat() == 0 && bbox.getMinLon() == 0 288 && bbox.getMaxLat() == 0 && bbox.getMaxLon() == 0)) { 289 this.bbox = null; 290 iSelectionRectStart = null; 291 iSelectionRectEnd = null; 292 repaint(); 293 return; 294 } 295 296 this.bbox = bbox; 297 iSelectionRectStart = new Coordinate(bbox.getMinLat(), bbox.getMinLon()); 298 iSelectionRectEnd = new Coordinate(bbox.getMaxLat(), bbox.getMaxLon()); 299 300 // calc the screen coordinates for the new selection rectangle 301 MapMarkerDot min = new MapMarkerDot(bbox.getMinLat(), bbox.getMinLon()); 302 MapMarkerDot max = new MapMarkerDot(bbox.getMaxLat(), bbox.getMaxLon()); 303 304 List<MapMarker> marker = new ArrayList<>(2); 305 marker.add(min); 306 marker.add(max); 307 setMapMarkerList(marker); 308 setDisplayToFitMapMarkers(); 309 zoomOut(); 310 repaint(); 311 } 312 313 /** 314 * Enables or disables painting of the shrink/enlarge button 315 * 316 * @param visible {@code true} to enable painting of the shrink/enlarge button 317 */ 318 public void setSizeButtonVisible(boolean visible) { 319 iSizeButton.setVisible(visible); 320 } 321 322 /** 323 * Refreshes the tile sources 324 * @since 6364 325 */ 326 public final void refreshTileSources() { 327 iSourceButton.setSources(getAllTileSources()); 328 } 329}