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

Last change on this file since 16966 was 16436, checked in by simon04, 4 years ago

see #19251 - Java 8: use Stream

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