source: josm/trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java@ 13270

Last change on this file since 13270 was 12955, checked in by bastiK, 7 years ago

applied #15414 - Redesign of SlippyMapBBoxChooser's SourceButton (patch by ris)

  • Property svn:eol-style set to native
File size: 16.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.bbox;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.awt.Dimension;
8import java.awt.Graphics;
9import java.awt.Graphics2D;
10import java.awt.Point;
11import java.awt.Rectangle;
12import java.awt.geom.Area;
13import java.awt.geom.Path2D;
14import java.util.ArrayList;
15import java.util.Arrays;
16import java.util.Collections;
17import java.util.HashMap;
18import java.util.HashSet;
19import java.util.List;
20import java.util.Map;
21import java.util.Set;
22import java.util.concurrent.CopyOnWriteArrayList;
23
24import javax.swing.ButtonModel;
25import javax.swing.JToggleButton;
26import javax.swing.JOptionPane;
27import javax.swing.SpringLayout;
28import javax.swing.event.ChangeListener;
29import javax.swing.event.ChangeEvent;
30
31import org.openstreetmap.gui.jmapviewer.Coordinate;
32import org.openstreetmap.gui.jmapviewer.JMapViewer;
33import org.openstreetmap.gui.jmapviewer.MapMarkerDot;
34import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
35import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
36import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
37import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
38import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
39import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
40import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
41import org.openstreetmap.josm.Main;
42import org.openstreetmap.josm.data.Bounds;
43import org.openstreetmap.josm.data.Version;
44import org.openstreetmap.josm.data.coor.LatLon;
45import org.openstreetmap.josm.data.imagery.ImageryInfo;
46import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
47import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
48import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
49import org.openstreetmap.josm.data.osm.BBox;
50import org.openstreetmap.josm.data.preferences.BooleanProperty;
51import org.openstreetmap.josm.data.preferences.StringProperty;
52import org.openstreetmap.josm.gui.MainApplication;
53import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer;
54import org.openstreetmap.josm.gui.layer.MainLayerManager;
55import org.openstreetmap.josm.gui.layer.OsmDataLayer;
56import org.openstreetmap.josm.gui.layer.TMSLayer;
57import org.openstreetmap.josm.spi.preferences.Config;
58import org.openstreetmap.josm.tools.Logging;
59
60/**
61 * This panel displays a map and lets the user chose a {@link BBox}.
62 */
63public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser, ChangeListener, MainLayerManager.ActiveLayerChangeListener {
64
65 /**
66 * A list of tile sources that can be used for displaying the map.
67 */
68 @FunctionalInterface
69 public interface TileSourceProvider {
70 /**
71 * Gets the tile sources that can be displayed
72 * @return The tile sources
73 */
74 List<TileSource> getTileSources();
75 }
76
77 /**
78 * TMS TileSource provider for the slippymap chooser
79 */
80 public static class TMSTileSourceProvider implements TileSourceProvider {
81 private static final Set<String> existingSlippyMapUrls = new HashSet<>();
82 static {
83 // Urls that already exist in the slippymap chooser and shouldn't be copied from TMS layer list
84 existingSlippyMapUrls.add("https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png"); // Mapnik
85 }
86
87 @Override
88 public List<TileSource> getTileSources() {
89 if (!TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get()) return Collections.<TileSource>emptyList();
90 List<TileSource> sources = new ArrayList<>();
91 for (ImageryInfo info : ImageryLayerInfo.instance.getLayers()) {
92 if (existingSlippyMapUrls.contains(info.getUrl())) {
93 continue;
94 }
95 try {
96 TileSource source = TMSLayer.getTileSourceStatic(info);
97 if (source != null) {
98 sources.add(source);
99 }
100 } catch (IllegalArgumentException ex) {
101 Logging.warn(ex);
102 if (ex.getMessage() != null && !ex.getMessage().isEmpty()) {
103 JOptionPane.showMessageDialog(Main.parent,
104 ex.getMessage(), tr("Warning"),
105 JOptionPane.WARNING_MESSAGE);
106 }
107 }
108 }
109 return sources;
110 }
111 }
112
113 /**
114 * Plugins that wish to add custom tile sources to slippy map choose should call this method
115 * @param tileSourceProvider new tile source provider
116 */
117 public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) {
118 providers.addIfAbsent(tileSourceProvider);
119 }
120
121 private static CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<>();
122 static {
123 addTileSourceProvider(() -> Arrays.<TileSource>asList(new OsmTileSource.Mapnik()));
124 addTileSourceProvider(new TMSTileSourceProvider());
125 }
126
127 private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik");
128 private static final BooleanProperty PROP_SHOWDLAREA = new BooleanProperty("slippy_map_chooser.show_downloaded_area", true);
129 /**
130 * The property name used for the resize button.
131 * @see #addPropertyChangeListener(java.beans.PropertyChangeListener)
132 */
133 public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize";
134
135 private final transient TileLoader cachedLoader;
136 private final transient OsmTileLoader uncachedLoader;
137
138 private final SizeButton iSizeButton;
139 private final ButtonModel showDownloadAreaButtonModel;
140 private final SourceButton iSourceButton;
141 private transient Bounds bbox;
142
143 // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX)
144 private transient ICoordinate iSelectionRectStart;
145 private transient ICoordinate iSelectionRectEnd;
146
147 /**
148 * Constructs a new {@code SlippyMapBBoxChooser}.
149 */
150 public SlippyMapBBoxChooser() {
151 debug = Logging.isDebugEnabled();
152 SpringLayout springLayout = new SpringLayout();
153 setLayout(springLayout);
154
155 Map<String, String> headers = new HashMap<>();
156 headers.put("User-Agent", Version.getInstance().getFullAgentString());
157
158 TileLoaderFactory cachedLoaderFactory = AbstractCachedTileSourceLayer.getTileLoaderFactory("TMS", TMSCachedTileLoader.class);
159 if (cachedLoaderFactory != null) {
160 cachedLoader = cachedLoaderFactory.makeTileLoader(this, headers);
161 } else {
162 cachedLoader = null;
163 }
164
165 uncachedLoader = new OsmTileLoader(this);
166 uncachedLoader.headers.putAll(headers);
167 setZoomContolsVisible(Config.getPref().getBoolean("slippy_map_chooser.zoomcontrols", false));
168 setMapMarkerVisible(false);
169 setMinimumSize(new Dimension(350, 350 / 2));
170 // We need to set an initial size - this prevents a wrong zoom selection
171 // for the area before the component has been displayed the first time
172 setBounds(new Rectangle(getMinimumSize()));
173 if (cachedLoader == null) {
174 setFileCacheEnabled(false);
175 } else {
176 setFileCacheEnabled(Config.getPref().getBoolean("slippy_map_chooser.file_cache", true));
177 }
178 setMaxTilesInMemory(Config.getPref().getInt("slippy_map_chooser.max_tiles", 1000));
179
180 List<TileSource> tileSources = getAllTileSources();
181
182 this.showDownloadAreaButtonModel = new JToggleButton.ToggleButtonModel();
183 this.showDownloadAreaButtonModel.setSelected(PROP_SHOWDLAREA.get());
184 this.showDownloadAreaButtonModel.addChangeListener(this);
185 iSourceButton = new SourceButton(this, tileSources, this.showDownloadAreaButtonModel);
186 add(iSourceButton);
187 springLayout.putConstraint(SpringLayout.EAST, iSourceButton, -2, SpringLayout.EAST, this);
188 springLayout.putConstraint(SpringLayout.NORTH, iSourceButton, 2, SpringLayout.NORTH, this);
189
190 iSizeButton = new SizeButton(this);
191 add(iSizeButton);
192
193 String mapStyle = PROP_MAPSTYLE.get();
194 boolean foundSource = false;
195 for (TileSource source: tileSources) {
196 if (source.getName().equals(mapStyle)) {
197 this.setTileSource(source);
198 iSourceButton.setCurrentMap(source);
199 foundSource = true;
200 break;
201 }
202 }
203 if (!foundSource) {
204 setTileSource(tileSources.get(0));
205 iSourceButton.setCurrentMap(tileSources.get(0));
206 }
207
208 MainApplication.getLayerManager().addActiveLayerChangeListener(this);
209
210 new SlippyMapControler(this, this);
211 }
212
213 private static List<TileSource> getAllTileSources() {
214 List<TileSource> tileSources = new ArrayList<>();
215 for (TileSourceProvider provider: providers) {
216 tileSources.addAll(provider.getTileSources());
217 }
218 return tileSources;
219 }
220
221 /**
222 * Handles a click/move on the attribution
223 * @param p The point in the view
224 * @param click true if it was a click, false for hover
225 * @return if the attribution handled the event
226 */
227 public boolean handleAttribution(Point p, boolean click) {
228 return attribution.handleAttribution(p, click);
229 }
230
231 /**
232 * Draw the map.
233 */
234 @Override
235 public void paintComponent(Graphics g) {
236 super.paintComponent(g);
237 Graphics2D g2d = (Graphics2D) g;
238
239 // draw shaded area for non-downloaded region of current "edit layer", but only if there *is* a current "edit layer",
240 // and it has defined bounds. Routine is analogous to that in OsmDataLayer's paint routine (but just different
241 // enough to make sharing code impractical)
242 final OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
243 if (editLayer != null && this.showDownloadAreaButtonModel.isSelected() && !editLayer.data.getDataSources().isEmpty()) {
244 // initialize area with current viewport
245 Rectangle b = this.getBounds();
246 // ensure we comfortably cover full area
247 b.grow(100, 100);
248 Path2D p = new Path2D.Float();
249
250 // combine successively downloaded areas after converting to screen-space
251 for (Bounds bounds : editLayer.data.getDataSourceBounds()) {
252 if (bounds.isCollapsed()) {
253 continue;
254 }
255 Rectangle r = new Rectangle(this.getMapPosition(bounds.getMinLat(), bounds.getMinLon(), false));
256 r.add(this.getMapPosition(bounds.getMaxLat(), bounds.getMaxLon(), false));
257 p.append(r, false);
258 }
259 // subtract combined areas
260 Area a = new Area(b);
261 a.subtract(new Area(p));
262
263 // paint remainder
264 g2d.setPaint(new Color(0, 0, 0, 32));
265 g2d.fill(a);
266 }
267
268 // draw selection rectangle
269 if (iSelectionRectStart != null && iSelectionRectEnd != null) {
270 Rectangle box = new Rectangle(getMapPosition(iSelectionRectStart, false));
271 box.add(getMapPosition(iSelectionRectEnd, false));
272
273 g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f));
274 g.fillRect(box.x, box.y, box.width, box.height);
275
276 g.setColor(Color.BLACK);
277 g.drawRect(box.x, box.y, box.width, box.height);
278 }
279 }
280
281 @Override
282 public void activeOrEditLayerChanged(MainLayerManager.ActiveLayerChangeEvent e) {
283 this.repaint();
284 }
285
286 @Override
287 public void stateChanged(ChangeEvent e) {
288 // fired for the stateChanged event of this.showDownloadAreaButtonModel
289 PROP_SHOWDLAREA.put(this.showDownloadAreaButtonModel.isSelected());
290 this.repaint();
291 }
292
293 /**
294 * Enables the disk tile cache.
295 * @param enabled true to enable, false to disable
296 */
297 public final void setFileCacheEnabled(boolean enabled) {
298 if (enabled && cachedLoader != null) {
299 setTileLoader(cachedLoader);
300 } else {
301 setTileLoader(uncachedLoader);
302 }
303 }
304
305 /**
306 * Sets the maximum number of tiles that may be held in memory
307 * @param tiles The maximum number of tiles.
308 */
309 public final void setMaxTilesInMemory(int tiles) {
310 ((MemoryTileCache) getTileCache()).setCacheSize(tiles);
311 }
312
313 /**
314 * Callback for the OsmMapControl. (Re-)Sets the start and end point of the selection rectangle.
315 *
316 * @param aStart selection start
317 * @param aEnd selection end
318 */
319 public void setSelection(Point aStart, Point aEnd) {
320 if (aStart == null || aEnd == null || aStart.x == aEnd.x || aStart.y == aEnd.y)
321 return;
322
323 Point pMax = new Point(Math.max(aEnd.x, aStart.x), Math.max(aEnd.y, aStart.y));
324 Point pMin = new Point(Math.min(aEnd.x, aStart.x), Math.min(aEnd.y, aStart.y));
325
326 iSelectionRectStart = getPosition(pMin);
327 iSelectionRectEnd = getPosition(pMax);
328
329 Bounds b = new Bounds(
330 new LatLon(
331 Math.min(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()),
332 LatLon.toIntervalLon(Math.min(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon()))
333 ),
334 new LatLon(
335 Math.max(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()),
336 LatLon.toIntervalLon(Math.max(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon())))
337 );
338 Bounds oldValue = this.bbox;
339 this.bbox = b;
340 repaint();
341 firePropertyChange(BBOX_PROP, oldValue, this.bbox);
342 }
343
344 /**
345 * Performs resizing of the DownloadDialog in order to enlarge or shrink the
346 * map.
347 */
348 public void resizeSlippyMap() {
349 boolean large = iSizeButton.isEnlarged();
350 firePropertyChange(RESIZE_PROP, !large, large);
351 }
352
353 /**
354 * Sets the active tile source
355 * @param tileSource The active tile source
356 */
357 public void toggleMapSource(TileSource tileSource) {
358 this.tileController.setTileCache(new MemoryTileCache());
359 this.setTileSource(tileSource);
360 PROP_MAPSTYLE.put(tileSource.getName()); // TODO Is name really unique?
361 if (this.iSourceButton.getCurrentSource() != tileSource) { // prevent infinite recursion
362 this.iSourceButton.setCurrentMap(tileSource);
363 }
364 }
365
366 @Override
367 public Bounds getBoundingBox() {
368 return bbox;
369 }
370
371 /**
372 * Sets the current bounding box in this bbox chooser without
373 * emiting a property change event.
374 *
375 * @param bbox the bounding box. null to reset the bounding box
376 */
377 @Override
378 public void setBoundingBox(Bounds bbox) {
379 if (bbox == null || (bbox.getMinLat() == 0 && bbox.getMinLon() == 0
380 && bbox.getMaxLat() == 0 && bbox.getMaxLon() == 0)) {
381 this.bbox = null;
382 iSelectionRectStart = null;
383 iSelectionRectEnd = null;
384 repaint();
385 return;
386 }
387
388 this.bbox = bbox;
389 iSelectionRectStart = new Coordinate(bbox.getMinLat(), bbox.getMinLon());
390 iSelectionRectEnd = new Coordinate(bbox.getMaxLat(), bbox.getMaxLon());
391
392 // calc the screen coordinates for the new selection rectangle
393 MapMarkerDot min = new MapMarkerDot(bbox.getMinLat(), bbox.getMinLon());
394 MapMarkerDot max = new MapMarkerDot(bbox.getMaxLat(), bbox.getMaxLon());
395
396 List<MapMarker> marker = new ArrayList<>(2);
397 marker.add(min);
398 marker.add(max);
399 setMapMarkerList(marker);
400 setDisplayToFitMapMarkers();
401 zoomOut();
402 repaint();
403 }
404
405 /**
406 * Enables or disables painting of the shrink/enlarge button
407 *
408 * @param visible {@code true} to enable painting of the shrink/enlarge button
409 */
410 public void setSizeButtonVisible(boolean visible) {
411 iSizeButton.setVisible(visible);
412 }
413
414 /**
415 * Refreshes the tile sources
416 * @since 6364
417 */
418 public final void refreshTileSources() {
419 iSourceButton.setSources(getAllTileSources());
420 }
421}
Note: See TracBrowser for help on using the repository browser.