source: josm/trunk/src/org/openstreetmap/josm/actions/SimplifyWayAction.java@ 4067

Last change on this file since 4067 was 3952, checked in by stoecker, 13 years ago

fix #5909 - remove deleted nodes from selection after simplify way call - maybe we should additionally modify DeleteAction directly?

  • Property svn:eol-style set to native
File size: 13.0 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.util.ArrayList;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashSet;
14import java.util.LinkedList;
15import java.util.List;
16
17import javax.swing.JOptionPane;
18
19import org.openstreetmap.josm.Main;
20import org.openstreetmap.josm.command.ChangeCommand;
21import org.openstreetmap.josm.command.Command;
22import org.openstreetmap.josm.command.DeleteCommand;
23import org.openstreetmap.josm.command.SequenceCommand;
24import org.openstreetmap.josm.data.Bounds;
25import org.openstreetmap.josm.data.osm.DataSet;
26import org.openstreetmap.josm.data.osm.DataSource;
27import org.openstreetmap.josm.data.osm.Node;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.Way;
30import org.openstreetmap.josm.gui.HelpAwareOptionPane;
31import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
32import org.openstreetmap.josm.gui.help.HelpUtil;
33import org.openstreetmap.josm.gui.layer.OsmDataLayer;
34import org.openstreetmap.josm.tools.ImageProvider;
35import org.openstreetmap.josm.tools.Shortcut;
36
37public class SimplifyWayAction extends JosmAction {
38 public SimplifyWayAction() {
39 super(tr("Simplify Way"), "simplify", tr("Delete unnecessary nodes from a way."), Shortcut.registerShortcut("tools:simplify", tr("Tool: {0}", tr("Simplify Way")),
40 KeyEvent.VK_Y, Shortcut.GROUP_EDIT, Shortcut.SHIFT_DEFAULT), true);
41 putValue("help", ht("/Action/SimplifyWay"));
42 }
43
44 protected List<Bounds> getCurrentEditBounds() {
45 LinkedList<Bounds> bounds = new LinkedList<Bounds>();
46 OsmDataLayer dataLayer = Main.map.mapView.getEditLayer();
47 for (DataSource ds : dataLayer.data.dataSources) {
48 if (ds.bounds != null) {
49 bounds.add(ds.bounds);
50 }
51 }
52 return bounds;
53 }
54
55 protected boolean isInBounds(Node node, List<Bounds> bounds) {
56 for (Bounds b : bounds) {
57 if (b.contains(node.getCoor()))
58 return true;
59 }
60 return false;
61 }
62
63 protected boolean confirmWayWithNodesOutsideBoundingBox() {
64 ButtonSpec[] options = new ButtonSpec[] {
65 new ButtonSpec(
66 tr("Yes, delete nodes"),
67 ImageProvider.get("ok"),
68 tr("Delete nodes outside of downloaded data regions"),
69 null
70 ),
71 new ButtonSpec(
72 tr("No, abort"),
73 ImageProvider.get("cancel"),
74 tr("Cancel operation"),
75 null
76 )
77 };
78 int ret = HelpAwareOptionPane.showOptionDialog(
79 Main.parent,
80 "<html>"
81 + trn("The selected way has nodes outside of the downloaded data region.",
82 "The selected ways have nodes outside of the downloaded data region.",
83 getCurrentDataSet().getSelectedWays().size())
84
85 + "<br>"
86 + tr("This can lead to nodes being deleted accidentally.") + "<br>"
87 + tr("Do you want to delete them anyway?")
88 + "</html>",
89 tr("Delete nodes outside of data regions?"),
90 JOptionPane.WARNING_MESSAGE,
91 null, // no special icon
92 options,
93 options[0],
94 HelpUtil.ht("/Action/SimplifyWay#ConfirmDeleteNodesOutsideBoundingBoxes")
95 );
96 return ret == 0;
97 }
98
99 protected void alertSelectAtLeastOneWay() {
100 HelpAwareOptionPane.showOptionDialog(
101 Main.parent,
102 tr("Please select at least one way to simplify."),
103 tr("Warning"),
104 JOptionPane.WARNING_MESSAGE,
105 HelpUtil.ht("/Action/SimplifyWay#SelectAWayToSimplify")
106 );
107 }
108
109 protected boolean confirmSimplifyManyWays(int numWays) {
110 ButtonSpec[] options = new ButtonSpec[] {
111 new ButtonSpec(
112 tr("Yes"),
113 ImageProvider.get("ok"),
114 tr("Simplify all selected ways"),
115 null
116 ),
117 new ButtonSpec(
118 tr("Cancel"),
119 ImageProvider.get("cancel"),
120 tr("Cancel operation"),
121 null
122 )
123 };
124 int ret = HelpAwareOptionPane.showOptionDialog(
125 Main.parent,
126 tr(
127 "The selection contains {0} ways. Are you sure you want to simplify them all?",
128 numWays
129 ),
130 tr("Simplify ways?"),
131 JOptionPane.WARNING_MESSAGE,
132 null, // no special icon
133 options,
134 options[0],
135 HelpUtil.ht("/Action/SimplifyWay#ConfirmSimplifyAll")
136 );
137 return ret == 0;
138 }
139
140 public void actionPerformed(ActionEvent e) {
141 DataSet ds = getCurrentDataSet();
142 ds.beginUpdate();
143 try
144 {
145 Collection<OsmPrimitive> selection = ds.getSelected();
146
147 List<Bounds> bounds = getCurrentEditBounds();
148 for (OsmPrimitive prim : selection) {
149 if (! (prim instanceof Way)) {
150 continue;
151 }
152 if (bounds.size() > 0) {
153 Way way = (Way) prim;
154 // We check if each node of each way is at least in one download
155 // bounding box. Otherwise nodes may get deleted that are necessary by
156 // unloaded ways (see Ticket #1594)
157 for (Node node : way.getNodes()) {
158 if (!isInBounds(node, bounds)) {
159 if (!confirmWayWithNodesOutsideBoundingBox())
160 return;
161 break;
162 }
163 }
164 }
165 }
166 List<Way> ways = OsmPrimitive.getFilteredList(selection, Way.class);
167 if (ways.isEmpty()) {
168 alertSelectAtLeastOneWay();
169 return;
170 } else if (ways.size() > 10) {
171 if (!confirmSimplifyManyWays(ways.size()))
172 return;
173 }
174
175 Collection<Command> allCommands = new LinkedList<Command>();
176 for (Way way: ways) {
177 SequenceCommand simplifyCommand = simplifyWay(way, ds);
178 if (simplifyCommand == null) {
179 continue;
180 }
181 allCommands.add(simplifyCommand);
182 }
183 if (allCommands.isEmpty()) return;
184 SequenceCommand rootCommand = new SequenceCommand(
185 trn("Simplify {0} way", "Simplify {0} ways", allCommands.size(), allCommands.size()),
186 allCommands
187 );
188 Main.main.undoRedo.add(rootCommand);
189 } finally {
190 ds.endUpdate();
191 }
192 Main.map.repaint();
193 }
194
195 /**
196 * Replies true if <code>node</code> is a required node which can't be removed
197 * in order to simplify the way.
198 *
199 * @param way the way to be simplified
200 * @param node the node to check
201 * @return true if <code>node</code> is a required node which can't be removed
202 * in order to simplify the way.
203 */
204 protected boolean isRequiredNode(Way way, Node node) {
205 boolean isRequired = Collections.frequency(way.getNodes(), node) > 1;
206 if (! isRequired) {
207 List<OsmPrimitive> parents = new LinkedList<OsmPrimitive>();
208 parents.addAll(node.getReferrers());
209 parents.remove(way);
210 isRequired = !parents.isEmpty();
211 }
212 if (!isRequired) {
213 isRequired = node.isTagged();
214 }
215 return isRequired;
216 }
217
218 /**
219 * Simplifies a way
220 *
221 * @param w the way to simplify
222 */
223 public SequenceCommand simplifyWay(Way w, DataSet ds) {
224 double threshold = Double.parseDouble(Main.pref.get("simplify-way.max-error", "3"));
225 int lower = 0;
226 int i = 0;
227 List<Node> newNodes = new ArrayList<Node>(w.getNodesCount());
228 while(i < w.getNodesCount()){
229 if (isRequiredNode(w,w.getNode(i))) {
230 // copy a required node to the list of new nodes. Simplify not
231 // possible
232 newNodes.add(w.getNode(i));
233 i++;
234 lower++;
235 continue;
236 }
237 i++;
238 // find the longest sequence of not required nodes ...
239 while(i<w.getNodesCount() && !isRequiredNode(w,w.getNode(i))) {
240 i++;
241 }
242 // ... and simplify them
243 buildSimplifiedNodeList(w.getNodes(), lower, Math.min(w.getNodesCount()-1, i), threshold,newNodes);
244 lower=i;
245 i++;
246 }
247
248 HashSet<Node> delNodes = new HashSet<Node>();
249 delNodes.addAll(w.getNodes());
250 delNodes.removeAll(newNodes);
251
252 if (delNodes.isEmpty()) return null;
253
254 Collection<Command> cmds = new LinkedList<Command>();
255 Way newWay = new Way(w);
256 newWay.setNodes(newNodes);
257 cmds.add(new ChangeCommand(w, newWay));
258 cmds.add(new DeleteCommand(delNodes));
259 ds.clearSelection(delNodes);
260 return new SequenceCommand(trn("Simplify Way (remove {0} node)", "Simplify Way (remove {0} nodes)", delNodes.size(), delNodes.size()), cmds);
261 }
262
263 /**
264 * Builds the simplified list of nodes for a way segment given by a lower index <code>from</code>
265 * and an upper index <code>to</code>
266 *
267 * @param wnew the way to simplify
268 * @param from the lower index
269 * @param to the upper index
270 * @param threshold
271 */
272 protected void buildSimplifiedNodeList(List<Node> wnew, int from, int to, double threshold, List<Node> simplifiedNodes) {
273
274 Node fromN = wnew.get(from);
275 Node toN = wnew.get(to);
276
277 // Get max xte
278 int imax = -1;
279 double xtemax = 0;
280 for (int i = from + 1; i < to; i++) {
281 Node n = wnew.get(i);
282 double xte = Math.abs(EARTH_RAD
283 * xtd(fromN.getCoor().lat() * Math.PI / 180, fromN.getCoor().lon() * Math.PI / 180, toN.getCoor().lat() * Math.PI
284 / 180, toN.getCoor().lon() * Math.PI / 180, n.getCoor().lat() * Math.PI / 180, n.getCoor().lon() * Math.PI
285 / 180));
286 if (xte > xtemax) {
287 xtemax = xte;
288 imax = i;
289 }
290 }
291
292 if (imax != -1 && xtemax >= threshold) {
293 // Segment cannot be simplified - try shorter segments
294 buildSimplifiedNodeList(wnew, from, imax,threshold,simplifiedNodes);
295 //simplifiedNodes.add(wnew.get(imax));
296 buildSimplifiedNodeList(wnew, imax, to, threshold,simplifiedNodes);
297 } else {
298 // Simplify segment
299 if (simplifiedNodes.isEmpty() || simplifiedNodes.get(simplifiedNodes.size()-1) != fromN) {
300 simplifiedNodes.add(fromN);
301 }
302 if (fromN != toN) {
303 simplifiedNodes.add(toN);
304 }
305 }
306 }
307
308 public static double EARTH_RAD = 6378137.0;
309
310 /* From Aviaton Formulary v1.3
311 * http://williams.best.vwh.net/avform.htm
312 */
313 public static double dist(double lat1, double lon1, double lat2, double lon2) {
314 return 2 * Math.asin(Math.sqrt(Math.pow(Math.sin((lat1 - lat2) / 2), 2) + Math.cos(lat1) * Math.cos(lat2)
315 * Math.pow(Math.sin((lon1 - lon2) / 2), 2)));
316 }
317
318 public static double course(double lat1, double lon1, double lat2, double lon2) {
319 return Math.atan2(Math.sin(lon1 - lon2) * Math.cos(lat2), Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1)
320 * Math.cos(lat2) * Math.cos(lon1 - lon2))
321 % (2 * Math.PI);
322 }
323
324 public static double xtd(double lat1, double lon1, double lat2, double lon2, double lat3, double lon3) {
325 double dist_AD = dist(lat1, lon1, lat3, lon3);
326 double crs_AD = course(lat1, lon1, lat3, lon3);
327 double crs_AB = course(lat1, lon1, lat2, lon2);
328 return Math.asin(Math.sin(dist_AD) * Math.sin(crs_AD - crs_AB));
329 }
330
331 @Override
332 protected void updateEnabledState() {
333 if (getCurrentDataSet() == null) {
334 setEnabled(false);
335 } else {
336 updateEnabledState(getCurrentDataSet().getSelected());
337 }
338 }
339
340 @Override
341 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
342 setEnabled(selection != null && !selection.isEmpty());
343 }
344}
Note: See TracBrowser for help on using the repository browser.