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