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

Last change on this file since 7746 was 7668, checked in by stoecker, 9 years ago

cleanup icons, mark undetected icons, set proper mimetype, delete unused icons, update geticons script

  • Property svn:eol-style set to native
File size: 16.9 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.MultipolygonBuilder;
30import org.openstreetmap.josm.data.osm.MultipolygonBuilder.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), /* ICON */ "multipoly_create", getName(update),
67 /* atleast three lines for each shortcut or the server extractor fails */
68 update ? Shortcut.registerShortcut("tools:multipoly_update",
69 tr("Tool: {0}", getName(true)),
70 KeyEvent.VK_B, Shortcut.CTRL_SHIFT)
71 : Shortcut.registerShortcut("tools:multipoly_create",
72 tr("Tool: {0}", getName(false)),
73 KeyEvent.VK_B, Shortcut.CTRL),
74 true, update ? "multipoly_update" : "multipoly_create", true);
75 this.update = update;
76 }
77
78 private static String getName(boolean update) {
79 return update ? tr("Update multipolygon") : tr("Create multipolygon");
80 }
81
82 private static class CreateUpdateMultipolygonTask implements Runnable {
83 private final Collection<Way> selectedWays;
84 private final Relation multipolygonRelation;
85
86 public CreateUpdateMultipolygonTask(Collection<Way> selectedWays, Relation multipolygonRelation) {
87 this.selectedWays = selectedWays;
88 this.multipolygonRelation = multipolygonRelation;
89 }
90
91 @Override
92 public void run() {
93 final Pair<SequenceCommand, Relation> commandAndRelation = createMultipolygonCommand(selectedWays, multipolygonRelation);
94 if (commandAndRelation == null) {
95 return;
96 }
97 final Command command = commandAndRelation.a;
98 final Relation relation = commandAndRelation.b;
99
100 // to avoid EDT violations
101 SwingUtilities.invokeLater(new Runnable() {
102 @Override
103 public void run() {
104 Main.main.undoRedo.add(command);
105
106 // Use 'SwingUtilities.invokeLater' to make sure the relationListDialog
107 // knows about the new relation before we try to select it.
108 // (Yes, we are already in event dispatch thread. But DatasetEventManager
109 // uses 'SwingUtilities.invokeLater' to fire events so we have to do the same.)
110 SwingUtilities.invokeLater(new Runnable() {
111 @Override
112 public void run() {
113 Main.map.relationListDialog.selectRelation(relation);
114 if (Main.pref.getBoolean("multipoly.show-relation-editor", false)) {
115 //Open relation edit window, if set up in preferences
116 RelationEditor editor = RelationEditor.getEditor(Main.main.getEditLayer(), relation, null);
117
118 editor.setModal(true);
119 editor.setVisible(true);
120 }
121 }
122 });
123 }
124 });
125 }
126 }
127
128 @Override
129 public void actionPerformed(ActionEvent e) {
130 if (!Main.main.hasEditLayer()) {
131 new Notification(
132 tr("No data loaded."))
133 .setIcon(JOptionPane.WARNING_MESSAGE)
134 .setDuration(Notification.TIME_SHORT)
135 .show();
136 return;
137 }
138
139 final Collection<Way> selectedWays = Main.main.getCurrentDataSet().getSelectedWays();
140 final Collection<Relation> selectedRelations = Main.main.getCurrentDataSet().getSelectedRelations();
141
142 if (selectedWays.size() < 1) {
143 // Sometimes it make sense creating multipoly of only one way (so it will form outer way)
144 // and then splitting the way later (so there are multiple ways forming outer way)
145 new Notification(
146 tr("You must select at least one way."))
147 .setIcon(JOptionPane.INFORMATION_MESSAGE)
148 .setDuration(Notification.TIME_SHORT)
149 .show();
150 return;
151 }
152
153 final Relation multipolygonRelation = update
154 ? getSelectedMultipolygonRelation(selectedWays, selectedRelations)
155 : null;
156
157 // download incomplete relation if necessary
158 if (multipolygonRelation != null && (multipolygonRelation.isIncomplete() || multipolygonRelation.hasIncompleteMembers())) {
159 Main.worker.submit(new DownloadRelationTask(Collections.singleton(multipolygonRelation), Main.main.getEditLayer()));
160 }
161 // create/update multipolygon relation
162 Main.worker.submit(new CreateUpdateMultipolygonTask(selectedWays, multipolygonRelation));
163
164 }
165
166 private Relation getSelectedMultipolygonRelation() {
167 return getSelectedMultipolygonRelation(getCurrentDataSet().getSelectedWays(), getCurrentDataSet().getSelectedRelations());
168 }
169
170 private static Relation getSelectedMultipolygonRelation(Collection<Way> selectedWays, Collection<Relation> selectedRelations) {
171 if (selectedRelations.size() == 1 && "multipolygon".equals(selectedRelations.iterator().next().get("type"))) {
172 return selectedRelations.iterator().next();
173 } else {
174 final HashSet<Relation> relatedRelations = new HashSet<>();
175 for (final Way w : selectedWays) {
176 relatedRelations.addAll(Utils.filteredCollection(w.getReferrers(), Relation.class));
177 }
178 return relatedRelations.size() == 1 ? relatedRelations.iterator().next() : null;
179 }
180 }
181
182 /**
183 * Returns a {@link Pair} of the old multipolygon {@link Relation} (or null) and the newly created/modified multipolygon {@link Relation}.
184 */
185 public static Pair<Relation, Relation> updateMultipolygonRelation(Collection<Way> selectedWays, Relation selectedMultipolygonRelation) {
186
187 // add ways of existing relation to include them in polygon analysis
188 Set<Way> ways = new HashSet<>(selectedWays);
189 ways.addAll(selectedMultipolygonRelation.getMemberPrimitives(Way.class));
190
191 final MultipolygonBuilder polygon = analyzeWays(ways, true);
192 if (polygon == null) {
193 return null; //could not make multipolygon.
194 } else {
195 return Pair.create(selectedMultipolygonRelation, createRelation(polygon, new Relation(selectedMultipolygonRelation)));
196 }
197 }
198
199 /**
200 * Returns a {@link Pair} null and the newly created/modified multipolygon {@link Relation}.
201 */
202 public static Pair<Relation, Relation> createMultipolygonRelation(Collection<Way> selectedWays, boolean showNotif) {
203
204 final MultipolygonBuilder polygon = analyzeWays(selectedWays, showNotif);
205 if (polygon == null) {
206 return null; //could not make multipolygon.
207 } else {
208 return Pair.create(null, createRelation(polygon, new Relation()));
209 }
210 }
211
212 /**
213 * Returns a {@link Pair} of a multipolygon creating/modifying {@link Command} as well as the multipolygon {@link Relation}.
214 */
215 public static Pair<SequenceCommand, Relation> createMultipolygonCommand(Collection<Way> selectedWays, Relation selectedMultipolygonRelation) {
216
217 final Pair<Relation, Relation> rr = selectedMultipolygonRelation == null
218 ? createMultipolygonRelation(selectedWays, true)
219 : updateMultipolygonRelation(selectedWays, selectedMultipolygonRelation);
220 if (rr == null) {
221 return null;
222 }
223 final Relation existingRelation = rr.a;
224 final Relation relation = rr.b;
225
226 final List<Command> list = removeTagsFromWaysIfNeeded(relation);
227 final String commandName;
228 if (existingRelation == null) {
229 list.add(new AddCommand(relation));
230 commandName = getName(false);
231 } else {
232 list.add(new ChangeCommand(existingRelation, relation));
233 commandName = getName(true);
234 }
235 return Pair.create(new SequenceCommand(commandName, list), relation);
236 }
237
238 /** Enable this action only if something is selected */
239 @Override
240 protected void updateEnabledState() {
241 if (getCurrentDataSet() == null) {
242 setEnabled(false);
243 } else {
244 updateEnabledState(getCurrentDataSet().getSelected());
245 }
246 }
247
248 /**
249 * Enable this action only if something is selected
250 *
251 * @param selection the current selection, gets tested for emptyness
252 */
253 @Override
254 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 MultipolygonBuilder analyzeWays(Collection<Way> selectedWays, boolean showNotif) {
268
269 MultipolygonBuilder pol = new MultipolygonBuilder();
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(MultipolygonBuilder 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 boolean fixed = false;
405 Relation r2 = new Relation(relation);
406 for (Entry<String, String> entry : values.entrySet()) {
407 String key = entry.getKey();
408 if (!r2.hasKey(key) && !"area".equals(key) ) {
409 if (relation.isNew())
410 relation.put(key, entry.getValue());
411 else
412 r2.put(key, entry.getValue());
413 fixed = true;
414 }
415 }
416 if (fixed && !relation.isNew())
417 commands.add(new ChangeCommand(relation, r2));
418 }
419
420 return commands;
421 }
422}
Note: See TracBrowser for help on using the repository browser.