source: josm/trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java@ 1823

Last change on this file since 1823 was 1823, checked in by stoecker, 15 years ago

some projection and zoom cleanups - projection classes still need better handling of outside-world coordinates

  • Property svn:eol-style set to native
File size: 19.3 KB
Line 
1// License: GPL. See LICENSE file for details.
2
3package org.openstreetmap.josm.gui.layer;
4
5import static org.openstreetmap.josm.tools.I18n.marktr;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.AlphaComposite;
10import java.awt.Color;
11import java.awt.Component;
12import java.awt.Composite;
13import java.awt.Graphics;
14import java.awt.Graphics2D;
15import java.awt.GridBagLayout;
16import java.awt.Point;
17import java.awt.Rectangle;
18import java.awt.TexturePaint;
19import java.awt.event.ActionEvent;
20import java.awt.event.KeyEvent;
21import java.awt.geom.Area;
22import java.awt.image.BufferedImage;
23import java.io.File;
24import java.util.ArrayList;
25import java.util.Collection;
26import java.util.HashSet;
27import java.util.Iterator;
28import java.util.LinkedList;
29import java.util.Set;
30
31import javax.swing.AbstractAction;
32import javax.swing.Icon;
33import javax.swing.JLabel;
34import javax.swing.JMenuItem;
35import javax.swing.JOptionPane;
36import javax.swing.JPanel;
37import javax.swing.JSeparator;
38
39import org.openstreetmap.josm.Main;
40import org.openstreetmap.josm.actions.GpxExportAction;
41import org.openstreetmap.josm.actions.RenameLayerAction;
42import org.openstreetmap.josm.actions.SaveAction;
43import org.openstreetmap.josm.actions.SaveAsAction;
44import org.openstreetmap.josm.data.conflict.Conflict;
45import org.openstreetmap.josm.data.conflict.ConflictCollection;
46import org.openstreetmap.josm.data.coor.EastNorth;
47import org.openstreetmap.josm.data.coor.LatLon;
48import org.openstreetmap.josm.data.gpx.GpxData;
49import org.openstreetmap.josm.data.gpx.GpxTrack;
50import org.openstreetmap.josm.data.gpx.WayPoint;
51import org.openstreetmap.josm.data.osm.DataSet;
52import org.openstreetmap.josm.data.osm.DataSource;
53import org.openstreetmap.josm.data.osm.Node;
54import org.openstreetmap.josm.data.osm.OsmPrimitive;
55import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
56import org.openstreetmap.josm.data.osm.Relation;
57import org.openstreetmap.josm.data.osm.Way;
58import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
59import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
60import org.openstreetmap.josm.data.osm.visitor.MapPaintVisitor;
61import org.openstreetmap.josm.data.osm.visitor.MergeVisitor;
62import org.openstreetmap.josm.data.osm.visitor.SimplePaintVisitor;
63import org.openstreetmap.josm.gui.MapView;
64import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
65import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
66import org.openstreetmap.josm.tools.DateUtils;
67import org.openstreetmap.josm.tools.GBC;
68import org.openstreetmap.josm.tools.ImageProvider;
69import org.openstreetmap.josm.tools.Shortcut;
70
71/**
72 * A layer holding data from a specific dataset.
73 * The data can be fully edited.
74 *
75 * @author imi
76 */
77public class OsmDataLayer extends Layer {
78
79 /** the global counter for created data layers */
80 static private int dataLayerCounter = 0;
81
82 /**
83 * Replies a new unique name for a data layer
84 *
85 * @return a new unique name for a data layer
86 */
87 static public String createNewName() {
88 dataLayerCounter++;
89 return tr("Data Layer {0}", dataLayerCounter);
90 }
91
92 public final static class DataCountVisitor extends AbstractVisitor {
93 public final int[] normal = new int[3];
94 public final int[] deleted = new int[3];
95 public final String[] names = {
96 OsmPrimitiveType.NODE.getAPIName(),
97 OsmPrimitiveType.WAY.getAPIName(),
98 OsmPrimitiveType.RELATION.getAPIName()
99 };
100
101 private void inc(final OsmPrimitive osm, final int i) {
102 normal[i]++;
103 if (osm.deleted) {
104 deleted[i]++;
105 }
106 }
107
108 public void visit(final Node n) {
109 inc(n, 0);
110 }
111
112 public void visit(final Way w) {
113 inc(w, 1);
114 }
115 public void visit(final Relation w) {
116 inc(w, 2);
117 }
118 }
119
120 public interface ModifiedChangedListener {
121 void modifiedChanged(boolean value, OsmDataLayer source);
122 }
123 public interface CommandQueueListener {
124 void commandChanged(int queueSize, int redoSize);
125 }
126
127 /**
128 * The data behind this layer.
129 */
130 public final DataSet data;
131
132 /**
133 * the collection of conflicts detected in this layer
134 */
135 private ConflictCollection conflicts;
136
137 /**
138 * Whether the data of this layer was modified during the session.
139 */
140 private boolean modified = false;
141 /**
142 * Whether the data was modified due an upload of the data to the server.
143 */
144 public boolean uploadedModified = false;
145
146 public final LinkedList<ModifiedChangedListener> listenerModified = new LinkedList<ModifiedChangedListener>();
147 public final LinkedList<DataChangeListener> listenerDataChanged = new LinkedList<DataChangeListener>();
148
149 /**
150 * a paint texture for non-downloaded area
151 */
152 private static TexturePaint hatched;
153
154 static {
155 createHatchTexture();
156 }
157
158 /**
159 * Initialize the hatch pattern used to paint the non-downloaded area
160 */
161 public static void createHatchTexture() {
162 BufferedImage bi = new BufferedImage(15, 15, BufferedImage.TYPE_INT_ARGB);
163 Graphics2D big = bi.createGraphics();
164 big.setColor(Main.pref.getColor(marktr("background"), Color.BLACK));
165 Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
166 big.setComposite(comp);
167 big.fillRect(0,0,15,15);
168 big.setColor(Main.pref.getColor(marktr("outside downloaded area"), Color.YELLOW));
169 big.drawLine(0,15,15,0);
170 Rectangle r = new Rectangle(0, 0, 15,15);
171 hatched = new TexturePaint(bi, r);
172 }
173
174 /**
175 * Construct a OsmDataLayer.
176 */
177 public OsmDataLayer(final DataSet data, final String name, final File associatedFile) {
178 super(name);
179 this.data = data;
180 this.setAssociatedFile(associatedFile);
181 conflicts = new ConflictCollection();
182 }
183
184 /**
185 * TODO: @return Return a dynamic drawn icon of the map data. The icon is
186 * updated by a background thread to not disturb the running programm.
187 */
188 @Override public Icon getIcon() {
189 return ImageProvider.get("layer", "osmdata_small");
190 }
191
192 /**
193 * Draw all primitives in this layer but do not draw modified ones (they
194 * are drawn by the edit layer).
195 * Draw nodes last to overlap the ways they belong to.
196 */
197 @Override public void paint(final Graphics g, final MapView mv) {
198 boolean active = mv.getActiveLayer() == this;
199 boolean inactive = !active && Main.pref.getBoolean("draw.data.inactive_color", true);
200 boolean virtual = !inactive && mv.isVirtualNodesEnabled();
201
202 // draw the hatched area for non-downloaded region. only draw if we're the active
203 // and bounds are defined; don't draw for inactive layers or loaded GPX files etc
204 if (active && Main.pref.getBoolean("draw.data.downloaded_area", true) && !data.dataSources.isEmpty()) {
205 // initialize area with current viewport
206 Rectangle b = mv.getBounds();
207 // on some platforms viewport bounds seem to be offset from the left,
208 // over-grow it just to be sure
209 b.grow(100, 100);
210 Area a = new Area(b);
211
212 // now succesively subtract downloaded areas
213 for (DataSource src : data.dataSources) {
214 if (src.bounds != null && !src.bounds.min.equals(src.bounds.max)) {
215 EastNorth en1 = mv.getProjection().latlon2eastNorth(src.bounds.min);
216 EastNorth en2 = mv.getProjection().latlon2eastNorth(src.bounds.max);
217 Point p1 = mv.getPoint(en1);
218 Point p2 = mv.getPoint(en2);
219 Rectangle r = new Rectangle(Math.min(p1.x, p2.x),Math.min(p1.y, p2.y),Math.abs(p2.x-p1.x),Math.abs(p2.y-p1.y));
220 a.subtract(new Area(r));
221 }
222 }
223
224 // paint remainder
225 ((Graphics2D)g).setPaint(hatched);
226 ((Graphics2D)g).fill(a);
227 }
228
229 SimplePaintVisitor painter;
230 if (Main.pref.getBoolean("draw.wireframe")) {
231 painter = new SimplePaintVisitor();
232 } else {
233 painter = new MapPaintVisitor();
234 }
235 painter.setGraphics(g);
236 painter.setNavigatableComponent(mv);
237 painter.inactive = inactive;
238 painter.visitAll(data, virtual);
239 Main.map.conflictDialog.paintConflicts(g, mv);
240 }
241
242 @Override public String getToolTipText() {
243 String tool = "";
244 tool += undeletedSize(data.nodes)+" "+trn("node", "nodes", undeletedSize(data.nodes))+", ";
245 tool += undeletedSize(data.ways)+" "+trn("way", "ways", undeletedSize(data.ways));
246 if (data.version != null) {
247 tool += ", " + tr("version {0}", data.version);
248 }
249 File f = getAssociatedFile();
250 if (f != null) {
251 tool = "<html>"+tool+"<br>"+f.getPath()+"</html>";
252 }
253 return tool;
254 }
255
256 @Override public void mergeFrom(final Layer from) {
257 mergeFrom(((OsmDataLayer)from).data);
258 }
259
260 /**
261 * merges the primitives in dataset <code>from</code> into the dataset of
262 * this layer
263 *
264 * @param from the source data set
265 */
266 public void mergeFrom(final DataSet from) {
267 final MergeVisitor visitor = new MergeVisitor(data,from);
268 visitor.merge();
269
270 Area a = data.getDataSourceArea();
271
272 // copy the merged layer's data source info;
273 // only add source rectangles if they are not contained in the
274 // layer already.
275 for (DataSource src : from.dataSources) {
276 if (a == null || !a.contains(src.bounds.asRect())) {
277 data.dataSources.add(src);
278 }
279 }
280
281 // copy the merged layer's API version, downgrade if required
282 if (data.version == null) {
283 data.version = from.version;
284 } else {
285 if ("0.5".equals(data.version) ^ "0.5".equals(from.version)) {
286 System.err.println(tr("Warning: mixing 0.6 and 0.5 data results in version 0.5"));
287 data.version = "0.5";
288 }
289 }
290 fireDataChange();
291 // repaint to make sure new data is displayed properly.
292 Main.map.mapView.repaint();
293
294 int numNewConflicts = 0;
295 for (Conflict c : visitor.getConflicts()) {
296 if (!conflicts.hasConflict(c)) {
297 numNewConflicts++;
298 conflicts.add(c);
299 }
300 }
301 if (numNewConflicts > 0) {
302 JOptionPane.showMessageDialog(Main.parent,tr("There were {0} conflicts during import.", numNewConflicts));
303 }
304 }
305
306 @Override public boolean isMergable(final Layer other) {
307 return other instanceof OsmDataLayer;
308 }
309
310 @Override public void visitBoundingBox(final BoundingXYVisitor v) {
311 for (final Node n : data.nodes)
312 if (!n.deleted && !n.incomplete) {
313 v.visit(n);
314 }
315 }
316
317 /**
318 * Clean out the data behind the layer. This means clearing the redo/undo lists,
319 * really deleting all deleted objects and reset the modified flags. This is done
320 * after a successfull upload.
321 *
322 * @param processed A list of all objects that were actually uploaded.
323 * May be <code>null</code>, which means nothing has been uploaded but
324 * saved to disk instead. Note that an empty collection for "processed"
325 * means that an upload has been attempted but failed.
326 */
327 public void cleanData(final Collection<OsmPrimitive> processed, boolean dataAdded) {
328
329 // return immediately if an upload attempt failed
330 if (processed != null && processed.isEmpty() && !dataAdded)
331 return;
332
333 Main.main.undoRedo.clean();
334
335 // if uploaded, clean the modified flags as well
336 if (processed != null) {
337 final Set<OsmPrimitive> processedSet = new HashSet<OsmPrimitive>(processed);
338 for (final Iterator<Node> it = data.nodes.iterator(); it.hasNext();) {
339 cleanIterator(it, processedSet);
340 }
341 for (final Iterator<Way> it = data.ways.iterator(); it.hasNext();) {
342 cleanIterator(it, processedSet);
343 }
344 for (final Iterator<Relation> it = data.relations.iterator(); it.hasNext();) {
345 cleanIterator(it, processedSet);
346 }
347 }
348
349 // update the modified flag
350 boolean b = (getAssociatedFile() != null && processed != null);
351 if (b && !dataAdded)
352 return; // do nothing when uploading non-harmful changes.
353
354 // modified if server changed the data (esp. the id).
355 uploadedModified = b && dataAdded;
356 setModified(uploadedModified);
357 }
358
359 /**
360 * Clean the modified flag for the given iterator over a collection if it is in the
361 * list of processed entries.
362 *
363 * @param it The iterator to change the modified and remove the items if deleted.
364 * @param processed A list of all objects that have been successfully progressed.
365 * If the object in the iterator is not in the list, nothing will be changed on it.
366 */
367 private void cleanIterator(final Iterator<? extends OsmPrimitive> it, final Collection<OsmPrimitive> processed) {
368 final OsmPrimitive osm = it.next();
369 if (!processed.remove(osm))
370 return;
371 osm.modified = false;
372 if (osm.deleted) {
373 it.remove();
374 }
375 }
376
377 public boolean isModified() {
378 return modified;
379 }
380
381 public void setModified(final boolean modified) {
382 if (modified == this.modified)
383 return;
384 this.modified = modified;
385 for (final ModifiedChangedListener l : listenerModified) {
386 l.modifiedChanged(modified, this);
387 }
388 }
389
390 /**
391 * @return The number of not-deleted primitives in the list.
392 */
393 private int undeletedSize(final Collection<? extends OsmPrimitive> list) {
394 int size = 0;
395 for (final OsmPrimitive osm : list)
396 if (!osm.deleted) {
397 size++;
398 }
399 return size;
400 }
401
402 @Override public Object getInfoComponent() {
403 final DataCountVisitor counter = new DataCountVisitor();
404 for (final OsmPrimitive osm : data.allPrimitives()) {
405 osm.visit(counter);
406 }
407 final JPanel p = new JPanel(new GridBagLayout());
408 p.add(new JLabel(tr("{0} consists of:", name)), GBC.eol());
409 for (int i = 0; i < counter.normal.length; ++i) {
410 String s = counter.normal[i]+" "+trn(counter.names[i],counter.names[i]+"s",counter.normal[i]);
411 if (counter.deleted[i] > 0) {
412 s += tr(" ({0} deleted.)",counter.deleted[i]);
413 }
414 p.add(new JLabel(s, ImageProvider.get("data", counter.names[i]), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0));
415 }
416 p.add(new JLabel(tr("API version: {0}", (data.version != null) ? data.version : tr("unset"))));
417
418 return p;
419 }
420
421 @Override public Component[] getMenuEntries() {
422 if (Main.applet)
423 return new Component[]{
424 new JMenuItem(new LayerListDialog.ShowHideLayerAction(this)),
425 new JMenuItem(new LayerListDialog.DeleteLayerAction(this)),
426 new JSeparator(),
427 new JMenuItem(new RenameLayerAction(getAssociatedFile(), this)),
428 new JSeparator(),
429 new JMenuItem(new LayerListPopup.InfoAction(this))};
430 return new Component[]{
431 new JMenuItem(new LayerListDialog.ShowHideLayerAction(this)),
432 new JMenuItem(new LayerListDialog.DeleteLayerAction(this)),
433 new JSeparator(),
434 new JMenuItem(new LayerSaveAction(this)),
435 new JMenuItem(new LayerSaveAsAction(this)),
436 new JMenuItem(new LayerGpxExportAction(this)),
437 new JMenuItem(new ConvertToGpxLayerAction()),
438 new JSeparator(),
439 new JMenuItem(new RenameLayerAction(getAssociatedFile(), this)),
440 new JSeparator(),
441 new JMenuItem(new LayerListPopup.InfoAction(this))};
442 }
443
444 public void fireDataChange() {
445 for (DataChangeListener dcl : listenerDataChanged) {
446 dcl.dataChanged(this);
447 }
448 }
449
450 public static GpxData toGpxData(DataSet data, File file) {
451 GpxData gpxData = new GpxData();
452 gpxData.storageFile = file;
453 HashSet<Node> doneNodes = new HashSet<Node>();
454 for (Way w : data.ways) {
455 if (w.incomplete || w.deleted) {
456 continue;
457 }
458 GpxTrack trk = new GpxTrack();
459 gpxData.tracks.add(trk);
460
461 if (w.get("name") != null) {
462 trk.attr.put("name", w.get("name"));
463 }
464
465 ArrayList<WayPoint> trkseg = null;
466 for (Node n : w.nodes) {
467 if (n.incomplete || n.deleted) {
468 trkseg = null;
469 continue;
470 }
471 if (trkseg == null) {
472 trkseg = new ArrayList<WayPoint>();
473 trk.trackSegs.add(trkseg);
474 }
475 if (!n.isTagged()) {
476 doneNodes.add(n);
477 }
478 WayPoint wpt = new WayPoint(n.getCoor());
479 if (!n.isTimestampEmpty())
480 {
481 wpt.attr.put("time", DateUtils.fromDate(n.getTimestamp()));
482 wpt.setTime();
483 }
484 trkseg.add(wpt);
485 }
486 }
487
488 // what is this loop meant to do? it creates waypoints but never
489 // records them?
490 for (Node n : data.nodes) {
491 if (n.incomplete || n.deleted || doneNodes.contains(n)) {
492 continue;
493 }
494 WayPoint wpt = new WayPoint(n.getCoor());
495 if (!n.isTimestampEmpty()) {
496 wpt.attr.put("time", DateUtils.fromDate(n.getTimestamp()));
497 wpt.setTime();
498 }
499 if (n.keys != null && n.keys.containsKey("name")) {
500 wpt.attr.put("name", n.keys.get("name"));
501 }
502 }
503 return gpxData;
504 }
505
506 public GpxData toGpxData() {
507 return toGpxData(data, getAssociatedFile());
508 }
509
510 public class ConvertToGpxLayerAction extends AbstractAction {
511 public ConvertToGpxLayerAction() {
512 super(tr("Convert to GPX layer"), ImageProvider.get("converttogpx"));
513 }
514 public void actionPerformed(ActionEvent e) {
515 Main.main.addLayer(new GpxLayer(toGpxData(), tr("Converted from: {0}", name)));
516 Main.main.removeLayer(OsmDataLayer.this);
517 }
518 }
519
520 public boolean containsPoint(LatLon coor) {
521 // we'll assume that if this has no data sources
522 // that it also has no borders
523 if (this.data.dataSources.isEmpty())
524 return true;
525
526 boolean layer_bounds_point = false;
527 for (DataSource src : this.data.dataSources) {
528 if (src.bounds.contains(coor)) {
529 layer_bounds_point = true;
530 break;
531 }
532 }
533 return layer_bounds_point;
534 }
535
536 /**
537 * replies the set of conflicts currently managed in this layer
538 *
539 * @return the set of conflicts currently managed in this layer
540 */
541 public ConflictCollection getConflicts() {
542 return conflicts;
543 }
544}
Note: See TracBrowser for help on using the repository browser.