001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.actions; 003 004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005 import static org.openstreetmap.josm.tools.I18n.tr; 006 import static org.openstreetmap.josm.tools.I18n.trn; 007 008 import java.awt.event.ActionEvent; 009 import java.awt.event.KeyEvent; 010 import java.util.ArrayList; 011 import java.util.Collection; 012 import java.util.Collections; 013 import java.util.HashSet; 014 import java.util.LinkedList; 015 import java.util.List; 016 017 import javax.swing.JOptionPane; 018 019 import org.openstreetmap.josm.Main; 020 import org.openstreetmap.josm.command.ChangeCommand; 021 import org.openstreetmap.josm.command.Command; 022 import org.openstreetmap.josm.command.DeleteCommand; 023 import org.openstreetmap.josm.command.SequenceCommand; 024 import org.openstreetmap.josm.data.osm.DataSet; 025 import org.openstreetmap.josm.data.osm.Node; 026 import org.openstreetmap.josm.data.osm.OsmPrimitive; 027 import org.openstreetmap.josm.data.osm.Way; 028 import org.openstreetmap.josm.gui.HelpAwareOptionPane; 029 import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 030 import org.openstreetmap.josm.gui.help.HelpUtil; 031 import org.openstreetmap.josm.tools.ImageProvider; 032 import org.openstreetmap.josm.tools.Shortcut; 033 034 public class SimplifyWayAction extends JosmAction { 035 public SimplifyWayAction() { 036 super(tr("Simplify Way"), "simplify", tr("Delete unnecessary nodes from a way."), Shortcut.registerShortcut("tools:simplify", tr("Tool: {0}", tr("Simplify Way")), 037 KeyEvent.VK_Y, Shortcut.SHIFT), true); 038 putValue("help", ht("/Action/SimplifyWay")); 039 } 040 041 protected boolean confirmWayWithNodesOutsideBoundingBox(List<? extends OsmPrimitive> primitives) { 042 System.out.println(primitives); 043 return DeleteCommand.checkAndConfirmOutlyingDelete(Main.map.mapView.getEditLayer(), primitives, null); 044 } 045 046 protected void alertSelectAtLeastOneWay() { 047 HelpAwareOptionPane.showOptionDialog( 048 Main.parent, 049 tr("Please select at least one way to simplify."), 050 tr("Warning"), 051 JOptionPane.WARNING_MESSAGE, 052 HelpUtil.ht("/Action/SimplifyWay#SelectAWayToSimplify") 053 ); 054 } 055 056 protected boolean confirmSimplifyManyWays(int numWays) { 057 ButtonSpec[] options = new ButtonSpec[] { 058 new ButtonSpec( 059 tr("Yes"), 060 ImageProvider.get("ok"), 061 tr("Simplify all selected ways"), 062 null 063 ), 064 new ButtonSpec( 065 tr("Cancel"), 066 ImageProvider.get("cancel"), 067 tr("Cancel operation"), 068 null 069 ) 070 }; 071 int ret = HelpAwareOptionPane.showOptionDialog( 072 Main.parent, 073 tr( 074 "The selection contains {0} ways. Are you sure you want to simplify them all?", 075 numWays 076 ), 077 tr("Simplify ways?"), 078 JOptionPane.WARNING_MESSAGE, 079 null, // no special icon 080 options, 081 options[0], 082 HelpUtil.ht("/Action/SimplifyWay#ConfirmSimplifyAll") 083 ); 084 return ret == 0; 085 } 086 087 public void actionPerformed(ActionEvent e) { 088 DataSet ds = getCurrentDataSet(); 089 ds.beginUpdate(); 090 try 091 { 092 List<Way> ways = OsmPrimitive.getFilteredList(ds.getSelected(), Way.class); 093 if (ways.isEmpty()) { 094 alertSelectAtLeastOneWay(); 095 return; 096 } else if (!confirmWayWithNodesOutsideBoundingBox(ways)) 097 return; 098 else if (ways.size() > 10) { 099 if (!confirmSimplifyManyWays(ways.size())) 100 return; 101 } 102 103 Collection<Command> allCommands = new LinkedList<Command>(); 104 for (Way way: ways) { 105 SequenceCommand simplifyCommand = simplifyWay(way, ds); 106 if (simplifyCommand == null) { 107 continue; 108 } 109 allCommands.add(simplifyCommand); 110 } 111 if (allCommands.isEmpty()) return; 112 SequenceCommand rootCommand = new SequenceCommand( 113 trn("Simplify {0} way", "Simplify {0} ways", allCommands.size(), allCommands.size()), 114 allCommands 115 ); 116 Main.main.undoRedo.add(rootCommand); 117 } finally { 118 ds.endUpdate(); 119 } 120 Main.map.repaint(); 121 } 122 123 /** 124 * Replies true if <code>node</code> is a required node which can't be removed 125 * in order to simplify the way. 126 * 127 * @param way the way to be simplified 128 * @param node the node to check 129 * @return true if <code>node</code> is a required node which can't be removed 130 * in order to simplify the way. 131 */ 132 protected boolean isRequiredNode(Way way, Node node) { 133 boolean isRequired = Collections.frequency(way.getNodes(), node) > 1; 134 if (! isRequired) { 135 List<OsmPrimitive> parents = new LinkedList<OsmPrimitive>(); 136 parents.addAll(node.getReferrers()); 137 parents.remove(way); 138 isRequired = !parents.isEmpty(); 139 } 140 if (!isRequired) { 141 isRequired = node.isTagged(); 142 } 143 return isRequired; 144 } 145 146 /** 147 * Simplifies a way 148 * 149 * @param w the way to simplify 150 */ 151 public SequenceCommand simplifyWay(Way w, DataSet ds) { 152 double threshold = Double.parseDouble(Main.pref.get("simplify-way.max-error", "3")); 153 int lower = 0; 154 int i = 0; 155 List<Node> newNodes = new ArrayList<Node>(w.getNodesCount()); 156 while(i < w.getNodesCount()){ 157 if (isRequiredNode(w,w.getNode(i))) { 158 // copy a required node to the list of new nodes. Simplify not 159 // possible 160 newNodes.add(w.getNode(i)); 161 i++; 162 lower++; 163 continue; 164 } 165 i++; 166 // find the longest sequence of not required nodes ... 167 while(i<w.getNodesCount() && !isRequiredNode(w,w.getNode(i))) { 168 i++; 169 } 170 // ... and simplify them 171 buildSimplifiedNodeList(w.getNodes(), lower, Math.min(w.getNodesCount()-1, i), threshold,newNodes); 172 lower=i; 173 i++; 174 } 175 176 HashSet<Node> delNodes = new HashSet<Node>(); 177 delNodes.addAll(w.getNodes()); 178 delNodes.removeAll(newNodes); 179 180 if (delNodes.isEmpty()) return null; 181 182 Collection<Command> cmds = new LinkedList<Command>(); 183 Way newWay = new Way(w); 184 newWay.setNodes(newNodes); 185 cmds.add(new ChangeCommand(w, newWay)); 186 cmds.add(new DeleteCommand(delNodes)); 187 ds.clearSelection(delNodes); 188 return new SequenceCommand(trn("Simplify Way (remove {0} node)", "Simplify Way (remove {0} nodes)", delNodes.size(), delNodes.size()), cmds); 189 } 190 191 /** 192 * Builds the simplified list of nodes for a way segment given by a lower index <code>from</code> 193 * and an upper index <code>to</code> 194 * 195 * @param wnew the way to simplify 196 * @param from the lower index 197 * @param to the upper index 198 * @param threshold 199 */ 200 protected void buildSimplifiedNodeList(List<Node> wnew, int from, int to, double threshold, List<Node> simplifiedNodes) { 201 202 Node fromN = wnew.get(from); 203 Node toN = wnew.get(to); 204 205 // Get max xte 206 int imax = -1; 207 double xtemax = 0; 208 for (int i = from + 1; i < to; i++) { 209 Node n = wnew.get(i); 210 double xte = Math.abs(EARTH_RAD 211 * xtd(fromN.getCoor().lat() * Math.PI / 180, fromN.getCoor().lon() * Math.PI / 180, toN.getCoor().lat() * Math.PI 212 / 180, toN.getCoor().lon() * Math.PI / 180, n.getCoor().lat() * Math.PI / 180, n.getCoor().lon() * Math.PI 213 / 180)); 214 if (xte > xtemax) { 215 xtemax = xte; 216 imax = i; 217 } 218 } 219 220 if (imax != -1 && xtemax >= threshold) { 221 // Segment cannot be simplified - try shorter segments 222 buildSimplifiedNodeList(wnew, from, imax,threshold,simplifiedNodes); 223 //simplifiedNodes.add(wnew.get(imax)); 224 buildSimplifiedNodeList(wnew, imax, to, threshold,simplifiedNodes); 225 } else { 226 // Simplify segment 227 if (simplifiedNodes.isEmpty() || simplifiedNodes.get(simplifiedNodes.size()-1) != fromN) { 228 simplifiedNodes.add(fromN); 229 } 230 if (fromN != toN) { 231 simplifiedNodes.add(toN); 232 } 233 } 234 } 235 236 public static final double EARTH_RAD = 6378137.0; 237 238 /* From Aviaton Formulary v1.3 239 * http://williams.best.vwh.net/avform.htm 240 */ 241 public static double dist(double lat1, double lon1, double lat2, double lon2) { 242 return 2 * Math.asin(Math.sqrt(Math.pow(Math.sin((lat1 - lat2) / 2), 2) + Math.cos(lat1) * Math.cos(lat2) 243 * Math.pow(Math.sin((lon1 - lon2) / 2), 2))); 244 } 245 246 public static double course(double lat1, double lon1, double lat2, double lon2) { 247 return Math.atan2(Math.sin(lon1 - lon2) * Math.cos(lat2), Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) 248 * Math.cos(lat2) * Math.cos(lon1 - lon2)) 249 % (2 * Math.PI); 250 } 251 252 public static double xtd(double lat1, double lon1, double lat2, double lon2, double lat3, double lon3) { 253 double dist_AD = dist(lat1, lon1, lat3, lon3); 254 double crs_AD = course(lat1, lon1, lat3, lon3); 255 double crs_AB = course(lat1, lon1, lat2, lon2); 256 return Math.asin(Math.sin(dist_AD) * Math.sin(crs_AD - crs_AB)); 257 } 258 259 @Override 260 protected void updateEnabledState() { 261 if (getCurrentDataSet() == null) { 262 setEnabled(false); 263 } else { 264 updateEnabledState(getCurrentDataSet().getSelected()); 265 } 266 } 267 268 @Override 269 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 270 setEnabled(selection != null && !selection.isEmpty()); 271 } 272 }