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