source: josm/trunk/src/org/openstreetmap/josm/actions/CreateMultipolygonAction.java@ 6565

Last change on this file since 6565 was 6564, checked in by simon04, 10 years ago

fix 9492 - Tools>"Create Multipolygon": updates multipolygon if a multipolygon relation is selected

  • Property svn:eol-style set to native
File size: 14.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.event.ActionEvent;
7import java.awt.event.KeyEvent;
8import java.util.ArrayList;
9import java.util.Arrays;
10import java.util.Collection;
11import java.util.HashMap;
12import java.util.HashSet;
13import java.util.List;
14import java.util.Map;
15import java.util.Map.Entry;
16import java.util.Set;
17import java.util.TreeSet;
18
19import javax.swing.JOptionPane;
20import javax.swing.SwingUtilities;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.command.AddCommand;
24import org.openstreetmap.josm.command.ChangeCommand;
25import org.openstreetmap.josm.command.ChangePropertyCommand;
26import org.openstreetmap.josm.command.Command;
27import org.openstreetmap.josm.command.SequenceCommand;
28import org.openstreetmap.josm.data.osm.MultipolygonCreate;
29import org.openstreetmap.josm.data.osm.MultipolygonCreate.JoinedPolygon;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.data.osm.Relation;
32import org.openstreetmap.josm.data.osm.RelationMember;
33import org.openstreetmap.josm.data.osm.Way;
34import org.openstreetmap.josm.gui.Notification;
35import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
36import org.openstreetmap.josm.tools.Pair;
37import org.openstreetmap.josm.tools.Shortcut;
38import org.openstreetmap.josm.tools.Utils;
39
40/**
41 * Create multipolygon from selected ways automatically.
42 *
43 * New relation with type=multipolygon is created
44 *
45 * If one or more of ways is already in relation with type=multipolygon or the
46 * way is not closed, then error is reported and no relation is created
47 *
48 * The "inner" and "outer" roles are guessed automatically. First, bbox is
49 * calculated for each way. then the largest area is assumed to be outside and
50 * the rest inside. In cases with one "outside" area and several cut-ins, the
51 * guess should be always good ... In more complex (multiple outer areas) or
52 * buggy (inner and outer ways intersect) scenarios the result is likely to be
53 * wrong.
54 */
55public class CreateMultipolygonAction extends JosmAction {
56
57 /**
58 * Constructs a new {@code CreateMultipolygonAction}.
59 */
60 public CreateMultipolygonAction() {
61 super(tr("Create multipolygon"), "multipoly_create", tr("Create multipolygon"),
62 Shortcut.registerShortcut("tools:multipoly", tr("Tool: {0}", tr("Create multipolygon")),
63 KeyEvent.VK_A, Shortcut.ALT_CTRL), true);
64 }
65 /**
66 * The action button has been clicked
67 *
68 * @param e Action Event
69 */
70 @Override
71 public void actionPerformed(ActionEvent e) {
72 if (!Main.main.hasEditLayer()) {
73 new Notification(
74 tr("No data loaded."))
75 .setIcon(JOptionPane.WARNING_MESSAGE)
76 .setDuration(Notification.TIME_SHORT)
77 .show();
78 return;
79 }
80
81 final Collection<Way> selectedWays = Main.main.getCurrentDataSet().getSelectedWays();
82 final Collection<Relation> selectedRelations = Main.main.getCurrentDataSet().getSelectedRelations();
83
84 if (selectedWays.size() < 1) {
85 // Sometimes it make sense creating multipoly of only one way (so it will form outer way)
86 // and then splitting the way later (so there are multiple ways forming outer way)
87 new Notification(
88 tr("You must select at least one way."))
89 .setIcon(JOptionPane.INFORMATION_MESSAGE)
90 .setDuration(Notification.TIME_SHORT)
91 .show();
92 return;
93 }
94
95 final Pair<SequenceCommand, Relation> commandAndRelation = createMultipolygonCommand(selectedWays, selectedRelations);
96 final Command command = commandAndRelation.a;
97 final Relation relation = commandAndRelation.b;
98 if (command == null) {
99 return;
100 }
101 Main.main.undoRedo.add(command);
102
103 // Use 'SwingUtilities.invokeLater' to make sure the relationListDialog
104 // knows about the new relation before we try to select it.
105 // (Yes, we are already in event dispatch thread. But DatasetEventManager
106 // uses 'SwingUtilities.invokeLater' to fire events so we have to do
107 // the same.)
108 SwingUtilities.invokeLater(new Runnable() {
109 @Override
110 public void run() {
111 Main.map.relationListDialog.selectRelation(relation);
112 if (Main.pref.getBoolean("multipoly.show-relation-editor", false)) {
113 //Open relation edit window, if set up in preferences
114 RelationEditor editor = RelationEditor.getEditor(Main.main.getEditLayer(), relation, null);
115
116 editor.setModal(true);
117 editor.setVisible(true);
118 }
119 }
120 });
121
122 }
123
124 private static Relation getSelectedMultipolygonRelation(Collection<Relation> selectedRelations) {
125 return selectedRelations.size() == 1 && "multipolygon".equals(selectedRelations.iterator().next().get("type"))
126 ? selectedRelations.iterator().next()
127 : null;
128 }
129
130 /**
131 * Returns a {@link Pair} of the old multipolygon {@link Relation} (or null) and the newly created/modified multipolygon {@link Relation}.
132 */
133 public static Pair<Relation, Relation> createMultipolygonRelation(Collection<Way> selectedWays, Collection<Relation> selectedRelations) {
134
135 final Relation selectedMultipolygonRelation = getSelectedMultipolygonRelation(selectedRelations);
136 if (selectedMultipolygonRelation != null) {
137 // add ways of existing relation to include them in polygon analysis
138 selectedWays = new HashSet<Way>(selectedWays);
139 selectedWays.addAll(Utils.filteredCollection(selectedMultipolygonRelation.getMemberPrimitives(), Way.class));
140 }
141
142 final MultipolygonCreate polygon = analyzeWays(selectedWays);
143 if (polygon == null) {
144 return null; //could not make multipolygon.
145 }
146
147 if (selectedMultipolygonRelation != null) {
148 return Pair.create(selectedMultipolygonRelation, createRelation(polygon, new Relation(selectedMultipolygonRelation)));
149 } else {
150 return Pair.create(null, createRelation(polygon, new Relation()));
151 }
152 }
153
154 /**
155 * Returns a pair of a multipolygon creating/modifying {@link Command} as well as the multipolygon {@link Relation}.
156 */
157 public static Pair<SequenceCommand, Relation> createMultipolygonCommand(Collection<Way> selectedWays, Collection<Relation> selectedRelations) {
158
159 final Pair<Relation, Relation> rr = createMultipolygonRelation(selectedWays, selectedRelations);
160 if (rr == null) {
161 return null;
162 }
163 final Relation existingRelation = rr.a;
164 final Relation relation = rr.b;
165
166 final List<Command> list = removeTagsFromWaysIfNeeded(relation);
167 final String commandName;
168 if (existingRelation == null) {
169 list.add(new AddCommand(relation));
170 commandName = tr("Create multipolygon");
171 } else {
172 list.add(new ChangeCommand(existingRelation, relation));
173 commandName = tr("Update multipolygon");
174 }
175 return Pair.create(new SequenceCommand(commandName, list), relation);
176 }
177
178 /** Enable this action only if something is selected */
179 @Override protected void updateEnabledState() {
180 if (getCurrentDataSet() == null) {
181 setEnabled(false);
182 } else {
183 updateEnabledState(getCurrentDataSet().getSelected());
184 }
185 }
186
187 /**
188 * Enable this action only if something is selected
189 *
190 * @param selection the current selection, gets tested for emptyness
191 */
192 @Override protected void updateEnabledState(Collection < ? extends OsmPrimitive > selection) {
193 setEnabled(selection != null && !selection.isEmpty());
194 putValue(NAME, getSelectedMultipolygonRelation(getCurrentDataSet().getSelectedRelations()) != null
195 ? tr("Update multipolygon")
196 : tr("Create multipolygon")
197 );
198 }
199
200 /**
201 * This method analyzes ways and creates multipolygon.
202 * @param selectedWays list of selected ways
203 * @return <code>null</code>, if there was a problem with the ways.
204 */
205 private static MultipolygonCreate analyzeWays(Collection < Way > selectedWays) {
206
207 MultipolygonCreate pol = new MultipolygonCreate();
208 String error = pol.makeFromWays(selectedWays);
209
210 if (error != null) {
211 new Notification(error)
212 .setIcon(JOptionPane.INFORMATION_MESSAGE)
213 .show();
214 return null;
215 } else {
216 return pol;
217 }
218 }
219
220 /**
221 * Builds a relation from polygon ways.
222 * @param pol data storage class containing polygon information
223 * @return multipolygon relation
224 */
225 private static Relation createRelation(MultipolygonCreate pol, final Relation rel) {
226 // Create new relation
227 rel.put("type", "multipolygon");
228 // Add ways to it
229 for (JoinedPolygon jway:pol.outerWays) {
230 addMembers(jway, rel, "outer");
231 }
232
233 for (JoinedPolygon jway:pol.innerWays) {
234 addMembers(jway, rel, "inner");
235 }
236 return rel;
237 }
238
239 private static void addMembers(JoinedPolygon polygon, Relation rel, String role) {
240 final int count = rel.getMembersCount();
241 final HashSet<Way> ways = new HashSet<Way>(polygon.ways);
242 for (int i = 0; i < count; i++) {
243 final RelationMember m = rel.getMember(i);
244 if (ways.contains(m.getMember()) && !role.equals(m.getRole())) {
245 rel.setMember(i, new RelationMember(role, m.getMember()));
246 }
247 }
248 ways.removeAll(rel.getMemberPrimitives());
249 for (final Way way : ways) {
250 rel.addMember(new RelationMember(role, way));
251 }
252 }
253
254 static public final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList(new String[] {"barrier", "source"});
255
256 /**
257 * This method removes tags/value pairs from inner and outer ways and put them on relation if necessary
258 * Function was extended in reltoolbox plugin by Zverikk and copied back to the core
259 * @param relation the multipolygon style relation to process
260 * @return a list of commands to execute
261 */
262 private static List<Command> removeTagsFromWaysIfNeeded( Relation relation ) {
263 Map<String, String> values = new HashMap<String, String>();
264
265 if( relation.hasKeys() ) {
266 for( String key : relation.keySet() ) {
267 values.put(key, relation.get(key));
268 }
269 }
270
271 List<Way> innerWays = new ArrayList<Way>();
272 List<Way> outerWays = new ArrayList<Way>();
273
274 Set<String> conflictingKeys = new TreeSet<String>();
275
276 for( RelationMember m : relation.getMembers() ) {
277
278 if( m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys() ) {
279 innerWays.add(m.getWay());
280 }
281
282 if( m.hasRole() && "outer".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys() ) {
283 Way way = m.getWay();
284 outerWays.add(way);
285
286 for( String key : way.keySet() ) {
287 if( !values.containsKey(key) ) { //relation values take precedence
288 values.put(key, way.get(key));
289 } else if( !relation.hasKey(key) && !values.get(key).equals(way.get(key)) ) {
290 conflictingKeys.add(key);
291 }
292 }
293 }
294 }
295
296 // filter out empty key conflicts - we need second iteration
297 if( !Main.pref.getBoolean("multipoly.alltags", false) )
298 for( RelationMember m : relation.getMembers() )
299 if( m.hasRole() && m.getRole().equals("outer") && m.isWay() )
300 for( String key : values.keySet() )
301 if( !m.getWay().hasKey(key) && !relation.hasKey(key) )
302 conflictingKeys.add(key);
303
304 for( String key : conflictingKeys )
305 values.remove(key);
306
307 for( String linearTag : Main.pref.getCollection("multipoly.lineartagstokeep", DEFAULT_LINEAR_TAGS) )
308 values.remove(linearTag);
309
310 if( values.containsKey("natural") && values.get("natural").equals("coastline") )
311 values.remove("natural");
312
313 values.put("area", "yes");
314
315 List<Command> commands = new ArrayList<Command>();
316 boolean moveTags = Main.pref.getBoolean("multipoly.movetags", true);
317
318 for (Entry<String, String> entry : values.entrySet()) {
319 List<OsmPrimitive> affectedWays = new ArrayList<OsmPrimitive>();
320 String key = entry.getKey();
321 String value = entry.getValue();
322
323 for (Way way : innerWays) {
324 if (way.hasKey(key) && (value.equals(way.get(key)))) {
325 affectedWays.add(way);
326 }
327 }
328
329 if (moveTags) {
330 // remove duplicated tags from outer ways
331 for( Way way : outerWays ) {
332 if( way.hasKey(key) ) {
333 affectedWays.add(way);
334 }
335 }
336 }
337
338 if (!affectedWays.isEmpty()) {
339 // reset key tag on affected ways
340 commands.add(new ChangePropertyCommand(affectedWays, key, null));
341 }
342 }
343
344 if (moveTags) {
345 // add those tag values to the relation
346
347 boolean fixed = false;
348 Relation r2 = new Relation(relation);
349 for (Entry<String, String> entry : values.entrySet()) {
350 String key = entry.getKey();
351 if (!r2.hasKey(key) && !key.equals("area") ) {
352 if (relation.isNew())
353 relation.put(key, entry.getValue());
354 else
355 r2.put(key, entry.getValue());
356 fixed = true;
357 }
358 }
359 if (fixed && !relation.isNew())
360 commands.add(new ChangeCommand(relation, r2));
361 }
362
363 return commands;
364 }
365}
Note: See TracBrowser for help on using the repository browser.