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

Last change on this file since 3747 was 3530, checked in by stoecker, 14 years ago

fix array preferences

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