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

Last change on this file since 7302 was 7012, checked in by Don-vip, 10 years ago

see #8465 - use String switch/case where applicable

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