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

Last change on this file since 3408 was 3408, checked in by jttt, 14 years ago

Show only actions that can work on all selected layers in LayerListDialog popup menu

  • Property svn:eol-style set to native
File size: 23.8 KB
Line 
1// License: GPL. See LICENSE file for details.
2
3package org.openstreetmap.josm.gui.layer;
4
5import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
6import static org.openstreetmap.josm.tools.I18n.marktr;
7import static org.openstreetmap.josm.tools.I18n.tr;
8import static org.openstreetmap.josm.tools.I18n.trn;
9
10import java.awt.AlphaComposite;
11import java.awt.Color;
12import java.awt.Composite;
13import java.awt.Graphics2D;
14import java.awt.GridBagLayout;
15import java.awt.Point;
16import java.awt.Rectangle;
17import java.awt.TexturePaint;
18import java.awt.event.ActionEvent;
19import java.awt.geom.Area;
20import java.awt.image.BufferedImage;
21import java.io.File;
22import java.util.ArrayList;
23import java.util.Collection;
24import java.util.HashMap;
25import java.util.HashSet;
26import java.util.Map;
27
28import javax.swing.AbstractAction;
29import javax.swing.Action;
30import javax.swing.Icon;
31import javax.swing.JLabel;
32import javax.swing.JOptionPane;
33import javax.swing.JPanel;
34import javax.swing.JScrollPane;
35import javax.swing.JTextArea;
36
37import org.openstreetmap.josm.Main;
38import org.openstreetmap.josm.actions.RenameLayerAction;
39import org.openstreetmap.josm.data.Bounds;
40import org.openstreetmap.josm.data.SelectionChangedListener;
41import org.openstreetmap.josm.data.conflict.Conflict;
42import org.openstreetmap.josm.data.conflict.ConflictCollection;
43import org.openstreetmap.josm.data.coor.EastNorth;
44import org.openstreetmap.josm.data.coor.LatLon;
45import org.openstreetmap.josm.data.gpx.GpxData;
46import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
47import org.openstreetmap.josm.data.gpx.WayPoint;
48import org.openstreetmap.josm.data.osm.DataSet;
49import org.openstreetmap.josm.data.osm.DataSetMerger;
50import org.openstreetmap.josm.data.osm.DataSource;
51import org.openstreetmap.josm.data.osm.DatasetCollection;
52import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
53import org.openstreetmap.josm.data.osm.Node;
54import org.openstreetmap.josm.data.osm.OsmPrimitive;
55import org.openstreetmap.josm.data.osm.Relation;
56import org.openstreetmap.josm.data.osm.Way;
57import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
58import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
59import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
60import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
61import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
62import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintVisitor;
63import org.openstreetmap.josm.data.osm.visitor.paint.PaintVisitor;
64import org.openstreetmap.josm.data.osm.visitor.paint.SimplePaintVisitor;
65import org.openstreetmap.josm.gui.HelpAwareOptionPane;
66import org.openstreetmap.josm.gui.MapView;
67import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
68import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
69import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
70import org.openstreetmap.josm.tools.DateUtils;
71import org.openstreetmap.josm.tools.GBC;
72import org.openstreetmap.josm.tools.ImageProvider;
73
74/**
75 * A layer holding data from a specific dataset.
76 * The data can be fully edited.
77 *
78 * @author imi
79 */
80public class OsmDataLayer extends Layer implements Listener, SelectionChangedListener {
81 static public final String REQUIRES_SAVE_TO_DISK_PROP = OsmDataLayer.class.getName() + ".requiresSaveToDisk";
82 static public final String REQUIRES_UPLOAD_TO_SERVER_PROP = OsmDataLayer.class.getName() + ".requiresUploadToServer";
83
84 private boolean requiresSaveToFile = false;
85 private boolean requiresUploadToServer = false;
86 private boolean isChanged = true;
87 private int highlightUpdateCount;
88
89 protected void setRequiresSaveToFile(boolean newValue) {
90 boolean oldValue = requiresSaveToFile;
91 requiresSaveToFile = newValue;
92 if (oldValue != newValue) {
93 propertyChangeSupport.firePropertyChange(REQUIRES_SAVE_TO_DISK_PROP, oldValue, newValue);
94 }
95 }
96
97 protected void setRequiresUploadToServer(boolean newValue) {
98 boolean oldValue = requiresUploadToServer;
99 requiresUploadToServer = newValue;
100 if (oldValue != newValue) {
101 propertyChangeSupport.firePropertyChange(REQUIRES_UPLOAD_TO_SERVER_PROP, oldValue, newValue);
102 }
103 }
104
105 /** the global counter for created data layers */
106 static private int dataLayerCounter = 0;
107
108 /**
109 * Replies a new unique name for a data layer
110 *
111 * @return a new unique name for a data layer
112 */
113 static public String createNewName() {
114 dataLayerCounter++;
115 return tr("Data Layer {0}", dataLayerCounter);
116 }
117
118 public final static class DataCountVisitor extends AbstractVisitor {
119 public int nodes;
120 public int ways;
121 public int relations;
122 public int deletedNodes;
123 public int deletedWays;
124 public int deletedRelations;
125
126 public void visit(final Node n) {
127 nodes++;
128 if (n.isDeleted()) {
129 deletedNodes++;
130 }
131 }
132
133 public void visit(final Way w) {
134 ways++;
135 if (w.isDeleted()) {
136 deletedWays++;
137 }
138 }
139
140 public void visit(final Relation r) {
141 relations++;
142 if (r.isDeleted()) {
143 deletedRelations++;
144 }
145 }
146 }
147
148 public interface CommandQueueListener {
149 void commandChanged(int queueSize, int redoSize);
150 }
151
152 /**
153 * The data behind this layer.
154 */
155 public final DataSet data;
156
157 /**
158 * the collection of conflicts detected in this layer
159 */
160 private ConflictCollection conflicts;
161
162 /**
163 * a paint texture for non-downloaded area
164 */
165 private static TexturePaint hatched;
166
167 static {
168 createHatchTexture();
169 }
170
171 public static Color getBackgroundColor() {
172 return Main.pref.getColor(marktr("background"), Color.BLACK);
173 }
174
175 public static Color getOutsideColor() {
176 return Main.pref.getColor(marktr("outside downloaded area"), Color.YELLOW);
177 }
178
179 /**
180 * Initialize the hatch pattern used to paint the non-downloaded area
181 */
182 public static void createHatchTexture() {
183 BufferedImage bi = new BufferedImage(15, 15, BufferedImage.TYPE_INT_ARGB);
184 Graphics2D big = bi.createGraphics();
185 big.setColor(getBackgroundColor());
186 Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
187 big.setComposite(comp);
188 big.fillRect(0,0,15,15);
189 big.setColor(getOutsideColor());
190 big.drawLine(0,15,15,0);
191 Rectangle r = new Rectangle(0, 0, 15,15);
192 hatched = new TexturePaint(bi, r);
193 }
194
195 /**
196 * Construct a OsmDataLayer.
197 */
198 public OsmDataLayer(final DataSet data, final String name, final File associatedFile) {
199 super(name);
200 this.data = data;
201 this.setAssociatedFile(associatedFile);
202 conflicts = new ConflictCollection();
203 data.addDataSetListener(new DataSetListenerAdapter(this));
204 DataSet.selListeners.add(this);
205 }
206
207 /**
208 * TODO: @return Return a dynamic drawn icon of the map data. The icon is
209 * updated by a background thread to not disturb the running programm.
210 */
211 @Override public Icon getIcon() {
212 return ImageProvider.get("layer", "osmdata_small");
213 }
214
215 /**
216 * Draw all primitives in this layer but do not draw modified ones (they
217 * are drawn by the edit layer).
218 * Draw nodes last to overlap the ways they belong to.
219 */
220 @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) {
221 isChanged = false;
222 highlightUpdateCount = data.getHighlightUpdateCount();
223
224 boolean active = mv.getActiveLayer() == this;
225 boolean inactive = !active && Main.pref.getBoolean("draw.data.inactive_color", true);
226 boolean virtual = !inactive && mv.isVirtualNodesEnabled();
227
228 // draw the hatched area for non-downloaded region. only draw if we're the active
229 // and bounds are defined; don't draw for inactive layers or loaded GPX files etc
230 if (active && Main.pref.getBoolean("draw.data.downloaded_area", true) && !data.dataSources.isEmpty()) {
231 // initialize area with current viewport
232 Rectangle b = mv.getBounds();
233 // on some platforms viewport bounds seem to be offset from the left,
234 // over-grow it just to be sure
235 b.grow(100, 100);
236 Area a = new Area(b);
237
238 // now succesively subtract downloaded areas
239 for (DataSource src : data.dataSources) {
240 if (src.bounds != null && !src.bounds.getMin().equals(src.bounds.getMax())) {
241 EastNorth en1 = mv.getProjection().latlon2eastNorth(src.bounds.getMin());
242 EastNorth en2 = mv.getProjection().latlon2eastNorth(src.bounds.getMax());
243 Point p1 = mv.getPoint(en1);
244 Point p2 = mv.getPoint(en2);
245 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));
246 a.subtract(new Area(r));
247 }
248 }
249
250 // paint remainder
251 g.setPaint(hatched);
252 g.fill(a);
253 }
254
255 PaintVisitor painter;
256 if (Main.pref.getBoolean("draw.wireframe")) {
257 painter = new SimplePaintVisitor();
258 } else {
259 painter = new MapPaintVisitor();
260 }
261 painter.setGraphics(g);
262 painter.setNavigatableComponent(mv);
263 painter.setInactive(inactive);
264 painter.visitAll(data, virtual, box);
265 Main.map.conflictDialog.paintConflicts(g, mv);
266 }
267
268 @Override public String getToolTipText() {
269 int nodes = new DatasetCollection<OsmPrimitive>(data.getNodes(), OsmPrimitive.nonDeletedPredicate).size();
270 int ways = new DatasetCollection<OsmPrimitive>(data.getWays(), OsmPrimitive.nonDeletedPredicate).size();
271
272 String tool = trn("{0} node", "{0} nodes", nodes, nodes)+", ";
273 tool += trn("{0} way", "{0} ways", ways, ways);
274
275 if (data.getVersion() != null) {
276 tool += ", " + tr("version {0}", data.getVersion());
277 }
278 File f = getAssociatedFile();
279 if (f != null) {
280 tool = "<html>"+tool+"<br>"+f.getPath()+"</html>";
281 }
282 return tool;
283 }
284
285 @Override public void mergeFrom(final Layer from) {
286 mergeFrom(((OsmDataLayer)from).data);
287 }
288
289 /**
290 * merges the primitives in dataset <code>from</code> into the dataset of
291 * this layer
292 *
293 * @param from the source data set
294 */
295 public void mergeFrom(final DataSet from) {
296 final DataSetMerger visitor = new DataSetMerger(data,from);
297 visitor.merge();
298
299 Area a = data.getDataSourceArea();
300
301 // copy the merged layer's data source info;
302 // only add source rectangles if they are not contained in the
303 // layer already.
304 for (DataSource src : from.dataSources) {
305 if (a == null || !a.contains(src.bounds.asRect())) {
306 data.dataSources.add(src);
307 }
308 }
309
310 // copy the merged layer's API version, downgrade if required
311 if (data.getVersion() == null) {
312 data.setVersion(from.getVersion());
313 } else if ("0.5".equals(data.getVersion()) ^ "0.5".equals(from.getVersion())) {
314 System.err.println(tr("Warning: mixing 0.6 and 0.5 data results in version 0.5"));
315 data.setVersion("0.5");
316 }
317
318 int numNewConflicts = 0;
319 for (Conflict<?> c : visitor.getConflicts()) {
320 if (!conflicts.hasConflict(c)) {
321 numNewConflicts++;
322 conflicts.add(c);
323 }
324 }
325 // repaint to make sure new data is displayed properly.
326 Main.map.mapView.repaint();
327 warnNumNewConflicts(numNewConflicts);
328 }
329
330 /**
331 * Warns the user about the number of detected conflicts
332 *
333 * @param numNewConflicts the number of detected conflicts
334 */
335 protected void warnNumNewConflicts(int numNewConflicts) {
336 if (numNewConflicts == 0) return;
337
338 String msg1 = trn(
339 "There was {0} conflict detected.",
340 "There were {0} conflicts detected.",
341 numNewConflicts,
342 numNewConflicts
343 );
344
345 StringBuffer sb = new StringBuffer();
346 sb.append("<html>").append(msg1).append("</html>");
347 if (numNewConflicts > 0) {
348 ButtonSpec[] options = new ButtonSpec[] {
349 new ButtonSpec(
350 tr("OK"),
351 ImageProvider.get("ok"),
352 tr("Click to close this dialog and continue editing"),
353 null /* no specific help */
354 )
355 };
356 HelpAwareOptionPane.showOptionDialog(
357 Main.parent,
358 sb.toString(),
359 tr("Conflicts detected"),
360 JOptionPane.WARNING_MESSAGE,
361 null, /* no icon */
362 options,
363 options[0],
364 ht("/Concepts/Conflict#WarningAboutDetectedConflicts")
365 );
366 Main.map.conflictDialog.unfurlDialog();
367 Main.map.repaint();
368 }
369 }
370
371
372 @Override public boolean isMergable(final Layer other) {
373 return other instanceof OsmDataLayer;
374 }
375
376 @Override public void visitBoundingBox(final BoundingXYVisitor v) {
377 for (final Node n: data.getNodes()) {
378 if (n.isUsable()) {
379 v.visit(n);
380 }
381 }
382 }
383
384 /**
385 * Clean out the data behind the layer. This means clearing the redo/undo lists,
386 * really deleting all deleted objects and reset the modified flags. This should
387 * be done after an upload, even after a partial upload.
388 *
389 * @param processed A list of all objects that were actually uploaded.
390 * May be <code>null</code>, which means nothing has been uploaded
391 */
392 public void cleanupAfterUpload(final Collection<OsmPrimitive> processed) {
393 // return immediately if an upload attempt failed
394 if (processed == null || processed.isEmpty())
395 return;
396
397 Main.main.undoRedo.clean(this);
398
399 // if uploaded, clean the modified flags as well
400 data.clenupDeletedPrimitives();
401 for (OsmPrimitive p: data.allPrimitives()) {
402 if (processed.contains(p)) {
403 p.setModified(false);
404 }
405 }
406 }
407
408
409 @Override public Object getInfoComponent() {
410 final DataCountVisitor counter = new DataCountVisitor();
411 for (final OsmPrimitive osm : data.allPrimitives()) {
412 osm.visit(counter);
413 }
414 final JPanel p = new JPanel(new GridBagLayout());
415
416 String nodeText = trn("{0} node", "{0} nodes", counter.nodes, counter.nodes);
417 if (counter.deletedNodes > 0) {
418 nodeText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedNodes, counter.deletedNodes)+")";
419 }
420
421 String wayText = trn("{0} way", "{0} ways", counter.ways, counter.ways);
422 if (counter.deletedWays > 0) {
423 wayText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedWays, counter.deletedWays)+")";
424 }
425
426 String relationText = trn("{0} relation", "{0} relations", counter.relations, counter.relations);
427 if (counter.deletedRelations > 0) {
428 relationText += " ("+trn("{0} deleted", "{0} deleted", counter.deletedRelations, counter.deletedRelations)+")";
429 }
430
431 p.add(new JLabel(tr("{0} consists of:", getName())), GBC.eol());
432 p.add(new JLabel(nodeText, ImageProvider.get("data", "node"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0));
433 p.add(new JLabel(wayText, ImageProvider.get("data", "way"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0));
434 p.add(new JLabel(relationText, ImageProvider.get("data", "relation"), JLabel.HORIZONTAL), GBC.eop().insets(15,0,0,0));
435 p.add(new JLabel(tr("API version: {0}", (data.getVersion() != null) ? data.getVersion() : tr("unset"))));
436
437 return p;
438 }
439
440 @Override public Action[] getMenuEntries() {
441 if (Main.applet)
442 return new Action[]{
443 LayerListDialog.getInstance().createActivateLayerAction(this),
444 LayerListDialog.getInstance().createShowHideLayerAction(),
445 LayerListDialog.getInstance().createDeleteLayerAction(),
446 SeparatorLayerAction.INSTANCE,
447 LayerListDialog.getInstance().createMergeLayerAction(this),
448 SeparatorLayerAction.INSTANCE,
449 new RenameLayerAction(getAssociatedFile(), this),
450 new ConsistencyTestAction(),
451 SeparatorLayerAction.INSTANCE,
452 new LayerListPopup.InfoAction(this)};
453 return new Action[]{
454 LayerListDialog.getInstance().createActivateLayerAction(this),
455 LayerListDialog.getInstance().createShowHideLayerAction(),
456 LayerListDialog.getInstance().createDeleteLayerAction(),
457 SeparatorLayerAction.INSTANCE,
458 LayerListDialog.getInstance().createMergeLayerAction(this),
459 new LayerSaveAction(this),
460 new LayerSaveAsAction(this),
461 new LayerGpxExportAction(this),
462 new ConvertToGpxLayerAction(),
463 SeparatorLayerAction.INSTANCE,
464 new RenameLayerAction(getAssociatedFile(), this),
465 new ConsistencyTestAction(),
466 SeparatorLayerAction.INSTANCE,
467 new LayerListPopup.InfoAction(this)};
468 }
469
470 public static GpxData toGpxData(DataSet data, File file) {
471 GpxData gpxData = new GpxData();
472 gpxData.storageFile = file;
473 HashSet<Node> doneNodes = new HashSet<Node>();
474 for (Way w : data.getWays()) {
475 if (!w.isUsable()) {
476 continue;
477 }
478 Collection<Collection<WayPoint>> trk = new ArrayList<Collection<WayPoint>>();
479 Map<String, Object> trkAttr = new HashMap<String, Object>();
480
481 if (w.get("name") != null) {
482 trkAttr.put("name", w.get("name"));
483 }
484
485 ArrayList<WayPoint> trkseg = null;
486 for (Node n : w.getNodes()) {
487 if (!n.isUsable()) {
488 trkseg = null;
489 continue;
490 }
491 if (trkseg == null) {
492 trkseg = new ArrayList<WayPoint>();
493 trk.add(trkseg);
494 }
495 if (!n.isTagged()) {
496 doneNodes.add(n);
497 }
498 WayPoint wpt = new WayPoint(n.getCoor());
499 if (!n.isTimestampEmpty()) {
500 wpt.attr.put("time", DateUtils.fromDate(n.getTimestamp()));
501 wpt.setTime();
502 }
503 trkseg.add(wpt);
504 }
505
506 gpxData.tracks.add(new ImmutableGpxTrack(trk, trkAttr));
507 }
508
509 // what is this loop meant to do? it creates waypoints but never
510 // records them?
511 for (Node n : data.getNodes()) {
512 if (n.isIncomplete() || n.isDeleted() || doneNodes.contains(n)) {
513 continue;
514 }
515 WayPoint wpt = new WayPoint(n.getCoor());
516 if (!n.isTimestampEmpty()) {
517 wpt.attr.put("time", DateUtils.fromDate(n.getTimestamp()));
518 wpt.setTime();
519 }
520 String name = n.get("name");
521 if (name != null) {
522 wpt.attr.put("name", name);
523 }
524 }
525 return gpxData;
526 }
527
528 public GpxData toGpxData() {
529 return toGpxData(data, getAssociatedFile());
530 }
531
532 public class ConvertToGpxLayerAction extends AbstractAction {
533 public ConvertToGpxLayerAction() {
534 super(tr("Convert to GPX layer"), ImageProvider.get("converttogpx"));
535 }
536 public void actionPerformed(ActionEvent e) {
537 Main.main.addLayer(new GpxLayer(toGpxData(), tr("Converted from: {0}", getName())));
538 Main.main.removeLayer(OsmDataLayer.this);
539 }
540 }
541
542 public boolean containsPoint(LatLon coor) {
543 // we'll assume that if this has no data sources
544 // that it also has no borders
545 if (this.data.dataSources.isEmpty())
546 return true;
547
548 boolean layer_bounds_point = false;
549 for (DataSource src : this.data.dataSources) {
550 if (src.bounds.contains(coor)) {
551 layer_bounds_point = true;
552 break;
553 }
554 }
555 return layer_bounds_point;
556 }
557
558 /**
559 * replies the set of conflicts currently managed in this layer
560 *
561 * @return the set of conflicts currently managed in this layer
562 */
563 public ConflictCollection getConflicts() {
564 return conflicts;
565 }
566
567 /**
568 * Replies true if the data managed by this layer needs to be uploaded to
569 * the server because it contains at least one modified primitive.
570 *
571 * @return true if the data managed by this layer needs to be uploaded to
572 * the server because it contains at least one modified primitive; false,
573 * otherwise
574 */
575 public boolean requiresUploadToServer() {
576 return requiresUploadToServer;
577 }
578
579 /**
580 * Replies true if the data managed by this layer needs to be saved to
581 * a file. Only replies true if a file is assigned to this layer and
582 * if the data managed by this layer has been modified since the last
583 * save operation to the file.
584 *
585 * @return true if the data managed by this layer needs to be saved to
586 * a file
587 */
588 public boolean requiresSaveToFile() {
589 return getAssociatedFile() != null && requiresSaveToFile;
590 }
591
592 /**
593 * Initializes the layer after a successful load of OSM data from a file
594 *
595 */
596 public void onPostLoadFromFile() {
597 setRequiresSaveToFile(false);
598 setRequiresUploadToServer(data.isModified());
599 }
600
601 public void onPostDownloadFromServer() {
602 setRequiresSaveToFile(true);
603 setRequiresUploadToServer(data.isModified());
604 }
605
606 @Override
607 public boolean isChanged() {
608 return isChanged || highlightUpdateCount != data.getHighlightUpdateCount();
609 }
610
611 /**
612 * Initializes the layer after a successful save of OSM data to a file
613 *
614 */
615 public void onPostSaveToFile() {
616 setRequiresSaveToFile(false);
617 setRequiresUploadToServer(data.isModified());
618 }
619
620 /**
621 * Initializes the layer after a successful upload to the server
622 *
623 */
624 public void onPostUploadToServer() {
625 setRequiresUploadToServer(data.isModified());
626 // keep requiresSaveToDisk unchanged
627 }
628
629 private class ConsistencyTestAction extends AbstractAction {
630
631 public ConsistencyTestAction() {
632 super(tr("Dataset consistency test"));
633 }
634
635 public void actionPerformed(ActionEvent e) {
636 String result = DatasetConsistencyTest.runTests(data);
637 if (result.length() == 0) {
638 JOptionPane.showMessageDialog(Main.parent, tr("No problems found"));
639 } else {
640 JPanel p = new JPanel(new GridBagLayout());
641 p.add(new JLabel(tr("Following problems found:")), GBC.eol());
642 JTextArea info = new JTextArea(result, 20, 60);
643 info.setCaretPosition(0);
644 info.setEditable(false);
645 p.add(new JScrollPane(info), GBC.eop());
646
647 JOptionPane.showMessageDialog(Main.parent, p, tr("Warning"), JOptionPane.WARNING_MESSAGE);
648 }
649 }
650
651 }
652
653 @Override
654 public void destroy() {
655 DataSet.selListeners.remove(this);
656 }
657
658 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
659 isChanged = true;
660 setRequiresSaveToFile(true);
661 setRequiresUploadToServer(true);
662 }
663
664 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
665 isChanged = true;
666 }
667}
Note: See TracBrowser for help on using the repository browser.