| 1 | // License: GPL. For details, see LICENSE file. |
|---|
| 2 | package org.openstreetmap.josm.actions; |
|---|
| 3 | |
|---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr; |
|---|
| 5 | |
|---|
| 6 | import java.awt.event.ActionEvent; |
|---|
| 7 | import java.awt.event.KeyEvent; |
|---|
| 8 | import java.util.ArrayList; |
|---|
| 9 | import java.util.Arrays; |
|---|
| 10 | import java.util.Collection; |
|---|
| 11 | import java.util.HashMap; |
|---|
| 12 | import java.util.List; |
|---|
| 13 | import java.util.Map; |
|---|
| 14 | |
|---|
| 15 | import java.util.Set; |
|---|
| 16 | import java.util.TreeSet; |
|---|
| 17 | import javax.swing.JOptionPane; |
|---|
| 18 | |
|---|
| 19 | import javax.swing.SwingUtilities; |
|---|
| 20 | import org.openstreetmap.josm.Main; |
|---|
| 21 | import org.openstreetmap.josm.command.AddCommand; |
|---|
| 22 | import org.openstreetmap.josm.command.ChangeCommand; |
|---|
| 23 | import org.openstreetmap.josm.command.ChangePropertyCommand; |
|---|
| 24 | import org.openstreetmap.josm.command.Command; |
|---|
| 25 | import org.openstreetmap.josm.command.SequenceCommand; |
|---|
| 26 | import org.openstreetmap.josm.data.osm.MultipolygonCreate; |
|---|
| 27 | import org.openstreetmap.josm.data.osm.MultipolygonCreate.JoinedPolygon; |
|---|
| 28 | import org.openstreetmap.josm.data.osm.OsmPrimitive; |
|---|
| 29 | import org.openstreetmap.josm.data.osm.Relation; |
|---|
| 30 | import org.openstreetmap.josm.data.osm.RelationMember; |
|---|
| 31 | import org.openstreetmap.josm.data.osm.Way; |
|---|
| 32 | import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor; |
|---|
| 33 | import org.openstreetmap.josm.tools.Shortcut; |
|---|
| 34 | |
|---|
| 35 | /** |
|---|
| 36 | * Create multipolygon from selected ways automatically. |
|---|
| 37 | * |
|---|
| 38 | * New relation with type=multipolygon is created |
|---|
| 39 | * |
|---|
| 40 | * If one or more of ways is already in relation with type=multipolygon or the |
|---|
| 41 | * way is not closed, then error is reported and no relation is created |
|---|
| 42 | * |
|---|
| 43 | * The "inner" and "outer" roles are guessed automatically. First, bbox is |
|---|
| 44 | * calculated for each way. then the largest area is assumed to be outside and |
|---|
| 45 | * the rest inside. In cases with one "outside" area and several cut-ins, the |
|---|
| 46 | * guess should be always good ... In more complex (multiple outer areas) or |
|---|
| 47 | * buggy (inner and outer ways intersect) scenarios the result is likely to be |
|---|
| 48 | * wrong. |
|---|
| 49 | */ |
|---|
| 50 | public class CreateMultipolygonAction extends JosmAction { |
|---|
| 51 | |
|---|
| 52 | public CreateMultipolygonAction() { |
|---|
| 53 | super(tr("Create multipolygon"), "multipoly_create", tr("Create multipolygon."), |
|---|
| 54 | Shortcut.registerShortcut("tools:multipoly", tr("Tool: {0}", tr("Create multipolygon")), |
|---|
| 55 | KeyEvent.VK_A, Shortcut.ALT_CTRL), true); |
|---|
| 56 | } |
|---|
| 57 | /** |
|---|
| 58 | * The action button has been clicked |
|---|
| 59 | * |
|---|
| 60 | * @param e Action Event |
|---|
| 61 | */ |
|---|
| 62 | public void actionPerformed(ActionEvent e) { |
|---|
| 63 | if (Main.main.getEditLayer() == null) { |
|---|
| 64 | JOptionPane.showMessageDialog(Main.parent, tr("No data loaded.")); |
|---|
| 65 | return; |
|---|
| 66 | } |
|---|
| 67 | |
|---|
| 68 | Collection<Way> selectedWays = Main.main.getCurrentDataSet().getSelectedWays(); |
|---|
| 69 | |
|---|
| 70 | if (selectedWays.size() < 1) { |
|---|
| 71 | // Sometimes it make sense creating multipoly of only one way (so it will form outer way) |
|---|
| 72 | // and then splitting the way later (so there are multiple ways forming outer way) |
|---|
| 73 | JOptionPane.showMessageDialog(Main.parent, tr("You must select at least one way.")); |
|---|
| 74 | return; |
|---|
| 75 | } |
|---|
| 76 | |
|---|
| 77 | MultipolygonCreate polygon = this.analyzeWays(selectedWays); |
|---|
| 78 | |
|---|
| 79 | if (polygon == null) |
|---|
| 80 | return; //could not make multipolygon. |
|---|
| 81 | |
|---|
| 82 | final Relation relation = this.createRelation(polygon); |
|---|
| 83 | |
|---|
| 84 | if (Main.pref.getBoolean("multipoly.show-relation-editor", false)) { |
|---|
| 85 | //Open relation edit window, if set up in preferences |
|---|
| 86 | RelationEditor editor = RelationEditor.getEditor(Main.main.getEditLayer(), relation, null); |
|---|
| 87 | |
|---|
| 88 | editor.setModal(true); |
|---|
| 89 | editor.setVisible(true); |
|---|
| 90 | |
|---|
| 91 | //TODO: cannot get the resulting relation from RelationEditor :(. |
|---|
| 92 | /* |
|---|
| 93 | if (relationCountBefore < relationCountAfter) { |
|---|
| 94 | //relation saved, clean up the tags |
|---|
| 95 | List<Command> list = this.removeTagsFromInnerWays(relation); |
|---|
| 96 | if (list.size() > 0) |
|---|
| 97 | { |
|---|
| 98 | Main.main.undoRedo.add(new SequenceCommand(tr("Remove tags from multipolygon inner ways"), list)); |
|---|
| 99 | } |
|---|
| 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 | } |
|---|