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

Last change on this file since 1690 was 1690, checked in by Gubaer, 15 years ago

new: MultiFetchServerObjectReader using APIs Multi Fetch method
update: now uses Multi Fetch to check for deleted primitives on the server
update: now uses Multi Fetch to update the selected primitives with the state from the server
fixed: cleaned up merging in MergeVisitor
new: conflict resolution dialog; now resolves conflicts due to different visibilities
new: replacement for realEqual() on OsmPrimitive and derived classes; realEqual now @deprecated
fixed: cleaning up OsmReader
fixed: progress indication in OsmApi

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