001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.relation;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.beans.PropertyChangeListener;
007import java.beans.PropertyChangeSupport;
008import java.lang.reflect.Method;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.List;
012
013import org.openstreetmap.josm.Main;
014import org.openstreetmap.josm.data.osm.Relation;
015import org.openstreetmap.josm.data.osm.RelationMember;
016import org.openstreetmap.josm.gui.ExtendedDialog;
017import org.openstreetmap.josm.gui.layer.OsmDataLayer;
018import org.openstreetmap.josm.tools.CheckParameterUtil;
019
020/**
021 * Abstract relation editor.
022 * @since 1599
023 */
024public abstract class RelationEditor extends ExtendedDialog implements IRelationEditor {
025
026    /** the property name for the current relation.
027     * @see #setRelation(Relation)
028     * @see #getRelation()
029     */
030    public static final String RELATION_PROP = RelationEditor.class.getName() + ".relation";
031
032    /** the property name for the current relation snapshot
033     * @see #getRelationSnapshot()
034     */
035    public static final String RELATION_SNAPSHOT_PROP = RelationEditor.class.getName() + ".relationSnapshot";
036
037    /** the list of registered relation editor classes */
038    private static List<Class<RelationEditor>> editors = new ArrayList<>();
039
040    /** The relation that this editor is working on. */
041    private transient Relation relation;
042
043    /** The version of the relation when editing is started. This is null if a new relation is created. */
044    private transient Relation relationSnapshot;
045
046    /** The data layer the relation belongs to */
047    private final transient OsmDataLayer layer;
048
049    private final PropertyChangeSupport support = new PropertyChangeSupport(this);
050
051    /**
052     * Creates a new relation editor
053     *
054     * @param layer the {@link OsmDataLayer} in whose context a relation is edited. Must not be null.
055     * @param relation the relation. Can be null if a new relation is to be edited.
056     * @throws IllegalArgumentException if layer is null
057     */
058    protected RelationEditor(OsmDataLayer layer, Relation relation) {
059        super(Main.parent,
060                "",
061                new String[] {tr("Apply Changes"), tr("Cancel")},
062                false,
063                false
064        );
065        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
066        this.layer = layer;
067        setRelation(relation);
068        layer.removeRecentRelation(relation);
069    }
070
071    /**
072     * Registers a relation editor class. Depending on the type of relation to be edited
073     * {@link #getEditor(OsmDataLayer, Relation, Collection)} will create an instance of
074     * this class.
075     *
076     * @param clazz the class
077     */
078    public void registerRelationEditor(Class<RelationEditor> clazz) {
079        if (clazz != null && !editors.contains(clazz)) {
080            editors.add(clazz);
081        }
082    }
083
084    /**
085     * This is a factory method that creates an appropriate RelationEditor instance suitable for editing the relation
086     * that was passed in as an argument.
087     *
088     * This method is guaranteed to return a working RelationEditor. If no specific editor has been registered for the
089     * type of relation, then a generic editor will be returned.
090     *
091     * Editors can be registered by adding their class to the static list "editors" in the RelationEditor class.
092     * When it comes to editing a relation, all registered editors are queried via their static "canEdit" method whether
093     * they feel responsible for that kind of relation, and if they return true then an instance of that class will be used.
094     *
095     * @param layer the data layer the relation is a member of
096     * @param r the relation to be edited
097     * @param selectedMembers a collection of relation members which shall be selected when the editor is first launched
098     * @return an instance of RelationEditor suitable for editing that kind of relation
099     */
100    public static RelationEditor getEditor(OsmDataLayer layer, Relation r, Collection<RelationMember> selectedMembers) {
101        for (Class<RelationEditor> e : editors) {
102            try {
103                Method m = e.getMethod("canEdit", Relation.class);
104                Boolean canEdit = (Boolean) m.invoke(null, r);
105                if (canEdit) {
106                    return e.getConstructor(Relation.class, Collection.class).newInstance(layer, r, selectedMembers);
107                }
108            } catch (ReflectiveOperationException ex) {
109                Main.warn(ex);
110            }
111        }
112        if (RelationDialogManager.getRelationDialogManager().isOpenInEditor(layer, r))
113            return RelationDialogManager.getRelationDialogManager().getEditorForRelation(layer, r);
114        else {
115            RelationEditor editor = new GenericRelationEditor(layer, r, selectedMembers);
116            RelationDialogManager.getRelationDialogManager().positionOnScreen(editor);
117            RelationDialogManager.getRelationDialogManager().register(layer, r, editor);
118            return editor;
119        }
120    }
121
122    /**
123     * updates the title of the relation editor
124     */
125    protected void updateTitle() {
126        if (getRelation() == null) {
127            setTitle(tr("Create new relation in layer ''{0}''", layer.getName()));
128        } else if (getRelation().isNew()) {
129            setTitle(tr("Edit new relation in layer ''{0}''", layer.getName()));
130        } else {
131            setTitle(tr("Edit relation #{0} in layer ''{1}''", relation.getId(), layer.getName()));
132        }
133    }
134
135    @Override
136    public final Relation getRelation() {
137        return relation;
138    }
139
140    @Override
141    public final void setRelation(Relation relation) {
142        setRelationSnapshot((relation == null) ? null : new Relation(relation));
143        Relation oldValue = this.relation;
144        this.relation = relation;
145        if (this.relation != oldValue) {
146            support.firePropertyChange(RELATION_PROP, oldValue, this.relation);
147        }
148        updateTitle();
149    }
150
151    @Override
152    public final OsmDataLayer getLayer() {
153        return layer;
154    }
155
156    @Override
157    public final Relation getRelationSnapshot() {
158        return relationSnapshot;
159    }
160
161    protected final void setRelationSnapshot(Relation snapshot) {
162        Relation oldValue = relationSnapshot;
163        relationSnapshot = snapshot;
164        if (relationSnapshot != oldValue) {
165            support.firePropertyChange(RELATION_SNAPSHOT_PROP, oldValue, relationSnapshot);
166        }
167    }
168
169    @Override
170    public final boolean isDirtyRelation() {
171        return !relation.hasEqualSemanticAttributes(relationSnapshot);
172    }
173
174    /* ----------------------------------------------------------------------- */
175    /* property change support                                                 */
176    /* ----------------------------------------------------------------------- */
177
178    @Override
179    public final void addPropertyChangeListener(PropertyChangeListener listener) {
180        this.support.addPropertyChangeListener(listener);
181    }
182
183    @Override
184    public final void removePropertyChangeListener(PropertyChangeListener listener) {
185        this.support.removePropertyChangeListener(listener);
186    }
187
188    @Override
189    public void dispose() {
190        layer.setRecentRelation(relation);
191        super.dispose();
192    }
193}