source: josm/trunk/src/org/openstreetmap/josm/tools/Geometry.java

Last change on this file was 19446, checked in by GerdP, 6 weeks ago

fix #24485: DataIntegrityProblemException: Primitive cannot be modified in read-only dataset

  • create copies of nodes before adding them to a temporary way. This should also fix a memory leak.
  • Property svn:eol-style set to native
File size: 71.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.data.projection.Ellipsoid.WGS84;
5
6import java.awt.geom.Area;
7import java.awt.geom.Line2D;
8import java.awt.geom.Path2D;
9import java.awt.geom.PathIterator;
10import java.awt.geom.Rectangle2D;
11import java.math.BigDecimal;
12import java.math.MathContext;
13import java.util.ArrayList;
14import java.util.Collection;
15import java.util.Collections;
16import java.util.Comparator;
17import java.util.Iterator;
18import java.util.LinkedHashSet;
19import java.util.List;
20import java.util.Map;
21import java.util.Set;
22import java.util.TreeSet;
23import java.util.function.Predicate;
24import java.util.stream.Collectors;
25
26import org.openstreetmap.josm.command.AddCommand;
27import org.openstreetmap.josm.command.ChangeNodesCommand;
28import org.openstreetmap.josm.command.Command;
29import org.openstreetmap.josm.data.coor.EastNorth;
30import org.openstreetmap.josm.data.coor.ILatLon;
31import org.openstreetmap.josm.data.coor.LatLon;
32import org.openstreetmap.josm.data.osm.BBox;
33import org.openstreetmap.josm.data.osm.DataSet;
34import org.openstreetmap.josm.data.osm.INode;
35import org.openstreetmap.josm.data.osm.IPrimitive;
36import org.openstreetmap.josm.data.osm.IRelation;
37import org.openstreetmap.josm.data.osm.IWay;
38import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
39import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon;
40import org.openstreetmap.josm.data.osm.Node;
41import org.openstreetmap.josm.data.osm.NodePositionComparator;
42import org.openstreetmap.josm.data.osm.OsmPrimitive;
43import org.openstreetmap.josm.data.osm.Relation;
44import org.openstreetmap.josm.data.osm.Way;
45import org.openstreetmap.josm.data.osm.WaySegment;
46import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
47import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
48import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
49import org.openstreetmap.josm.data.projection.Projection;
50import org.openstreetmap.josm.data.projection.ProjectionRegistry;
51import org.openstreetmap.josm.data.projection.Projections;
52
53/**
54 * Some tools for geometry related tasks.
55 *
56 * @author viesturs
57 */
58public final class Geometry {
59
60 private Geometry() {
61 // Hide default constructor for utils classes
62 }
63
64 /**
65 * The result types for a {@link Geometry#polygonIntersection(Area, Area)} test
66 */
67 public enum PolygonIntersection {
68 /**
69 * The first polygon is inside the second one
70 */
71 FIRST_INSIDE_SECOND,
72 /**
73 * The second one is inside the first
74 */
75 SECOND_INSIDE_FIRST,
76 /**
77 * The polygons do not overlap
78 */
79 OUTSIDE,
80 /**
81 * The polygon borders cross each other
82 */
83 CROSSING
84 }
85
86 /** threshold value for size of intersection area given in east/north space */
87 public static final double INTERSECTION_EPS_EAST_NORTH = 1e-4;
88
89 /**
90 * Will find all intersection and add nodes there for list of given ways.
91 * Handles self-intersections too.
92 * And makes commands to add the intersection points to ways.
93 * <p>
94 * Prerequisite: no two nodes have the same coordinates.
95 *
96 * @param ways a list of ways to test
97 * @param test if true, do not build list of Commands, just return nodes
98 * @param cmds list of commands, typically empty when handed to this method.
99 * Will be filled with commands that add intersection nodes to
100 * the ways.
101 * @return set of new nodes, if test is true the list might not contain all intersections
102 */
103 public static Set<Node> addIntersections(List<Way> ways, boolean test, List<Command> cmds) {
104
105 int n = ways.size();
106 @SuppressWarnings("unchecked")
107 List<Node>[] newNodes = new ArrayList[n];
108 BBox[] wayBounds = new BBox[n];
109 boolean[] changedWays = new boolean[n];
110
111 Set<Node> intersectionNodes = new LinkedHashSet<>();
112
113 //copy node arrays for local usage.
114 for (int pos = 0; pos < n; pos++) {
115 newNodes[pos] = new ArrayList<>(ways.get(pos).getNodes());
116 wayBounds[pos] = ways.get(pos).getBBox();
117 changedWays[pos] = false;
118 }
119
120 DataSet dataset = ways.get(0).getDataSet();
121
122 //iterate over all way pairs and introduce the intersections
123 Comparator<Node> coordsComparator = new NodePositionComparator();
124 for (int seg1Way = 0; seg1Way < n; seg1Way++) {
125 for (int seg2Way = seg1Way; seg2Way < n; seg2Way++) {
126
127 //do not waste time on bounds that do not intersect
128 if (!wayBounds[seg1Way].intersects(wayBounds[seg2Way])) {
129 continue;
130 }
131
132 List<Node> way1Nodes = newNodes[seg1Way];
133 List<Node> way2Nodes = newNodes[seg2Way];
134
135 //iterate over primary segment
136 for (int seg1Pos = 0; seg1Pos + 1 < way1Nodes.size(); seg1Pos++) {
137
138 //iterate over secondary segment
139 int seg2Start = seg1Way != seg2Way ? 0 : seg1Pos + 2; //skip the adjacent segment
140
141 for (int seg2Pos = seg2Start; seg2Pos + 1 < way2Nodes.size(); seg2Pos++) {
142
143 //need to get them again every time, because other segments may be changed
144 Node seg1Node1 = way1Nodes.get(seg1Pos);
145 Node seg1Node2 = way1Nodes.get(seg1Pos + 1);
146 Node seg2Node1 = way2Nodes.get(seg2Pos);
147 Node seg2Node2 = way2Nodes.get(seg2Pos + 1);
148
149 int commonCount = 0;
150 //test if we have common nodes to add.
151 if (seg1Node1 == seg2Node1 || seg1Node1 == seg2Node2) {
152 commonCount++;
153
154 if (seg1Way == seg2Way &&
155 seg1Pos == 0 &&
156 seg2Pos == way2Nodes.size() -2) {
157 //do not add - this is first and last segment of the same way.
158 } else {
159 intersectionNodes.add(seg1Node1);
160 }
161 }
162
163 if (seg1Node2 == seg2Node1 || seg1Node2 == seg2Node2) {
164 commonCount++;
165
166 intersectionNodes.add(seg1Node2);
167 }
168
169 //no common nodes - find intersection
170 if (commonCount == 0) {
171 EastNorth intersection = getSegmentSegmentIntersection(
172 seg1Node1.getEastNorth(), seg1Node2.getEastNorth(),
173 seg2Node1.getEastNorth(), seg2Node2.getEastNorth());
174
175 if (intersection != null) {
176 Node newNode = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(intersection));
177 Node intNode = newNode;
178 boolean insertInSeg1 = false;
179 boolean insertInSeg2 = false;
180 //find if the intersection point is at end point of one of the segments, if so use that point
181
182 //segment 1
183 if (coordsComparator.compare(newNode, seg1Node1) == 0) {
184 intNode = seg1Node1;
185 } else if (coordsComparator.compare(newNode, seg1Node2) == 0) {
186 intNode = seg1Node2;
187 } else {
188 insertInSeg1 = true;
189 }
190
191 //segment 2
192 if (coordsComparator.compare(newNode, seg2Node1) == 0) {
193 intNode = seg2Node1;
194 } else if (coordsComparator.compare(newNode, seg2Node2) == 0) {
195 intNode = seg2Node2;
196 } else {
197 insertInSeg2 = true;
198 }
199
200 if (test) {
201 intersectionNodes.add(intNode);
202 return intersectionNodes;
203 }
204
205 if (insertInSeg1) {
206 way1Nodes.add(seg1Pos +1, intNode);
207 changedWays[seg1Way] = true;
208
209 //fix seg2 position, as indexes have changed, seg2Pos is always bigger than seg1Pos on the same segment.
210 if (seg2Way == seg1Way) {
211 seg2Pos++;
212 }
213 }
214
215 if (insertInSeg2) {
216 way2Nodes.add(seg2Pos +1, intNode);
217 changedWays[seg2Way] = true;
218
219 //Do not need to compare again to already split segment
220 seg2Pos++;
221 }
222
223 intersectionNodes.add(intNode);
224
225 if (intNode == newNode) {
226 cmds.add(new AddCommand(dataset, intNode));
227 }
228 }
229 } else if (test && !intersectionNodes.isEmpty())
230 return intersectionNodes;
231 }
232 }
233 }
234 }
235
236
237 for (int pos = 0; pos < ways.size(); pos++) {
238 if (changedWays[pos]) {
239 cmds.add(new ChangeNodesCommand(dataset, ways.get(pos), newNodes[pos]));
240 }
241 }
242
243 return intersectionNodes;
244 }
245
246 /**
247 * Tests if given point is to the right side of path consisting of 3 points.
248 * <p>
249 * (Imagine the path is continued beyond the endpoints, so you get two rays
250 * starting from lineP2 and going through lineP1 and lineP3 respectively
251 * which divide the plane into two parts. The test returns true, if testPoint
252 * lies in the part that is to the right when traveling in the direction
253 * lineP1, lineP2, lineP3.)
254 *
255 * @param <N> type of node
256 * @param lineP1 first point in path
257 * @param lineP2 second point in path
258 * @param lineP3 third point in path
259 * @param testPoint point to test
260 * @return true if to the right side, false otherwise
261 */
262 public static <N extends INode> boolean isToTheRightSideOfLine(N lineP1, N lineP2, N lineP3, N testPoint) {
263 boolean pathBendToRight = angleIsClockwise(lineP1, lineP2, lineP3);
264 boolean rightOfSeg1 = angleIsClockwise(lineP1, lineP2, testPoint);
265 boolean rightOfSeg2 = angleIsClockwise(lineP2, lineP3, testPoint);
266
267 if (pathBendToRight)
268 return rightOfSeg1 && rightOfSeg2;
269 else
270 return !(!rightOfSeg1 && !rightOfSeg2);
271 }
272
273 /**
274 * This method tests if secondNode is clockwise to first node.
275 * @param <N> type of node
276 * @param commonNode starting point for both vectors
277 * @param firstNode first vector end node
278 * @param secondNode second vector end node
279 * @return true if first vector is clockwise before second vector.
280 */
281 public static <N extends INode> boolean angleIsClockwise(N commonNode, N firstNode, N secondNode) {
282 return angleIsClockwise(commonNode.getEastNorth(), firstNode.getEastNorth(), secondNode.getEastNorth());
283 }
284
285 /**
286 * Finds the intersection of two line segments.
287 * @param p1 the coordinates of the start point of the first specified line segment
288 * @param p2 the coordinates of the end point of the first specified line segment
289 * @param p3 the coordinates of the start point of the second specified line segment
290 * @param p4 the coordinates of the end point of the second specified line segment
291 * @return EastNorth null if no intersection was found, the EastNorth coordinates of the intersection otherwise
292 * @see #getSegmentSegmentIntersection(ILatLon, ILatLon, ILatLon, ILatLon)
293 */
294 public static EastNorth getSegmentSegmentIntersection(EastNorth p1, EastNorth p2, EastNorth p3, EastNorth p4) {
295 // see the ILatLon version for an explanation why the checks are in the if statement
296 if (!(p1.isValid() && p2.isValid() && p3.isValid() && p4.isValid())) {
297 CheckParameterUtil.ensureThat(p1.isValid(), () -> p1 + " invalid");
298 CheckParameterUtil.ensureThat(p2.isValid(), () -> p2 + " invalid");
299 CheckParameterUtil.ensureThat(p3.isValid(), () -> p3 + " invalid");
300 CheckParameterUtil.ensureThat(p4.isValid(), () -> p4 + " invalid");
301 }
302
303 double x1 = p1.getX();
304 double y1 = p1.getY();
305 double x2 = p2.getX();
306 double y2 = p2.getY();
307 double x3 = p3.getX();
308 double y3 = p3.getY();
309 double x4 = p4.getX();
310 double y4 = p4.getY();
311 double[] en = getSegmentSegmentIntersection(x1, y1, x2, y2, x3, y3, x4, y4);
312 if (en != null && en.length == 2) {
313 return new EastNorth(en[0], en[1]);
314 }
315 return null;
316 }
317
318 /**
319 * Finds the intersection of two line segments.
320 * @param p1 the coordinates of the start point of the first specified line segment
321 * @param p2 the coordinates of the end point of the first specified line segment
322 * @param p3 the coordinates of the start point of the second specified line segment
323 * @param p4 the coordinates of the end point of the second specified line segment
324 * @return LatLon null if no intersection was found, the LatLon coordinates of the intersection otherwise
325 * @see #getSegmentSegmentIntersection(EastNorth, EastNorth, EastNorth, EastNorth)
326 * @since 18553
327 */
328 public static ILatLon getSegmentSegmentIntersection(ILatLon p1, ILatLon p2, ILatLon p3, ILatLon p4) {
329 // Avoid lambda creation if at all possible -- this pretty much removes all memory allocations
330 // from this method (11.4 GB to 0) when testing #20716 with Mesa County, CO (overpass download).
331 // There was also a 2/3 decrease in CPU samples for the method.
332 if (!(p1.isLatLonKnown() && p2.isLatLonKnown() && p3.isLatLonKnown() && p4.isLatLonKnown())) {
333 CheckParameterUtil.ensureThat(p1.isLatLonKnown(), () -> p1 + " invalid");
334 CheckParameterUtil.ensureThat(p2.isLatLonKnown(), () -> p2 + " invalid");
335 CheckParameterUtil.ensureThat(p3.isLatLonKnown(), () -> p3 + " invalid");
336 CheckParameterUtil.ensureThat(p4.isLatLonKnown(), () -> p4 + " invalid");
337 }
338
339 double x1 = p1.lon();
340 double y1 = p1.lat();
341 double x2 = p2.lon();
342 double y2 = p2.lat();
343 double x3 = p3.lon();
344 double y3 = p3.lat();
345 double x4 = p4.lon();
346 double y4 = p4.lat();
347 double[] en = getSegmentSegmentIntersection(x1, y1, x2, y2, x3, y3, x4, y4);
348 if (en != null && en.length == 2) {
349 return new LatLon(en[1], en[0]);
350 }
351 return null;
352 }
353
354 /**
355 * Get the segment-segment intersection of two line segments
356 * @param x1 The x coordinate of the first point (first segment)
357 * @param y1 The y coordinate of the first point (first segment)
358 * @param x2 The x coordinate of the second point (first segment)
359 * @param y2 The y coordinate of the second point (first segment)
360 * @param x3 The x coordinate of the third point (second segment)
361 * @param y3 The y coordinate of the third point (second segment)
362 * @param x4 The x coordinate of the fourth point (second segment)
363 * @param y4 The y coordinate of the fourth point (second segment)
364 * @return {@code null} if no intersection was found, otherwise [x, y]
365 */
366 private static double[] getSegmentSegmentIntersection(double x1, double y1, double x2, double y2, double x3, double y3,
367 double x4, double y4) {
368
369 //TODO: do this locally.
370 //TODO: remove this check after careful testing
371 if (!Line2D.linesIntersect(x1, y1, x2, y2, x3, y3, x4, y4)) return null;
372
373 // solve line-line intersection in parametric form:
374 // (x1,y1) + (x2-x1,y2-y1)* u = (x3,y3) + (x4-x3,y4-y3)* v
375 // (x2-x1,y2-y1)*u - (x4-x3,y4-y3)*v = (x3-x1,y3-y1)
376 // if 0<= u,v <=1, intersection exists at ( x1+ (x2-x1)*u, y1 + (y2-y1)*u )
377
378 double a1 = x2 - x1;
379 double b1 = x3 - x4;
380 double c1 = x3 - x1;
381
382 double a2 = y2 - y1;
383 double b2 = y3 - y4;
384 double c2 = y3 - y1;
385
386 // Solve the equations
387 double det = a1*b2 - a2*b1;
388
389 double uu = b2*c1 - b1*c2;
390 double vv = a1*c2 - a2*c1;
391 double mag = Math.abs(uu)+Math.abs(vv);
392
393 if (Math.abs(det) > 1e-12 * mag) {
394 double u = uu/det, v = vv/det;
395 if (u > -1e-8 && u < 1+1e-8 && v > -1e-8 && v < 1+1e-8) {
396 if (u < 0) u = 0;
397 if (u > 1) u = 1.0;
398 return new double[] {x1+a1*u, y1+a2*u};
399 } else {
400 return null;
401 }
402 } else {
403 // parallel lines
404 return null;
405 }
406 }
407
408 /**
409 * Finds the intersection of two lines of infinite length.
410 *
411 * @param p1 first point on first line
412 * @param p2 second point on first line
413 * @param p3 first point on second line
414 * @param p4 second point on second line
415 * @return EastNorth null if no intersection was found, the coordinates of the intersection otherwise
416 * @throws IllegalArgumentException if a parameter is null or without valid coordinates
417 */
418 public static EastNorth getLineLineIntersection(EastNorth p1, EastNorth p2, EastNorth p3, EastNorth p4) {
419
420 CheckParameterUtil.ensureThat(p1.isValid(), () -> p1 + " invalid");
421 CheckParameterUtil.ensureThat(p2.isValid(), () -> p2 + " invalid");
422 CheckParameterUtil.ensureThat(p3.isValid(), () -> p3 + " invalid");
423 CheckParameterUtil.ensureThat(p4.isValid(), () -> p4 + " invalid");
424
425 // Basically, the formula from wikipedia is used:
426 // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
427 // However, large numbers lead to rounding errors (see #10286).
428 // To avoid this, p1 is first subtracted from each of the points:
429 // p1' = 0
430 // p2' = p2 - p1
431 // p3' = p3 - p1
432 // p4' = p4 - p1
433 // In the end, p1 is added to the intersection point of segment p1'/p2'
434 // and segment p3'/p4'.
435
436 // Convert line from (point, point) form to ax+by=c
437 double a1 = p2.getY() - p1.getY();
438 double b1 = p1.getX() - p2.getX();
439
440 double a2 = p4.getY() - p3.getY();
441 double b2 = p3.getX() - p4.getX();
442
443 // Solve the equations
444 double det = a1 * b2 - a2 * b1;
445 if (det == 0)
446 return null; // Lines are parallel
447
448 double c2 = (p4.getX() - p1.getX()) * (p3.getY() - p1.getY()) - (p3.getX() - p1.getX()) * (p4.getY() - p1.getY());
449
450 return new EastNorth(b1 * c2 / det + p1.getX(), -a1 * c2 / det + p1.getY());
451 }
452
453 /**
454 * Check if the segment p1 - p2 is parallel to p3 - p4
455 * @param p1 First point for first segment
456 * @param p2 Second point for first segment
457 * @param p3 First point for second segment
458 * @param p4 Second point for second segment
459 * @return <code>true</code> if they are parallel or close to parallel
460 */
461 public static boolean segmentsParallel(EastNorth p1, EastNorth p2, EastNorth p3, EastNorth p4) {
462
463 CheckParameterUtil.ensureThat(p1.isValid(), () -> p1 + " invalid");
464 CheckParameterUtil.ensureThat(p2.isValid(), () -> p2 + " invalid");
465 CheckParameterUtil.ensureThat(p3.isValid(), () -> p3 + " invalid");
466 CheckParameterUtil.ensureThat(p4.isValid(), () -> p4 + " invalid");
467
468 // Convert line from (point, point) form to ax+by=c
469 double a1 = p2.getY() - p1.getY();
470 double b1 = p1.getX() - p2.getX();
471
472 double a2 = p4.getY() - p3.getY();
473 double b2 = p3.getX() - p4.getX();
474
475 // Solve the equations
476 double det = a1 * b2 - a2 * b1;
477 // remove influence of of scaling factor
478 det /= Math.sqrt(a1*a1 + b1*b1) * Math.sqrt(a2*a2 + b2*b2);
479 return Math.abs(det) < 1e-3;
480 }
481
482 private static EastNorth closestPointTo(EastNorth p1, EastNorth p2, EastNorth point, boolean segmentOnly) {
483 CheckParameterUtil.ensureParameterNotNull(p1, "p1");
484 CheckParameterUtil.ensureParameterNotNull(p2, "p2");
485 CheckParameterUtil.ensureParameterNotNull(point, "point");
486
487 double ldx = p2.getX() - p1.getX();
488 double ldy = p2.getY() - p1.getY();
489
490 //segment zero length
491 if (ldx == 0 && ldy == 0)
492 return p1;
493
494 double pdx = point.getX() - p1.getX();
495 double pdy = point.getY() - p1.getY();
496
497 double offset = (pdx * ldx + pdy * ldy) / (ldx * ldx + ldy * ldy);
498
499 if (segmentOnly && offset <= 0)
500 return p1;
501 else if (segmentOnly && offset >= 1)
502 return p2;
503 else
504 return p1.interpolate(p2, offset);
505 }
506
507 /**
508 * Calculates closest point to a line segment.
509 * @param segmentP1 First point determining line segment
510 * @param segmentP2 Second point determining line segment
511 * @param point Point for which a closest point is searched on line segment [P1,P2]
512 * @return segmentP1 if it is the closest point, segmentP2 if it is the closest point,
513 * a new point if closest point is between segmentP1 and segmentP2.
514 * @see #closestPointToLine
515 * @since 3650
516 */
517 public static EastNorth closestPointToSegment(EastNorth segmentP1, EastNorth segmentP2, EastNorth point) {
518 return closestPointTo(segmentP1, segmentP2, point, true);
519 }
520
521 /**
522 * Calculates closest point to a line.
523 * @param lineP1 First point determining line
524 * @param lineP2 Second point determining line
525 * @param point Point for which a closest point is searched on line (P1,P2)
526 * @return The closest point found on line. It may be outside the segment [P1,P2].
527 * @see #closestPointToSegment
528 * @since 4134
529 */
530 public static EastNorth closestPointToLine(EastNorth lineP1, EastNorth lineP2, EastNorth point) {
531 return closestPointTo(lineP1, lineP2, point, false);
532 }
533
534 /**
535 * This method tests if secondNode is clockwise to first node.
536 * <p>
537 * The line through the two points commonNode and firstNode divides the
538 * plane into two parts. The test returns true, if secondNode lies in
539 * the part that is to the right when traveling in the direction from
540 * commonNode to firstNode.
541 *
542 * @param commonNode starting point for both vectors
543 * @param firstNode first vector end node
544 * @param secondNode second vector end node
545 * @return true if first vector is clockwise before second vector.
546 */
547 public static boolean angleIsClockwise(EastNorth commonNode, EastNorth firstNode, EastNorth secondNode) {
548
549 CheckParameterUtil.ensureThat(commonNode.isValid(), () -> commonNode + " invalid");
550 CheckParameterUtil.ensureThat(firstNode.isValid(), () -> firstNode + " invalid");
551 CheckParameterUtil.ensureThat(secondNode.isValid(), () -> secondNode + " invalid");
552
553 double dy1 = firstNode.getY() - commonNode.getY();
554 double dy2 = secondNode.getY() - commonNode.getY();
555 double dx1 = firstNode.getX() - commonNode.getX();
556 double dx2 = secondNode.getX() - commonNode.getX();
557
558 return dy1 * dx2 - dx1 * dy2 > 0;
559 }
560
561 /**
562 * Returns the Area of a polygon, from its list of nodes.
563 * @param polygon List of nodes forming polygon
564 * @return Area for the given list of nodes (EastNorth coordinates)
565 * @since 6841
566 */
567 public static Area getArea(List<? extends INode> polygon) {
568 Path2D path = new Path2D.Double(Path2D.WIND_NON_ZERO, polygon.size());
569
570 boolean begin = true;
571 for (INode n : polygon) {
572 EastNorth en = n.getEastNorth();
573 if (en != null) {
574 if (begin) {
575 path.moveTo(en.getX(), en.getY());
576 begin = false;
577 } else {
578 path.lineTo(en.getX(), en.getY());
579 }
580 }
581 }
582 if (!begin) {
583 path.closePath();
584 }
585
586 return new Area(path);
587 }
588
589 /**
590 * Builds a path from a list of nodes
591 * @param polygon Nodes, forming a closed polygon
592 * @param path2d path to add to; can be null, then a new path is created
593 * @return the path (LatLon coordinates)
594 * @since 13638 (signature)
595 */
596 public static Path2D buildPath2DLatLon(List<? extends ILatLon> polygon, Path2D path2d) {
597 Path2D path = path2d != null ? path2d : new Path2D.Double();
598 boolean begin = true;
599 for (ILatLon n : polygon) {
600 if (begin) {
601 path.moveTo(n.lon(), n.lat());
602 begin = false;
603 } else {
604 path.lineTo(n.lon(), n.lat());
605 }
606 }
607 if (!begin) {
608 path.closePath();
609 }
610 return path;
611 }
612
613 /**
614 * Calculate area in east/north space for given primitive. Uses {@link MultipolygonCache} for multipolygon relations.
615 * @param p the primitive
616 * @return the area in east/north space, might be empty if the primitive is incomplete or not closed or a node
617 * since 15938
618 */
619 public static Area getAreaEastNorth(IPrimitive p) {
620 if (p instanceof Way && ((Way) p).isClosed()) {
621 return Geometry.getArea(((Way) p).getNodes());
622 }
623 if (p instanceof Relation && p.isMultipolygon() && !p.isIncomplete()) {
624 Multipolygon mp = MultipolygonCache.getInstance().get((Relation) p);
625 if (mp.getOpenEnds().isEmpty()) {
626 Path2D path = new Path2D.Double();
627 path.setWindingRule(Path2D.WIND_EVEN_ODD);
628 for (PolyData pd : mp.getCombinedPolygons()) {
629 path.append(pd.get(), false);
630 }
631 return new Area(path);
632 }
633 }
634 return new Area();
635 }
636
637 /**
638 * Returns the Area of a polygon, from the multipolygon relation.
639 * @param multipolygon the multipolygon relation
640 * @return Area for the multipolygon (LatLon coordinates)
641 */
642 public static Area getAreaLatLon(Relation multipolygon) {
643 final Multipolygon mp = MultipolygonCache.getInstance().get(multipolygon);
644 Path2D path = new Path2D.Double();
645 path.setWindingRule(Path2D.WIND_EVEN_ODD);
646 for (Multipolygon.PolyData pd : mp.getCombinedPolygons()) {
647 buildPath2DLatLon(pd.getNodes(), path);
648 for (Multipolygon.PolyData pdInner : pd.getInners()) {
649 buildPath2DLatLon(pdInner.getNodes(), path);
650 }
651 }
652 return new Area(path);
653 }
654
655 /**
656 * Tests if two polygons intersect.
657 * @param first List of nodes forming first polygon
658 * @param second List of nodes forming second polygon
659 * @return intersection kind
660 */
661 public static PolygonIntersection polygonIntersection(List<? extends INode> first, List<? extends INode> second) {
662 Area a1 = getArea(first);
663 Area a2 = getArea(second);
664 return polygonIntersection(a1, a2, INTERSECTION_EPS_EAST_NORTH);
665 }
666
667 /**
668 * Tests if two polygons intersect. It is assumed that the area is given in East North points.
669 * @param a1 Area of first polygon
670 * @param a2 Area of second polygon
671 * @return intersection kind
672 * @since 6841
673 */
674 public static PolygonIntersection polygonIntersection(Area a1, Area a2) {
675 return polygonIntersection(a1, a2, INTERSECTION_EPS_EAST_NORTH);
676 }
677
678 /**
679 * Tests if two polygons intersect.
680 * @param a1 Area of first polygon
681 * @param a2 Area of second polygon
682 * @param eps an area threshold, everything below is considered an empty intersection
683 * @return intersection kind
684 */
685 public static PolygonIntersection polygonIntersection(Area a1, Area a2, double eps) {
686 return polygonIntersectionResult(a1, a2, eps).a;
687 }
688
689 /**
690 * Calculate intersection area and kind of intersection between two polygons.
691 * @param a1 Area of first polygon
692 * @param a2 Area of second polygon
693 * @param eps an area threshold, everything below is considered an empty intersection
694 * @return pair with intersection kind and intersection area (never null, but maybe empty)
695 * @since 15938
696 */
697 public static Pair<PolygonIntersection, Area> polygonIntersectionResult(Area a1, Area a2, double eps) {
698 // Simple intersect check (if their bounds don't intersect, don't bother going further; there will be no intersection)
699 // This avoids the more expensive Area#intersect call some of the time (decreases CPU and memory allocation by ~95%)
700 // in Mesa County, CO geometry validator test runs.
701 final Rectangle2D a12d = a1.getBounds2D();
702 final Rectangle2D a22d = a2.getBounds2D();
703 if (!a12d.intersects(a22d) || !a1.intersects(a22d) || !a2.intersects(a12d)) {
704 return new Pair<>(PolygonIntersection.OUTSIDE, new Area());
705 }
706 Area inter = new Area(a1);
707 inter.intersect(a2);
708
709 // Note: Area has an equals method that takes Area; it does _not_ override the Object.equals method.
710 if (inter.isEmpty() || !checkIntersection(inter, eps)) {
711 return new Pair<>(PolygonIntersection.OUTSIDE, inter);
712 } else if (a22d.contains(a12d) && inter.equals(a1)) {
713 return new Pair<>(PolygonIntersection.FIRST_INSIDE_SECOND, inter);
714 } else if (a12d.contains(a22d) && inter.equals(a2)) {
715 return new Pair<>(PolygonIntersection.SECOND_INSIDE_FIRST, inter);
716 } else {
717 return new Pair<>(PolygonIntersection.CROSSING, inter);
718 }
719 }
720
721 /**
722 * Check an intersection area which might describe multiple small polygons.
723 * Return true if any of the polygons is bigger than the given threshold.
724 * @param inter the intersection area
725 * @param eps an area threshold, everything below is considered an empty intersection
726 * @return true if any of the polygons is bigger than the given threshold
727 */
728 private static boolean checkIntersection(Area inter, double eps) {
729 PathIterator pit = inter.getPathIterator(null);
730 double[] res = new double[6];
731 Rectangle2D r = new Rectangle2D.Double();
732 while (!pit.isDone()) {
733 int type = pit.currentSegment(res);
734 switch (type) {
735 case PathIterator.SEG_MOVETO:
736 r = new Rectangle2D.Double(res[0], res[1], 0, 0);
737 break;
738 case PathIterator.SEG_LINETO:
739 r.add(res[0], res[1]);
740 break;
741 case PathIterator.SEG_CLOSE:
742 if (r.getWidth() > eps || r.getHeight() > eps)
743 return true;
744 break;
745 default:
746 break;
747 }
748 pit.next();
749 }
750 return false;
751 }
752
753 /**
754 * Tests if point is inside a polygon. The polygon can be self-intersecting. In such case the contains function works in xor-like manner.
755 * @param polygonNodes list of nodes from polygon path.
756 * @param point the point to test
757 * @return true if the point is inside polygon.
758 */
759 public static boolean nodeInsidePolygon(INode point, List<? extends INode> polygonNodes) {
760 if (polygonNodes.size() < 2)
761 return false;
762
763 //iterate each side of the polygon, start with the last segment
764 INode oldPoint = polygonNodes.get(polygonNodes.size() - 1);
765
766 if (!oldPoint.isLatLonKnown()) {
767 return false;
768 }
769
770 boolean inside = false;
771 INode p1, p2;
772
773 for (INode newPoint : polygonNodes) {
774 //skip duplicate points
775 if (newPoint.equals(oldPoint)) {
776 continue;
777 }
778
779 if (!newPoint.isLatLonKnown()) {
780 return false;
781 }
782
783 //order points so p1.lat <= p2.lat
784 if (newPoint.getEastNorth().getY() > oldPoint.getEastNorth().getY()) {
785 p1 = oldPoint;
786 p2 = newPoint;
787 } else {
788 p1 = newPoint;
789 p2 = oldPoint;
790 }
791
792 EastNorth pEN = point.getEastNorth();
793 EastNorth opEN = oldPoint.getEastNorth();
794 EastNorth npEN = newPoint.getEastNorth();
795 EastNorth p1EN = p1.getEastNorth();
796 EastNorth p2EN = p2.getEastNorth();
797
798 if (pEN != null && opEN != null && npEN != null && p1EN != null && p2EN != null) {
799 //test if the line is crossed and if so invert the inside flag.
800 if ((npEN.getY() < pEN.getY()) == (pEN.getY() <= opEN.getY())
801 && (pEN.getX() - p1EN.getX()) * (p2EN.getY() - p1EN.getY())
802 < (p2EN.getX() - p1EN.getX()) * (pEN.getY() - p1EN.getY())) {
803 inside = !inside;
804 }
805 }
806
807 oldPoint = newPoint;
808 }
809
810 return inside;
811 }
812
813 /**
814 * Returns area of a closed way in square meters.
815 *
816 * @param way Way to measure, should be closed (first node is the same as last node)
817 * @return area of the closed way.
818 */
819 public static double closedWayArea(Way way) {
820 return getAreaAndPerimeter(way.getNodes(), Projections.getProjectionByCode("EPSG:54008")).getArea();
821 }
822
823 /**
824 * Returns area of a multipolygon in square meters.
825 *
826 * @param multipolygon the multipolygon to measure
827 * @return area of the multipolygon.
828 */
829 public static double multipolygonArea(Relation multipolygon) {
830 final Multipolygon mp = MultipolygonCache.getInstance().get(multipolygon);
831 return mp.getCombinedPolygons().stream()
832 .mapToDouble(pd -> pd.getAreaAndPerimeter(Projections.getProjectionByCode("EPSG:54008")).getArea())
833 .sum();
834 }
835
836 /**
837 * Computes the area of a closed way and multipolygon in square meters, or {@code null} for other primitives
838 *
839 * @param osm the primitive to measure
840 * @return area of the primitive, or {@code null}
841 * @since 13638 (signature)
842 */
843 public static Double computeArea(IPrimitive osm) {
844 if (osm instanceof Way && ((Way) osm).isClosed()) {
845 return closedWayArea((Way) osm);
846 } else if (osm instanceof Relation && osm.isMultipolygon() && !((Relation) osm).hasIncompleteMembers()) {
847 return multipolygonArea((Relation) osm);
848 } else {
849 return null;
850 }
851 }
852
853 /**
854 * Determines whether a way is oriented clockwise.
855 * <p>
856 * Internals: Assuming a closed non-looping way, compute twice the area
857 * of the polygon using the formula {@code 2 * area = sum (X[n] * Y[n+1] - X[n+1] * Y[n])}.
858 * If the area is negative the way is ordered in a clockwise direction.
859 * <p>
860 * See <a href="https://web.archive.org/web/20120722100030/http://paulbourke.net/geometry/polyarea/">
861 * https://paulbourke.net/geometry/polyarea/
862 * </a>
863 *
864 * @param w the way to be checked.
865 * @return true if and only if way is oriented clockwise.
866 * @throws IllegalArgumentException if way is not closed (see {@link Way#isClosed}).
867 */
868 public static boolean isClockwise(Way w) {
869 return isClockwise(w.getNodes());
870 }
871
872 /**
873 * Determines whether path from nodes list is oriented clockwise.
874 * @param nodes Nodes list to be checked.
875 * @return true if and only if way is oriented clockwise.
876 * @throws IllegalArgumentException if way is not closed (see {@link Way#isClosed}).
877 * @see #isClockwise(Way)
878 */
879 public static boolean isClockwise(List<? extends INode> nodes) {
880 int nodesCount = nodes.size();
881 if (nodesCount < 3 || nodes.get(0) != nodes.get(nodesCount - 1)) {
882 throw new IllegalArgumentException("Way must be closed to check orientation.");
883 }
884 double area2 = 0.;
885
886 for (int node = 1; node <= /*sic! consider last-first as well*/ nodesCount; node++) {
887 INode coorPrev = nodes.get(node - 1);
888 INode coorCurr = nodes.get(node % nodesCount);
889 area2 += coorPrev.lon() * coorCurr.lat();
890 area2 -= coorCurr.lon() * coorPrev.lat();
891 }
892 return area2 < 0;
893 }
894
895 /**
896 * Returns angle of a segment defined with 2 point coordinates.
897 *
898 * @param p1 first point
899 * @param p2 second point
900 * @return Angle in radians (-pi, pi]
901 */
902 public static double getSegmentAngle(EastNorth p1, EastNorth p2) {
903
904 CheckParameterUtil.ensureThat(p1.isValid(), () -> p1 + " invalid");
905 CheckParameterUtil.ensureThat(p2.isValid(), () -> p2 + " invalid");
906
907 return Math.atan2(p2.north() - p1.north(), p2.east() - p1.east());
908 }
909
910 /**
911 * Returns angle of a corner defined with 3 point coordinates.
912 *
913 * @param p1 first point
914 * @param common Common end point
915 * @param p3 third point
916 * @return Angle in radians (-pi, pi]
917 */
918 public static double getCornerAngle(EastNorth p1, EastNorth common, EastNorth p3) {
919
920 CheckParameterUtil.ensureThat(p1.isValid(), () -> p1 + " invalid");
921 CheckParameterUtil.ensureThat(common.isValid(), () -> common + " invalid");
922 CheckParameterUtil.ensureThat(p3.isValid(), () -> p3 + " invalid");
923
924 double result = getSegmentAngle(common, p1) - getSegmentAngle(common, p3);
925 if (result <= -Math.PI) {
926 result += 2 * Math.PI;
927 }
928
929 if (result > Math.PI) {
930 result -= 2 * Math.PI;
931 }
932
933 return result;
934 }
935
936 /**
937 * Get angles in radians and return its value in range [0, 180].
938 *
939 * @param angle the angle in radians
940 * @return normalized angle in degrees
941 * @since 13670
942 */
943 public static double getNormalizedAngleInDegrees(double angle) {
944 return Math.abs(180 * angle / Math.PI);
945 }
946
947 /**
948 * Compute the centroid/barycenter of nodes
949 * @param nodes Nodes for which the centroid is wanted
950 * @return the centroid of nodes
951 * @see Geometry#getCenter
952 */
953 public static EastNorth getCentroid(List<? extends INode> nodes) {
954 return getCentroidEN(nodes.stream().filter(INode::isLatLonKnown).map(INode::getEastNorth).collect(Collectors.toList()));
955 }
956
957 /**
958 * Compute the centroid/barycenter of nodes
959 * @param nodes Coordinates for which the centroid is wanted
960 * @return the centroid of nodes
961 * @since 13712
962 */
963 public static EastNorth getCentroidEN(List<EastNorth> nodes) {
964
965 final int size = nodes.size();
966 if (size == 1) {
967 return nodes.get(0);
968 } else if (size == 2) {
969 return nodes.get(0).getCenter(nodes.get(1));
970 } else if (size == 0) {
971 return null;
972 }
973
974 BigDecimal area = BigDecimal.ZERO;
975 BigDecimal north = BigDecimal.ZERO;
976 BigDecimal east = BigDecimal.ZERO;
977
978 // See https://en.wikipedia.org/wiki/Centroid#Of_a_polygon for the equation used here
979 for (int i = 0; i < size; i++) {
980 EastNorth n0 = nodes.get(i);
981 EastNorth n1 = nodes.get((i+1) % size);
982
983 if (n0 != null && n1 != null && n0.isValid() && n1.isValid()) {
984 BigDecimal x0 = BigDecimal.valueOf(n0.east());
985 BigDecimal y0 = BigDecimal.valueOf(n0.north());
986 BigDecimal x1 = BigDecimal.valueOf(n1.east());
987 BigDecimal y1 = BigDecimal.valueOf(n1.north());
988
989 BigDecimal k = x0.multiply(y1, MathContext.DECIMAL128).subtract(y0.multiply(x1, MathContext.DECIMAL128));
990
991 area = area.add(k, MathContext.DECIMAL128);
992 east = east.add(k.multiply(x0.add(x1, MathContext.DECIMAL128), MathContext.DECIMAL128));
993 north = north.add(k.multiply(y0.add(y1, MathContext.DECIMAL128), MathContext.DECIMAL128));
994 }
995 }
996
997 BigDecimal d = new BigDecimal(3, MathContext.DECIMAL128); // 1/2 * 6 = 3
998 area = area.multiply(d, MathContext.DECIMAL128);
999 if (area.compareTo(BigDecimal.ZERO) != 0) {
1000 north = north.divide(area, MathContext.DECIMAL128);
1001 east = east.divide(area, MathContext.DECIMAL128);
1002 }
1003
1004 return new EastNorth(east.doubleValue(), north.doubleValue());
1005 }
1006
1007 /**
1008 * Compute the center of the circle closest to different nodes.
1009 * <p>
1010 * Ensure exact center computation in case nodes are already aligned in circle.
1011 * This is done by least square method.
1012 * Let be a_i x + b_i y + c_i = 0 equations of bisectors of each edges.
1013 * Center must be intersection of all bisectors.
1014 * <pre>
1015 * [ a1 b1 ] [ -c1 ]
1016 * With A = [ ... ... ] and Y = [ ... ]
1017 * [ an bn ] [ -cn ]
1018 * </pre>
1019 * An approximation of center of circle is (At.A)^-1.At.Y
1020 * @param nodes Nodes parts of the circle (at least 3)
1021 * @return An approximation of the center, of null if there is no solution.
1022 * @see Geometry#getCentroid
1023 * @since 6934
1024 */
1025 public static EastNorth getCenter(List<? extends INode> nodes) {
1026 int nc = nodes.size();
1027 if (nc < 3) return null;
1028 /*
1029 * Equation of each bisector ax + by + c = 0
1030 */
1031 double[] a = new double[nc];
1032 double[] b = new double[nc];
1033 double[] c = new double[nc];
1034 // Compute equation of bisector
1035 for (int i = 0; i < nc; i++) {
1036 EastNorth pt1 = nodes.get(i).getEastNorth();
1037 EastNorth pt2 = nodes.get((i+1) % nc).getEastNorth();
1038 a[i] = pt1.east() - pt2.east();
1039 b[i] = pt1.north() - pt2.north();
1040 double d = Math.sqrt(a[i]*a[i] + b[i]*b[i]);
1041 if (d == 0) return null;
1042 a[i] /= d;
1043 b[i] /= d;
1044 double xC = (pt1.east() + pt2.east()) / 2;
1045 double yC = (pt1.north() + pt2.north()) / 2;
1046 c[i] = -(a[i]*xC + b[i]*yC);
1047 }
1048 // At.A = [aij]
1049 double a11 = 0, a12 = 0, a22 = 0;
1050 // At.Y = [bi]
1051 double b1 = 0, b2 = 0;
1052 for (int i = 0; i < nc; i++) {
1053 a11 += a[i]*a[i];
1054 a12 += a[i]*b[i];
1055 a22 += b[i]*b[i];
1056 b1 -= a[i]*c[i];
1057 b2 -= b[i]*c[i];
1058 }
1059 // (At.A)^-1 = [invij]
1060 double det = a11*a22 - a12*a12;
1061 if (Math.abs(det) < 1e-5) return null;
1062 double inv11 = a22/det;
1063 double inv12 = -a12/det;
1064 double inv22 = a11/det;
1065 // center (xC, yC) = (At.A)^-1.At.y
1066 double xC = inv11*b1 + inv12*b2;
1067 double yC = inv12*b1 + inv22*b2;
1068 return new EastNorth(xC, yC);
1069 }
1070
1071 /**
1072 * Tests if the {@code node} is inside the multipolygon {@code multiPolygon}. The nullable argument
1073 * {@code isOuterWayAMatch} allows to decide if the immediate {@code outer} way of the multipolygon is a match.
1074 * For repeated tests against {@code multiPolygon} better use {@link Geometry#filterInsideMultipolygon}.
1075 * @param node node
1076 * @param multiPolygon multipolygon
1077 * @param isOuterWayAMatch allows to decide if the immediate {@code outer} way of the multipolygon is a match
1078 * @return {@code true} if the node is inside the multipolygon
1079 */
1080 public static boolean isNodeInsideMultiPolygon(INode node, Relation multiPolygon, Predicate<Way> isOuterWayAMatch) {
1081 return isPolygonInsideMultiPolygon(Collections.singletonList(node), multiPolygon, isOuterWayAMatch);
1082 }
1083
1084 /**
1085 * Tests if the polygon formed by {@code nodes} is inside the multipolygon {@code multiPolygon}. The nullable argument
1086 * {@code isOuterWayAMatch} allows to decide if the immediate {@code outer} way of the multipolygon is a match.
1087 * For repeated tests against {@code multiPolygon} better use {@link Geometry#filterInsideMultipolygon}.
1088 * <p>
1089 * If {@code nodes} contains exactly one element, then it is checked whether that one node is inside the multipolygon.
1090 * @param nodes nodes forming the polygon
1091 * @param multiPolygon multipolygon
1092 * @param isOuterWayAMatch allows to decide if the immediate {@code outer} way of the multipolygon is a match
1093 * @return {@code true} if the multipolygon is valid and the polygon formed by nodes is inside the multipolygon
1094 */
1095 public static boolean isPolygonInsideMultiPolygon(List<? extends INode> nodes, Relation multiPolygon, Predicate<Way> isOuterWayAMatch) {
1096 try {
1097 return isPolygonInsideMultiPolygon(nodes, MultipolygonBuilder.joinWays(multiPolygon), isOuterWayAMatch);
1098 } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) {
1099 Logging.trace(ex);
1100 Logging.debug("Invalid multipolygon " + multiPolygon);
1101 return false;
1102 }
1103 }
1104
1105 /**
1106 * Tests if the polygon formed by {@code nodes} is inside the multipolygon {@code multiPolygon}. The nullable argument
1107 * {@code isOuterWayAMatch} allows to decide if the immediate {@code outer} way of the multipolygon is a match.
1108 * For repeated tests against {@code multiPolygon} better use {@link Geometry#filterInsideMultipolygon}.
1109 * <p>
1110 * If {@code nodes} contains exactly one element, then it is checked whether that one node is inside the multipolygon.
1111 * @param nodes nodes forming the polygon
1112 * @param outerInner result of {@link MultipolygonBuilder#joinWays(Relation)}
1113 * @param isOuterWayAMatch allows to decide if the immediate {@code outer} way of the multipolygon is a match
1114 * @return {@code true} if the multipolygon is valid and the polygon formed by nodes is inside the multipolygon
1115 * @since 15069
1116 */
1117 public static boolean isPolygonInsideMultiPolygon(List<? extends INode> nodes, Pair<List<JoinedPolygon>,
1118 List<JoinedPolygon>> outerInner, Predicate<Way> isOuterWayAMatch) {
1119 Area a1 = nodes.size() == 1 ? null : getArea(nodes);
1120 // Test if object is inside an outer member
1121 for (JoinedPolygon out : outerInner.a) {
1122 if (a1 == null
1123 ? nodeInsidePolygon(nodes.get(0), out.nodes)
1124 : PolygonIntersection.FIRST_INSIDE_SECOND == polygonIntersection(a1, out.area)) {
1125 // If inside an outer, check it is not inside an inner
1126 boolean insideInner = outerInner.b.stream().anyMatch(in -> a1 == null
1127 ? nodeInsidePolygon(nodes.get(0), in.nodes)
1128 : in.area.getBounds2D().contains(a1.getBounds2D())
1129 && polygonIntersection(a1, in.area) == PolygonIntersection.FIRST_INSIDE_SECOND
1130 && polygonIntersection(in.area, out.area) == PolygonIntersection.FIRST_INSIDE_SECOND);
1131 if (!insideInner) {
1132 // Final check using predicate
1133 if (isOuterWayAMatch == null || isOuterWayAMatch.test(out.ways.get(0)
1134 /* TODO give a better representation of the outer ring to the predicate */)) {
1135 return true;
1136 }
1137 }
1138 }
1139 }
1140 return false;
1141 }
1142
1143 /**
1144 * Find all primitives in the given collection which are inside the given polygon.
1145 *
1146 * @param primitives the primitives
1147 * @param polygon the closed way or multipolygon relation
1148 * @return a new list containing the found primitives, empty if polygon is invalid or nothing was found.
1149 * @see Geometry#filterInsidePolygon
1150 * @see Geometry#filterInsideMultipolygon
1151 * @since 15730
1152 */
1153 public static List<IPrimitive> filterInsideAnyPolygon(Collection<IPrimitive> primitives, IPrimitive polygon) {
1154 if (polygon instanceof IWay<?>) {
1155 return filterInsidePolygon(primitives, (IWay<?>) polygon);
1156 } else if (polygon instanceof Relation && polygon.isMultipolygon()) {
1157 return filterInsideMultipolygon(primitives, (Relation) polygon);
1158 }
1159 return Collections.emptyList();
1160 }
1161
1162 /**
1163 * Find all primitives in the given collection which are inside the given polygon.
1164 * Unclosed ways and multipolygon relations with unclosed outer rings are ignored.
1165 *
1166 * @param primitives the primitives
1167 * @param polygon the polygon
1168 * @return a new list containing the found primitives, empty if polygon is invalid or nothing was found.
1169 * @since 15069 (for {@link List} of {@code primitives}, 15730 for a {@link Collection} of {@code primitives})
1170 */
1171 public static List<IPrimitive> filterInsidePolygon(Collection<IPrimitive> primitives, IWay<?> polygon) {
1172 List<IPrimitive> res = new ArrayList<>();
1173 if (!polygon.isClosed() || polygon.getNodesCount() <= 3)
1174 return res;
1175 /* polygon area in east north space, calculated only when really needed */
1176 Area polygonArea = null;
1177 for (IPrimitive p : primitives) {
1178 if (p instanceof INode) {
1179 if (nodeInsidePolygon((INode) p, polygon.getNodes())) {
1180 res.add(p);
1181 }
1182 } else if (p instanceof IWay) {
1183 if (((IWay<?>) p).isClosed()) {
1184 if (polygonArea == null) {
1185 polygonArea = getArea(polygon.getNodes());
1186 }
1187 if (PolygonIntersection.FIRST_INSIDE_SECOND == polygonIntersection(getArea(((IWay<?>) p).getNodes()),
1188 polygonArea)) {
1189 res.add(p);
1190 }
1191 }
1192 } else if (p.isMultipolygon()) {
1193 if (polygonArea == null) {
1194 polygonArea = getArea(polygon.getNodes());
1195 }
1196 Multipolygon mp = p.getDataSet() != null ? MultipolygonCache.getInstance().get((Relation) p) : new Multipolygon((Relation) p);
1197 boolean inside = true;
1198 // a (valid) multipolygon is inside the polygon if all outer rings are inside
1199 for (PolyData outer : mp.getOuterPolygons()) {
1200 if (!outer.isClosed()
1201 || !polygonArea.getBounds2D().contains(outer.getBounds())
1202 || PolygonIntersection.FIRST_INSIDE_SECOND != polygonIntersection(getArea(outer.getNodes()),
1203 polygonArea)) {
1204 inside = false;
1205 break;
1206 }
1207 }
1208 if (inside) {
1209 res.add(p);
1210 }
1211 }
1212 }
1213 return res;
1214 }
1215
1216 /**
1217 * Find all primitives in the given collection which are inside the given multipolygon. Members of the multipolygon are
1218 * ignored. Unclosed ways and multipolygon relations with unclosed outer rings are ignored.
1219 * @param primitives the primitives
1220 * @param multiPolygon the multipolygon relation
1221 * @return a new list containing the found primitives, empty if multipolygon is invalid or nothing was found.
1222 * @since 15069
1223 */
1224 public static List<IPrimitive> filterInsideMultipolygon(Collection<IPrimitive> primitives, Relation multiPolygon) {
1225 return filterInsideMultipolygon(primitives, multiPolygon, null);
1226 }
1227
1228 /**
1229 * Find all primitives in the given collection which are inside the given multipolygon. Members of the multipolygon are
1230 * ignored. Unclosed ways and multipolygon relations with unclosed outer rings are ignored.
1231 * @param primitives the primitives
1232 * @param multiPolygon the multipolygon relation
1233 * @param cache The cache to avoid calculating joined inner/outer ways multiple times (see {@link MultipolygonBuilder#joinWays(Relation)})
1234 * @return a new list containing the found primitives, empty if multipolygon is invalid or nothing was found.
1235 * @since 19336
1236 */
1237 public static List<IPrimitive> filterInsideMultipolygon(Collection<IPrimitive> primitives, Relation multiPolygon,
1238 Map<IRelation<?>, Pair<List<JoinedPolygon>, List<JoinedPolygon>>> cache) {
1239 List<IPrimitive> res = new ArrayList<>();
1240 if (primitives.isEmpty())
1241 return res;
1242
1243 final Pair<List<JoinedPolygon>, List<JoinedPolygon>> outerInner;
1244 try {
1245 outerInner = MultipolygonBuilder.joinWays(cache, multiPolygon);
1246 } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) {
1247 Logging.trace(ex);
1248 Logging.debug("Invalid multipolygon " + multiPolygon);
1249 return res;
1250 }
1251
1252 Set<OsmPrimitive> members = multiPolygon.getMemberPrimitives();
1253 for (IPrimitive p : primitives) {
1254 if (members.contains(p))
1255 continue;
1256 if (p instanceof Node) {
1257 if (isPolygonInsideMultiPolygon(Collections.singletonList((Node) p), outerInner, null)) {
1258 res.add(p);
1259 }
1260 } else if (p instanceof Way) {
1261 if (((IWay<?>) p).isClosed() && isPolygonInsideMultiPolygon(((Way) p).getNodes(), outerInner, null)) {
1262 res.add(p);
1263 }
1264 } else if (p.isMultipolygon()) {
1265 Multipolygon mp = new Multipolygon((Relation) p);
1266 // a (valid) multipolygon is inside multiPolygon if all outer rings are inside
1267 boolean inside = mp.getOuterPolygons().stream()
1268 .allMatch(outer -> outer.isClosed() && isPolygonInsideMultiPolygon(outer.getNodes(), outerInner, null));
1269 if (inside) {
1270 res.add(p);
1271 }
1272 }
1273 }
1274 return res;
1275 }
1276
1277 /**
1278 * Data class to hold two double values (area and perimeter of a polygon).
1279 */
1280 public static class AreaAndPerimeter {
1281 private final double area;
1282 private final double perimeter;
1283
1284 /**
1285 * Create a new {@link AreaAndPerimeter}
1286 * @param area The area
1287 * @param perimeter The perimeter
1288 */
1289 public AreaAndPerimeter(double area, double perimeter) {
1290 this.area = area;
1291 this.perimeter = perimeter;
1292 }
1293
1294 /**
1295 * Gets the area
1296 * @return The area size
1297 */
1298 public double getArea() {
1299 return area;
1300 }
1301
1302 /**
1303 * Gets the perimeter
1304 * @return The perimeter length
1305 */
1306 public double getPerimeter() {
1307 return perimeter;
1308 }
1309 }
1310
1311 /**
1312 * Calculate area and perimeter length of a polygon.
1313 * <p>
1314 * Uses current projection; units are that of the projected coordinates.
1315 *
1316 * @param nodes the list of nodes representing the polygon
1317 * @return area and perimeter
1318 */
1319 public static AreaAndPerimeter getAreaAndPerimeter(List<? extends ILatLon> nodes) {
1320 return getAreaAndPerimeter(nodes, null);
1321 }
1322
1323 /**
1324 * Calculate area and perimeter length of a polygon in the given projection.
1325 *
1326 * @param nodes the list of nodes representing the polygon
1327 * @param projection the projection to use for the calculation, {@code null} defaults to {@link ProjectionRegistry#getProjection()}
1328 * @return area and perimeter
1329 * @since 13638 (signature)
1330 */
1331 public static AreaAndPerimeter getAreaAndPerimeter(List<? extends ILatLon> nodes, Projection projection) {
1332 CheckParameterUtil.ensureParameterNotNull(nodes, "nodes");
1333 double area = 0;
1334 double perimeter = 0;
1335 Projection useProjection = projection == null ? ProjectionRegistry.getProjection() : projection;
1336
1337 if (!nodes.isEmpty()) {
1338 boolean closed = nodes.get(0) == nodes.get(nodes.size() - 1);
1339 int numSegments = closed ? nodes.size() - 1 : nodes.size();
1340 EastNorth p1 = nodes.get(0).getEastNorth(useProjection);
1341 for (int i = 1; i <= numSegments; i++) {
1342 final ILatLon node = nodes.get(i == numSegments ? 0 : i);
1343 final EastNorth p2 = node.getEastNorth(useProjection);
1344 if (p1 != null && p2 != null) {
1345 area += p1.east() * p2.north() - p2.east() * p1.north();
1346 perimeter += p1.distance(p2);
1347 }
1348 p1 = p2;
1349 }
1350 }
1351 return new AreaAndPerimeter(Math.abs(area) / 2, perimeter);
1352 }
1353
1354 /**
1355 * Get the closest primitive to {@code osm} from the collection of
1356 * OsmPrimitive {@code primitives}
1357 * <p>
1358 * The {@code primitives} should be fully downloaded to ensure accuracy.
1359 * <p>
1360 * Note: The complexity of this method is O(n*m), where n is the number of
1361 * children {@code osm} has plus 1, m is the number of children the
1362 * collection of primitives have plus the number of primitives in the
1363 * collection.
1364 *
1365 * @param <T> The return type of the primitive
1366 * @param osm The primitive to get the distances from
1367 * @param primitives The collection of primitives to get the distance to
1368 * @return The closest {@link OsmPrimitive}. This is not determinative.
1369 * To get all primitives that share the same distance, use
1370 * {@link Geometry#getClosestPrimitives}.
1371 * @since 15035
1372 */
1373 public static <T extends OsmPrimitive> T getClosestPrimitive(OsmPrimitive osm, Collection<T> primitives) {
1374 Collection<T> collection = getClosestPrimitives(osm, primitives);
1375 return collection.iterator().next();
1376 }
1377
1378 /**
1379 * Get the closest primitives to {@code osm} from the collection of
1380 * OsmPrimitive {@code primitives}
1381 * <p>
1382 * The {@code primitives} should be fully downloaded to ensure accuracy.
1383 * <p>
1384 * Note: The complexity of this method is O(n*m), where n is the number of
1385 * children {@code osm} has plus 1, m is the number of children the
1386 * collection of primitives have plus the number of primitives in the
1387 * collection.
1388 *
1389 * @param <T> The return type of the primitive
1390 * @param osm The primitive to get the distances from
1391 * @param primitives The collection of primitives to get the distance to
1392 * @return The closest {@link OsmPrimitive}s. May be empty.
1393 * @since 15035
1394 */
1395 public static <T extends OsmPrimitive> Collection<T> getClosestPrimitives(OsmPrimitive osm, Collection<T> primitives) {
1396 double lowestDistance = Double.MAX_VALUE;
1397 TreeSet<T> closest = new TreeSet<>();
1398 for (T primitive : primitives) {
1399 double distance = getDistance(osm, primitive);
1400 if (Double.isNaN(distance)) continue;
1401 int comp = Double.compare(distance, lowestDistance);
1402 if (comp < 0) {
1403 closest.clear();
1404 lowestDistance = distance;
1405 closest.add(primitive);
1406 } else if (comp == 0) {
1407 closest.add(primitive);
1408 }
1409 }
1410 return closest;
1411 }
1412
1413 /**
1414 * Get the furthest primitive to {@code osm} from the collection of
1415 * OsmPrimitive {@code primitives}
1416 * <p>
1417 * The {@code primitives} should be fully downloaded to ensure accuracy.
1418 * <p>
1419 * It does NOT give the furthest primitive based off of the furthest
1420 * part of that primitive
1421 * <p>
1422 * Note: The complexity of this method is O(n*m), where n is the number of
1423 * children {@code osm} has plus 1, m is the number of children the
1424 * collection of primitives have plus the number of primitives in the
1425 * collection.
1426 *
1427 * @param <T> The return type of the primitive
1428 * @param osm The primitive to get the distances from
1429 * @param primitives The collection of primitives to get the distance to
1430 * @return The furthest {@link OsmPrimitive}. This is not determinative.
1431 * To get all primitives that share the same distance, use
1432 * {@link Geometry#getFurthestPrimitives}
1433 * @since 15035
1434 */
1435 public static <T extends OsmPrimitive> T getFurthestPrimitive(OsmPrimitive osm, Collection<T> primitives) {
1436 return getFurthestPrimitives(osm, primitives).iterator().next();
1437 }
1438
1439 /**
1440 * Get the furthest primitives to {@code osm} from the collection of
1441 * OsmPrimitive {@code primitives}
1442 * <p>
1443 * The {@code primitives} should be fully downloaded to ensure accuracy.
1444 * <p>
1445 * It does NOT give the furthest primitive based off of the furthest
1446 * part of that primitive
1447 * <p>
1448 * Note: The complexity of this method is O(n*m), where n is the number of
1449 * children {@code osm} has plus 1, m is the number of children the
1450 * collection of primitives have plus the number of primitives in the
1451 * collection.
1452 *
1453 * @param <T> The return type of the primitive
1454 * @param osm The primitive to get the distances from
1455 * @param primitives The collection of primitives to get the distance to
1456 * @return The furthest {@link OsmPrimitive}s. It may return an empty collection.
1457 * @since 15035
1458 */
1459 public static <T extends OsmPrimitive> Collection<T> getFurthestPrimitives(OsmPrimitive osm, Collection<T> primitives) {
1460 double furthestDistance = Double.NEGATIVE_INFINITY;
1461 TreeSet<T> furthest = new TreeSet<>();
1462 for (T primitive : primitives) {
1463 double distance = getDistance(osm, primitive);
1464 if (Double.isNaN(distance)) continue;
1465 int comp = Double.compare(distance, furthestDistance);
1466 if (comp > 0) {
1467 furthest.clear();
1468 furthestDistance = distance;
1469 furthest.add(primitive);
1470 } else if (comp == 0) {
1471 furthest.add(primitive);
1472 }
1473 }
1474 return furthest;
1475 }
1476
1477 /**
1478 * Get the distance between different {@link OsmPrimitive}s
1479 * @param one The primitive to get the distance from
1480 * @param two The primitive to get the distance to
1481 * @return The distance between the primitives in meters
1482 * (or the unit of the current projection, see {@link Projection}).
1483 * May return {@link Double#NaN} if one of the primitives is incomplete.
1484 * <p>
1485 * Note: The complexity is O(n*m), where (n,m) are the number of child
1486 * objects the {@link OsmPrimitive}s have.
1487 * @since 15035
1488 */
1489 public static double getDistance(OsmPrimitive one, OsmPrimitive two) {
1490 double rValue = Double.MAX_VALUE;
1491 if (one == null || two == null || one.isIncomplete()
1492 || two.isIncomplete()) return Double.NaN;
1493 if (one instanceof ILatLon && two instanceof ILatLon) {
1494 rValue = ((ILatLon) one).greatCircleDistance(((ILatLon) two));
1495 } else if (one instanceof Node && two instanceof Way) {
1496 rValue = getDistanceWayNode((Way) two, (Node) one);
1497 } else if (one instanceof Way && two instanceof Node) {
1498 rValue = getDistanceWayNode((Way) one, (Node) two);
1499 } else if (one instanceof Way && two instanceof Way) {
1500 rValue = getDistanceWayWay((Way) one, (Way) two);
1501 } else if (one instanceof Relation && !(two instanceof Relation)) {
1502 for (OsmPrimitive osmPrimitive: ((Relation) one).getMemberPrimitives()) {
1503 double currentDistance = getDistance(osmPrimitive, two);
1504 if (currentDistance < rValue) rValue = currentDistance;
1505 }
1506 } else if (!(one instanceof Relation) && two instanceof Relation) {
1507 rValue = getDistance(two, one);
1508 } else if (one instanceof Relation && two instanceof Relation) {
1509 for (OsmPrimitive osmPrimitive1 : ((Relation) one).getMemberPrimitives()) {
1510 for (OsmPrimitive osmPrimitive2 : ((Relation) two).getMemberPrimitives()) {
1511 double currentDistance = getDistance(osmPrimitive1, osmPrimitive2);
1512 if (currentDistance < rValue) rValue = currentDistance;
1513 }
1514 }
1515 }
1516 return rValue != Double.MAX_VALUE ? rValue : Double.NaN;
1517 }
1518
1519 /**
1520 * Get the distance between a way and a node
1521 * @param way The way to get the distance from
1522 * @param node The node to get the distance to
1523 * @return The distance between the {@code way} and the {@code node} in
1524 * meters (or the unit of the current projection, see {@link Projection}).
1525 * May return {@link Double#NaN} if the primitives are incomplete.
1526 * @since 15035
1527 */
1528 public static double getDistanceWayNode(Way way, Node node) {
1529 if (way == null || node == null || way.getNodesCount() < 2 || !node.isLatLonKnown())
1530 return Double.NaN;
1531
1532 double smallest = Double.MAX_VALUE;
1533 EastNorth en0 = node.getEastNorth();
1534 // go through the nodes as if they were paired
1535 Iterator<Node> iter = way.getNodes().iterator();
1536 EastNorth en1 = iter.next().getEastNorth();
1537 while (iter.hasNext()) {
1538 EastNorth en2 = iter.next().getEastNorth();
1539 double distance = getSegmentNodeDistSq(en1, en2, en0);
1540 if (distance < smallest)
1541 smallest = distance;
1542 en1 = en2;
1543 }
1544 return smallest != Double.MAX_VALUE ? Math.sqrt(smallest) : Double.NaN;
1545 }
1546
1547 /**
1548 * Get the closest {@link WaySegment} from a way to a primitive.
1549 * @param way The {@link Way} to get the distance from and the {@link WaySegment}
1550 * @param primitive The {@link OsmPrimitive} to get the distance to
1551 * @return The {@link WaySegment} that is closest to {@code primitive} from {@code way}.
1552 * If there are multiple {@link WaySegment}s with the same distance, the last
1553 * {@link WaySegment} with the same distance will be returned.
1554 * May return {@code null} if the way has fewer than two nodes or one
1555 * of the primitives is incomplete.
1556 * @since 15035
1557 */
1558 public static WaySegment getClosestWaySegment(Way way, OsmPrimitive primitive) {
1559 if (way == null || primitive == null || way.isIncomplete()
1560 || primitive.isIncomplete()) return null;
1561 double lowestDistance = Double.MAX_VALUE;
1562 Pair<Node, Node> closestNodes = null;
1563 for (Pair<Node, Node> nodes : way.getNodePairs(false)) {
1564 Way tWay = new Way();
1565 tWay.addNode(new Node(nodes.a));
1566 tWay.addNode(new Node(nodes.b));
1567 double distance = getDistance(tWay, primitive);
1568 if (distance < lowestDistance) {
1569 lowestDistance = distance;
1570 closestNodes = nodes;
1571 }
1572 }
1573 if (closestNodes == null) return null;
1574 return lowestDistance != Double.MAX_VALUE ? WaySegment.forNodePair(way, closestNodes.a, closestNodes.b) : null;
1575 }
1576
1577 /**
1578 * Get the distance between different ways. Iterates over the nodes of the ways, complexity is O(n*m)
1579 * (n,m giving the number of nodes)
1580 * @param w1 The first {@link Way}
1581 * @param w2 The second {@link Way}
1582 * @return The shortest distance between the ways in meters
1583 * (or the unit of the current projection, see {@link Projection}).
1584 * May return {@link Double#NaN}.
1585 * @since 15035
1586 */
1587 public static double getDistanceWayWay(Way w1, Way w2) {
1588 if (w1 == null || w2 == null || w1.getNodesCount() < 2 || w2.getNodesCount() < 2)
1589 return Double.NaN;
1590 double rValue = Double.MAX_VALUE;
1591 Iterator<Node> iter1 = w1.getNodes().iterator();
1592 List<Node> w2Nodes = w2.getNodes();
1593 Node w1N1 = iter1.next();
1594 while (iter1.hasNext()) {
1595 Node w1N2 = iter1.next();
1596 Iterator<Node> iter2 = w2Nodes.iterator();
1597 Node w2N1 = iter2.next();
1598 while (iter2.hasNext()) {
1599 Node w2N2 = iter2.next();
1600 double distance = getDistanceSegmentSegment(w1N1, w1N2, w2N1, w2N2);
1601 if (distance < rValue)
1602 rValue = distance;
1603 w2N1 = w2N2;
1604 }
1605 w1N1 = w1N2;
1606 }
1607 return rValue != Double.MAX_VALUE ? rValue : Double.NaN;
1608 }
1609
1610 /**
1611 * Get the distance between different {@link WaySegment}s
1612 * @param ws1 A {@link WaySegment}
1613 * @param ws2 A {@link WaySegment}
1614 * @return The distance between the two {@link WaySegment}s in meters
1615 * (or the unit of the current projection, see {@link Projection}).
1616 * May return {@link Double#NaN}.
1617 * @since 15035
1618 */
1619 public static double getDistanceSegmentSegment(WaySegment ws1, WaySegment ws2) {
1620 return getDistanceSegmentSegment(ws1.getFirstNode(), ws1.getSecondNode(), ws2.getFirstNode(), ws2.getSecondNode());
1621 }
1622
1623 /**
1624 * Get the distance between different {@link WaySegment}s
1625 * @param ws1Node1 The first node of the first WaySegment
1626 * @param ws1Node2 The second node of the second WaySegment
1627 * @param ws2Node1 The first node of the second WaySegment
1628 * @param ws2Node2 The second node of the second WaySegment
1629 * @return The distance between the two {@link WaySegment}s in meters
1630 * (or the unit of the current projection, see {@link Projection}).
1631 * May return {@link Double#NaN}.
1632 * @since 15035
1633 */
1634 public static double getDistanceSegmentSegment(Node ws1Node1, Node ws1Node2, Node ws2Node1, Node ws2Node2) {
1635 if (!ws1Node1.isLatLonKnown() || !ws1Node2.isLatLonKnown() || !ws2Node1.isLatLonKnown() || !ws2Node2.isLatLonKnown()) {
1636 return Double.NaN;
1637 }
1638 EastNorth enWs1Node1 = ws1Node1.getEastNorth();
1639 EastNorth enWs1Node2 = ws1Node2.getEastNorth();
1640 EastNorth enWs2Node1 = ws2Node1.getEastNorth();
1641 EastNorth enWs2Node2 = ws2Node2.getEastNorth();
1642 if (getSegmentSegmentIntersection(enWs1Node1, enWs1Node2, enWs2Node1, enWs2Node2) != null)
1643 return 0;
1644
1645 double dist1sq = getSegmentNodeDistSq(enWs1Node1, enWs1Node2, enWs2Node1);
1646 double dist2sq = getSegmentNodeDistSq(enWs1Node1, enWs1Node2, enWs2Node2);
1647 double dist3sq = getSegmentNodeDistSq(enWs2Node1, enWs2Node2, enWs1Node1);
1648 double dist4sq = getSegmentNodeDistSq(enWs2Node1, enWs2Node2, enWs1Node2);
1649 double smallest = Math.min(Math.min(dist1sq, dist2sq), Math.min(dist3sq, dist4sq));
1650 return smallest != Double.MAX_VALUE ? Math.sqrt(smallest) : Double.NaN;
1651 }
1652
1653 /**
1654 * Create a new LatLon at a specified distance. Currently uses WGS84, but may change randomly in the future.
1655 * This does not currently attempt to be hugely accurate. The actual location may be off
1656 * depending upon the distance and the elevation, but should be within 0.0002 meters.
1657 *
1658 * @param original The originating point
1659 * @param angle The angle (from true north) in radians
1660 * @param offset The distance to the new point in the current projection's units
1661 * @return The location at the specified angle and distance from the originating point
1662 * @since 18109
1663 */
1664 public static ILatLon getLatLonFrom(final ILatLon original, final double angle, final double offset) {
1665 final double meterOffset = ProjectionRegistry.getProjection().getMetersPerUnit() * offset;
1666 final double radianLat = Math.toRadians(original.lat());
1667 final double radianLon = Math.toRadians(original.lon());
1668 final double angularDistance = meterOffset / WGS84.a;
1669 final double lat = Math.asin(Math.sin(radianLat) * Math.cos(angularDistance)
1670 + Math.cos(radianLat) * Math.sin(angularDistance) * Math.cos(angle));
1671 final double lon = radianLon + Math.atan2(Math.sin(angle) * Math.sin(angularDistance) * Math.cos(radianLat),
1672 Math.cos(angularDistance) - Math.sin(radianLat) * Math.sin(lat));
1673 return new LatLon(Math.toDegrees(lat), Math.toDegrees(lon));
1674 }
1675
1676 /**
1677 * Calculate closest distance between a line segment s1-s2 and a point p
1678 * @param s1 start of segment
1679 * @param s2 end of segment
1680 * @param p the point
1681 * @return the square of the euclidean distance from p to the closest point on the segment
1682 */
1683 private static double getSegmentNodeDistSq(EastNorth s1, EastNorth s2, EastNorth p) {
1684 EastNorth c1 = closestPointTo(s1, s2, p, true);
1685 return c1.distanceSq(p);
1686 }
1687}
Note: See TracBrowser for help on using the repository browser.