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

Last change on this file since 2612 was 2512, checked in by stoecker, 14 years ago

i18n updated, fixed files to reduce problems when applying patches, fix #4017

  • Property svn:eol-style set to native
File size: 21.9 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 public int getLayerPos(Layer layer) {
326 int curLayerPos = layers.indexOf(layer);
327 if (curLayerPos == -1)
328 throw new IllegalArgumentException(tr("Layer not in list."));
329 return curLayerPos;
330 }
331
332 /**
333 * Creates a list of the visible layers in Z-Order, the layer with the lowest Z-Order
334 * first, layer with the highest Z-Order last.
335 *
336 * @return a list of the visible in Z-Order, the layer with the lowest Z-Order
337 * first, layer with the highest Z-Order last.
338 */
339 protected List<Layer> getVisibleLayersInZOrder() {
340 ArrayList<Layer> ret = new ArrayList<Layer>();
341 for (Layer l: layers) {
342 if (l.isVisible()) {
343 ret.add(l);
344 }
345 }
346 // sort according to position in the list of layers, with one exception:
347 // an active data layer always becomes a higher Z-Order than all other
348 // data layers
349 //
350 Collections.sort(
351 ret,
352 new Comparator<Layer>() {
353 public int compare(Layer l1, Layer l2) {
354 if (l1 instanceof OsmDataLayer && l2 instanceof OsmDataLayer) {
355 if (l1 == getActiveLayer()) return -1;
356 if (l2 == getActiveLayer()) return 1;
357 return new Integer(layers.indexOf(l1)).compareTo(layers.indexOf(l2));
358 } else
359 return new Integer(layers.indexOf(l1)).compareTo(layers.indexOf(l2));
360 }
361 }
362 );
363 Collections.reverse(ret);
364 return ret;
365 }
366
367 /**
368 * Draw the component.
369 */
370 @Override public void paint(Graphics g) {
371 if (center == null)
372 return; // no data loaded yet.
373
374 // re-create offscreen-buffer if we've been resized, otherwise
375 // just re-use it.
376 if (null == offscreenBuffer || offscreenBuffer.getWidth() != getWidth()
377 || offscreenBuffer.getHeight() != getHeight()) {
378 offscreenBuffer = new BufferedImage(getWidth(), getHeight(),
379 BufferedImage.TYPE_INT_ARGB);
380 }
381
382 Graphics2D tempG = offscreenBuffer.createGraphics();
383 tempG.setColor(Main.pref.getColor("background", Color.BLACK));
384 tempG.fillRect(0, 0, getWidth(), getHeight());
385
386 Bounds box = getLatLonBounds(g.getClipBounds());
387
388 for (Layer l: getVisibleLayersInZOrder()) {
389 l.paint(tempG, this, box);
390 }
391 for (MapViewPaintable mvp : temporaryLayers) {
392 mvp.paint(tempG, this, box);
393 }
394
395 // draw world borders
396 tempG.setColor(Color.WHITE);
397 Bounds b = getProjection().getWorldBoundsLatLon();
398 double lat = b.getMin().lat();
399 double lon = b.getMin().lon();
400
401 Point p = getPoint(b.getMin());
402
403 GeneralPath path = new GeneralPath();
404
405 path.moveTo(p.x, p.y);
406 double max = b.getMax().lat();
407 for(; lat <= max; lat += 1.0)
408 {
409 p = getPoint(new LatLon(lat >= max ? max : lat, lon));
410 path.lineTo(p.x, p.y);
411 }
412 lat = max; max = b.getMax().lon();
413 for(; lon <= max; lon += 1.0)
414 {
415 p = getPoint(new LatLon(lat, lon >= max ? max : lon));
416 path.lineTo(p.x, p.y);
417 }
418 lon = max; max = b.getMin().lat();
419 for(; lat >= max; lat -= 1.0)
420 {
421 p = getPoint(new LatLon(lat <= max ? max : lat, lon));
422 path.lineTo(p.x, p.y);
423 }
424 lat = max; max = b.getMin().lon();
425 for(; lon >= max; lon -= 1.0)
426 {
427 p = getPoint(new LatLon(lat, lon <= max ? max : lon));
428 path.lineTo(p.x, p.y);
429 }
430
431 int w = offscreenBuffer.getWidth();
432 int h = offscreenBuffer.getHeight();
433
434 // Work around OpenJDK having problems when drawing out of bounds
435 final Area border = new Area(path);
436 // Make the viewport 1px larger in every direction to prevent an
437 // additional 1px border when zooming in
438 final Area viewport = new Area(new Rectangle(-1, -1, w + 2, h + 2));
439 border.intersect(viewport);
440 tempG.draw(border);
441
442 if (playHeadMarker != null) {
443 playHeadMarker.paint(tempG, this);
444 }
445
446 g.drawImage(offscreenBuffer, 0, 0, null);
447 super.paint(g);
448 }
449
450 /**
451 * Set the new dimension to the view.
452 */
453 public void recalculateCenterScale(BoundingXYVisitor box) {
454 if (box == null) {
455 box = new BoundingXYVisitor();
456 }
457 if (box.getBounds() == null) {
458 box.visit(getProjection().getWorldBoundsLatLon());
459 }
460 if (!box.hasExtend()) {
461 box.enlargeBoundingBox();
462 }
463
464 zoomTo(box.getBounds());
465 }
466
467 /**
468 * @return An unmodifiable collection of all layers
469 */
470 public Collection<Layer> getAllLayers() {
471 return Collections.unmodifiableCollection(layers);
472 }
473
474 /**
475 * @return An unmodifiable ordered list of all layers
476 */
477 public List<Layer> getAllLayersAsList() {
478 return Collections.unmodifiableList(layers);
479 }
480
481 /**
482 * Replies an unmodifiable list of layers of a certain type.
483 *
484 * Example:
485 * <pre>
486 * List<WMSLayer> wmsLayers = getLayersOfType(WMSLayer.class);
487 * </pre>
488 *
489 * @return an unmodifiable list of layers of a certain type.
490 */
491 public <T> List<T> getLayersOfType(Class<T> ofType) {
492 ArrayList<T> ret = new ArrayList<T>();
493 for (Layer layer : getAllLayersAsList()) {
494 if (ofType.isInstance(layer)) {
495 ret.add(ofType.cast(layer));
496 }
497 }
498 return ret;
499 }
500
501 /**
502 * Replies the number of layers managed by this mav view
503 *
504 * @return the number of layers managed by this mav view
505 */
506 public int getNumLayers() {
507 return layers.size();
508 }
509
510 /**
511 * Replies true if there is at least one layer in this map view
512 *
513 * @return true if there is at least one layer in this map view
514 */
515 public boolean hasLayers() {
516 return getNumLayers() > 0;
517 }
518
519 /**
520 * Sets the active layer to <code>layer</code>. If <code>layer</code> is an instance
521 * of {@see OsmDataLayer} also sets {@see #editLayer} to <code>layer</code>.
522 *
523 * @param layer the layer to be activate; must be one of the layers in the list of layers
524 * @exception IllegalArgumentException thrown if layer is not in the lis of layers
525 */
526 public void setActiveLayer(Layer layer) {
527 if (!layers.contains(layer))
528 throw new IllegalArgumentException(tr("Layer ''{0}'' must be in list of layers", layer.toString()));
529 if (! (layer instanceof OsmDataLayer)) {
530 if (getCurrentDataSet() != null) {
531 getCurrentDataSet().setSelected();
532 }
533 }
534 Layer old = activeLayer;
535 activeLayer = layer;
536 if (old != layer) {
537 for (Layer.LayerChangeListener l : Layer.listeners) {
538 l.activeLayerChange(old, layer);
539 }
540 }
541 if (layer instanceof OsmDataLayer) {
542 refreshTitle((OsmDataLayer)layer);
543 }
544
545 /* This only makes the buttons look disabled. Disabling the actions as well requires
546 * the user to re-select the tool after i.e. moving a layer. While testing I found
547 * that I switch layers and actions at the same time and it was annoying to mind the
548 * order. This way it works as visual clue for new users */
549 for (Enumeration<AbstractButton> e = Main.map.toolGroup.getElements() ; e.hasMoreElements() ;) {
550 AbstractButton x=e.nextElement();
551 x.setEnabled(((MapMode)x.getAction()).layerIsSupported(layer));
552 }
553 AudioPlayer.reset();
554 repaint();
555 }
556
557 /**
558 * Replies the currently active layer
559 *
560 * @return the currently active layer (may be null)
561 */
562 public Layer getActiveLayer() {
563 return activeLayer;
564 }
565
566 /**
567 * Replies the current edit layer, if any
568 *
569 * @return the current edit layer. May be null.
570 */
571 public OsmDataLayer getEditLayer() {
572 if (activeLayer instanceof OsmDataLayer)
573 return (OsmDataLayer)activeLayer;
574
575 // the first OsmDataLayer is the edit layer
576 //
577 for (Layer layer : layers) {
578 if (layer instanceof OsmDataLayer)
579 return (OsmDataLayer)layer;
580 }
581 return null;
582 }
583
584 /**
585 * replies true if the list of layers managed by this map view contain layer
586 *
587 * @param layer the layer
588 * @return true if the list of layers managed by this map view contain layer
589 */
590 public boolean hasLayer(Layer layer) {
591 return layers.contains(layer);
592 }
593
594 /**
595 * Tries to zoom to the download boundingbox[es] of the current edit layer
596 * (aka {@link OsmDataLayer}). If the edit layer has multiple download bounding
597 * boxes it zooms to a large virtual bounding box containing all smaller ones.
598 * This implementation can be used for resolving ticket #1461.
599 *
600 * @return <code>true</code> if a zoom operation has been performed
601 */
602 public boolean zoomToEditLayerBoundingBox() {
603 // workaround for #1461 (zoom to download bounding box instead of all data)
604 // In case we already have an existing data layer ...
605 OsmDataLayer layer= getEditLayer();
606 if (layer == null)
607 return false;
608 Collection<DataSource> dataSources = layer.data.dataSources;
609 // ... with bounding box[es] of data loaded from OSM or a file...
610 BoundingXYVisitor bbox = new BoundingXYVisitor();
611 for (DataSource ds : dataSources) {
612 bbox.visit(ds.bounds);
613 if (bbox.hasExtend()) {
614 // ... we zoom to it's bounding box
615 recalculateCenterScale(bbox);
616 return true;
617 }
618 }
619 return false;
620 }
621
622 public boolean addTemporaryLayer(MapViewPaintable mvp) {
623 if (temporaryLayers.contains(mvp)) return false;
624 return temporaryLayers.add(mvp);
625 }
626
627 public boolean removeTemporaryLayer(MapViewPaintable mvp) {
628 return temporaryLayers.remove(mvp);
629 }
630
631 public void propertyChange(PropertyChangeEvent evt) {
632 if (evt.getPropertyName().equals(Layer.VISIBLE_PROP)) {
633 repaint();
634 } else if (evt.getPropertyName().equals(OsmDataLayer.REQUIRES_SAVE_TO_DISK_PROP)
635 || evt.getPropertyName().equals(OsmDataLayer.REQUIRES_UPLOAD_TO_SERVER_PROP)) {
636 OsmDataLayer layer = (OsmDataLayer)evt.getSource();
637 if (layer == getEditLayer()) {
638 refreshTitle(layer);
639 }
640 }
641 }
642
643 protected void refreshTitle(OsmDataLayer layer) {
644 boolean dirty = layer.requiresSaveToFile() || layer.requiresUploadToServer();
645 if (dirty) {
646 JOptionPane.getFrameForComponent(Main.parent).setTitle("* " + tr("Java OpenStreetMap Editor"));
647 } else {
648 JOptionPane.getFrameForComponent(Main.parent).setTitle(tr("Java OpenStreetMap Editor"));
649 }
650 }
651}
Note: See TracBrowser for help on using the repository browser.