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

Last change on this file since 16920 was 16920, checked in by simon04, 4 years ago

fix #7638 - Download dialog: add status bar with lat/lon of mouse cursor and selected download area (for experts)

  • Property svn:eol-style set to native
File size: 15.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.bbox;
3
4import java.awt.Color;
5import java.awt.Dimension;
6import java.awt.Graphics;
7import java.awt.Graphics2D;
8import java.awt.Point;
9import java.awt.Rectangle;
10import java.awt.geom.Area;
11import java.awt.geom.Path2D;
12import java.util.ArrayList;
13import java.util.LinkedHashMap;
14import java.util.List;
15import java.util.concurrent.CopyOnWriteArrayList;
16import java.util.stream.Collectors;
17
18import javax.swing.ButtonModel;
19import javax.swing.JToggleButton;
20import javax.swing.SpringLayout;
21import javax.swing.event.ChangeEvent;
22import javax.swing.event.ChangeListener;
23
24import org.openstreetmap.gui.jmapviewer.Coordinate;
25import org.openstreetmap.gui.jmapviewer.MapMarkerDot;
26import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
27import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
28import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
29import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
30import org.openstreetmap.josm.data.Bounds;
31import org.openstreetmap.josm.data.coor.LatLon;
32import org.openstreetmap.josm.data.osm.BBox;
33import org.openstreetmap.josm.data.osm.DataSet;
34import org.openstreetmap.josm.data.preferences.BooleanProperty;
35import org.openstreetmap.josm.data.preferences.StringProperty;
36import org.openstreetmap.josm.gui.MainApplication;
37import org.openstreetmap.josm.gui.MapScaler;
38import org.openstreetmap.josm.gui.NavigatableComponent;
39import org.openstreetmap.josm.gui.layer.ImageryLayer;
40import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
41import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
42import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
43import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
44import org.openstreetmap.josm.gui.layer.MainLayerManager;
45import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
46import org.openstreetmap.josm.spi.preferences.Config;
47import org.openstreetmap.josm.tools.Logging;
48
49/**
50 * This panel displays a map and lets the user chose a {@link BBox}.
51 */
52public class SlippyMapBBoxChooser extends JosmMapViewer implements BBoxChooser, ChangeListener, ActiveLayerChangeListener, LayerChangeListener {
53
54 /**
55 * Plugins that wish to add custom tile sources to slippy map choose should call this method
56 * @param tileSourceProvider new tile source provider
57 */
58 public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) {
59 providers.addIfAbsent(tileSourceProvider);
60 }
61
62 private static final CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<>();
63 static {
64 addTileSourceProvider(new DefaultOsmTileSourceProvider());
65 addTileSourceProvider(new TMSTileSourceProvider());
66 addTileSourceProvider(new CurrentLayersTileSourceProvider());
67 }
68
69 private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik");
70 private static final BooleanProperty PROP_SHOWDLAREA = new BooleanProperty("slippy_map_chooser.show_downloaded_area", true);
71
72 /**
73 * The property name used for the resize button.
74 * @see #addPropertyChangeListener(java.beans.PropertyChangeListener)
75 */
76 public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize";
77
78 /**
79 * The property name used for the {@link org.openstreetmap.josm.data.coor.ILatLon} of the mouse cursor on the map.
80 * @see #addPropertyChangeListener(java.beans.PropertyChangeListener)
81 */
82 public static final String CURSOR_COORDINATE_PROP = SlippyMapBBoxChooser.class.getName() + ".coordinate";
83
84 private final SizeButton iSizeButton;
85 private final ButtonModel showDownloadAreaButtonModel;
86 private final SourceButton iSourceButton;
87 private transient Bounds bbox;
88
89 // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX)
90 private transient ICoordinate iSelectionRectStart;
91 private transient ICoordinate iSelectionRectEnd;
92
93 static {
94 debug = Logging.isDebugEnabled();
95 }
96
97 /**
98 * Constructs a new {@code SlippyMapBBoxChooser}.
99 */
100 public SlippyMapBBoxChooser() {
101 SpringLayout springLayout = new SpringLayout();
102 setLayout(springLayout);
103
104 setZoomControlsVisible(Config.getPref().getBoolean("slippy_map_chooser.zoomcontrols", false));
105 setMapMarkerVisible(false);
106 setMinimumSize(new Dimension(350, 350 / 2));
107 // We need to set an initial size - this prevents a wrong zoom selection
108 // for the area before the component has been displayed the first time
109 setBounds(new Rectangle(getMinimumSize()));
110 if (cachedLoader == null) {
111 setFileCacheEnabled(false);
112 } else {
113 setFileCacheEnabled(Config.getPref().getBoolean("slippy_map_chooser.file_cache", true));
114 }
115 setMaxTilesInMemory(Config.getPref().getInt("slippy_map_chooser.max_tiles", 1000));
116
117 List<TileSource> tileSources = new ArrayList<>(getAllTileSources().values());
118
119 this.showDownloadAreaButtonModel = new JToggleButton.ToggleButtonModel();
120 this.showDownloadAreaButtonModel.setSelected(PROP_SHOWDLAREA.get());
121 this.showDownloadAreaButtonModel.addChangeListener(this);
122 iSourceButton = new SourceButton(this, tileSources, this.showDownloadAreaButtonModel);
123 add(iSourceButton);
124 springLayout.putConstraint(SpringLayout.EAST, iSourceButton, -2, SpringLayout.EAST, this);
125 springLayout.putConstraint(SpringLayout.NORTH, iSourceButton, 2, SpringLayout.NORTH, this);
126
127 iSizeButton = new SizeButton(this);
128 add(iSizeButton);
129
130 MapScaler scaler = new MapScaler(this::getDist100Pixel, () -> Color.BLACK);
131 add(scaler);
132 springLayout.putConstraint(SpringLayout.SOUTH, scaler, 5, SpringLayout.SOUTH, this);
133
134 String mapStyle = PROP_MAPSTYLE.get();
135 final TileSource tileSource = tileSources.stream()
136 .filter(source -> source.getName().equals(mapStyle))
137 .findFirst()
138 .orElse(tileSources.get(0));
139 setTileSource(tileSource);
140 iSourceButton.setCurrentMap(tileSource);
141
142 MainApplication.getLayerManager().addActiveLayerChangeListener(this);
143
144 new SlippyMapControler(this, this);
145 }
146
147 private static LinkedHashMap<String, TileSource> getAllTileSources() {
148 // using a LinkedHashMap of <id, TileSource> to retain ordering but provide deduplication
149 return providers.stream().flatMap(
150 provider -> provider.getTileSources().stream()
151 ).collect(Collectors.toMap(
152 TileSource::getId,
153 ts -> ts,
154 (oldTs, newTs) -> oldTs,
155 LinkedHashMap::new
156 ));
157 }
158
159 /**
160 * Get the distance in meter that correspond to 100 px on screen.
161 * @return the distance in meter that correspond to 100 px on screen
162 * @see NavigatableComponent#getDist100Pixel
163 */
164 private double getDist100Pixel() {
165 int w = getWidth() / 2;
166 int h = getHeight() / 2;
167 ICoordinate c1 = getPosition(w - 50, h);
168 ICoordinate c2 = getPosition(w + 50, h);
169 final LatLon ll1 = new LatLon(c1.getLat(), c1.getLon());
170 final LatLon ll2 = new LatLon(c2.getLat(), c2.getLon());
171 double gcd = ll1.greatCircleDistance(ll2);
172 return gcd <= 0 ? 0.1 : gcd;
173 }
174
175 /**
176 * Handles a click/move on the attribution
177 * @param p The point in the view
178 * @param click true if it was a click, false for hover
179 * @return if the attribution handled the event
180 */
181 public boolean handleAttribution(Point p, boolean click) {
182 return attribution.handleAttribution(p, click);
183 }
184
185 /**
186 * Draw the map.
187 */
188 @Override
189 public void paintComponent(Graphics g) {
190 super.paintComponent(g);
191 Graphics2D g2d = (Graphics2D) g;
192
193 // draw shaded area for non-downloaded region of current data set, but only if there *is* a current data set,
194 // and it has defined bounds. Routine is analogous to that in OsmDataLayer's paint routine (but just different
195 // enough to make sharing code impractical)
196 final DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
197 if (ds != null && this.showDownloadAreaButtonModel.isSelected() && !ds.getDataSources().isEmpty()) {
198 // initialize area with current viewport
199 Rectangle b = this.getBounds();
200 // ensure we comfortably cover full area
201 b.grow(100, 100);
202 Path2D p = new Path2D.Float();
203
204 // combine successively downloaded areas after converting to screen-space
205 for (Bounds bounds : ds.getDataSourceBounds()) {
206 if (bounds.isCollapsed()) {
207 continue;
208 }
209 Rectangle r = new Rectangle(this.getMapPosition(bounds.getMinLat(), bounds.getMinLon(), false));
210 r.add(this.getMapPosition(bounds.getMaxLat(), bounds.getMaxLon(), false));
211 p.append(r, false);
212 }
213 // subtract combined areas
214 Area a = new Area(b);
215 a.subtract(new Area(p));
216
217 // paint remainder
218 g2d.setPaint(new Color(0, 0, 0, 32));
219 g2d.fill(a);
220 }
221
222 // draw selection rectangle
223 if (iSelectionRectStart != null && iSelectionRectEnd != null) {
224 Rectangle box = new Rectangle(getMapPosition(iSelectionRectStart, false));
225 box.add(getMapPosition(iSelectionRectEnd, false));
226
227 g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f));
228 g.fillRect(box.x, box.y, box.width, box.height);
229
230 g.setColor(Color.BLACK);
231 g.drawRect(box.x, box.y, box.width, box.height);
232 }
233 }
234
235 @Override
236 public void activeOrEditLayerChanged(MainLayerManager.ActiveLayerChangeEvent e) {
237 this.repaint();
238 }
239
240 @Override
241 public void stateChanged(ChangeEvent e) {
242 // fired for the stateChanged event of this.showDownloadAreaButtonModel
243 PROP_SHOWDLAREA.put(this.showDownloadAreaButtonModel.isSelected());
244 this.repaint();
245 }
246
247 /**
248 * Handles a {@link SlippyMapControler#mouseMoved} event
249 * @param point The point in the view
250 */
251 public void handleMouseMoved(Point point) {
252 final ICoordinate coordinate = getPosition(point);
253 final LatLon latLon = new LatLon(coordinate.getLat(), coordinate.getLon());
254 firePropertyChange(CURSOR_COORDINATE_PROP, null, latLon);
255 }
256
257 /**
258 * Callback for the OsmMapControl. (Re-)Sets the start and end point of the selection rectangle.
259 *
260 * @param aStart selection start
261 * @param aEnd selection end
262 */
263 public void setSelection(Point aStart, Point aEnd) {
264 if (aStart == null || aEnd == null || aStart.x == aEnd.x || aStart.y == aEnd.y)
265 return;
266
267 Point pMax = new Point(Math.max(aEnd.x, aStart.x), Math.max(aEnd.y, aStart.y));
268 Point pMin = new Point(Math.min(aEnd.x, aStart.x), Math.min(aEnd.y, aStart.y));
269
270 iSelectionRectStart = getPosition(pMin);
271 iSelectionRectEnd = getPosition(pMax);
272
273 Bounds b = new Bounds(
274 new LatLon(
275 Math.min(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()),
276 LatLon.toIntervalLon(Math.min(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon()))
277 ),
278 new LatLon(
279 Math.max(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()),
280 LatLon.toIntervalLon(Math.max(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon())))
281 );
282 Bounds oldValue = this.bbox;
283 this.bbox = b;
284 repaint();
285 firePropertyChange(BBOX_PROP, oldValue, this.bbox);
286 }
287
288 /**
289 * Performs resizing of the DownloadDialog in order to enlarge or shrink the
290 * map.
291 */
292 public void resizeSlippyMap() {
293 boolean large = iSizeButton.isEnlarged();
294 firePropertyChange(RESIZE_PROP, !large, large);
295 }
296
297 /**
298 * Sets the active tile source
299 * @param tileSource The active tile source
300 */
301 public void toggleMapSource(TileSource tileSource) {
302 this.tileController.setTileCache(new MemoryTileCache());
303 this.setTileSource(tileSource);
304 PROP_MAPSTYLE.put(tileSource.getName()); // TODO Is name really unique?
305
306 // we need to refresh the tile sources in case the deselected source should no longer be present
307 // (and only remained there because its removal was deferred while the source was still the
308 // selected one). this should also have the effect of propagating the new selection to the
309 // iSourceButton & menu: it attempts to re-select the current source when rebuilding its menu.
310 this.refreshTileSources();
311 }
312
313 @Override
314 public Bounds getBoundingBox() {
315 return bbox;
316 }
317
318 /**
319 * Sets the current bounding box in this bbox chooser without
320 * emitting a property change event.
321 *
322 * @param bbox the bounding box. null to reset the bounding box
323 */
324 @Override
325 public void setBoundingBox(Bounds bbox) {
326 if (bbox == null || (bbox.getMinLat() == 0 && bbox.getMinLon() == 0
327 && bbox.getMaxLat() == 0 && bbox.getMaxLon() == 0)) {
328 this.bbox = null;
329 iSelectionRectStart = null;
330 iSelectionRectEnd = null;
331 repaint();
332 return;
333 }
334
335 this.bbox = bbox;
336 iSelectionRectStart = new Coordinate(bbox.getMinLat(), bbox.getMinLon());
337 iSelectionRectEnd = new Coordinate(bbox.getMaxLat(), bbox.getMaxLon());
338
339 // calc the screen coordinates for the new selection rectangle
340 MapMarkerDot min = new MapMarkerDot(bbox.getMinLat(), bbox.getMinLon());
341 MapMarkerDot max = new MapMarkerDot(bbox.getMaxLat(), bbox.getMaxLon());
342
343 List<MapMarker> marker = new ArrayList<>(2);
344 marker.add(min);
345 marker.add(max);
346 setMapMarkerList(marker);
347 setDisplayToFitMapMarkers();
348 zoomOut();
349 repaint();
350 }
351
352 /**
353 * Enables or disables painting of the shrink/enlarge button
354 *
355 * @param visible {@code true} to enable painting of the shrink/enlarge button
356 */
357 public void setSizeButtonVisible(boolean visible) {
358 iSizeButton.setVisible(visible);
359 }
360
361 /**
362 * Refreshes the tile sources
363 * @since 6364
364 */
365 public final void refreshTileSources() {
366 final LinkedHashMap<String, TileSource> newTileSources = getAllTileSources();
367 final TileSource currentTileSource = this.getTileController().getTileSource();
368
369 // re-add the currently active TileSource to prevent inconsistent display of menu
370 newTileSources.putIfAbsent(currentTileSource.getId(), currentTileSource);
371
372 this.iSourceButton.setSources(new ArrayList<>(newTileSources.values()));
373 }
374
375 @Override
376 public void layerAdded(LayerAddEvent e) {
377 if (e.getAddedLayer() instanceof ImageryLayer) {
378 this.refreshTileSources();
379 }
380 }
381
382 @Override
383 public void layerRemoving(LayerRemoveEvent e) {
384 if (e.getRemovedLayer() instanceof ImageryLayer) {
385 this.refreshTileSources();
386 }
387 }
388
389 @Override
390 public void layerOrderChanged(LayerOrderChangeEvent e) {
391 // Do nothing
392 }
393
394 /**
395 * Returns the currently visible map area
396 * @return the currently visible map area
397 */
398 public Bounds getVisibleMapArea() {
399 final ICoordinate topLeft = getPosition(0, 0);
400 final ICoordinate bottomRight = getPosition(getWidth(), getHeight());
401 final Bounds bounds = new Bounds(topLeft.getLat(), topLeft.getLon(), false);
402 bounds.extend(bottomRight.getLat(), bottomRight.getLon());
403 return bounds;
404 }
405}
Note: See TracBrowser for help on using the repository browser.