[postgis-users] Minimum Bounding Circle - was Roeck Test
Bruce Rindahl
rindahl at lrcwe.com
Thu Oct 30 09:23:42 PDT 2008
Lee Meilleur wrote:
> Thanks Bruce, it works well. When I compare it with our commercial software it's within a couple of hundredths of the Roeck test, but not in any predictable fashion. Some are .02 over the Roeck, some are .01 under, the majority are right on, but there were several that were 1 to 3 tenths off, fairly significant. I have no way of knowing how the commercial software computes the Roeck test. There was one district that created an error, I had to run the query on each of the districts to figure it out. Here's the message:
>
> Error: getPoint2d_p: point offset out of range
>
>
Lee and anyone else interested.
There are two issues to be resolved here. The first is the one that I
mentioned previously - the computation of the diameter of a convex
polygon efficiently is a nasty problem. I am looking into correcting
the original code. Below is a revised version that will work by brute
force - it checks every pair of points. If the convex polygon doesn't
have too many points it works well. Increasing the number of points
increases the computation time exponentially. You can test this by the
following simple query:
SELECT mbc(buffer(MakePoint(0,0),100,8)) from mytable
Buffering a point actually creates a polygon - not a circle. The number
of points in the polygon is 4 times the last parameter (8) in the above
query. Eight is the default. Watch the computation time increase as you
increase the value of 8 in the query. However Lee's data set and mine
run very fast using the Brute Force approach.
The second issue is the different results of the area of the circle. I
think this is again the result of a circle is actually a polygon. In
the code the function returns a buffer around the computed center with
the computed radius. I set the number of points to 48 * 4 - see the
RETURN line. This is probably close for small areas but it may need to
be increased if you need a polygon result. For an exact calculation,
the radius could be returned (or an area) if desired.
Bruce
-- Function: mbc(geometry)
-- DROP FUNCTION mbc(geometry);
CREATE OR REPLACE FUNCTION mbc(geometry)
RETURNS geometry AS
$BODY$
DECLARE
hull GEOMETRY;
ring GEOMETRY;
center GEOMETRY;
radius DOUBLE PRECISION;
dist DOUBLE PRECISION;
d DOUBLE PRECISION;
idx1 integer;
idx2 integer;
l1 GEOMETRY;
l2 GEOMETRY;
p1 GEOMETRY;
p2 GEOMETRY;
a1 DOUBLE PRECISION;
a2 DOUBLE PRECISION;
BEGIN
-- First compute the ConvexHull of the geometry
hull = convexHull($1);
-- convert the hull perimeter to a linestring so we can manipulate
individual points
ring = exteriorRing(hull);
dist = 0;
-- Brute Force - check every pair
FOR i in 1 .. (NumPoints(ring)-2)
LOOP
FOR j in i .. (NumPoints(ring)-1)
LOOP
d = distance(PointN(ring,i),PointN(ring,j));
-- Check the distance and update if larger
IF (d > dist) THEN
dist = d;
idx1 = i;
idx2 = j;
END IF;
END LOOP;
END LOOP;
-- We now have the diameter of the convex hull. The following line
returns it if desired.
-- RETURN MakeLine(PointN(ring,idx1),PointN(ring,idx2));
-- Now for the Minimum Bounding Circle. Since we know the two
points furthest from each
-- other, the MBC must go through those two points. Start with those
points as a diameter of a circle.
-- The radius is half the distance between them and the center is
midway between them
radius = distance(PointN(ring,idx1),PointN(ring,idx2)) / 2.0;
center =
line_interpolate_point(MakeLine(PointN(ring,idx1),PointN(ring,idx2)),0.5);
-- Loop through each vertex and check if the distance from the
center to the point
-- is greater than the current radius.
FOR k in 1 .. (NumPoints(ring)-1)
LOOP
IF(k <> idx1 and k <> idx2) THEN
dist = distance(center,PointN(ring,k));
IF (dist > radius) THEN
-- We have to expand the circle. The new circle must
pass trhough
-- three points - the two original diameters and this point.
-- Draw a line from the first diameter to this point
l1 = makeline(PointN(ring,idx1),PointN(ring,k));
-- Compute the midpoint
p1 = line_interpolate_point(l1,0.5);
-- Rotate the line 90 degrees around the midpoint
(perpendicular bisector)
l1 =
translate(rotate(translate(l1,-X(p1),-Y(p1)),pi()/2),X(p1),Y(p1));
-- Compute the azimuth of the bisector
a1 = azimuth(PointN(l1,1),PointN(l1,2));
-- Extend the line in each direction the new computed
distance to insure they will intersect
l1 =
addPoint(l1,makepoint(X(PointN(l1,2))+sin(a1)*dist,Y(PointN(l1,2))+cos(a1)*dist),-1);
l1 =
addPoint(l1,makepoint(X(PointN(l1,1))-sin(a1)*dist,Y(PointN(l1,1))-cos(a1)*dist),0);
-- Repeat for the line from the point to the other
diameter point
l2 = makeline(PointN(ring,idx2),PointN(ring,k));
p2 = line_interpolate_point(l2,0.5);
l2 =
translate(rotate(translate(l2,-X(p2),-Y(p2)),pi()/2),X(p2),Y(p2));
a2 = azimuth(PointN(l2,1),PointN(l2,2));
l2 =
addPoint(l2,makepoint(X(PointN(l2,2))+sin(a2)*dist,Y(PointN(l2,2))+cos(a2)*dist),-1);
l2 =
addPoint(l2,makepoint(X(PointN(l2,1))-sin(a2)*dist,Y(PointN(l2,1))-cos(a2)*dist),0);
-- The new center is the intersection of the two bisectors
center = intersection(l1,l2);
-- The new radius is the distance to any of the three points
radius = distance(center,PointN(ring,idx1));
END IF;
END IF;
END LOOP;
--DONE!! Return the MBC via the buffer command
RETURN buffer(center,radius,48);
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE
More information about the postgis-users
mailing list