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