001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.event.ActionEvent;
007import java.awt.event.KeyEvent;
008import java.util.ArrayList;
009import java.util.Arrays;
010import java.util.Collection;
011import java.util.HashMap;
012import java.util.List;
013import java.util.Map;
014import java.util.Map.Entry;
015import java.util.Set;
016import java.util.TreeSet;
017
018import javax.swing.JOptionPane;
019import javax.swing.SwingUtilities;
020
021import org.openstreetmap.josm.Main;
022import org.openstreetmap.josm.command.AddCommand;
023import org.openstreetmap.josm.command.ChangeCommand;
024import org.openstreetmap.josm.command.ChangePropertyCommand;
025import org.openstreetmap.josm.command.Command;
026import org.openstreetmap.josm.command.SequenceCommand;
027import org.openstreetmap.josm.data.osm.MultipolygonCreate;
028import org.openstreetmap.josm.data.osm.MultipolygonCreate.JoinedPolygon;
029import org.openstreetmap.josm.data.osm.OsmPrimitive;
030import org.openstreetmap.josm.data.osm.Relation;
031import org.openstreetmap.josm.data.osm.RelationMember;
032import org.openstreetmap.josm.data.osm.Way;
033import org.openstreetmap.josm.gui.Notification;
034import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
035import org.openstreetmap.josm.tools.Shortcut;
036
037/**
038 * Create multipolygon from selected ways automatically.
039 *
040 * New relation with type=multipolygon is created
041 *
042 * If one or more of ways is already in relation with type=multipolygon or the
043 * way is not closed, then error is reported and no relation is created
044 *
045 * The "inner" and "outer" roles are guessed automatically. First, bbox is
046 * calculated for each way. then the largest area is assumed to be outside and
047 * the rest inside. In cases with one "outside" area and several cut-ins, the
048 * guess should be always good ... In more complex (multiple outer areas) or
049 * buggy (inner and outer ways intersect) scenarios the result is likely to be
050 * wrong.
051 */
052public class CreateMultipolygonAction extends JosmAction {
053
054    /**
055     * Constructs a new {@code CreateMultipolygonAction}.
056     */
057    public CreateMultipolygonAction() {
058        super(tr("Create multipolygon"), "multipoly_create", tr("Create multipolygon."),
059            Shortcut.registerShortcut("tools:multipoly", tr("Tool: {0}", tr("Create multipolygon")),
060            KeyEvent.VK_A, Shortcut.ALT_CTRL), true);
061    }
062    /**
063     * The action button has been clicked
064     *
065     * @param e Action Event
066     */
067    @Override
068    public void actionPerformed(ActionEvent e) {
069        if (!Main.main.hasEditLayer()) {
070            new Notification(
071                    tr("No data loaded."))
072                    .setIcon(JOptionPane.WARNING_MESSAGE)
073                    .setDuration(Notification.TIME_SHORT)
074                    .show();
075            return;
076        }
077
078        Collection<Way> selectedWays = Main.main.getCurrentDataSet().getSelectedWays();
079
080        if (selectedWays.size() < 1) {
081            // Sometimes it make sense creating multipoly of only one way (so it will form outer way)
082            // and then splitting the way later (so there are multiple ways forming outer way)
083            new Notification(
084                    tr("You must select at least one way."))
085                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
086                    .setDuration(Notification.TIME_SHORT)
087                    .show();
088            return;
089        }
090
091        MultipolygonCreate polygon = this.analyzeWays(selectedWays);
092
093        if (polygon == null)
094            return;                   //could not make multipolygon.
095
096        final Relation relation = this.createRelation(polygon);
097
098        if (Main.pref.getBoolean("multipoly.show-relation-editor", false)) {
099            //Open relation edit window, if set up in preferences
100            RelationEditor editor = RelationEditor.getEditor(Main.main.getEditLayer(), relation, null);
101
102            editor.setModal(true);
103            editor.setVisible(true);
104
105            //TODO: cannot get the resulting relation from RelationEditor :(.
106            /*
107            if (relationCountBefore < relationCountAfter) {
108                //relation saved, clean up the tags
109                List<Command> list = this.removeTagsFromInnerWays(relation);
110                if (list.size() > 0)
111                {
112                    Main.main.undoRedo.add(new SequenceCommand(tr("Remove tags from multipolygon inner ways"), list));
113                }
114            }
115             */
116
117        } else {
118            //Just add the relation
119            List<Command> list = this.removeTagsFromWaysIfNeeded(relation);
120            list.add(new AddCommand(relation));
121            Main.main.undoRedo.add(new SequenceCommand(tr("Create multipolygon"), list));
122            // Use 'SwingUtilities.invokeLater' to make sure the relationListDialog
123            // knows about the new relation before we try to select it.
124            // (Yes, we are already in event dispatch thread. But DatasetEventManager
125            // uses 'SwingUtilities.invokeLater' to fire events so we have to do
126            // the same.)
127            SwingUtilities.invokeLater(new Runnable() {
128                @Override
129                public void run() {
130                    Main.map.relationListDialog.selectRelation(relation);
131                }
132            });
133        }
134
135
136    }
137
138    /** Enable this action only if something is selected */
139    @Override protected void updateEnabledState() {
140        if (getCurrentDataSet() == null) {
141            setEnabled(false);
142        } else {
143            updateEnabledState(getCurrentDataSet().getSelected());
144        }
145    }
146
147    /**
148      * Enable this action only if something is selected
149      *
150      * @param selection the current selection, gets tested for emptyness
151      */
152    @Override protected void updateEnabledState(Collection < ? extends OsmPrimitive > selection) {
153        setEnabled(selection != null && !selection.isEmpty());
154    }
155
156    /**
157     * This method analyzes ways and creates multipolygon.
158     * @param selectedWays list of selected ways
159     * @return <code>null</code>, if there was a problem with the ways.
160     */
161    private MultipolygonCreate analyzeWays(Collection < Way > selectedWays) {
162
163        MultipolygonCreate pol = new MultipolygonCreate();
164        String error = pol.makeFromWays(selectedWays);
165
166        if (error != null) {
167            new Notification(error)
168                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
169                    .show();
170            return null;
171        } else {
172            return pol;
173        }
174    }
175
176    /**
177     * Builds a relation from polygon ways.
178     * @param pol data storage class containing polygon information
179     * @return multipolygon relation
180     */
181    private Relation createRelation(MultipolygonCreate pol) {
182        // Create new relation
183        Relation rel = new Relation();
184        rel.put("type", "multipolygon");
185        // Add ways to it
186        for (JoinedPolygon jway:pol.outerWays) {
187            for (Way way:jway.ways) {
188                rel.addMember(new RelationMember("outer", way));
189            }
190        }
191
192        for (JoinedPolygon jway:pol.innerWays) {
193            for (Way way:jway.ways) {
194                rel.addMember(new RelationMember("inner", way));
195            }
196        }
197        return rel;
198    }
199
200    static public final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList(new String[] {"barrier", "source"});
201
202    /**
203     * This method removes tags/value pairs from inner and outer ways and put them on relation if necessary
204     * Function was extended in reltoolbox plugin by Zverikk and copied back to the core
205     * @param relation the multipolygon style relation to process
206     * @return a list of commands to execute
207     */
208    private List<Command> removeTagsFromWaysIfNeeded( Relation relation ) {
209        Map<String, String> values = new HashMap<String, String>();
210
211        if( relation.hasKeys() ) {
212            for( String key : relation.keySet() ) {
213                values.put(key, relation.get(key));
214            }
215        }
216
217        List<Way> innerWays = new ArrayList<Way>();
218        List<Way> outerWays = new ArrayList<Way>();
219
220        Set<String> conflictingKeys = new TreeSet<String>();
221
222        for( RelationMember m : relation.getMembers() ) {
223
224            if( m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys() ) {
225                innerWays.add(m.getWay());
226            }
227
228            if( m.hasRole() && "outer".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys() ) {
229                Way way = m.getWay();
230                outerWays.add(way);
231
232                for( String key : way.keySet() ) {
233                    if( !values.containsKey(key) ) { //relation values take precedence
234                        values.put(key, way.get(key));
235                    } else if( !relation.hasKey(key) && !values.get(key).equals(way.get(key)) ) {
236                        conflictingKeys.add(key);
237                    }
238                }
239            }
240        }
241
242        // filter out empty key conflicts - we need second iteration
243        if( !Main.pref.getBoolean("multipoly.alltags", false) )
244            for( RelationMember m : relation.getMembers() )
245                if( m.hasRole() && m.getRole().equals("outer") && m.isWay() )
246                    for( String key : values.keySet() )
247                        if( !m.getWay().hasKey(key) && !relation.hasKey(key) )
248                            conflictingKeys.add(key);
249
250        for( String key : conflictingKeys )
251            values.remove(key);
252
253        for( String linearTag : Main.pref.getCollection("multipoly.lineartagstokeep", DEFAULT_LINEAR_TAGS) )
254            values.remove(linearTag);
255
256        if( values.containsKey("natural") && values.get("natural").equals("coastline") )
257            values.remove("natural");
258
259        values.put("area", "yes");
260
261        List<Command> commands = new ArrayList<Command>();
262        boolean moveTags = Main.pref.getBoolean("multipoly.movetags", true);
263
264        for (Entry<String, String> entry : values.entrySet()) {
265            List<OsmPrimitive> affectedWays = new ArrayList<OsmPrimitive>();
266            String key = entry.getKey();
267            String value = entry.getValue();
268
269            for (Way way : innerWays) {
270                if (way.hasKey(key) && (value.equals(way.get(key)))) {
271                    affectedWays.add(way);
272                }
273            }
274
275            if (moveTags) {
276                // remove duplicated tags from outer ways
277                for( Way way : outerWays ) {
278                    if( way.hasKey(key) ) {
279                        affectedWays.add(way);
280                    }
281                }
282            }
283
284            if (!affectedWays.isEmpty()) {
285                // reset key tag on affected ways
286                commands.add(new ChangePropertyCommand(affectedWays, key, null));
287            }
288        }
289
290        if (moveTags) {
291            // add those tag values to the relation
292
293            boolean fixed = false;
294            Relation r2 = new Relation(relation);
295            for (Entry<String, String> entry : values.entrySet()) {
296                String key = entry.getKey();
297                if (!r2.hasKey(key) && !key.equals("area") ) {
298                    if (relation.isNew())
299                        relation.put(key, entry.getValue());
300                    else
301                        r2.put(key, entry.getValue());
302                    fixed = true;
303                }
304            }
305            if (fixed && !relation.isNew())
306                commands.add(new ChangeCommand(relation, r2));
307        }
308
309        return commands;
310    }
311}