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}