[postgis-users] Minimum Bounding Circle - was Roeck Test
Lee Meilleur
lee.meilleur at gis.leg.mn
Thu Oct 30 11:19:22 PDT 2008
Bruce, it works great, thanks so much. I'm sitting on a panel at an upcoming National Conference of State Legislatures meeting. The panel is on Web Technologies for Redistricting and I'll be discussing PostgreSQL/PostGIS and MapServer as alternatives to some of the commercial software available. Having a working MBC (Roeck test) is key. Much appreciated.
Lee
-----Original Message-----
From: Bruce Rindahl [mailto:rindahl at lrcwe.com]
Sent: Thursday, October 30, 2008 11:24 AM
To: Lee Meilleur
Cc: 'PostGIS Users Discussion'
Subject: Re: [postgis-users] Minimum Bounding Circle - was Roeck Test
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