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