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