source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/ParallelWays.java@ 12308

Last change on this file since 12308 was 12308, checked in by stoecker, 7 years ago

typo

  • Property svn:eol-style set to native
File size: 7.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.mapmode;
3
4import java.util.ArrayList;
5import java.util.Collection;
6import java.util.Collections;
7import java.util.HashMap;
8import java.util.HashSet;
9import java.util.List;
10import java.util.Map;
11import java.util.Set;
12
13import org.openstreetmap.josm.Main;
14import org.openstreetmap.josm.actions.CombineWayAction;
15import org.openstreetmap.josm.command.AddCommand;
16import org.openstreetmap.josm.command.Command;
17import org.openstreetmap.josm.command.SequenceCommand;
18import org.openstreetmap.josm.data.coor.EastNorth;
19import org.openstreetmap.josm.data.osm.Node;
20import org.openstreetmap.josm.data.osm.Way;
21import org.openstreetmap.josm.tools.Geometry;
22
23/**
24 * Helper for {@link ParallelWayAction}.
25 *
26 * @author Ole Jørgen Brønner (olejorgenb)
27 */
28public class ParallelWays {
29 private final List<Way> ways;
30 private final List<Node> sortedNodes;
31
32 private final int nodeCount;
33
34 private final EastNorth[] pts;
35 private final EastNorth[] normals;
36
37 /**
38 * Constructs a new {@code ParallelWays}.
39 * @param sourceWays source ways
40 * @param copyTags whether tags should be copied
41 * @param refWayIndex Need a reference way to determine the direction of the offset when we manage multiple ways
42 */
43 public ParallelWays(Collection<Way> sourceWays, boolean copyTags, int refWayIndex) {
44 // Possible/sensible to use PrimetiveDeepCopy here?
45
46 // Make a deep copy of the ways, keeping the copied ways connected
47 // TODO: This assumes the first/last nodes of the ways are the only possible shared nodes.
48 Map<Node, Node> splitNodeMap = new HashMap<>(sourceWays.size());
49 for (Way w : sourceWays) {
50 if (!splitNodeMap.containsKey(w.firstNode())) {
51 splitNodeMap.put(w.firstNode(), copyNode(w.firstNode(), copyTags));
52 }
53 if (!splitNodeMap.containsKey(w.lastNode())) {
54 splitNodeMap.put(w.lastNode(), copyNode(w.lastNode(), copyTags));
55 }
56 }
57 ways = new ArrayList<>(sourceWays.size());
58 for (Way w : sourceWays) {
59 Way wCopy = new Way();
60 wCopy.addNode(splitNodeMap.get(w.firstNode()));
61 for (int i = 1; i < w.getNodesCount() - 1; i++) {
62 wCopy.addNode(copyNode(w.getNode(i), copyTags));
63 }
64 wCopy.addNode(splitNodeMap.get(w.lastNode()));
65 if (copyTags) {
66 wCopy.setKeys(w.getKeys());
67 }
68 ways.add(wCopy);
69 }
70
71 // Find a linear ordering of the nodes. Fail if there isn't one.
72 CombineWayAction.NodeGraph nodeGraph = CombineWayAction.NodeGraph.createUndirectedGraphFromNodeWays(ways);
73 List<Node> sortedNodesPath = nodeGraph.buildSpanningPath();
74 if (sortedNodesPath == null)
75 throw new IllegalArgumentException("Ways must have spanning path"); // Create a dedicated exception?
76
77 // Fix #8631 - Remove duplicated nodes from graph to be robust with self-intersecting ways
78 Set<Node> removedNodes = new HashSet<>();
79 sortedNodes = new ArrayList<>();
80 for (int i = 0; i < sortedNodesPath.size(); i++) {
81 Node n = sortedNodesPath.get(i);
82 if (i < sortedNodesPath.size()-1 && sortedNodesPath.get(i+1).getCoor().equals(n.getCoor())) {
83 removedNodes.add(n);
84 for (Way w : ways) {
85 w.removeNode(n);
86 }
87 continue;
88 }
89 if (!removedNodes.contains(n)) {
90 sortedNodes.add(n);
91 }
92 }
93
94 // Ugly method of ensuring that the offset isn't inverted. I'm sure there is a better and more elegant way
95 Way refWay = ways.get(refWayIndex);
96 boolean refWayReversed = true;
97 for (int i = 0; i < sortedNodes.size() - 1; i++) {
98 if (sortedNodes.get(i) == refWay.firstNode() && sortedNodes.get(i + 1) == refWay.getNode(1)) {
99 refWayReversed = false;
100 break;
101 }
102 }
103 if (refWayReversed) {
104 Collections.reverse(sortedNodes); // need to keep the orientation of the reference way.
105 }
106
107 // Initialize the required parameters. (segment normals, etc.)
108 nodeCount = sortedNodes.size();
109 pts = new EastNorth[nodeCount];
110 normals = new EastNorth[nodeCount - 1];
111 int i = 0;
112 for (Node n : sortedNodes) {
113 EastNorth t = n.getEastNorth();
114 pts[i] = t;
115 i++;
116 }
117 for (i = 0; i < nodeCount - 1; i++) {
118 double dx = pts[i + 1].getX() - pts[i].getX();
119 double dy = pts[i + 1].getY() - pts[i].getY();
120 double len = Math.sqrt(dx * dx + dy * dy);
121 normals[i] = new EastNorth(-dy / len, dx / len);
122 }
123 }
124
125 /**
126 * Determines if the nodes graph from a closed path
127 * @return {@code true} if the nodes graph from a closed path
128 */
129 public boolean isClosedPath() {
130 return sortedNodes.get(0) == sortedNodes.get(sortedNodes.size() - 1);
131 }
132
133 /**
134 * Offsets the way(s) d units. Positive d means to the left (relative to the reference way)
135 * @param d offset
136 */
137 public void changeOffset(double d) {
138 // This is the core algorithm:
139 /* 1. Calculate a parallel line, offset by 'd', to each segment in the path
140 * 2. Find the intersection of lines belonging to neighboring segments. These become the new node positions
141 * 3. Do some special casing for closed paths
142 *
143 * Simple and probably not even close to optimal performance wise
144 */
145
146 EastNorth[] ppts = new EastNorth[nodeCount];
147
148 EastNorth prevA = pts[0].add(normals[0].scale(d));
149 EastNorth prevB = pts[1].add(normals[0].scale(d));
150 for (int i = 1; i < nodeCount - 1; i++) {
151 EastNorth a = pts[i].add(normals[i].scale(d));
152 EastNorth b = pts[i + 1].add(normals[i].scale(d));
153 if (Geometry.segmentsParallel(a, b, prevA, prevB)) {
154 ppts[i] = a;
155 } else {
156 ppts[i] = Geometry.getLineLineIntersection(a, b, prevA, prevB);
157 }
158 prevA = a;
159 prevB = b;
160 }
161 if (isClosedPath()) {
162 EastNorth a = pts[0].add(normals[0].scale(d));
163 EastNorth b = pts[1].add(normals[0].scale(d));
164 if (Geometry.segmentsParallel(a, b, prevA, prevB)) {
165 ppts[0] = a;
166 } else {
167 ppts[0] = Geometry.getLineLineIntersection(a, b, prevA, prevB);
168 }
169 ppts[nodeCount - 1] = ppts[0];
170 } else {
171 ppts[0] = pts[0].add(normals[0].scale(d));
172 ppts[nodeCount - 1] = pts[nodeCount - 1].add(normals[nodeCount - 2].scale(d));
173 }
174
175 for (int i = 0; i < nodeCount; i++) {
176 sortedNodes.get(i).setEastNorth(ppts[i]);
177 }
178 }
179
180 /**
181 * Performs the action by adding a new sequence command to the undo/redo queue.
182 */
183 public void commit() {
184 Main.main.undoRedo.add(new SequenceCommand("Make parallel way(s)", makeAddWayAndNodesCommandList()));
185 }
186
187 private List<Command> makeAddWayAndNodesCommandList() {
188 List<Command> commands = new ArrayList<>(sortedNodes.size() + ways.size());
189 for (int i = 0; i < sortedNodes.size() - (isClosedPath() ? 1 : 0); i++) {
190 commands.add(new AddCommand(sortedNodes.get(i)));
191 }
192 for (Way w : ways) {
193 commands.add(new AddCommand(w));
194 }
195 return commands;
196 }
197
198 private static Node copyNode(Node source, boolean copyTags) {
199 if (copyTags)
200 return new Node(source, true);
201 else {
202 Node n = new Node();
203 n.setCoor(source.getCoor());
204 return n;
205 }
206 }
207
208 /**
209 * Returns the resulting parallel ways.
210 * @return the resulting parallel ways
211 */
212 public final List<Way> getWays() {
213 return ways;
214 }
215}
Note: See TracBrowser for help on using the repository browser.