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

Last change on this file since 7756 was 7668, checked in by stoecker, 10 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.