source: osm/applications/editors/josm/plugins/reltoolbox/src/relcontext/actions/CreateMultipolygonAction.java

Last change on this file was 36217, checked in by GerdP, 4 months ago

fix #23521: fix some memory leaks

  • dispose dialogs
  • either avoid to create clones of ways or relations or use setNodes(null) / setMembers(null)
  • replaces most ChangeCommand instances by better specialized alternatives
  • add some comments
  • fix some checkstyle / sonar issues
File size: 17.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package relcontext.actions;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Dialog.ModalityType;
7import java.awt.GridBagLayout;
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.HashMap;
15import java.util.HashSet;
16import java.util.List;
17import java.util.Map;
18import java.util.Map.Entry;
19import java.util.Set;
20import java.util.TreeSet;
21
22import javax.swing.Box;
23import javax.swing.JDialog;
24import javax.swing.JLabel;
25import javax.swing.JOptionPane;
26import javax.swing.JPanel;
27import javax.swing.JTextField;
28
29import org.openstreetmap.josm.actions.JosmAction;
30import org.openstreetmap.josm.command.AddCommand;
31import org.openstreetmap.josm.command.ChangePropertyCommand;
32import org.openstreetmap.josm.command.Command;
33import org.openstreetmap.josm.command.SequenceCommand;
34import org.openstreetmap.josm.data.UndoRedoHandler;
35import org.openstreetmap.josm.data.osm.DataSet;
36import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
37import org.openstreetmap.josm.data.osm.Node;
38import org.openstreetmap.josm.data.osm.OsmPrimitive;
39import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
40import org.openstreetmap.josm.data.osm.Relation;
41import org.openstreetmap.josm.data.osm.RelationMember;
42import org.openstreetmap.josm.data.osm.TagMap;
43import org.openstreetmap.josm.data.osm.Way;
44import org.openstreetmap.josm.gui.MainApplication;
45import org.openstreetmap.josm.spi.preferences.Config;
46import org.openstreetmap.josm.tools.GBC;
47import org.openstreetmap.josm.tools.Shortcut;
48
49import relcontext.ChosenRelation;
50
51/**
52 * Creates new multipolygon from selected ways.
53 * Choose relation afterwards.
54 *
55 * @author Zverik
56 */
57public class CreateMultipolygonAction extends JosmAction {
58 private static final String PREF_MULTIPOLY = "reltoolbox.multipolygon.";
59 protected transient ChosenRelation chRel;
60
61 public CreateMultipolygonAction(ChosenRelation chRel) {
62 super("Multi", "data/multipolygon", tr("Create a multipolygon from selected objects"),
63 Shortcut.registerShortcut("reltoolbox:multipolygon", tr("Relation Toolbox: {0}", tr("Create multipolygon")),
64 KeyEvent.VK_A, Shortcut.ALT_CTRL), false);
65 this.chRel = chRel;
66 updateEnabledState();
67 }
68
69 public CreateMultipolygonAction() {
70 this(null);
71 }
72
73 public static boolean getDefaultPropertyValue(String property) {
74 switch (property) {
75 case "boundary":
76 case "alltags":
77 case "allowsplit":
78 return false;
79 case "boundaryways":
80 case "tags":
81 case "single":
82 return true;
83 }
84 throw new IllegalArgumentException(property);
85 }
86
87 private static boolean getPref(String property) {
88 return Config.getPref().getBoolean(PREF_MULTIPOLY + property, getDefaultPropertyValue(property));
89 }
90
91 @Override
92 public void actionPerformed(ActionEvent e) {
93 boolean isBoundary = getPref("boundary");
94 DataSet ds = getLayerManager().getEditDataSet();
95 Collection<Way> selectedWays = ds.getSelectedWays();
96 if (!isBoundary && getPref("tags")) {
97 List<Relation> rels = null;
98 if (getPref("allowsplit") || selectedWays.size() == 1) {
99 if (SplittingMultipolygons.canProcess(selectedWays)) {
100 rels = SplittingMultipolygons.process(selectedWays);
101 }
102 } else {
103 if (TheRing.areAllOfThoseRings(selectedWays)) {
104 List<Command> commands = new ArrayList<>();
105 rels = TheRing.makeManySimpleMultipolygons(ds.getSelectedWays(), commands);
106 if (!commands.isEmpty()) {
107 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Create multipolygons from rings"), commands));
108 }
109 }
110 }
111 if (rels != null && !rels.isEmpty()) {
112 if (chRel != null) {
113 chRel.set(rels.size() == 1 ? rels.get(0) : null);
114 }
115 if (rels.size() == 1) {
116 ds.setSelected(rels);
117 } else {
118 ds.clearSelection();
119 }
120 return;
121 }
122 }
123
124 // for now, just copying standard action
125 MultipolygonBuilder mpc = new MultipolygonBuilder();
126 String error = mpc.makeFromWays(ds.getSelectedWays());
127 if (error != null) {
128 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), error);
129 return;
130 }
131 Relation rel = new Relation();
132 if (isBoundary) {
133 rel.put("type", "boundary");
134 rel.put("boundary", "administrative");
135 } else {
136 rel.put("type", "multipolygon");
137 }
138 for (MultipolygonBuilder.JoinedPolygon poly : mpc.outerWays) {
139 for (Way w : poly.ways) {
140 rel.addMember(new RelationMember("outer", w));
141 }
142 }
143 for (MultipolygonBuilder.JoinedPolygon poly : mpc.innerWays) {
144 for (Way w : poly.ways) {
145 rel.addMember(new RelationMember("inner", w));
146 }
147 }
148 List<Command> list = removeTagsFromInnerWays(rel);
149 if (!list.isEmpty() && isBoundary) {
150 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Move tags from ways to relation"), list));
151 list = new ArrayList<>();
152 }
153 if (isBoundary) {
154 if (!askForAdminLevelAndName(rel))
155 return;
156 addBoundaryMembers(rel);
157 if (getPref("boundaryways")) {
158 list.addAll(fixWayTagsForBoundary(rel));
159 }
160 }
161 list.add(new AddCommand(ds, rel));
162 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Create multipolygon"), list));
163
164 if (chRel != null) {
165 chRel.set(rel);
166 }
167
168 ds.setSelected(rel);
169 }
170
171 @Override
172 protected void updateEnabledState() {
173 if (getLayerManager().getEditDataSet() == null) {
174 setEnabled(false);
175 } else {
176 updateEnabledState(getLayerManager().getEditDataSet().getSelected());
177 }
178 }
179
180 @Override
181 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
182 boolean isEnabled = true;
183 if (selection == null || selection.isEmpty()) {
184 isEnabled = false;
185 } else {
186 if (!getPref("boundary")) {
187 for (OsmPrimitive p : selection) {
188 if (!(p instanceof Way)) {
189 isEnabled = false;
190 break;
191 }
192 }
193 }
194 }
195 setEnabled(isEnabled);
196 }
197
198 /**
199 * Add selected nodes and relations with corresponding roles.
200 */
201 private void addBoundaryMembers(Relation rel) {
202 for (OsmPrimitive p : getLayerManager().getEditDataSet().getSelected()) {
203 String role = null;
204 if (p.getType() == OsmPrimitiveType.RELATION) {
205 role = "subarea";
206 } else if (p.getType() == OsmPrimitiveType.NODE) {
207 Node n = (Node) p;
208 if (!n.isIncomplete()) {
209 if (n.hasKey("place")) {
210 role = "admin_centre";
211 } else {
212 role = "label";
213 }
214 }
215 }
216 if (role != null) {
217 rel.addMember(new RelationMember(role, p));
218 }
219 }
220 }
221
222 /**
223 * For all untagged ways in relation, add tags boundary and admin_level.
224 */
225 private List<Command> fixWayTagsForBoundary(Relation rel) {
226 List<Command> commands = new ArrayList<>();
227 if (!rel.hasKey("boundary") || !rel.hasKey("admin_level"))
228 return commands;
229 String adminLevelStr = rel.get("admin_level");
230 int adminLevel = 0;
231 try {
232 adminLevel = Integer.parseInt(adminLevelStr);
233 } catch (NumberFormatException e) {
234 return commands;
235 }
236 Set<OsmPrimitive> waysBoundary = new HashSet<>();
237 Set<OsmPrimitive> waysAdminLevel = new HashSet<>();
238 for (OsmPrimitive p : rel.getMemberPrimitives()) {
239 if (p instanceof Way) {
240 int count = 0;
241 if (p.hasKey("boundary") && p.get("boundary").equals("administrative")) {
242 count++;
243 }
244 if (p.hasKey("admin_level")) {
245 count++;
246 }
247 if (p.keySet().size() - count == 0) {
248 if (!p.hasKey("boundary")) {
249 waysBoundary.add(p);
250 }
251 if (!p.hasKey("admin_level")) {
252 waysAdminLevel.add(p);
253 } else {
254 try {
255 int oldAdminLevel = Integer.parseInt(p.get("admin_level"));
256 if (oldAdminLevel > adminLevel) {
257 waysAdminLevel.add(p);
258 }
259 } catch (NumberFormatException e) {
260 waysAdminLevel.add(p); // some garbage, replace it
261 }
262 }
263 }
264 }
265 }
266 if (!waysBoundary.isEmpty()) {
267 commands.add(new ChangePropertyCommand(waysBoundary, "boundary", "administrative"));
268 }
269 if (!waysAdminLevel.isEmpty()) {
270 commands.add(new ChangePropertyCommand(waysAdminLevel, "admin_level", adminLevelStr));
271 }
272 return commands;
273 }
274
275 public static final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList("barrier", "source");
276
277 private static final Set<String> REMOVE_FROM_BOUNDARY_TAGS =
278 new TreeSet<>(Arrays.asList("boundary", "boundary_type", "type", "admin_level"));
279
280 /**
281 * This method removes tags/value pairs from inner ways that are present in relation or outer ways.
282 * It was copypasted from the standard {@link org.openstreetmap.josm.actions.CreateMultipolygonAction}.
283 * Todo: rewrite it.
284 */
285 private List<Command> removeTagsFromInnerWays(Relation relation) {
286 Map<String, String> values = new HashMap<>();
287
288 if (relation.hasKeys()) {
289 for (String key : relation.keySet()) {
290 values.put(key, relation.get(key));
291 }
292 }
293
294 List<Way> innerWays = new ArrayList<>();
295 List<Way> outerWays = new ArrayList<>();
296
297 Set<String> conflictingKeys = new TreeSet<>();
298
299 for (RelationMember m : relation.getMembers()) {
300
301 if (m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) {
302 innerWays.add(m.getWay());
303 }
304
305 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) {
306 Way way = m.getWay();
307 outerWays.add(way);
308 for (String key : way.keySet()) {
309 if (!values.containsKey(key)) { //relation values take precedence
310 values.put(key, way.get(key));
311 } else if (!relation.hasKey(key) && !values.get(key).equals(way.get(key))) {
312 conflictingKeys.add(key);
313 }
314 }
315 }
316 }
317
318 // filter out empty key conflicts - we need second iteration
319 boolean isBoundary = getPref("boundary");
320 if (isBoundary || !getPref("alltags")) {
321 for (RelationMember m : relation.getMembers()) {
322 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay()) {
323 for (String key : values.keySet()) {
324 if (!m.getWay().hasKey(key) && !relation.hasKey(key)) {
325 conflictingKeys.add(key);
326 }
327 }
328 }
329 }
330 }
331
332 for (String key : conflictingKeys) {
333 values.remove(key);
334 }
335
336 for (String linearTag : Config.getPref().getList(PREF_MULTIPOLY + "lineartags", DEFAULT_LINEAR_TAGS)) {
337 values.remove(linearTag);
338 }
339
340 if ("coastline".equals(values.get("natural"))) {
341 values.remove("natural");
342 }
343
344 String name = values.get("name");
345 if (isBoundary) {
346 Set<String> keySet = new TreeSet<>(values.keySet());
347 for (String key : keySet) {
348 if (!REMOVE_FROM_BOUNDARY_TAGS.contains(key)) {
349 values.remove(key);
350 }
351 }
352 }
353
354 values.put("area", "yes");
355
356 List<Command> commands = new ArrayList<>();
357 boolean moveTags = getPref("tags");
358
359 for (Entry<String, String> entry : values.entrySet()) {
360 String key = entry.getKey();
361 List<OsmPrimitive> affectedWays = new ArrayList<>();
362 String value = entry.getValue();
363
364 for (Way way : innerWays) {
365 if (way.hasKey(key) && (isBoundary || value.equals(way.get(key)))) {
366 affectedWays.add(way);
367 }
368 }
369
370 if (moveTags) {
371 // remove duplicated tags from outer ways
372 for (Way way : outerWays) {
373 if (way.hasKey(key)) {
374 affectedWays.add(way);
375 }
376 }
377 }
378
379 if (!affectedWays.isEmpty()) {
380 commands.add(new ChangePropertyCommand(affectedWays, key, null));
381 }
382 }
383
384 if (moveTags) {
385 // add those tag values to the relation
386 if (isBoundary) {
387 values.put("name", name);
388 }
389 boolean fixed = false;
390 TagMap tags = relation.getKeys();
391 for (Entry<String, String> e: values.entrySet()) {
392 final String key = e.getKey();
393 final String val = e.getValue();
394 if (!tags.containsKey(key) && !"area".equals(key)
395 && (!isBoundary || "admin_level".equals(key) || "name".equals(key))) {
396 if (relation.getDataSet() == null) {
397 relation.put(key, val);
398 } else {
399 tags.put(key, val);
400 }
401 fixed = true;
402 }
403 }
404 if (fixed && relation.getDataSet() != null) {
405 commands.add(new ChangePropertyCommand(Collections.singleton(relation), tags));
406 }
407 }
408
409 return commands;
410 }
411
412 /**
413 *
414 * @param rel relation
415 * @return false if user pressed "cancel".
416 */
417 private boolean askForAdminLevelAndName(Relation rel) {
418 String relAL = rel.get("admin_level");
419 String relName = rel.get("name");
420 if (relAL != null && relName != null)
421 return true;
422
423 JPanel panel = new JPanel(new GridBagLayout());
424 panel.add(new JLabel(tr("Enter admin level and name for the border relation:")), GBC.eol().insets(0, 0, 0, 5));
425
426 final JTextField admin = new JTextField();
427 admin.setText(relAL != null ? relAL : Config.getPref().get(PREF_MULTIPOLY + "lastadmin", ""));
428 panel.add(new JLabel(tr("Admin level")), GBC.std());
429 panel.add(Box.createHorizontalStrut(10), GBC.std());
430 panel.add(admin, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5));
431
432 final JTextField name = new JTextField();
433 if (relName != null) {
434 name.setText(relName);
435 }
436 panel.add(new JLabel(tr("Name")), GBC.std());
437 panel.add(Box.createHorizontalStrut(10), GBC.std());
438 panel.add(name, GBC.eol().fill(GBC.HORIZONTAL));
439
440 final JOptionPane optionPane = new JOptionPane(panel, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION) {
441 @Override
442 public void selectInitialValue() {
443 admin.requestFocusInWindow();
444 admin.selectAll();
445 }
446 };
447 final JDialog dlg = optionPane.createDialog(MainApplication.getMainFrame(), tr("Create a new relation"));
448 dlg.setModalityType(ModalityType.DOCUMENT_MODAL);
449
450 name.addActionListener(e -> {
451 dlg.setVisible(false);
452 optionPane.setValue(JOptionPane.OK_OPTION);
453 });
454
455 dlg.setVisible(true);
456
457 Object answer = optionPane.getValue();
458 dlg.dispose();
459 if (answer == null || answer == JOptionPane.UNINITIALIZED_VALUE
460 || (answer instanceof Integer && (Integer) answer != JOptionPane.OK_OPTION))
461 return false;
462
463 String adminLevel = admin.getText().trim();
464 String newName = name.getText().trim();
465 if ("10".equals(adminLevel) || (adminLevel.length() == 1 && Character.isDigit(adminLevel.charAt(0)))) {
466 rel.put("admin_level", adminLevel);
467 Config.getPref().put(PREF_MULTIPOLY + "lastadmin", adminLevel);
468 }
469 if (!newName.isEmpty()) {
470 rel.put("name", newName);
471 }
472 return true;
473 }
474}
Note: See TracBrowser for help on using the repository browser.