source: josm/trunk/src/org/openstreetmap/josm/gui/MapView.java@ 2446

Last change on this file since 2446 was 2446, checked in by Gubaer, 14 years ago

fixed #3839: Layer order: gpx defaults above data-layer
A new GPX-Layer is now created below the lowest data layer

  • Property svn:eol-style set to native
File size: 21.8 KB
Line 
1// License: GPL. See LICENSE file for details.
2
3package org.openstreetmap.josm.gui;
4
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.Color;
8import java.awt.Graphics;
9import java.awt.Graphics2D;
10import java.awt.Point;
11import java.awt.Rectangle;
12import java.awt.event.ComponentAdapter;
13import java.awt.event.ComponentEvent;
14import java.awt.event.MouseEvent;
15import java.awt.event.MouseMotionListener;
16import java.awt.geom.Area;
17import java.awt.geom.GeneralPath;
18import java.awt.image.BufferedImage;
19import java.beans.PropertyChangeEvent;
20import java.beans.PropertyChangeListener;
21import java.util.ArrayList;
22import java.util.Collection;
23import java.util.Collections;
24import java.util.Comparator;
25import java.util.Enumeration;
26import java.util.LinkedList;
27import java.util.List;
28
29import javax.swing.AbstractButton;
30import javax.swing.JComponent;
31import javax.swing.JOptionPane;
32
33import org.openstreetmap.josm.Main;
34import org.openstreetmap.josm.actions.AutoScaleAction;
35import org.openstreetmap.josm.actions.JosmAction;
36import org.openstreetmap.josm.actions.MoveAction;
37import org.openstreetmap.josm.actions.mapmode.MapMode;
38import org.openstreetmap.josm.data.Bounds;
39import org.openstreetmap.josm.data.SelectionChangedListener;
40import org.openstreetmap.josm.data.coor.LatLon;
41import org.openstreetmap.josm.data.osm.DataSet;
42import org.openstreetmap.josm.data.osm.DataSource;
43import org.openstreetmap.josm.data.osm.OsmPrimitive;
44import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
45import org.openstreetmap.josm.gui.layer.GpxLayer;
46import org.openstreetmap.josm.gui.layer.Layer;
47import org.openstreetmap.josm.gui.layer.MapViewPaintable;
48import org.openstreetmap.josm.gui.layer.OsmDataLayer;
49import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
50import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker;
51import org.openstreetmap.josm.tools.AudioPlayer;
52
53/**
54 * This is a component used in the MapFrame for browsing the map. It use is to
55 * provide the MapMode's enough capabilities to operate.
56 *
57 * MapView hold meta-data about the data set currently displayed, as scale level,
58 * center point viewed, what scrolling mode or editing mode is selected or with
59 * what projection the map is viewed etc..
60 *
61 * MapView is able to administrate several layers.
62 *
63 * @author imi
64 */
65public class MapView extends NavigatableComponent implements PropertyChangeListener {
66
67 /**
68 * A list of all layers currently loaded.
69 */
70 private ArrayList<Layer> layers = new ArrayList<Layer>();
71 /**
72 * The play head marker: there is only one of these so it isn't in any specific layer
73 */
74 public PlayHeadMarker playHeadMarker = null;
75
76 /**
77 * The layer from the layers list that is currently active.
78 */
79 private Layer activeLayer;
80
81 /**
82 * The last event performed by mouse.
83 */
84 public MouseEvent lastMEvent;
85
86 private LinkedList<MapViewPaintable> temporaryLayers = new LinkedList<MapViewPaintable>();
87
88 private BufferedImage offscreenBuffer;
89
90 public MapView() {
91 addComponentListener(new ComponentAdapter(){
92 @Override public void componentResized(ComponentEvent e) {
93 removeComponentListener(this);
94
95 MapSlider zoomSlider = new MapSlider(MapView.this);
96 add(zoomSlider);
97 zoomSlider.setBounds(3, 0, 114, 30);
98
99 MapScaler scaler = new MapScaler(MapView.this);
100 add(scaler);
101 scaler.setLocation(10,30);
102
103 if (!zoomToEditLayerBoundingBox()) {
104 new AutoScaleAction("data").actionPerformed(null);
105 }
106
107 new MapMover(MapView.this, Main.contentPane);
108 JosmAction mv;
109 mv = new MoveAction(MoveAction.Direction.UP);
110 if (mv.getShortcut() != null) {
111 Main.contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(mv.getShortcut().getKeyStroke(), "UP");
112 Main.contentPane.getActionMap().put("UP", mv);
113 }
114 mv = new MoveAction(MoveAction.Direction.DOWN);
115 if (mv.getShortcut() != null) {
116 Main.contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(mv.getShortcut().getKeyStroke(), "DOWN");
117 Main.contentPane.getActionMap().put("DOWN", mv);
118 }
119 mv = new MoveAction(MoveAction.Direction.LEFT);
120 if (mv.getShortcut() != null) {
121 Main.contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(mv.getShortcut().getKeyStroke(), "LEFT");
122 Main.contentPane.getActionMap().put("LEFT", mv);
123 }
124 mv = new MoveAction(MoveAction.Direction.RIGHT);
125 if (mv.getShortcut() != null) {
126 Main.contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(mv.getShortcut().getKeyStroke(), "RIGHT");
127 Main.contentPane.getActionMap().put("RIGHT", mv);
128 }
129 }
130 });
131
132 // listend to selection changes to redraw the map
133 DataSet.selListeners.add(new SelectionChangedListener(){
134 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
135 repaint();
136 }
137 });
138
139 //store the last mouse action
140 this.addMouseMotionListener(new MouseMotionListener() {
141 public void mouseDragged(MouseEvent e) {
142 mouseMoved(e);
143 }
144 public void mouseMoved(MouseEvent e) {
145 lastMEvent = e;
146 }
147 });
148 }
149
150 /**
151 * Adds a GPX layer. A GPX layer is added below the lowest data layer.
152 *
153 * @param layer the GPX layer
154 */
155 protected void addGpxLayer(GpxLayer layer) {
156 if (layers.isEmpty()) {
157 layers.add(layer);
158 return;
159 }
160 for (int i=layers.size()-1; i>= 0; i--) {
161 if (layers.get(i) instanceof OsmDataLayer) {
162 if (i == layers.size()-1) {
163 layers.add(layer);
164 } else {
165 layers.add(i+1, layer);
166 }
167 return;
168 }
169 }
170 layers.add(layer);
171 }
172
173 /**
174 * Add a layer to the current MapView. The layer will be added at topmost
175 * position.
176 */
177 public void addLayer(Layer layer) {
178 if (layer instanceof MarkerLayer && playHeadMarker == null) {
179 playHeadMarker = PlayHeadMarker.create();
180 }
181
182 if (layer instanceof GpxLayer) {
183 addGpxLayer((GpxLayer)layer);
184 } else if (layer.isBackgroundLayer() || layers.isEmpty()) {
185 layers.add(layer);
186 } else {
187 layers.add(0, layer);
188 }
189
190 for (Layer.LayerChangeListener l : Layer.listeners) {
191 l.layerAdded(layer);
192 }
193 if (layer instanceof OsmDataLayer || activeLayer == null) {
194 // autoselect the new layer
195 Layer old = activeLayer;
196 setActiveLayer(layer);
197 for (Layer.LayerChangeListener l : Layer.listeners) {
198 l.activeLayerChange(old, layer);
199 }
200 }
201 layer.addPropertyChangeListener(this);
202 AudioPlayer.reset();
203 repaint();
204 }
205
206 @Override
207 protected DataSet getCurrentDataSet() {
208 if(activeLayer != null && activeLayer instanceof OsmDataLayer)
209 return ((OsmDataLayer)activeLayer).data;
210 return null;
211 }
212
213 /**
214 * Replies true if the active layer is drawable.
215 *
216 * @return true if the active layer is drawable, false otherwise
217 */
218 public boolean isActiveLayerDrawable() {
219 return activeLayer != null && activeLayer instanceof OsmDataLayer;
220 }
221
222 /**
223 * Replies true if the active layer is visible.
224 *
225 * @return true if the active layer is visible, false otherwise
226 */
227 public boolean isActiveLayerVisible() {
228 return isActiveLayerDrawable() && activeLayer.isVisible();
229 }
230
231 protected void fireActiveLayerChanged(Layer oldLayer, Layer newLayer) {
232 for (Layer.LayerChangeListener l : Layer.listeners) {
233 l.activeLayerChange(oldLayer, newLayer);
234 }
235 }
236
237 /**
238 * Determines the next active data layer according to the following
239 * rules:
240 * <ul>
241 * <li>if there is at least one {@see OsmDataLayer} the first one
242 * becomes active</li>
243 * <li>otherwise, the top most layer of any type becomes active</li>
244 * </ul>
245 *
246 * @return the next active data layer
247 */
248 protected Layer determineNextActiveLayer() {
249 if (layers.isEmpty()) return null;
250 // if possible, activate the first data layer
251 //
252 List<OsmDataLayer> dataLayers = getLayersOfType(OsmDataLayer.class);
253 if (!dataLayers.isEmpty())
254 return dataLayers.get(0);
255
256 // else the first layer of any type
257 //
258 return layers.get(0);
259 }
260
261 /**
262 * Remove the layer from the mapview. If the layer was in the list before,
263 * an LayerChange event is fired.
264 */
265 public void removeLayer(Layer layer) {
266 boolean deletedLayerWasActiveLayer = false;
267
268 if (layer == activeLayer) {
269 activeLayer = null;
270 deletedLayerWasActiveLayer = true;
271 fireActiveLayerChanged(layer, null);
272 }
273 if (layers.remove(layer)) {
274 for (Layer.LayerChangeListener l : Layer.listeners) {
275 l.layerRemoved(layer);
276 }
277 }
278 layer.removePropertyChangeListener(this);
279 layer.destroy();
280 AudioPlayer.reset();
281 if (deletedLayerWasActiveLayer) {
282 Layer l = determineNextActiveLayer();
283 if (l != null) {
284 activeLayer = l;
285 fireActiveLayerChanged(null, l);
286 }
287 }
288 repaint();
289 }
290
291 private boolean virtualNodesEnabled = false;
292 public void setVirtualNodesEnabled(boolean enabled) {
293 if(virtualNodesEnabled != enabled) {
294 virtualNodesEnabled = enabled;
295 repaint();
296 }
297 }
298 public boolean isVirtualNodesEnabled() {
299 return virtualNodesEnabled;
300 }
301
302 /**
303 * Moves the layer to the given new position. No event is fired, but repaints
304 * according to the new Z-Order of the layers.
305 *
306 * @param layer The layer to move
307 * @param pos The new position of the layer
308 */
309 public void moveLayer(Layer layer, int pos) {
310 int curLayerPos = layers.indexOf(layer);
311 if (curLayerPos == -1)
312 throw new IllegalArgumentException(tr("Layer not in list."));
313 if (pos == curLayerPos)
314 return; // already in place.
315 layers.remove(curLayerPos);
316 if (pos >= layers.size()) {
317 layers.add(layer);
318 } else {
319 layers.add(pos, layer);
320 }
321 AudioPlayer.reset();
322 repaint();
323 }
324
325
326 public int getLayerPos(Layer layer) {
327 int curLayerPos = layers.indexOf(layer);
328 if (curLayerPos == -1)
329 throw new IllegalArgumentException(tr("Layer not in list."));
330 return curLayerPos;
331 }
332
333 /**
334 * Creates a list of the visible layers in Z-Order, the layer with the lowest Z-Order
335 * first, layer with the highest Z-Order last.
336 *
337 * @return a list of the visible in Z-Order, the layer with the lowest Z-Order
338 * first, layer with the highest Z-Order last.
339 */
340 protected List<Layer> getVisibleLayersInZOrder() {
341 ArrayList<Layer> ret = new ArrayList<Layer>();
342 for (Layer l: layers) {
343 if (l.isVisible()) {
344 ret.add(l);
345 }
346 }
347 // sort according to position in the list of layers, with one exception:
348 // an active data layer always becomes a higher Z-Order than all other
349 // data layers
350 //
351 Collections.sort(
352 ret,
353 new Comparator<Layer>() {
354 public int compare(Layer l1, Layer l2) {
355 if (l1 instanceof OsmDataLayer && l2 instanceof OsmDataLayer) {
356 if (l1 == getActiveLayer()) return -1;
357 if (l2 == getActiveLayer()) return 1;
358 return new Integer(layers.indexOf(l1)).compareTo(layers.indexOf(l2));
359 } else
360 return new Integer(layers.indexOf(l1)).compareTo(layers.indexOf(l2));
361 }
362 }
363 );
364 Collections.reverse(ret);
365 return ret;
366 }
367
368 /**
369 * Draw the component.
370 */
371 @Override public void paint(Graphics g) {
372 if (center == null)
373 return; // no data loaded yet.
374
375 // re-create offscreen-buffer if we've been resized, otherwise
376 // just re-use it.
377 if (null == offscreenBuffer || offscreenBuffer.getWidth() != getWidth()
378 || offscreenBuffer.getHeight() != getHeight()) {
379 offscreenBuffer = new BufferedImage(getWidth(), getHeight(),
380 BufferedImage.TYPE_INT_ARGB);
381 }
382
383 Graphics2D tempG = offscreenBuffer.createGraphics();
384 tempG.setColor(Main.pref.getColor("background", Color.BLACK));
385 tempG.fillRect(0, 0, getWidth(), getHeight());
386
387 for (Layer l: getVisibleLayersInZOrder()) {
388 l.paint(tempG, this);
389 }
390 for (MapViewPaintable mvp : temporaryLayers) {
391 mvp.paint(tempG, this);
392 }
393
394 // draw world borders
395 tempG.setColor(Color.WHITE);
396 Bounds b = getProjection().getWorldBoundsLatLon();
397 double lat = b.getMin().lat();
398 double lon = b.getMin().lon();
399
400 Point p = getPoint(b.getMin());
401
402 GeneralPath path = new GeneralPath();
403
404 path.moveTo(p.x, p.y);
405 double max = b.getMax().lat();
406 for(; lat <= max; lat += 1.0)
407 {
408 p = getPoint(new LatLon(lat >= max ? max : lat, lon));
409 path.lineTo(p.x, p.y);
410 }
411 lat = max; max = b.getMax().lon();
412 for(; lon <= max; lon += 1.0)
413 {
414 p = getPoint(new LatLon(lat, lon >= max ? max : lon));
415 path.lineTo(p.x, p.y);
416 }
417 lon = max; max = b.getMin().lat();
418 for(; lat >= max; lat -= 1.0)
419 {
420 p = getPoint(new LatLon(lat <= max ? max : lat, lon));
421 path.lineTo(p.x, p.y);
422 }
423 lat = max; max = b.getMin().lon();
424 for(; lon >= max; lon -= 1.0)
425 {
426 p = getPoint(new LatLon(lat, lon <= max ? max : lon));
427 path.lineTo(p.x, p.y);
428 }
429
430 int w = offscreenBuffer.getWidth();
431 int h = offscreenBuffer.getHeight();
432
433 // Work around OpenJDK having problems when drawing out of bounds
434 final Area border = new Area(path);
435 // Make the viewport 1px larger in every direction to prevent an
436 // additional 1px border when zooming in
437 final Area viewport = new Area(new Rectangle(-1, -1, w + 2, h + 2));
438 border.intersect(viewport);
439 tempG.draw(border);
440
441 if (playHeadMarker != null) {
442 playHeadMarker.paint(tempG, this);
443 }
444
445 g.drawImage(offscreenBuffer, 0, 0, null);
446 super.paint(g);
447 }
448
449 /**
450 * Set the new dimension to the view.
451 */
452 public void recalculateCenterScale(BoundingXYVisitor box) {
453 if (box == null) {
454 box = new BoundingXYVisitor();
455 }
456 if (box.getBounds() == null) {
457 box.visit(getProjection().getWorldBoundsLatLon());
458 }
459 if (!box.hasExtend()) {
460 box.enlargeBoundingBox();
461 }
462
463 zoomTo(box.getBounds());
464 }
465
466 /**
467 * @return An unmodifiable collection of all layers
468 */
469 public Collection<Layer> getAllLayers() {
470 return Collections.unmodifiableCollection(layers);
471 }
472
473 /**
474 * @return An unmodifiable ordered list of all layers
475 */
476 public List<Layer> getAllLayersAsList() {
477 return Collections.unmodifiableList(layers);
478 }
479
480 /**
481 * Replies an unmodifiable list of layers of a certain type.
482 *
483 * Example:
484 * <pre>
485 * List<WMSLayer> wmsLayers = getLayersOfType(WMSLayer.class);
486 * </pre>
487 *
488 * @return an unmodifiable list of layers of a certain type.
489 */
490 public <T> List<T> getLayersOfType(Class<T> ofType) {
491 ArrayList<T> ret = new ArrayList<T>();
492 for (Layer layer : getAllLayersAsList()) {
493 if (ofType.isInstance(layer)) {
494 ret.add(ofType.cast(layer));
495 }
496 }
497 return ret;
498 }
499
500 /**
501 * Replies the number of layers managed by this mav view
502 *
503 * @return the number of layers managed by this mav view
504 */
505 public int getNumLayers() {
506 return layers.size();
507 }
508
509 /**
510 * Replies true if there is at least one layer in this map view
511 *
512 * @return true if there is at least one layer in this map view
513 */
514 public boolean hasLayers() {
515 return getNumLayers() > 0;
516 }
517
518 /**
519 * Sets the active layer to <code>layer</code>. If <code>layer</code> is an instance
520 * of {@see OsmDataLayer} also sets {@see #editLayer} to <code>layer</code>.
521 *
522 * @param layer the layer to be activate; must be one of the layers in the list of layers
523 * @exception IllegalArgumentException thrown if layer is not in the lis of layers
524 */
525 public void setActiveLayer(Layer layer) {
526 if (!layers.contains(layer))
527 throw new IllegalArgumentException(tr("Layer ''{0}'' must be in list of layers", layer.toString()));
528 if (! (layer instanceof OsmDataLayer)) {
529 if (getCurrentDataSet() != null) {
530 getCurrentDataSet().setSelected();
531 }
532 }
533 Layer old = activeLayer;
534 activeLayer = layer;
535 if (old != layer) {
536 for (Layer.LayerChangeListener l : Layer.listeners) {
537 l.activeLayerChange(old, layer);
538 }
539 }
540 if (layer instanceof OsmDataLayer) {
541 refreshTitle((OsmDataLayer)layer);
542 }
543
544 /* This only makes the buttons look disabled. Disabling the actions as well requires
545 * the user to re-select the tool after i.e. moving a layer. While testing I found
546 * that I switch layers and actions at the same time and it was annoying to mind the
547 * order. This way it works as visual clue for new users */
548 for (Enumeration<AbstractButton> e = Main.map.toolGroup.getElements() ; e.hasMoreElements() ;) {
549 AbstractButton x=e.nextElement();
550 x.setEnabled(((MapMode)x.getAction()).layerIsSupported(layer));
551 }
552 AudioPlayer.reset();
553 repaint();
554 }
555
556 /**
557 * Replies the currently active layer
558 *
559 * @return the currently active layer (may be null)
560 */
561 public Layer getActiveLayer() {
562 return activeLayer;
563 }
564
565 /**
566 * Replies the current edit layer, if any
567 *
568 * @return the current edit layer. May be null.
569 */
570 public OsmDataLayer getEditLayer() {
571 if (activeLayer instanceof OsmDataLayer)
572 return (OsmDataLayer)activeLayer;
573
574 // the first OsmDataLayer is the edit layer
575 //
576 for (Layer layer : layers) {
577 if (layer instanceof OsmDataLayer)
578 return (OsmDataLayer)layer;
579 }
580 return null;
581 }
582
583 /**
584 * replies true if the list of layers managed by this map view contain layer
585 *
586 * @param layer the layer
587 * @return true if the list of layers managed by this map view contain layer
588 */
589 public boolean hasLayer(Layer layer) {
590 return layers.contains(layer);
591 }
592
593 /**
594 * Tries to zoom to the download boundingbox[es] of the current edit layer
595 * (aka {@link OsmDataLayer}). If the edit layer has multiple download bounding
596 * boxes it zooms to a large virtual bounding box containing all smaller ones.
597 * This implementation can be used for resolving ticket #1461.
598 *
599 * @return <code>true</code> if a zoom operation has been performed
600 */
601 public boolean zoomToEditLayerBoundingBox() {
602 // workaround for #1461 (zoom to download bounding box instead of all data)
603 // In case we already have an existing data layer ...
604 OsmDataLayer layer= getEditLayer();
605 if (layer == null)
606 return false;
607 Collection<DataSource> dataSources = layer.data.dataSources;
608 // ... with bounding box[es] of data loaded from OSM or a file...
609 BoundingXYVisitor bbox = new BoundingXYVisitor();
610 for (DataSource ds : dataSources) {
611 bbox.visit(ds.bounds);
612 if (bbox.hasExtend()) {
613 // ... we zoom to it's bounding box
614 recalculateCenterScale(bbox);
615 return true;
616 }
617 }
618 return false;
619 }
620
621 public boolean addTemporaryLayer(MapViewPaintable mvp) {
622 if (temporaryLayers.contains(mvp)) return false;
623 return temporaryLayers.add(mvp);
624 }
625
626 public boolean removeTemporaryLayer(MapViewPaintable mvp) {
627 return temporaryLayers.remove(mvp);
628 }
629
630 public void propertyChange(PropertyChangeEvent evt) {
631 if (evt.getPropertyName().equals(Layer.VISIBLE_PROP)) {
632 repaint();
633 } else if (evt.getPropertyName().equals(OsmDataLayer.REQUIRES_SAVE_TO_DISK_PROP)
634 || evt.getPropertyName().equals(OsmDataLayer.REQUIRES_UPLOAD_TO_SERVER_PROP)) {
635 OsmDataLayer layer = (OsmDataLayer)evt.getSource();
636 if (layer == getEditLayer()) {
637 refreshTitle(layer);
638 }
639 }
640 }
641
642 protected void refreshTitle(OsmDataLayer layer) {
643 boolean dirty = layer.requiresSaveToFile() || layer.requiresUploadToServer();
644 if (dirty) {
645 JOptionPane.getFrameForComponent(Main.parent).setTitle("* " + tr("Java OpenStreetMap Editor"));
646 } else {
647 JOptionPane.getFrameForComponent(Main.parent).setTitle(tr("Java OpenStreetMap Editor"));
648 }
649 }
650}
Note: See TracBrowser for help on using the repository browser.