Growing a PostGIS geometry by a percentage

The comments suggest the 5% does not need to be attained with high accuracy. (If it does, it will take a long time to buffer a million polygons!) We can therefore invoke the Pizza Principle: linearly rescaling a 2D feature by a factor a rescales its area by a^2.

Here's how the reasoning goes:

  • When the shape is not too convoluted--especially if it's convex--then buffering produces a result comparable to rescaling the shape around a central point. (It is important to understand, though, that buffering is not ever equivalent to a rescaling for any shapes other than disks. For some concave shapes, a "buffer" computed via rescaling might actually not include parts of the original shape itself! Therefore ultimately we will compute a genuine buffer of the shape, but are only using this approximate equivalence as a heuristic to estimate how much to buffer by.)

  • If the buffered area is to be 5% greater, then the amount of rescaling therefore should be sqrt(1 + 5/100), which is close to 1.025: that is, we should want to expand the shape by 2.5% in all directions.

  • Equivalently, if we think of the shape as having a "diameter" (equal to a typical distance across), its radius should increase by 2.5%. That's equal to 2.5%/2 = 1.25% of the diameter.

  • We can estimate a typical diameter from the shape's bounding box. Use, say, an arithmetic or geometric mean of the box's side lengths.

This suggests the following workflow:

  1. Obtain the shape's bounding box.

  2. Let e be the average of the side lengths of the box.

  3. Buffer the shape by 1.25% of e; that is, by (5/100)/4 * e.

Because steps 1 and 2 require very little computation, this offers itself as one of the most expeditious possible solutions. As a check of accuracy, you can (of course) compute the areas of the buffered shapes and compare them to the original areas to see how closely they come to the desired 5% increase. Sometimes the buffered areas will be even more than 5% greater, but it should be rare that they are less, and it's impossible for them to be appreciably less.

Examples

As a check and illustration, let's consider some simple shapes.

  1. A disk of radius r has a bounding box with sides of length 2 r. Our formula computes e = (5/100)/4 * 2 * r = r / 40. The buffered shape obviously is a concentric disk of radius r + r / 40 = 1.025 r. The old area was pi * r ^2 while the new area is pi * (1.025 r)^2 = pi * 1.0506 * r ^2, which is 5.06% greater.

  2. A rectangle with sides parallel to the coordinates axes of lengths r and s gives e = (r + s)/2. The additional area from buffering the rectangle comes from four rectangles of width (5/100)/4 e = e / 80 = (r + s)/160 bordering the sides plus four quarter-circles of radius e / 80 at the corners. Neglecting the quarter-circles, which will be small compared to the other areas, The total new area equals

    2(r + s) * (r + s)/160 = (r^2 + s^2 + 2 r * s) / 80.

    When r and s are not too different, we can figure r^2 + s^2 is approximately 2 r * s. This approximation simplifies the total new area to 4 r * s / 80 = 5% of the original area of r * s, as intended.


You want to use a combination of ST_Scale ( http://postgis.net/docs/ST_Scale.html) and ST_Translate ( http://postgis.net/docs/ST_Translate.html ) I think. We have an example of this in PostGIS in Action and similar in Chapter 8. If you don't have the book, you can download the code for that chapter here:

http://www.postgis.us/chapter_08

Snippet from book Look at example 8.26:

    -- Listing 8.26 Combining Scale and Translation to maintain centroid
    SELECT xfactor, yfactor, 
       ST_Translate(ST_Scale(hex.the_geom, xfactor, yfactor), 
       ST_X(ST_Centroid(the_geom))*(1 - xfactor), 
       ST_Y(ST_Centroid(the_geom))*(1 - yfactor) ) As scaled_geometry
    FROM 
 ( SELECT ST_GeomFromText('POLYGON((0 0,64 64,64 128,0 192,-64 128,-64 64,0 0))') As the_geom)  As hex
    CROSS JOIN (SELECT x*0.5 As xfactor 
        FROM generate_series(1,4) As x) As xf
    CROSS JOIN (SELECT y*0.5 As yfactor
        FROM generate_series(1,4) As y) As yf;