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

Last change on this file since 6589 was 6575, checked in by simon04, 10 years ago

fix #7196 - Validator: add multipolygon role check

This is done by creating a new multipolygon using the logics from
"Create multipolygon" and see if the roles match.

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