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 }