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

Last change on this file since 15152 was 15152, checked in by Don-vip, 5 years ago

more uses of Java 8 stream API

  • Property svn:eol-style set to native
File size: 12.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
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.Arrays;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.HashSet;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Set;
18import java.util.stream.Collectors;
19
20import javax.swing.JOptionPane;
21import javax.swing.SwingUtilities;
22
23import org.openstreetmap.josm.command.ChangeCommand;
24import org.openstreetmap.josm.command.Command;
25import org.openstreetmap.josm.command.DeleteCommand;
26import org.openstreetmap.josm.command.SequenceCommand;
27import org.openstreetmap.josm.data.UndoRedoHandler;
28import org.openstreetmap.josm.data.osm.DataSet;
29import org.openstreetmap.josm.data.osm.Node;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.data.osm.Way;
32import org.openstreetmap.josm.data.projection.Ellipsoid;
33import org.openstreetmap.josm.gui.HelpAwareOptionPane;
34import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
35import org.openstreetmap.josm.gui.MainApplication;
36import org.openstreetmap.josm.gui.Notification;
37import org.openstreetmap.josm.spi.preferences.Config;
38import org.openstreetmap.josm.tools.ImageProvider;
39import org.openstreetmap.josm.tools.Shortcut;
40
41/**
42 * Delete unnecessary nodes from a way
43 * @since 2575
44 */
45public class SimplifyWayAction extends JosmAction {
46
47 /**
48 * Constructs a new {@code SimplifyWayAction}.
49 */
50 public SimplifyWayAction() {
51 super(tr("Simplify Way"), "simplify", tr("Delete unnecessary nodes from a way."),
52 Shortcut.registerShortcut("tools:simplify", tr("Tool: {0}", tr("Simplify Way")), KeyEvent.VK_Y, Shortcut.SHIFT), true);
53 setHelpId(ht("/Action/SimplifyWay"));
54 }
55
56 protected boolean confirmWayWithNodesOutsideBoundingBox(List<? extends OsmPrimitive> primitives) {
57 return DeleteAction.checkAndConfirmOutlyingDelete(primitives, null);
58 }
59
60 protected void alertSelectAtLeastOneWay() {
61 SwingUtilities.invokeLater(() ->
62 new Notification(
63 tr("Please select at least one way to simplify."))
64 .setIcon(JOptionPane.WARNING_MESSAGE)
65 .setDuration(Notification.TIME_SHORT)
66 .setHelpTopic(ht("/Action/SimplifyWay#SelectAWayToSimplify"))
67 .show()
68 );
69 }
70
71 protected boolean confirmSimplifyManyWays(int numWays) {
72 ButtonSpec[] options = new ButtonSpec[] {
73 new ButtonSpec(
74 tr("Yes"),
75 new ImageProvider("ok"),
76 tr("Simplify all selected ways"),
77 null),
78 new ButtonSpec(
79 tr("Cancel"),
80 new ImageProvider("cancel"),
81 tr("Cancel operation"),
82 null)
83 };
84 return 0 == HelpAwareOptionPane.showOptionDialog(
85 MainApplication.getMainFrame(),
86 tr("The selection contains {0} ways. Are you sure you want to simplify them all?", numWays),
87 tr("Simplify ways?"),
88 JOptionPane.WARNING_MESSAGE,
89 null, // no special icon
90 options,
91 options[0],
92 ht("/Action/SimplifyWay#ConfirmSimplifyAll")
93 );
94 }
95
96 @Override
97 public void actionPerformed(ActionEvent e) {
98 DataSet ds = getLayerManager().getEditDataSet();
99 ds.beginUpdate();
100 try {
101 List<Way> ways = ds.getSelectedWays().stream()
102 .filter(p -> !p.isIncomplete())
103 .collect(Collectors.toList());
104 if (ways.isEmpty()) {
105 alertSelectAtLeastOneWay();
106 return;
107 } else if (!confirmWayWithNodesOutsideBoundingBox(ways) || (ways.size() > 10 && !confirmSimplifyManyWays(ways.size()))) {
108 return;
109 }
110
111 Collection<Command> allCommands = new LinkedList<>();
112 for (Way way: ways) {
113 SequenceCommand simplifyCommand = simplifyWay(way);
114 if (simplifyCommand == null) {
115 continue;
116 }
117 allCommands.add(simplifyCommand);
118 }
119 if (allCommands.isEmpty()) return;
120 SequenceCommand rootCommand = new SequenceCommand(
121 trn("Simplify {0} way", "Simplify {0} ways", allCommands.size(), allCommands.size()),
122 allCommands
123 );
124 UndoRedoHandler.getInstance().add(rootCommand);
125 } finally {
126 ds.endUpdate();
127 }
128 }
129
130 /**
131 * Replies true if <code>node</code> is a required node which can't be removed
132 * in order to simplify the way.
133 *
134 * @param way the way to be simplified
135 * @param node the node to check
136 * @param multipleUseNodes set of nodes which is used more than once in the way
137 * @return true if <code>node</code> is a required node which can't be removed
138 * in order to simplify the way.
139 */
140 protected static boolean isRequiredNode(Way way, Node node, Set<Node> multipleUseNodes) {
141 boolean isRequired = node.isTagged();
142 if (!isRequired && multipleUseNodes.contains(node)) {
143 int frequency = Collections.frequency(way.getNodes(), node);
144 if ((way.getNode(0) == node) && (way.getNode(way.getNodesCount()-1) == node)) {
145 frequency = frequency - 1; // closed way closing node counted only once
146 }
147 isRequired = frequency > 1;
148 }
149 if (!isRequired) {
150 List<OsmPrimitive> parents = new LinkedList<>();
151 parents.addAll(node.getReferrers());
152 parents.remove(way);
153 isRequired = !parents.isEmpty();
154 }
155 return isRequired;
156 }
157
158 /**
159 * Simplifies a way with default threshold (read from preferences).
160 *
161 * @param w the way to simplify
162 * @return The sequence of commands to run
163 * @since 6411
164 */
165 public final SequenceCommand simplifyWay(Way w) {
166 return simplifyWay(w, Config.getPref().getDouble("simplify-way.max-error", 3.0));
167 }
168
169 /**
170 * Calculate a set of nodes which occurs more than once in the way
171 * @param w the way
172 * @return a set of nodes which occurs more than once in the way
173 */
174 private static Set<Node> getMultiUseNodes(Way w) {
175 Set<Node> multipleUseNodes = new HashSet<>();
176 Set<Node> allNodes = new HashSet<>();
177 for (Node n : w.getNodes()) {
178 if (!allNodes.add(n))
179 multipleUseNodes.add(n);
180 }
181 return multipleUseNodes;
182 }
183
184 /**
185 * Simplifies a way with a given threshold.
186 *
187 * @param w the way to simplify
188 * @param threshold the max error threshold
189 * @return The sequence of commands to run
190 * @since 6411
191 */
192 public static SequenceCommand simplifyWay(Way w, double threshold) {
193 int lower = 0;
194 int i = 0;
195
196 Set<Node> multipleUseNodes = getMultiUseNodes(w);
197 List<Node> newNodes = new ArrayList<>(w.getNodesCount());
198 while (i < w.getNodesCount()) {
199 if (isRequiredNode(w, w.getNode(i), multipleUseNodes)) {
200 // copy a required node to the list of new nodes. Simplify not possible
201 newNodes.add(w.getNode(i));
202 i++;
203 lower++;
204 continue;
205 }
206 i++;
207 // find the longest sequence of not required nodes ...
208 while (i < w.getNodesCount() && !isRequiredNode(w, w.getNode(i), multipleUseNodes)) {
209 i++;
210 }
211 // ... and simplify them
212 buildSimplifiedNodeList(w.getNodes(), lower, Math.min(w.getNodesCount()-1, i), threshold, newNodes);
213 lower = i;
214 i++;
215 }
216
217 // Closed way, check if the first node could also be simplified ...
218 if (newNodes.size() > 3 && newNodes.get(0) == newNodes.get(newNodes.size() - 1)
219 && !isRequiredNode(w, newNodes.get(0), multipleUseNodes)) {
220 final List<Node> l1 = Arrays.asList(newNodes.get(newNodes.size() - 2), newNodes.get(0), newNodes.get(1));
221 final List<Node> l2 = new ArrayList<>(3);
222 buildSimplifiedNodeList(l1, 0, 2, threshold, l2);
223 if (!l2.contains(newNodes.get(0))) {
224 newNodes.remove(0);
225 newNodes.set(newNodes.size() - 1, newNodes.get(0)); // close the way
226 }
227 }
228
229 if (newNodes.size() == w.getNodesCount()) return null;
230
231 Set<Node> delNodes = new HashSet<>(w.getNodes());
232 delNodes.removeAll(newNodes);
233
234 if (delNodes.isEmpty()) return null;
235
236 Collection<Command> cmds = new LinkedList<>();
237 Way newWay = new Way(w);
238 newWay.setNodes(newNodes);
239 cmds.add(new ChangeCommand(w, newWay));
240 cmds.add(new DeleteCommand(w.getDataSet(), delNodes));
241 w.getDataSet().clearSelection(delNodes);
242 return new SequenceCommand(
243 trn("Simplify Way (remove {0} node)", "Simplify Way (remove {0} nodes)", delNodes.size(), delNodes.size()), cmds);
244 }
245
246 /**
247 * Builds the simplified list of nodes for a way segment given by a lower index <code>from</code>
248 * and an upper index <code>to</code>
249 *
250 * @param wnew the way to simplify
251 * @param from the lower index
252 * @param to the upper index
253 * @param threshold the max error threshold
254 * @param simplifiedNodes list that will contain resulting nodes
255 */
256 protected static void buildSimplifiedNodeList(List<Node> wnew, int from, int to, double threshold, List<Node> simplifiedNodes) {
257
258 Node fromN = wnew.get(from);
259 Node toN = wnew.get(to);
260
261 // Get max xte
262 int imax = -1;
263 double xtemax = 0;
264 for (int i = from + 1; i < to; i++) {
265 Node n = wnew.get(i);
266 // CHECKSTYLE.OFF: SingleSpaceSeparator
267 double xte = Math.abs(Ellipsoid.WGS84.a
268 * xtd(fromN.lat() * Math.PI / 180, fromN.lon() * Math.PI / 180, toN.lat() * Math.PI / 180,
269 toN.lon() * Math.PI / 180, n.lat() * Math.PI / 180, n.lon() * Math.PI / 180));
270 // CHECKSTYLE.ON: SingleSpaceSeparator
271 if (xte > xtemax) {
272 xtemax = xte;
273 imax = i;
274 }
275 }
276
277 if (imax != -1 && xtemax >= threshold) {
278 // Segment cannot be simplified - try shorter segments
279 buildSimplifiedNodeList(wnew, from, imax, threshold, simplifiedNodes);
280 buildSimplifiedNodeList(wnew, imax, to, threshold, simplifiedNodes);
281 } else {
282 // Simplify segment
283 if (simplifiedNodes.isEmpty() || simplifiedNodes.get(simplifiedNodes.size()-1) != fromN) {
284 simplifiedNodes.add(fromN);
285 }
286 if (fromN != toN) {
287 simplifiedNodes.add(toN);
288 }
289 }
290 }
291
292 /* From Aviaton Formulary v1.3
293 * http://williams.best.vwh.net/avform.htm
294 */
295 private static double dist(double lat1, double lon1, double lat2, double lon2) {
296 return 2 * Math.asin(Math.sqrt(Math.pow(Math.sin((lat1 - lat2) / 2), 2) + Math.cos(lat1) * Math.cos(lat2)
297 * Math.pow(Math.sin((lon1 - lon2) / 2), 2)));
298 }
299
300 private static double course(double lat1, double lon1, double lat2, double lon2) {
301 return Math.atan2(Math.sin(lon1 - lon2) * Math.cos(lat2), Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1)
302 * Math.cos(lat2) * Math.cos(lon1 - lon2))
303 % (2 * Math.PI);
304 }
305
306 private static double xtd(double lat1, double lon1, double lat2, double lon2, double lat3, double lon3) {
307 double distAD = dist(lat1, lon1, lat3, lon3);
308 double crsAD = course(lat1, lon1, lat3, lon3);
309 double crsAB = course(lat1, lon1, lat2, lon2);
310 return Math.asin(Math.sin(distAD) * Math.sin(crsAD - crsAB));
311 }
312
313 @Override
314 protected void updateEnabledState() {
315 updateEnabledStateOnCurrentSelection();
316 }
317
318 @Override
319 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
320 updateEnabledStateOnModifiableSelection(selection);
321 }
322}
Note: See TracBrowser for help on using the repository browser.