001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.history;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.text.DateFormat;
007import java.util.ArrayList;
008import java.util.Collections;
009import java.util.HashSet;
010import java.util.List;
011import java.util.Set;
012
013import javax.swing.JTable;
014import javax.swing.table.AbstractTableModel;
015import javax.swing.table.TableModel;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.osm.Node;
019import org.openstreetmap.josm.data.osm.OsmPrimitive;
020import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
021import org.openstreetmap.josm.data.osm.Relation;
022import org.openstreetmap.josm.data.osm.RelationMember;
023import org.openstreetmap.josm.data.osm.RelationMemberData;
024import org.openstreetmap.josm.data.osm.User;
025import org.openstreetmap.josm.data.osm.UserInfo;
026import org.openstreetmap.josm.data.osm.Way;
027import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
028import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
029import org.openstreetmap.josm.data.osm.event.DataSetListener;
030import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
031import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
032import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
033import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
034import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
035import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
036import org.openstreetmap.josm.data.osm.history.History;
037import org.openstreetmap.josm.data.osm.history.HistoryNode;
038import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
039import org.openstreetmap.josm.data.osm.history.HistoryRelation;
040import org.openstreetmap.josm.data.osm.history.HistoryWay;
041import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
042import org.openstreetmap.josm.gui.JosmUserIdentityManager;
043import org.openstreetmap.josm.gui.layer.Layer;
044import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
045import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
046import org.openstreetmap.josm.gui.layer.OsmDataLayer;
047import org.openstreetmap.josm.gui.util.ChangeNotifier;
048import org.openstreetmap.josm.tools.CheckParameterUtil;
049import org.openstreetmap.josm.tools.date.DateUtils;
050
051/**
052 * This is the model used by the history browser.
053 *
054 * The model state consists of the following elements:
055 * <ul>
056 *   <li>the {@link History} of a specific {@link OsmPrimitive}</li>
057 *   <li>a dedicated version in this {@link History} called the {@link PointInTimeType#REFERENCE_POINT_IN_TIME}</li>
058 *   <li>another version in this {@link History} called the {@link PointInTimeType#CURRENT_POINT_IN_TIME}</li>
059 * </ul>
060 * {@link HistoryBrowser} always compares the {@link PointInTimeType#REFERENCE_POINT_IN_TIME} with the
061 * {@link PointInTimeType#CURRENT_POINT_IN_TIME}.
062
063 * This model provides various {@link TableModel}s for {@link JTable}s used in {@link HistoryBrowser}, for
064 * instance:
065 * <ul>
066 *  <li>{@link #getTagTableModel(PointInTimeType)} replies a {@link TableModel} for the tags of either of
067 *   the two selected versions</li>
068 *  <li>{@link #getNodeListTableModel(PointInTimeType)} replies a {@link TableModel} for the list of nodes of
069 *   the two selected versions (if the current history provides information about a {@link Way}</li>
070 *  <li> {@link #getRelationMemberTableModel(PointInTimeType)} replies a {@link TableModel} for the list of relation
071 *  members  of the two selected versions (if the current history provides information about a {@link Relation}</li>
072 *  </ul>
073 *
074 * @see HistoryBrowser
075 */
076public class HistoryBrowserModel extends ChangeNotifier implements ActiveLayerChangeListener, DataSetListener {
077    /** the history of an OsmPrimitive */
078    private History history;
079    private HistoryOsmPrimitive reference;
080    private HistoryOsmPrimitive current;
081    /**
082     * latest isn't a reference of history. It's a clone of the currently edited
083     * {@link OsmPrimitive} in the current edit layer.
084     */
085    private HistoryOsmPrimitive latest;
086
087    private final VersionTableModel versionTableModel;
088    private final TagTableModel currentTagTableModel;
089    private final TagTableModel referenceTagTableModel;
090    private final DiffTableModel currentRelationMemberTableModel;
091    private final DiffTableModel referenceRelationMemberTableModel;
092    private final DiffTableModel referenceNodeListTableModel;
093    private final DiffTableModel currentNodeListTableModel;
094
095    /**
096     * constructor
097     */
098    public HistoryBrowserModel() {
099        versionTableModel = new VersionTableModel();
100        currentTagTableModel = new TagTableModel(PointInTimeType.CURRENT_POINT_IN_TIME);
101        referenceTagTableModel = new TagTableModel(PointInTimeType.REFERENCE_POINT_IN_TIME);
102        referenceNodeListTableModel = new DiffTableModel();
103        currentNodeListTableModel = new DiffTableModel();
104        currentRelationMemberTableModel = new DiffTableModel();
105        referenceRelationMemberTableModel = new DiffTableModel();
106
107        if (Main.main != null) {
108            OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
109            if (editLayer != null) {
110                editLayer.data.addDataSetListener(this);
111            }
112        }
113        Main.getLayerManager().addActiveLayerChangeListener(this);
114    }
115
116    /**
117     * Creates a new history browser model for a given history.
118     *
119     * @param history the history. Must not be null.
120     * @throws IllegalArgumentException if history is null
121     */
122    public HistoryBrowserModel(History history) {
123        this();
124        CheckParameterUtil.ensureParameterNotNull(history, "history");
125        setHistory(history);
126    }
127
128    /**
129     * replies the history managed by this model
130     * @return the history
131     */
132    public History getHistory() {
133        return history;
134    }
135
136    protected boolean canShowAsLatest(OsmPrimitive primitive) {
137        if (primitive == null)
138            return false;
139        if (primitive.isNew() || !primitive.isUsable())
140            return false;
141
142        //try creating a history primitive. if that fails, the primitive cannot be used.
143        try {
144            HistoryOsmPrimitive.forOsmPrimitive(primitive);
145        } catch (IllegalArgumentException ign) {
146            return false;
147        }
148
149        if (history == null)
150            return false;
151        // only show latest of the same version if it is modified
152        if (history.getByVersion(primitive.getVersion()) != null)
153            return primitive.isModified();
154
155        // if latest version from history is higher than a non existing primitive version,
156        // that means this version has been redacted and the primitive cannot be used.
157        if (history.getLatest().getVersion() > primitive.getVersion())
158            return false;
159
160        // latest has a higher version than one of the primitives
161        // in the history (probably because the history got out of sync
162        // with uploaded data) -> show the primitive as latest
163        return true;
164    }
165
166    /**
167     * sets the history to be managed by this model
168     *
169     * @param history the history
170     *
171     */
172    public void setHistory(History history) {
173        this.history = history;
174        if (history.getNumVersions() > 0) {
175            HistoryOsmPrimitive newLatest = null;
176            OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
177            if (editLayer != null) {
178                OsmPrimitive p = editLayer.data.getPrimitiveById(history.getId(), history.getType());
179                if (canShowAsLatest(p)) {
180                    newLatest = new HistoryPrimitiveBuilder().build(p);
181                }
182            }
183            if (newLatest == null) {
184                current = history.getLatest();
185                int prevIndex = history.getNumVersions() - 2;
186                reference = prevIndex < 0 ? history.getEarliest() : history.get(prevIndex);
187            } else {
188                reference = history.getLatest();
189                current = newLatest;
190            }
191            setLatest(newLatest);
192        }
193        initTagTableModels();
194        fireModelChange();
195    }
196
197    protected void fireModelChange() {
198        initNodeListTableModels();
199        initMemberListTableModels();
200        fireStateChanged();
201        versionTableModel.fireTableDataChanged();
202    }
203
204    /**
205     * Replies the table model to be used in a {@link JTable} which
206     * shows the list of versions in this history.
207     *
208     * @return the table model
209     */
210    public VersionTableModel getVersionTableModel() {
211        return versionTableModel;
212    }
213
214    protected void initTagTableModels() {
215        currentTagTableModel.initKeyList();
216        referenceTagTableModel.initKeyList();
217    }
218
219    /**
220     * Should be called everytime either reference of current changes to update the diff.
221     * TODO: Maybe rename to reflect this? eg. updateNodeListTableModels
222     */
223    protected void initNodeListTableModels() {
224        if (current == null || current.getType() != OsmPrimitiveType.WAY
225         || reference == null || reference.getType() != OsmPrimitiveType.WAY)
226            return;
227        TwoColumnDiff diff = new TwoColumnDiff(
228                ((HistoryWay) reference).getNodes().toArray(),
229                ((HistoryWay) current).getNodes().toArray());
230        referenceNodeListTableModel.setRows(diff.referenceDiff, diff.referenceReversed);
231        currentNodeListTableModel.setRows(diff.currentDiff, false);
232    }
233
234    protected void initMemberListTableModels() {
235        if (current == null || current.getType() != OsmPrimitiveType.RELATION
236         || reference == null || reference.getType() != OsmPrimitiveType.RELATION)
237            return;
238        TwoColumnDiff diff = new TwoColumnDiff(
239                ((HistoryRelation) reference).getMembers().toArray(),
240                ((HistoryRelation) current).getMembers().toArray());
241        referenceRelationMemberTableModel.setRows(diff.referenceDiff, diff.referenceReversed);
242        currentRelationMemberTableModel.setRows(diff.currentDiff, false);
243    }
244
245    /**
246     * Replies the tag table model for the respective point in time.
247     *
248     * @param pointInTimeType the type of the point in time (must not be null)
249     * @return the tag table model
250     * @throws IllegalArgumentException if pointInTimeType is null
251     */
252    public TagTableModel getTagTableModel(PointInTimeType pointInTimeType) {
253        CheckParameterUtil.ensureParameterNotNull(pointInTimeType, "pointInTimeType");
254        if (pointInTimeType.equals(PointInTimeType.CURRENT_POINT_IN_TIME))
255            return currentTagTableModel;
256        else // REFERENCE_POINT_IN_TIME
257            return referenceTagTableModel;
258    }
259
260    /**
261     * Replies the node list table model for the respective point in time.
262     *
263     * @param pointInTimeType the type of the point in time (must not be null)
264     * @return the node list table model
265     * @throws IllegalArgumentException if pointInTimeType is null
266     */
267    public DiffTableModel getNodeListTableModel(PointInTimeType pointInTimeType) {
268        CheckParameterUtil.ensureParameterNotNull(pointInTimeType, "pointInTimeType");
269        if (pointInTimeType.equals(PointInTimeType.CURRENT_POINT_IN_TIME))
270            return currentNodeListTableModel;
271        else // REFERENCE_POINT_IN_TIME
272            return referenceNodeListTableModel;
273    }
274
275    /**
276     * Replies the relation member table model for the respective point in time.
277     *
278     * @param pointInTimeType the type of the point in time (must not be null)
279     * @return the relation member table model
280     * @throws IllegalArgumentException if pointInTimeType is null
281     */
282    public DiffTableModel getRelationMemberTableModel(PointInTimeType pointInTimeType) {
283        CheckParameterUtil.ensureParameterNotNull(pointInTimeType, "pointInTimeType");
284        if (pointInTimeType.equals(PointInTimeType.CURRENT_POINT_IN_TIME))
285            return currentRelationMemberTableModel;
286        else // REFERENCE_POINT_IN_TIME
287            return referenceRelationMemberTableModel;
288    }
289
290    /**
291     * Sets the {@link HistoryOsmPrimitive} which plays the role of a reference point
292     * in time (see {@link PointInTimeType}).
293     *
294     * @param reference the reference history primitive. Must not be null.
295     * @throws IllegalArgumentException if reference is null
296     * @throws IllegalStateException if this model isn't a assigned a history yet
297     * @throws IllegalArgumentException if reference isn't an history primitive for the history managed by this mode
298     *
299     * @see #setHistory(History)
300     * @see PointInTimeType
301     */
302    public void setReferencePointInTime(HistoryOsmPrimitive reference) {
303        CheckParameterUtil.ensureParameterNotNull(reference, "reference");
304        if (history == null)
305            throw new IllegalStateException(tr("History not initialized yet. Failed to set reference primitive."));
306        if (reference.getId() != history.getId())
307            throw new IllegalArgumentException(
308                    tr("Failed to set reference. Reference ID {0} does not match history ID {1}.", reference.getId(), history.getId()));
309        HistoryOsmPrimitive primitive = history.getByVersion(reference.getVersion());
310        if (primitive == null)
311            throw new IllegalArgumentException(
312                    tr("Failed to set reference. Reference version {0} not available in history.", reference.getVersion()));
313
314        this.reference = reference;
315        initTagTableModels();
316        initNodeListTableModels();
317        initMemberListTableModels();
318        fireStateChanged();
319    }
320
321    /**
322     * Sets the {@link HistoryOsmPrimitive} which plays the role of the current point
323     * in time (see {@link PointInTimeType}).
324     *
325     * @param current the reference history primitive. Must not be {@code null}.
326     * @throws IllegalArgumentException if reference is {@code null}
327     * @throws IllegalStateException if this model isn't a assigned a history yet
328     * @throws IllegalArgumentException if reference isn't an history primitive for the history managed by this mode
329     *
330     * @see #setHistory(History)
331     * @see PointInTimeType
332     */
333    public void setCurrentPointInTime(HistoryOsmPrimitive current) {
334        CheckParameterUtil.ensureParameterNotNull(current, "current");
335        if (history == null)
336            throw new IllegalStateException(tr("History not initialized yet. Failed to set current primitive."));
337        if (current.getId() != history.getId())
338            throw new IllegalArgumentException(
339                    tr("Failed to set reference. Reference ID {0} does not match history ID {1}.", current.getId(), history.getId()));
340        HistoryOsmPrimitive primitive = history.getByVersion(current.getVersion());
341        if (primitive == null)
342            throw new IllegalArgumentException(
343                    tr("Failed to set current primitive. Current version {0} not available in history.", current.getVersion()));
344        this.current = current;
345        initTagTableModels();
346        initNodeListTableModels();
347        initMemberListTableModels();
348        fireStateChanged();
349    }
350
351    /**
352     * Replies the history OSM primitive for the {@link PointInTimeType#CURRENT_POINT_IN_TIME}
353     *
354     * @return the history OSM primitive for the {@link PointInTimeType#CURRENT_POINT_IN_TIME} (may be null)
355     */
356    public HistoryOsmPrimitive getCurrentPointInTime() {
357        return getPointInTime(PointInTimeType.CURRENT_POINT_IN_TIME);
358    }
359
360    /**
361     * Replies the history OSM primitive for the {@link PointInTimeType#REFERENCE_POINT_IN_TIME}
362     *
363     * @return the history OSM primitive for the {@link PointInTimeType#REFERENCE_POINT_IN_TIME} (may be null)
364     */
365    public HistoryOsmPrimitive getReferencePointInTime() {
366        return getPointInTime(PointInTimeType.REFERENCE_POINT_IN_TIME);
367    }
368
369    /**
370     * replies the history OSM primitive for a given point in time
371     *
372     * @param type the type of the point in time (must not be null)
373     * @return the respective primitive. Can be null.
374     * @throws IllegalArgumentException if type is null
375     */
376    public HistoryOsmPrimitive getPointInTime(PointInTimeType type) {
377        CheckParameterUtil.ensureParameterNotNull(type, "type");
378        if (type.equals(PointInTimeType.CURRENT_POINT_IN_TIME))
379            return current;
380        else if (type.equals(PointInTimeType.REFERENCE_POINT_IN_TIME))
381            return reference;
382
383        // should not happen
384        return null;
385    }
386
387    /**
388     * Returns true if <code>primitive</code> is the latest primitive
389     * representing the version currently edited in the current data
390     * layer.
391     *
392     * @param primitive the primitive to check
393     * @return true if <code>primitive</code> is the latest primitive
394     */
395    public boolean isLatest(HistoryOsmPrimitive primitive) {
396        if (primitive == null)
397            return false;
398        return primitive == latest;
399    }
400
401    /**
402     * The table model for the list of versions in the current history
403     *
404     */
405    public final class VersionTableModel extends AbstractTableModel {
406
407        private VersionTableModel() {
408        }
409
410        @Override
411        public int getRowCount() {
412            if (history == null)
413                return 0;
414            int ret = history.getNumVersions();
415            if (latest != null) {
416                ret++;
417            }
418            return ret;
419        }
420
421        @Override
422        public Object getValueAt(int row, int column) {
423            switch (column) {
424            case 0:
425                return Long.toString(getPrimitive(row).getVersion());
426            case 1:
427                return isReferencePointInTime(row);
428            case 2:
429                return isCurrentPointInTime(row);
430            case 3:
431                HistoryOsmPrimitive p3 = getPrimitive(row);
432                if (p3 != null && p3.getTimestamp() != null)
433                    return DateUtils.formatDateTime(p3.getTimestamp(), DateFormat.SHORT, DateFormat.SHORT);
434                return null;
435            case 4:
436                HistoryOsmPrimitive p4 = getPrimitive(row);
437                if (p4 != null) {
438                    User user = p4.getUser();
439                    if (user != null)
440                        return user.getName();
441                }
442                return null;
443            }
444            return null;
445        }
446
447        @Override
448        public void setValueAt(Object aValue, int row, int column) {
449            if (!((Boolean) aValue))
450                return;
451            switch (column) {
452            case 1:
453                setReferencePointInTime(row);
454                break;
455            case 2:
456                setCurrentPointInTime(row);
457                break;
458            default:
459                return;
460            }
461            fireTableDataChanged();
462        }
463
464        @Override
465        public boolean isCellEditable(int row, int column) {
466            return column >= 1 && column <= 2;
467        }
468
469        public void setReferencePointInTime(int row) {
470            if (history == null)
471                return;
472            if (row == history.getNumVersions()) {
473                if (latest != null) {
474                    HistoryBrowserModel.this.setReferencePointInTime(latest);
475                }
476                return;
477            }
478            if (row < 0 || row > history.getNumVersions())
479                return;
480            HistoryOsmPrimitive reference = history.get(row);
481            HistoryBrowserModel.this.setReferencePointInTime(reference);
482        }
483
484        public void setCurrentPointInTime(int row) {
485            if (history == null)
486                return;
487            if (row == history.getNumVersions()) {
488                if (latest != null) {
489                    HistoryBrowserModel.this.setCurrentPointInTime(latest);
490                }
491                return;
492            }
493            if (row < 0 || row > history.getNumVersions())
494                return;
495            HistoryOsmPrimitive current = history.get(row);
496            HistoryBrowserModel.this.setCurrentPointInTime(current);
497        }
498
499        public boolean isReferencePointInTime(int row) {
500            if (history == null)
501                return false;
502            if (row == history.getNumVersions())
503                return latest == reference;
504            if (row < 0 || row > history.getNumVersions())
505                return false;
506            HistoryOsmPrimitive p = history.get(row);
507            return p == reference;
508        }
509
510        public boolean isCurrentPointInTime(int row) {
511            if (history == null)
512                return false;
513            if (row == history.getNumVersions())
514                return latest == current;
515            if (row < 0 || row > history.getNumVersions())
516                return false;
517            HistoryOsmPrimitive p = history.get(row);
518            return p == current;
519        }
520
521        public HistoryOsmPrimitive getPrimitive(int row) {
522            if (history == null)
523                return null;
524            return isLatest(row) ? latest : history.get(row);
525        }
526
527        public boolean isLatest(int row) {
528            return row >= history.getNumVersions();
529        }
530
531        public OsmPrimitive getLatest() {
532            if (latest == null)
533                return null;
534            OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
535            if (editLayer == null)
536                return null;
537            return editLayer.data.getPrimitiveById(latest.getId(), latest.getType());
538        }
539
540        @Override
541        public int getColumnCount() {
542            return 6;
543        }
544    }
545
546    /**
547     * The table model for the tags of the version at {@link PointInTimeType#REFERENCE_POINT_IN_TIME}
548     * or {@link PointInTimeType#CURRENT_POINT_IN_TIME}
549     *
550     */
551    public class TagTableModel extends AbstractTableModel {
552
553        private List<String> keys;
554        private final PointInTimeType pointInTimeType;
555
556        protected TagTableModel(PointInTimeType type) {
557            pointInTimeType = type;
558            initKeyList();
559        }
560
561        protected void initKeyList() {
562            Set<String> keySet = new HashSet<>();
563            if (current != null) {
564                keySet.addAll(current.getTags().keySet());
565            }
566            if (reference != null) {
567                keySet.addAll(reference.getTags().keySet());
568            }
569            keys = new ArrayList<>(keySet);
570            Collections.sort(keys);
571            fireTableDataChanged();
572        }
573
574        @Override
575        public int getRowCount() {
576            if (keys == null)
577                return 0;
578            return keys.size();
579        }
580
581        @Override
582        public Object getValueAt(int row, int column) {
583            return keys.get(row);
584        }
585
586        public boolean hasTag(String key) {
587            HistoryOsmPrimitive primitive = getPointInTime(pointInTimeType);
588            if (primitive == null)
589                return false;
590            return primitive.hasTag(key);
591        }
592
593        public String getValue(String key) {
594            HistoryOsmPrimitive primitive = getPointInTime(pointInTimeType);
595            if (primitive == null)
596                return null;
597            return primitive.get(key);
598        }
599
600        public boolean oppositeHasTag(String key) {
601            PointInTimeType opposite = pointInTimeType.opposite();
602            HistoryOsmPrimitive primitive = getPointInTime(opposite);
603            if (primitive == null)
604                return false;
605            return primitive.hasTag(key);
606        }
607
608        public String getOppositeValue(String key) {
609            PointInTimeType opposite = pointInTimeType.opposite();
610            HistoryOsmPrimitive primitive = getPointInTime(opposite);
611            if (primitive == null)
612                return null;
613            return primitive.get(key);
614        }
615
616        public boolean hasSameValueAsOpposite(String key) {
617            String value = getValue(key);
618            String oppositeValue = getOppositeValue(key);
619            if (value == null || oppositeValue == null)
620                return false;
621            return value.equals(oppositeValue);
622        }
623
624        public PointInTimeType getPointInTimeType() {
625            return pointInTimeType;
626        }
627
628        public boolean isCurrentPointInTime() {
629            return pointInTimeType.equals(PointInTimeType.CURRENT_POINT_IN_TIME);
630        }
631
632        public boolean isReferencePointInTime() {
633            return pointInTimeType.equals(PointInTimeType.REFERENCE_POINT_IN_TIME);
634        }
635
636        @Override
637        public int getColumnCount() {
638            return 1;
639        }
640    }
641
642    protected void setLatest(HistoryOsmPrimitive latest) {
643        if (latest == null) {
644            if (this.current == this.latest) {
645                this.current = history != null ? history.getLatest() : null;
646            }
647            if (this.reference == this.latest) {
648                this.reference = history != null ? history.getLatest() : null;
649            }
650            this.latest = null;
651        } else {
652            if (this.current == this.latest) {
653                this.current = latest;
654            }
655            if (this.reference == this.latest) {
656                this.reference = latest;
657            }
658            this.latest = latest;
659        }
660        fireModelChange();
661    }
662
663    /**
664     * Removes this model as listener for data change and layer change events.
665     *
666     */
667    public void unlinkAsListener() {
668        OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
669        if (editLayer != null) {
670            editLayer.data.removeDataSetListener(this);
671        }
672        Main.getLayerManager().removeActiveLayerChangeListener(this);
673    }
674
675    /* ---------------------------------------------------------------------- */
676    /* DataSetListener                                                        */
677    /* ---------------------------------------------------------------------- */
678    @Override
679    public void nodeMoved(NodeMovedEvent event) {
680        Node node = event.getNode();
681        if (!node.isNew() && node.getId() == history.getId()) {
682            setLatest(new HistoryPrimitiveBuilder().build(node));
683        }
684    }
685
686    @Override
687    public void primitivesAdded(PrimitivesAddedEvent event) {
688        for (OsmPrimitive p: event.getPrimitives()) {
689            if (canShowAsLatest(p)) {
690                setLatest(new HistoryPrimitiveBuilder().build(p));
691            }
692        }
693    }
694
695    @Override
696    public void primitivesRemoved(PrimitivesRemovedEvent event) {
697        for (OsmPrimitive p: event.getPrimitives()) {
698            if (!p.isNew() && p.getId() == history.getId()) {
699                setLatest(null);
700            }
701        }
702    }
703
704    @Override
705    public void relationMembersChanged(RelationMembersChangedEvent event) {
706        Relation r = event.getRelation();
707        if (!r.isNew() && r.getId() == history.getId()) {
708            setLatest(new HistoryPrimitiveBuilder().build(r));
709        }
710    }
711
712    @Override
713    public void tagsChanged(TagsChangedEvent event) {
714        OsmPrimitive prim = event.getPrimitive();
715        if (!prim.isNew() && prim.getId() == history.getId()) {
716            setLatest(new HistoryPrimitiveBuilder().build(prim));
717        }
718    }
719
720    @Override
721    public void wayNodesChanged(WayNodesChangedEvent event) {
722        Way way = event.getChangedWay();
723        if (!way.isNew() && way.getId() == history.getId()) {
724            setLatest(new HistoryPrimitiveBuilder().build(way));
725        }
726    }
727
728    @Override
729    public void dataChanged(DataChangedEvent event) {
730        if (history == null)
731            return;
732        OsmPrimitive primitive = event.getDataset().getPrimitiveById(history.getId(), history.getType());
733        HistoryOsmPrimitive latest;
734        if (canShowAsLatest(primitive)) {
735            latest = new HistoryPrimitiveBuilder().build(primitive);
736        } else {
737            latest = null;
738        }
739        setLatest(latest);
740        fireModelChange();
741    }
742
743    @Override
744    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
745        // Irrelevant
746    }
747
748    /* ---------------------------------------------------------------------- */
749    /* ActiveLayerChangeListener                                              */
750    /* ---------------------------------------------------------------------- */
751    @Override
752    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
753        Layer oldLayer = e.getPreviousActiveLayer();
754        if (oldLayer instanceof OsmDataLayer) {
755            OsmDataLayer l = (OsmDataLayer) oldLayer;
756            l.data.removeDataSetListener(this);
757        }
758        Layer newLayer = e.getSource().getActiveLayer();
759        if (!(newLayer instanceof OsmDataLayer)) {
760            latest = null;
761            fireModelChange();
762            return;
763        }
764        OsmDataLayer l = (OsmDataLayer) newLayer;
765        l.data.addDataSetListener(this);
766        OsmPrimitive primitive = history != null ? l.data.getPrimitiveById(history.getId(), history.getType()) : null;
767        HistoryOsmPrimitive newLatest;
768        if (canShowAsLatest(primitive)) {
769            newLatest = new HistoryPrimitiveBuilder().build(primitive);
770        } else {
771            newLatest = null;
772        }
773        setLatest(newLatest);
774        fireModelChange();
775    }
776
777    /**
778     * Creates a {@link HistoryOsmPrimitive} from a {@link OsmPrimitive}
779     *
780     */
781    static class HistoryPrimitiveBuilder extends AbstractVisitor {
782        private HistoryOsmPrimitive clone;
783
784        @Override
785        public void visit(Node n) {
786            clone = new HistoryNode(n.getId(), n.getVersion(), n.isVisible(), getCurrentUser(), 0, null, n.getCoor(), false);
787            clone.setTags(n.getKeys());
788        }
789
790        @Override
791        public void visit(Relation r) {
792            clone = new HistoryRelation(r.getId(), r.getVersion(), r.isVisible(), getCurrentUser(), 0, null, false);
793            clone.setTags(r.getKeys());
794            HistoryRelation hr = (HistoryRelation) clone;
795            for (RelationMember rm : r.getMembers()) {
796                hr.addMember(new RelationMemberData(rm.getRole(), rm.getType(), rm.getUniqueId()));
797            }
798        }
799
800        @Override
801        public void visit(Way w) {
802            clone = new HistoryWay(w.getId(), w.getVersion(), w.isVisible(), getCurrentUser(), 0, null, false);
803            clone.setTags(w.getKeys());
804            for (Node n: w.getNodes()) {
805                ((HistoryWay) clone).addNode(n.getUniqueId());
806            }
807        }
808
809        private static User getCurrentUser() {
810            UserInfo info = JosmUserIdentityManager.getInstance().getUserInfo();
811            return info == null ? User.getAnonymous() : User.createOsmUser(info.getId(), info.getDisplayName());
812        }
813
814        public HistoryOsmPrimitive build(OsmPrimitive primitive) {
815            primitive.accept(this);
816            return clone;
817        }
818    }
819
820}