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