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
RevLine 
[32395]1// License: GPL. For details, see LICENSE file.
[25669]2package relcontext.actions;
3
[32395]4import static org.openstreetmap.josm.tools.I18n.tr;
5
[25692]6import java.awt.Dialog.ModalityType;
7import java.awt.GridBagLayout;
[25669]8import java.awt.event.ActionEvent;
[25727]9import java.awt.event.KeyEvent;
[32395]10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.Collection;
[36217]13import java.util.Collections;
[32395]14import java.util.HashMap;
15import java.util.HashSet;
16import java.util.List;
17import java.util.Map;
[36217]18import java.util.Map.Entry;
[32395]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
[25669]29import org.openstreetmap.josm.actions.JosmAction;
[32395]30import org.openstreetmap.josm.command.AddCommand;
31import org.openstreetmap.josm.command.ChangePropertyCommand;
32import org.openstreetmap.josm.command.Command;
33import org.openstreetmap.josm.command.SequenceCommand;
[34551]34import org.openstreetmap.josm.data.UndoRedoHandler;
[32398]35import org.openstreetmap.josm.data.osm.DataSet;
[32395]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;
[36217]42import org.openstreetmap.josm.data.osm.TagMap;
[32395]43import org.openstreetmap.josm.data.osm.Way;
[33530]44import org.openstreetmap.josm.gui.MainApplication;
[34551]45import org.openstreetmap.josm.spi.preferences.Config;
[25692]46import org.openstreetmap.josm.tools.GBC;
[25727]47import org.openstreetmap.josm.tools.Shortcut;
[32395]48
[25669]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 {
[25692]58 private static final String PREF_MULTIPOLY = "reltoolbox.multipolygon.";
[36217]59 protected transient ChosenRelation chRel;
[25669]60
[32395]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();
[25669]67 }
68
69 public CreateMultipolygonAction() {
[32395]70 this(null);
[25669]71 }
[26811]72
[32395]73 public static boolean getDefaultPropertyValue(String property) {
[36102]74 switch (property) {
[36217]75 case "boundary":
76 case "alltags":
77 case "allowsplit":
78 return false;
79 case "boundaryways":
80 case "tags":
81 case "single":
82 return true;
[36102]83 }
[32395]84 throw new IllegalArgumentException(property);
[25692]85 }
[25669]86
[36217]87 private static boolean getPref(String property) {
[34551]88 return Config.getPref().getBoolean(PREF_MULTIPOLY + property, getDefaultPropertyValue(property));
[25692]89 }
90
[32395]91 @Override
92 public void actionPerformed(ActionEvent e) {
93 boolean isBoundary = getPref("boundary");
[32398]94 DataSet ds = getLayerManager().getEditDataSet();
95 Collection<Way> selectedWays = ds.getSelectedWays();
[32395]96 if (!isBoundary && getPref("tags")) {
97 List<Relation> rels = null;
98 if (getPref("allowsplit") || selectedWays.size() == 1) {
[32398]99 if (SplittingMultipolygons.canProcess(selectedWays)) {
[36217]100 rels = SplittingMultipolygons.process(selectedWays);
[32395]101 }
102 } else {
103 if (TheRing.areAllOfThoseRings(selectedWays)) {
104 List<Command> commands = new ArrayList<>();
[32398]105 rels = TheRing.makeManySimpleMultipolygons(ds.getSelectedWays(), commands);
106 if (!commands.isEmpty()) {
[34551]107 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Create multipolygons from rings"), commands));
[32395]108 }
109 }
110 }
111 if (rels != null && !rels.isEmpty()) {
[32398]112 if (chRel != null) {
[32395]113 chRel.set(rels.size() == 1 ? rels.get(0) : null);
114 }
[32398]115 if (rels.size() == 1) {
116 ds.setSelected(rels);
[32395]117 } else {
[32398]118 ds.clearSelection();
[32395]119 }
120 return;
121 }
122 }
123
124 // for now, just copying standard action
125 MultipolygonBuilder mpc = new MultipolygonBuilder();
[33694]126 String error = mpc.makeFromWays(ds.getSelectedWays());
[32395]127 if (error != null) {
[34551]128 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), error);
[32395]129 return;
130 }
131 Relation rel = new Relation();
132 if (isBoundary) {
133 rel.put("type", "boundary");
134 rel.put("boundary", "administrative");
[30738]135 } else {
[32395]136 rel.put("type", "multipolygon");
[30738]137 }
[32398]138 for (MultipolygonBuilder.JoinedPolygon poly : mpc.outerWays) {
139 for (Way w : poly.ways) {
[32395]140 rel.addMember(new RelationMember("outer", w));
141 }
[30738]142 }
[32398]143 for (MultipolygonBuilder.JoinedPolygon poly : mpc.innerWays) {
144 for (Way w : poly.ways) {
[32395]145 rel.addMember(new RelationMember("inner", w));
146 }
[30738]147 }
[32395]148 List<Command> list = removeTagsFromInnerWays(rel);
149 if (!list.isEmpty() && isBoundary) {
[34551]150 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Move tags from ways to relation"), list));
[32395]151 list = new ArrayList<>();
152 }
153 if (isBoundary) {
[32398]154 if (!askForAdminLevelAndName(rel))
[32395]155 return;
156 addBoundaryMembers(rel);
[32398]157 if (getPref("boundaryways")) {
[32395]158 list.addAll(fixWayTagsForBoundary(rel));
159 }
160 }
[33694]161 list.add(new AddCommand(ds, rel));
[34551]162 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Create multipolygon"), list));
[25721]163
[32398]164 if (chRel != null) {
[32395]165 chRel.set(rel);
166 }
[26811]167
[33694]168 ds.setSelected(rel);
[25669]169 }
170
171 @Override
172 protected void updateEnabledState() {
[32398]173 if (getLayerManager().getEditDataSet() == null) {
[32395]174 setEnabled(false);
175 } else {
[32398]176 updateEnabledState(getLayerManager().getEditDataSet().getSelected());
[32395]177 }
[25669]178 }
179
180 @Override
[32395]181 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
182 boolean isEnabled = true;
[32398]183 if (selection == null || selection.isEmpty()) {
[30738]184 isEnabled = false;
[32395]185 } else {
186 if (!getPref("boundary")) {
187 for (OsmPrimitive p : selection) {
188 if (!(p instanceof Way)) {
189 isEnabled = false;
190 break;
191 }
192 }
[30738]193 }
194 }
[32395]195 setEnabled(isEnabled);
[25669]196 }
[25670]197
198 /**
[25692]199 * Add selected nodes and relations with corresponding roles.
200 */
[32395]201 private void addBoundaryMembers(Relation rel) {
[32398]202 for (OsmPrimitive p : getLayerManager().getEditDataSet().getSelected()) {
[32395]203 String role = null;
[36217]204 if (p.getType() == OsmPrimitiveType.RELATION) {
[32395]205 role = "subarea";
[36217]206 } else if (p.getType() == OsmPrimitiveType.NODE) {
[32398]207 Node n = (Node) p;
[32395]208 if (!n.isIncomplete()) {
[32398]209 if (n.hasKey("place")) {
[32395]210 role = "admin_centre";
211 } else {
212 role = "label";
213 }
214 }
215 }
[32398]216 if (role != null) {
[32395]217 rel.addMember(new RelationMember(role, p));
218 }
[30738]219 }
[25692]220 }
221
222 /**
223 * For all untagged ways in relation, add tags boundary and admin_level.
224 */
[32395]225 private List<Command> fixWayTagsForBoundary(Relation rel) {
226 List<Command> commands = new ArrayList<>();
[32398]227 if (!rel.hasKey("boundary") || !rel.hasKey("admin_level"))
[32395]228 return commands;
229 String adminLevelStr = rel.get("admin_level");
230 int adminLevel = 0;
231 try {
232 adminLevel = Integer.parseInt(adminLevelStr);
[32398]233 } catch (NumberFormatException e) {
[32395]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;
[32398]241 if (p.hasKey("boundary") && p.get("boundary").equals("administrative")) {
[32395]242 count++;
243 }
[32398]244 if (p.hasKey("admin_level")) {
[32395]245 count++;
246 }
247 if (p.keySet().size() - count == 0) {
[32398]248 if (!p.hasKey("boundary")) {
[32395]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"));
[32398]256 if (oldAdminLevel > adminLevel) {
[32395]257 waysAdminLevel.add(p);
258 }
[32398]259 } catch (NumberFormatException e) {
[32395]260 waysAdminLevel.add(p); // some garbage, replace it
261 }
262 }
263 }
[30738]264 }
265 }
[32398]266 if (!waysBoundary.isEmpty()) {
[32395]267 commands.add(new ChangePropertyCommand(waysBoundary, "boundary", "administrative"));
[30738]268 }
[32398]269 if (!waysAdminLevel.isEmpty()) {
[32395]270 commands.add(new ChangePropertyCommand(waysAdminLevel, "admin_level", adminLevelStr));
271 }
272 return commands;
[30738]273 }
[32398]274
[36102]275 public static final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList("barrier", "source");
[32398]276
[36102]277 private static final Set<String> REMOVE_FROM_BOUNDARY_TAGS =
278 new TreeSet<>(Arrays.asList("boundary", "boundary_type", "type", "admin_level"));
[25702]279
[25692]280 /**
[25670]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 */
[32395]285 private List<Command> removeTagsFromInnerWays(Relation relation) {
286 Map<String, String> values = new HashMap<>();
[25670]287
[32395]288 if (relation.hasKeys()) {
289 for (String key : relation.keySet()) {
290 values.put(key, relation.get(key));
291 }
[30738]292 }
[25670]293
[32395]294 List<Way> innerWays = new ArrayList<>();
295 List<Way> outerWays = new ArrayList<>();
[25702]296
[32395]297 Set<String> conflictingKeys = new TreeSet<>();
[25670]298
[32395]299 for (RelationMember m : relation.getMembers()) {
[25670]300
[32395]301 if (m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) {
302 innerWays.add(m.getWay());
303 }
[25670]304
[32395]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 }
[30738]315 }
316 }
[32395]317
318 // filter out empty key conflicts - we need second iteration
319 boolean isBoundary = getPref("boundary");
[32398]320 if (isBoundary || !getPref("alltags")) {
321 for (RelationMember m : relation.getMembers()) {
[36217]322 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay()) {
[32398]323 for (String key : values.keySet()) {
324 if (!m.getWay().hasKey(key) && !relation.hasKey(key)) {
[32395]325 conflictingKeys.add(key);
326 }
[32398]327 }
[32395]328 }
[32398]329 }
[30738]330 }
[25670]331
[32398]332 for (String key : conflictingKeys) {
[32395]333 values.remove(key);
334 }
[25693]335
[34551]336 for (String linearTag : Config.getPref().getList(PREF_MULTIPOLY + "lineartags", DEFAULT_LINEAR_TAGS)) {
[32395]337 values.remove(linearTag);
338 }
[25693]339
[36217]340 if ("coastline".equals(values.get("natural"))) {
[32395]341 values.remove("natural");
342 }
[25693]343
[32395]344 String name = values.get("name");
345 if (isBoundary) {
346 Set<String> keySet = new TreeSet<>(values.keySet());
[32398]347 for (String key : keySet) {
348 if (!REMOVE_FROM_BOUNDARY_TAGS.contains(key)) {
[32395]349 values.remove(key);
350 }
[32398]351 }
[32395]352 }
[25693]353
[32395]354 values.put("area", "yes");
[25702]355
[32395]356 List<Command> commands = new ArrayList<>();
357 boolean moveTags = getPref("tags");
[25693]358
[36217]359 for (Entry<String, String> entry : values.entrySet()) {
360 String key = entry.getKey();
[32395]361 List<OsmPrimitive> affectedWays = new ArrayList<>();
[36217]362 String value = entry.getValue();
[25670]363
[32395]364 for (Way way : innerWays) {
365 if (way.hasKey(key) && (isBoundary || value.equals(way.get(key)))) {
366 affectedWays.add(way);
367 }
368 }
[25670]369
[32395]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 }
[25670]378
[36217]379 if (!affectedWays.isEmpty()) {
[32395]380 commands.add(new ChangePropertyCommand(affectedWays, key, null));
[30738]381 }
382 }
[25693]383
[32395]384 if (moveTags) {
385 // add those tag values to the relation
[32398]386 if (isBoundary) {
[32395]387 values.put("name", name);
388 }
389 boolean fixed = false;
[36217]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);
[32395]398 } else {
[36217]399 tags.put(key, val);
[32395]400 }
401 fixed = true;
402 }
403 }
[36217]404 if (fixed && relation.getDataSet() != null) {
405 commands.add(new ChangePropertyCommand(Collections.singleton(relation), tags));
[32395]406 }
[30738]407 }
[25670]408
[32395]409 return commands;
[30738]410 }
[25693]411
[25702]412 /**
413 *
[32398]414 * @param rel relation
[25702]415 * @return false if user pressed "cancel".
416 */
[32395]417 private boolean askForAdminLevelAndName(Relation rel) {
418 String relAL = rel.get("admin_level");
419 String relName = rel.get("name");
[32398]420 if (relAL != null && relName != null)
[32395]421 return true;
[25702]422
[32395]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));
[25692]425
[32395]426 final JTextField admin = new JTextField();
[34551]427 admin.setText(relAL != null ? relAL : Config.getPref().get(PREF_MULTIPOLY + "lastadmin", ""));
[32395]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));
[25692]431
[32395]432 final JTextField name = new JTextField();
[32398]433 if (relName != null) {
[32395]434 name.setText(relName);
[30738]435 }
[32395]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));
[25692]439
[32395]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 };
[34551]447 final JDialog dlg = optionPane.createDialog(MainApplication.getMainFrame(), tr("Create a new relation"));
[32395]448 dlg.setModalityType(ModalityType.DOCUMENT_MODAL);
[25692]449
[36217]450 name.addActionListener(e -> {
451 dlg.setVisible(false);
452 optionPane.setValue(JOptionPane.OK_OPTION);
[32395]453 });
[25692]454
[32395]455 dlg.setVisible(true);
[25692]456
[32395]457 Object answer = optionPane.getValue();
[36217]458 dlg.dispose();
[32395]459 if (answer == null || answer == JOptionPane.UNINITIALIZED_VALUE
[32398]460 || (answer instanceof Integer && (Integer) answer != JOptionPane.OK_OPTION))
[32395]461 return false;
462
[36217]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);
[32395]468 }
[36217]469 if (!newName.isEmpty()) {
470 rel.put("name", newName);
[32395]471 }
472 return true;
[25692]473 }
[25669]474}
Note: See TracBrowser for help on using the repository browser.