Creating a Buffer around a Polygon shape using Java and PostGIS

Published

This article will explore one way to use open source tools (i.e. PostGIS and Java) to create a Buffer around a Polygon shape. We will use Central Park in Boulder, CO as the Polygon in our example below. This procedure was developed to allow for automatic detection of adjacent roads to city parks and lakes (using OpenStreetMap data).

Finding the Park’s Polygon Geometry

We can use OpenStreeMap data (with convenient extracts available from GeoFabrik or BBBike) as a source for our park geometry. In this example, I’ve downloaded the Boulder, CO extract from BBBike, and then used imposm3 to load the OSM data into PostGIS tables (imposm3 configuration available here). The following query will pull our park geometry out of the osm_landusages table in PostGIS:

SELECT ST_AsText(geometry) from public.osm_landusages WHERE type='park' AND id=2454;

Our query returns the WKT representation of the park’s geometry:

POLYGON((-105.279057455338 40.0157700624913,-105.279021664612 40.0158066914082,-105.278825611896 40.0158601679504,-105.278177942238 40.0159837172032,-105.278111222289 40.0159908418209,-105.277966634459 40.0158862356693,-105.277770665563 40.0153484527617,-105.277472353629 40.0144385971722,-105.277516023345 40.0144085899588,-105.277577546514 40.0144604739395,-105.277956073261 40.0147031300363,-105.278068055488 40.0147474703041,-105.27818380957 40.0148268469272,-105.278272909201 40.0148850173352,-105.278353710748 40.0149219815282,-105.278412719346 40.0149662379769,-105.278506848119 40.0151822396217,-105.278514056555 40.0152136717586,-105.278559067375 40.0152571738361,-105.278624949134 40.0152834930121,-105.278688735418 40.0152967364191,-105.278781606905 40.0153124943971,-105.278852014891 40.0153315213173,-105.279010349042 40.0155112293213,-105.279043876655 40.0156956311911,-105.279057455338 40.0157700624913))

To confirm, when I drop the WKT into an online WKT plotter, it looks like:

 The outline of Central Park in Boulder, Colorado.

Creating a Buffer around our Park

We will create this Buffer in a few different steps within PostGIS:

  1. Creating Offset Curves – for each edge of the Polygon, draw a line that is parallel and the same length as the edge, but falls outside of the original Polygon (these lines will be referred to as Offset Curves).
  2. Take the Concave Hull of all of the Offset Curves – the Convex Hull will look like an enlarged version of our original Feature.
  3. Remove the original Feature geometry from our enlarged Polygon – after removing the original Polygon, we will be left with our Buffer.

1. Creating Offset Curves

Below is a visualization of the strategy we will use to generate the Offset Curves. The original Polygon edges are in red, while the Offset Curves are featured in blue, and fall outside of the original Polygon.

Summarizes the process for generating parallel lines for each side of the Polygon Shape. These parallel lines are joined together via a convex hull to create a buffer expanding the original polygon Shape.

We can start by looping through all of the Polygon edges. Essentially what we are looking to do here is split each edge into its own distinct LineString. I have accomplished this by exporting the Polygon as GeoJson, and then using a Java snippet to loop through each of the GeoJson coordinates and package each pair of coordinates into a WKT LineString.

Example GeoJson input:

{
  "type": "Polygon",
  "coordinates": [
    [
      
   [
        -105.27905745534,
        40.015770062491
      ],
      [
        -105.27902166461,
        40.015806691408
      ],
      [
        -105.2788256119,
        40.01586016795
      ],
      [
        -105.27817794224,
        40.015983717203
      ],
      
    //other coordinates eliminated for brevity
    
    ]
  ]
}

The Java snippet below will read in the GeoJSON, and return an ArrayList of WKT LineStrings. We will need the WKT LineStrings in the next step.

import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;

public ArrayList getPolygonLinestrings( String polygonGeoJson ) {

  //parse polygon geoJSON string into JSONObject
  JSONObject geomJsonObject = new JSONObject( polygonGeoJson );
 JSONArray polygonCoordinates = geomJsonObject.getJSONArray("coordinates");
  
  //logic to handle polygons with inner rings - we only want to take the outer ring for our purposes
  JSONArray outerPolygonCoordinates = new JSONArray();
  if ( polygonCoordinates.length() == 1 || polygonCoordinates.getJSONArray(0).getJSONArray(0).length() == 2 ) {
   outerPolygonCoordinates = polygonCoordinates.getJSONArray(0);
 } else {
    //omit inner ring if defined and retrieve outer ring
    outerPolygonCoordinates = polygonCoordinates.getJSONArray(0).getJSONArray(0);
 }
 
  //loop through coordinaes and retrieve Linestring WKT
 ArrayList polygonLinestrings = new ArrayList();     
  for (int i=0; i < outerPolygonCoordinates.length()-1; ++i) {
   
    //assemble start and end coordinates into WKT LINESTRING
    String wktLineString = "LINESTRING(" + outerPolygonCoordinates.getJSONArray(i).getDouble(0) + " " + outerPolygonCoordinates.getJSONArray(i).getDouble(1) + ", ";
    wktLineString += outerPolygonCoordinates.getJSONArray(i+1).getDouble(0) + " " + outerPolygonCoordinates.getJSONArray(i+1).getDouble(1) + ")";
   
    polygonLinestrings.add( wktLineString );
    
  }

 return polygonLinestrings;
  
}

Once we have all of our WKT LineStrings assembled, we can generate the Offset Curves using a simple PostGIS function ST_OffestCurve(). Below is an example PostGIS query for generating the Offset Curve with a distance of .00025 for the first Polygon edge shown above:

SELECT ST_AsText(ST_OffsetCurve(ST_GeomFromText('LINESTRING( -105.278852014891 40.0153315213173,-105.279010349042 40.0155112293213)'),.00025));

The query above will use the provided LineString (shown in red below), and generate the resulting Offset Curve (shown in blue below):

Depiction of the PostGIS Offset Curve created for a given Polygon shape's side.

We need to repeat this process for each of the Polygon Edges.

2. Creating the Concave Hull

Once we have generated the Offset Curves for each of the Polygon Edges, we need to assemble all of the Offset Curves into a new Polygon (which will resemble an “enlarged” copy of our original Polygon).

We can pass an array of all of our Offset Curve WKT LineStrings into the ST_Collect() PostGIS function to assemble them into a single MultiLineString. We can then pass the MultiLineString into the ST_ConcaveHull() PostGIS function to get our “enlarged” Polygon. An example PostGIS query is below; please keep in mind that the number of Offset Curve WKT has been drastically abbreviated for brevity.

SELECT ST_AsText( 

 ST_CONCAVEHULL(

   ST_COLLECT( ARRAY[

      ST_GeomFromText('LINESTRING( -105.278852014891 40.0153315213173,-105.279010349042 40.0155112293213)'),

      ST_GeomFromText('LINESTRING(  -105.279043876655 40.0156956311911,-105.279057455338 40.0157700624913)')

    ]),

   .99

 )

);

Once we execute this query on our full set of Offset Curves, PostGIS we return the “enlarged” copy of our original Polygon geometry:

Depicts the Buffer Shape created by joining all PostGIS Offset Curves via a convex hull calculation.

3. Removing the Original Feature from our “Enlarged” Polygon

Once we have assembled our “enlarged” polygon, the last step in identifying the Buffer is removing our original feature. We will use the ST_MakePolygon() PostGIS function perform to essentially create a new Polygon with our “enlarged” polygon WKT as the outer LineString (first parameter), and supply the original feature WKT as an interior LineString (second parameter, supplied as an array). When defining Polygons in this way, the Interior shapes will be removed from the exterior shape.

Below is an example PostGIS query to perform this operation. Notice to usage of the ST_ExteriorRing() function; this is strictly employed to convert our POLYGON geometry to a LINESTRING, which is required by the ST_MakePolygon() function. I have included the full “expanded” and original polygon WKT geometries below – I apologize for the cumbersomeness!

SELECT ST_AsText(

 ST_MakePolygon( 

    ST_ExteriorRing( ST_GeomFromText('POLYGON((-105.277374441497 40.0142025448657,-105.277501896363 40.0142088311058,-105.277629351229 40.014215117346,-105.277677193546 40.0142174769914,-105.277780088591 40.0142933557846,-105.277887489577 40.0143622056199,-105.277994890564 40.0144310554553,-105.278108577272 40.0144946306374,-105.278209440479 40.0145412900812,-105.278314549229 40.0146133668277,-105.278424766447 40.0146795693827,-105.278503710748 40.0147219815285,-105.278562719346 40.0147662379772,-105.278640239627 40.014864260861,-105.278683872358 40.0149626728186,-105.278843274022 40.0150703719891,-105.278945438836 40.0151077134937,-105.279039594307 40.0151662519531,-105.279123954266 40.0152619998362,-105.279197928458 40.0153459599571,-105.279253555363 40.0154608072782,-105.279256316519 40.0154665079613,-105.279279144053 40.0155920593968,-105.279302011287 40.0157176036161,-105.279303396184 40.015725194905,-105.279236266085 40.0159447814564,-105.279236266085 40.0159447814564,-105.279200475359 40.0159814103733,-105.279090478023 40.016046100758,-105.279087452688 40.0160478799838,-105.278964340589 40.0160814607955,-105.278891399972 40.016101356526,-105.278872457099 40.016105739787,-105.278747107612 40.0161296514142,-105.278621758126 40.0161535630414,-105.278496408639 40.0161774746687,-105.278371059152 40.0162013862959,-105.278245709665 40.0162252979231,-105.278224787441 40.0162292890398,-105.278204487303 40.0162323039276,-105.278137767354 40.0162394285453,-105.278014445366 40.0162066269131,-105.277964682502 40.016193390805,-105.277861293565 40.0161185911566,-105.277820094672 40.0160887846534,-105.277743175486 40.0159869627574,-105.277731743878 40.0159718301485,-105.277688053102 40.015851932793,-105.277644362327 40.0157320354375,-105.277600671551 40.015612138082,-105.277556980775 40.0154922407265,-105.277535774982 40.0154340472409,-105.277533108072 40.0154263401051,-105.27749335132 40.0153050814541,-105.277453594569 40.0151838228031,-105.277413837817 40.0150625641521,-105.277374081065 40.0149413055011,-105.277334324314 40.0148200468502,-105.277294567562 40.0146987881992,-105.277254810811 40.0145775295482,-105.277234796138 40.0145164845156,-105.277275659761 40.0143955943836,-105.277316523385 40.0142747042515,-105.277330771781 40.0142325520791,-105.277374441497 40.0142025448657))
',4326) ) , 

   ARRAY[ ST_ExteriorRing( ST_GeomFromText('POLYGON((-105.279057455338 40.0157700624913,-105.279021664612 40.0158066914082,-105.278825611896 40.0158601679504,-105.278177942238 40.0159837172032,-105.278111222289 40.0159908418209,-105.277966634459 40.0158862356693,-105.277770665563 40.0153484527617,-105.277472353629 40.0144385971722,-105.277516023345 40.0144085899588,-105.277577546514 40.0144604739395,-105.277956073261 40.0147031300363,-105.278068055488 40.0147474703041,-105.27818380957 40.0148268469272,-105.278272909201 40.0148850173352,-105.278353710748 40.0149219815282,-105.278412719346 40.0149662379769,-105.278506848119 40.0151822396217,-105.278514056555 40.0152136717586,-105.278559067375 40.0152571738361,-105.278624949134 40.0152834930121,-105.278688735418 40.0152967364191,-105.278781606905 40.0153124943971,-105.278852014891 40.0153315213173,-105.279010349042 40.0155112293213,-105.279043876655 40.0156956311911,-105.279057455338 40.0157700624913))
',4326) ) ]

 )

);

After executing the query above, we will be left with only our Buffer– notice the original park geometry has been removed. Quite a few steps just to expand a Polygon if you ask me. It feels good to automate this process in Java so that the logic can be re-used for more than one shape!

The outcome of our ST_MakePolygon() query gives us the Buffer around our original Polygon shape:

The Polygon Shape Buffer is shown with the original Polygon Shape of the park removed.

For reference, here is our Buffer in WKT format as well:

POLYGON((-105.277374441497 40.0142025448657,-105.277501896363 40.0142088311058,-105.277629351229 40.014215117346,-105.277677193546 40.0142174769914,-105.277780088591 40.0142933557846,-105.277887489577 40.0143622056199,-105.277994890564 40.0144310554553,-105.278108577272 40.0144946306374,-105.278209440479 40.0145412900812,-105.278314549229 40.0146133668277,-105.278424766447 40.0146795693827,-105.278503710748 40.0147219815285,-105.278562719346 40.0147662379772,-105.278640239627 40.014864260861,-105.278683872358 40.0149626728186,-105.278843274022 40.0150703719891,-105.278945438836 40.0151077134937,-105.279039594307 40.0151662519531,-105.279123954266 40.0152619998362,-105.279197928458 40.0153459599571,-105.279253555363 40.0154608072782,-105.279256316519 40.0154665079613,-105.279279144053 40.0155920593968,-105.279302011287 40.0157176036161,-105.279303396184 40.015725194905,-105.279236266085 40.0159447814564,-105.279236266085 40.0159447814564,-105.279200475359 40.0159814103733,-105.279090478023 40.016046100758,-105.279087452688 40.0160478799838,-105.278964340589 40.0160814607955,-105.278891399972 40.016101356526,-105.278872457099 40.016105739787,-105.278747107612 40.0161296514142,-105.278621758126 40.0161535630414,-105.278496408639 40.0161774746687,-105.278371059152 40.0162013862959,-105.278245709665 40.0162252979231,-105.278224787441 40.0162292890398,-105.278204487303 40.0162323039276,-105.278137767354 40.0162394285453,-105.278014445366 40.0162066269131,-105.277964682502 40.016193390805,-105.277861293565 40.0161185911566,-105.277820094672 40.0160887846534,-105.277743175486 40.0159869627574,-105.277731743878 40.0159718301485,-105.277688053102 40.015851932793,-105.277644362327 40.0157320354375,-105.277600671551 40.015612138082,-105.277556980775 40.0154922407265,-105.277535774982 40.0154340472409,-105.277533108072 40.0154263401051,-105.27749335132 40.0153050814541,-105.277453594569 40.0151838228031,-105.277413837817 40.0150625641521,-105.277374081065 40.0149413055011,-105.277334324314 40.0148200468502,-105.277294567562 40.0146987881992,-105.277254810811 40.0145775295482,-105.277234796138 40.0145164845156,-105.277275659761 40.0143955943836,-105.277316523385 40.0142747042515,-105.277330771781 40.0142325520791,-105.277374441497 40.0142025448657),(-105.279057455338 40.0157700624913,-105.279021664612 40.0158066914082,-105.278825611896 40.0158601679504,-105.278177942238 40.0159837172032,-105.278111222289 40.0159908418209,-105.277966634459 40.0158862356693,-105.277770665563 40.0153484527617,-105.277472353629 40.0144385971722,-105.277516023345 40.0144085899588,-105.277577546514 40.0144604739395,-105.277956073261 40.0147031300363,-105.278068055488 40.0147474703041,-105.27818380957 40.0148268469272,-105.278272909201 40.0148850173352,-105.278353710748 40.0149219815282,-105.278412719346 40.0149662379769,-105.278506848119 40.0151822396217,-105.278514056555 40.0152136717586,-105.278559067375 40.0152571738361,-105.278624949134 40.0152834930121,-105.278688735418 40.0152967364191,-105.278781606905 40.0153124943971,-105.278852014891 40.0153315213173,-105.279010349042 40.0155112293213,-105.279043876655 40.0156956311911,-105.279057455338 40.0157700624913))