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