source: osm/applications/editors/josm/plugins/opendata/includes/com/vividsolutions/jts/algorithm/ConvexHull.java@ 28000

Last change on this file since 28000 was 28000, checked in by donvip, 12 years ago

Import new "opendata" JOSM plugin

File size: 16.1 KB
Line 
1
2
3/*
4* The JTS Topology Suite is a collection of Java classes that
5* implement the fundamental operations required to validate a given
6* geo-spatial data set to a known topological specification.
7*
8* Copyright (C) 2001 Vivid Solutions
9*
10* This library is free software; you can redistribute it and/or
11* modify it under the terms of the GNU Lesser General Public
12* License as published by the Free Software Foundation; either
13* version 2.1 of the License, or (at your option) any later version.
14*
15* This library is distributed in the hope that it will be useful,
16* but WITHOUT ANY WARRANTY; without even the implied warranty of
17* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18* Lesser General Public License for more details.
19*
20* You should have received a copy of the GNU Lesser General Public
21* License along with this library; if not, write to the Free Software
22* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23*
24* For more information, contact:
25*
26* Vivid Solutions
27* Suite #1A
28* 2328 Government Street
29* Victoria BC V8T 5G5
30* Canada
31*
32* (250)385-6040
33* www.vividsolutions.com
34 */
35package com.vividsolutions.jts.algorithm;
36import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.Comparator;
39import java.util.Stack;
40import java.util.TreeSet;
41
42import com.vividsolutions.jts.geom.Coordinate;
43import com.vividsolutions.jts.geom.CoordinateArrays;
44import com.vividsolutions.jts.geom.CoordinateList;
45import com.vividsolutions.jts.geom.Geometry;
46import com.vividsolutions.jts.geom.GeometryCollection;
47import com.vividsolutions.jts.geom.GeometryFactory;
48import com.vividsolutions.jts.geom.LineString;
49import com.vividsolutions.jts.geom.LinearRing;
50import com.vividsolutions.jts.geom.Point;
51import com.vividsolutions.jts.geom.Polygon;
52import com.vividsolutions.jts.util.Assert;
53import com.vividsolutions.jts.util.UniqueCoordinateArrayFilter;
54
55/**
56 * Computes the convex hull of a {@link Geometry}.
57 * The convex hull is the smallest convex Geometry that contains all the
58 * points in the input Geometry.
59 * <p>
60 * Uses the Graham Scan algorithm.
61 *
62 *@version 1.7
63 */
64public class ConvexHull
65{
66 private GeometryFactory geomFactory;
67 private Coordinate[] inputPts;
68
69 /**
70 * Create a new convex hull construction for the input {@link Geometry}.
71 */
72 public ConvexHull(Geometry geometry)
73 {
74 this(extractCoordinates(geometry), geometry.getFactory());
75 }
76 /**
77 * Create a new convex hull construction for the input {@link Coordinate} array.
78 */
79 public ConvexHull(Coordinate[] pts, GeometryFactory geomFactory)
80 {
81 inputPts = pts;
82 this.geomFactory = geomFactory;
83 }
84
85 private static Coordinate[] extractCoordinates(Geometry geom)
86 {
87 UniqueCoordinateArrayFilter filter = new UniqueCoordinateArrayFilter();
88 geom.apply(filter);
89 return filter.getCoordinates();
90 }
91
92 /**
93 * Returns a {@link Geometry} that represents the convex hull of the input
94 * geometry.
95 * The returned geometry contains the minimal number of points needed to
96 * represent the convex hull. In particular, no more than two consecutive
97 * points will be collinear.
98 *
99 * @return if the convex hull contains 3 or more points, a {@link Polygon};
100 * 2 points, a {@link LineString};
101 * 1 point, a {@link Point};
102 * 0 points, an empty {@link GeometryCollection}.
103 */
104 public Geometry getConvexHull() {
105
106 if (inputPts.length == 0) {
107 return geomFactory.createGeometryCollection(null);
108 }
109 if (inputPts.length == 1) {
110 return geomFactory.createPoint(inputPts[0]);
111 }
112 if (inputPts.length == 2) {
113 return geomFactory.createLineString(inputPts);
114 }
115
116 Coordinate[] reducedPts = inputPts;
117 // use heuristic to reduce points, if large
118 if (inputPts.length > 50) {
119 reducedPts = reduce(inputPts);
120 }
121 // sort points for Graham scan.
122 Coordinate[] sortedPts = preSort(reducedPts);
123
124 // Use Graham scan to find convex hull.
125 Stack cHS = grahamScan(sortedPts);
126
127 // Convert stack to an array.
128 Coordinate[] cH = toCoordinateArray(cHS);
129
130 // Convert array to appropriate output geometry.
131 return lineOrPolygon(cH);
132 }
133
134 /**
135 * An alternative to Stack.toArray, which is not present in earlier versions
136 * of Java.
137 */
138 protected Coordinate[] toCoordinateArray(Stack stack) {
139 Coordinate[] coordinates = new Coordinate[stack.size()];
140 for (int i = 0; i < stack.size(); i++) {
141 Coordinate coordinate = (Coordinate) stack.get(i);
142 coordinates[i] = coordinate;
143 }
144 return coordinates;
145 }
146
147 /**
148 * Uses a heuristic to reduce the number of points scanned
149 * to compute the hull.
150 * The heuristic is to find a polygon guaranteed to
151 * be in (or on) the hull, and eliminate all points inside it.
152 * A quadrilateral defined by the extremal points
153 * in the four orthogonal directions
154 * can be used, but even more inclusive is
155 * to use an octilateral defined by the points in the 8 cardinal directions.
156 * <p>
157 * Note that even if the method used to determine the polygon vertices
158 * is not 100% robust, this does not affect the robustness of the convex hull.
159 * <p>
160 * To satisfy the requirements of the Graham Scan algorithm,
161 * the returned array has at least 3 entries.
162 *
163 * @param pts the points to reduce
164 * @return the reduced list of points (at least 3)
165 */
166 private Coordinate[] reduce(Coordinate[] inputPts)
167 {
168 //Coordinate[] polyPts = computeQuad(inputPts);
169 Coordinate[] polyPts = computeOctRing(inputPts);
170 //Coordinate[] polyPts = null;
171
172 // unable to compute interior polygon for some reason
173 if (polyPts == null)
174 return inputPts;
175
176// LinearRing ring = geomFactory.createLinearRing(polyPts);
177// System.out.println(ring);
178
179 // add points defining polygon
180 TreeSet reducedSet = new TreeSet();
181 for (int i = 0; i < polyPts.length; i++) {
182 reducedSet.add(polyPts[i]);
183 }
184 /**
185 * Add all unique points not in the interior poly.
186 * CGAlgorithms.isPointInRing is not defined for points actually on the ring,
187 * but this doesn't matter since the points of the interior polygon
188 * are forced to be in the reduced set.
189 */
190 for (int i = 0; i < inputPts.length; i++) {
191 if (! CGAlgorithms.isPointInRing(inputPts[i], polyPts)) {
192 reducedSet.add(inputPts[i]);
193 }
194 }
195 Coordinate[] reducedPts = CoordinateArrays.toCoordinateArray(reducedSet);
196
197 // ensure that computed array has at least 3 points (not necessarily unique)
198 if (reducedPts.length < 3)
199 return padArray3(reducedPts);
200 return reducedPts;
201 }
202
203 private Coordinate[] padArray3(Coordinate[] pts)
204 {
205 Coordinate[] pad = new Coordinate[3];
206 for (int i = 0; i < pad.length; i++) {
207 if (i < pts.length) {
208 pad[i] = pts[i];
209 }
210 else
211 pad[i] = pts[0];
212 }
213 return pad;
214 }
215
216 private Coordinate[] preSort(Coordinate[] pts) {
217 Coordinate t;
218
219 // find the lowest point in the set. If two or more points have
220 // the same minimum y coordinate choose the one with the minimu x.
221 // This focal point is put in array location pts[0].
222 for (int i = 1; i < pts.length; i++) {
223 if ((pts[i].y < pts[0].y) || ((pts[i].y == pts[0].y) && (pts[i].x < pts[0].x))) {
224 t = pts[0];
225 pts[0] = pts[i];
226 pts[i] = t;
227 }
228 }
229
230 // sort the points radially around the focal point.
231 Arrays.sort(pts, 1, pts.length, new RadialComparator(pts[0]));
232
233 //radialSort(pts);
234 return pts;
235 }
236
237 /**
238 * Uses the Graham Scan algorithm to compute the convex hull vertices.
239 *
240 * @param c a list of points, with at least 3 entries
241 * @return a Stack containing the ordered points of the convex hull ring
242 */
243 private Stack grahamScan(Coordinate[] c) {
244 Coordinate p;
245 Stack ps = new Stack();
246 p = (Coordinate) ps.push(c[0]);
247 p = (Coordinate) ps.push(c[1]);
248 p = (Coordinate) ps.push(c[2]);
249 for (int i = 3; i < c.length; i++) {
250 p = (Coordinate) ps.pop();
251 while (CGAlgorithms.computeOrientation((Coordinate) ps.peek(), p, c[i]) > 0) {
252 p = (Coordinate) ps.pop();
253 }
254 p = (Coordinate) ps.push(p);
255 p = (Coordinate) ps.push(c[i]);
256 }
257 p = (Coordinate) ps.push(c[0]);
258 return ps;
259 }
260
261 /**
262 *@return whether the three coordinates are collinear and c2 lies between
263 * c1 and c3 inclusive
264 */
265 private boolean isBetween(Coordinate c1, Coordinate c2, Coordinate c3) {
266 if (CGAlgorithms.computeOrientation(c1, c2, c3) != 0) {
267 return false;
268 }
269 if (c1.x != c3.x) {
270 if (c1.x <= c2.x && c2.x <= c3.x) {
271 return true;
272 }
273 if (c3.x <= c2.x && c2.x <= c1.x) {
274 return true;
275 }
276 }
277 if (c1.y != c3.y) {
278 if (c1.y <= c2.y && c2.y <= c3.y) {
279 return true;
280 }
281 if (c3.y <= c2.y && c2.y <= c1.y) {
282 return true;
283 }
284 }
285 return false;
286 }
287
288 private Coordinate[] computeOctRing(Coordinate[] inputPts) {
289 Coordinate[] octPts = computeOctPts(inputPts);
290 CoordinateList coordList = new CoordinateList();
291 coordList.add(octPts, false);
292
293 // points must all lie in a line
294 if (coordList.size() < 3) {
295 return null;
296 }
297 coordList.closeRing();
298 return coordList.toCoordinateArray();
299 }
300
301 private Coordinate[] computeOctPts(Coordinate[] inputPts)
302 {
303 Coordinate[] pts = new Coordinate[8];
304 for (int j = 0; j < pts.length; j++) {
305 pts[j] = inputPts[0];
306 }
307 for (int i = 1; i < inputPts.length; i++) {
308 if (inputPts[i].x < pts[0].x) {
309 pts[0] = inputPts[i];
310 }
311 if (inputPts[i].x - inputPts[i].y < pts[1].x - pts[1].y) {
312 pts[1] = inputPts[i];
313 }
314 if (inputPts[i].y > pts[2].y) {
315 pts[2] = inputPts[i];
316 }
317 if (inputPts[i].x + inputPts[i].y > pts[3].x + pts[3].y) {
318 pts[3] = inputPts[i];
319 }
320 if (inputPts[i].x > pts[4].x) {
321 pts[4] = inputPts[i];
322 }
323 if (inputPts[i].x - inputPts[i].y > pts[5].x - pts[5].y) {
324 pts[5] = inputPts[i];
325 }
326 if (inputPts[i].y < pts[6].y) {
327 pts[6] = inputPts[i];
328 }
329 if (inputPts[i].x + inputPts[i].y < pts[7].x + pts[7].y) {
330 pts[7] = inputPts[i];
331 }
332 }
333 return pts;
334
335 }
336
337/*
338 // MD - no longer used, but keep for reference purposes
339 private Coordinate[] computeQuad(Coordinate[] inputPts) {
340 BigQuad bigQuad = bigQuad(inputPts);
341
342 // Build a linear ring defining a big poly.
343 ArrayList bigPoly = new ArrayList();
344 bigPoly.add(bigQuad.westmost);
345 if (! bigPoly.contains(bigQuad.northmost)) {
346 bigPoly.add(bigQuad.northmost);
347 }
348 if (! bigPoly.contains(bigQuad.eastmost)) {
349 bigPoly.add(bigQuad.eastmost);
350 }
351 if (! bigPoly.contains(bigQuad.southmost)) {
352 bigPoly.add(bigQuad.southmost);
353 }
354 // points must all lie in a line
355 if (bigPoly.size() < 3) {
356 return null;
357 }
358 // closing point
359 bigPoly.add(bigQuad.westmost);
360
361 Coordinate[] bigPolyArray = CoordinateArrays.toCoordinateArray(bigPoly);
362
363 return bigPolyArray;
364 }
365
366 private BigQuad bigQuad(Coordinate[] pts) {
367 BigQuad bigQuad = new BigQuad();
368 bigQuad.northmost = pts[0];
369 bigQuad.southmost = pts[0];
370 bigQuad.westmost = pts[0];
371 bigQuad.eastmost = pts[0];
372 for (int i = 1; i < pts.length; i++) {
373 if (pts[i].x < bigQuad.westmost.x) {
374 bigQuad.westmost = pts[i];
375 }
376 if (pts[i].x > bigQuad.eastmost.x) {
377 bigQuad.eastmost = pts[i];
378 }
379 if (pts[i].y < bigQuad.southmost.y) {
380 bigQuad.southmost = pts[i];
381 }
382 if (pts[i].y > bigQuad.northmost.y) {
383 bigQuad.northmost = pts[i];
384 }
385 }
386 return bigQuad;
387 }
388
389 private static class BigQuad {
390 public Coordinate northmost;
391 public Coordinate southmost;
392 public Coordinate westmost;
393 public Coordinate eastmost;
394 }
395 */
396
397 /**
398 *@param vertices the vertices of a linear ring, which may or may not be
399 * flattened (i.e. vertices collinear)
400 *@return a 2-vertex <code>LineString</code> if the vertices are
401 * collinear; otherwise, a <code>Polygon</code> with unnecessary
402 * (collinear) vertices removed
403 */
404 private Geometry lineOrPolygon(Coordinate[] coordinates) {
405
406 coordinates = cleanRing(coordinates);
407 if (coordinates.length == 3) {
408 return geomFactory.createLineString(new Coordinate[]{coordinates[0], coordinates[1]});
409// return new LineString(new Coordinate[]{coordinates[0], coordinates[1]},
410// geometry.getPrecisionModel(), geometry.getSRID());
411 }
412 LinearRing linearRing = geomFactory.createLinearRing(coordinates);
413 return geomFactory.createPolygon(linearRing, null);
414 }
415
416 /**
417 *@param vertices the vertices of a linear ring, which may or may not be
418 * flattened (i.e. vertices collinear)
419 *@return the coordinates with unnecessary (collinear) vertices
420 * removed
421 */
422 private Coordinate[] cleanRing(Coordinate[] original) {
423 Assert.equals(original[0], original[original.length - 1]);
424 ArrayList cleanedRing = new ArrayList();
425 Coordinate previousDistinctCoordinate = null;
426 for (int i = 0; i <= original.length - 2; i++) {
427 Coordinate currentCoordinate = original[i];
428 Coordinate nextCoordinate = original[i+1];
429 if (currentCoordinate.equals(nextCoordinate)) {
430 continue;
431 }
432 if (previousDistinctCoordinate != null
433 && isBetween(previousDistinctCoordinate, currentCoordinate, nextCoordinate)) {
434 continue;
435 }
436 cleanedRing.add(currentCoordinate);
437 previousDistinctCoordinate = currentCoordinate;
438 }
439 cleanedRing.add(original[original.length - 1]);
440 Coordinate[] cleanedRingCoordinates = new Coordinate[cleanedRing.size()];
441 return (Coordinate[]) cleanedRing.toArray(cleanedRingCoordinates);
442 }
443
444
445 /**
446 * Compares {@link Coordinate}s for their angle and distance
447 * relative to an origin.
448 *
449 * @author Martin Davis
450 * @version 1.7
451 */
452 private static class RadialComparator
453 implements Comparator
454 {
455 private Coordinate origin;
456
457 public RadialComparator(Coordinate origin)
458 {
459 this.origin = origin;
460 }
461 public int compare(Object o1, Object o2)
462 {
463 Coordinate p1 = (Coordinate) o1;
464 Coordinate p2 = (Coordinate) o2;
465 return polarCompare(origin, p1, p2);
466 }
467
468 /**
469 * Given two points p and q compare them with respect to their radial
470 * ordering about point o. First checks radial ordering.
471 * If points are collinear, the comparison is based
472 * on their distance to the origin.
473 * <p>
474 * p < q iff
475 * <ul>
476 * <li>ang(o-p) < ang(o-q) (e.g. o-p-q is CCW)
477 * <li>or ang(o-p) == ang(o-q) && dist(o,p) < dist(o,q)
478 * </ul>
479 *
480 * @param o the origin
481 * @param p a point
482 * @param q another point
483 * @return -1, 0 or 1 depending on whether p is less than,
484 * equal to or greater than q
485 */
486 private static int polarCompare(Coordinate o, Coordinate p, Coordinate q)
487 {
488 double dxp = p.x - o.x;
489 double dyp = p.y - o.y;
490 double dxq = q.x - o.x;
491 double dyq = q.y - o.y;
492
493/*
494 // MD - non-robust
495 int result = 0;
496 double alph = Math.atan2(dxp, dyp);
497 double beta = Math.atan2(dxq, dyq);
498 if (alph < beta) {
499 result = -1;
500 }
501 if (alph > beta) {
502 result = 1;
503 }
504 if (result != 0) return result;
505 //*/
506
507 int orient = CGAlgorithms.computeOrientation(o, p, q);
508
509 if (orient == CGAlgorithms.COUNTERCLOCKWISE) return 1;
510 if (orient == CGAlgorithms.CLOCKWISE) return -1;
511
512 // points are collinear - check distance
513 double op = dxp * dxp + dyp * dyp;
514 double oq = dxq * dxq + dyq * dyq;
515 if (op < oq) {
516 return -1;
517 }
518 if (op > oq) {
519 return 1;
520 }
521 return 0;
522 }
523
524 }
525}
Note: See TracBrowser for help on using the repository browser.