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

Last change on this file since 2976 was 2976, checked in by Gubaer, 14 years ago

fixed #4512: "Simplify way" should not deletes nodes used by other ways
Migrated message dialogs to HelpawareOptionPane and added help topics

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