Skip to content

Buffer

This is an example of a buffer function recreating the @turf/buffer API. It uses WKB to pass geometries to the GEOS C++ library and then parses the result back to GeoJSON. Most of the code is taken from the original @turf/buffer implementation.

Source code

js
import center from '@turf/center'
import { geomEach, featureEach } from '@turf/meta'
import { geoAzimuthalEquidistant } from 'd3-geo'
import {
  feature,
  featureCollection,
  radiansToLength,
  lengthToRadians,
  earthRadius
} from '@turf/helpers'
import { geojsonToGeosGeom, geosGeomToGeojson } from 'geos-wasm/helpers'
let GEOSFunctions

/**
 * Calculates a buffer for input features for a given radius. Units supported are miles, kilometers, and degrees.
 *
 * When using a negative radius, the resulting geometry may be invalid if
 * it's too small compared to the radius magnitude. If the input is a
 * FeatureCollection, only valid members will be returned in the output
 * FeatureCollection - i.e., the output collection may have fewer members than
 * the input, or even be empty.
 *
 * @name buffer
 * @param {FeatureCollection|Geometry|Feature<any>} geojson input to be buffered
 * @param {number} radius distance to draw the buffer (negative values are allowed)
 * @param {Object} [options={}] Optional parameters
 * @param {string} [options.units="kilometers"] any of the options supported by turf units
 * @param {number} [options.steps=8] number of steps
 * @param {number} [options.endCapStyle=1] end cap style (1 = round, 2 = flat, 3 = square)
 * @param {number} [options.joinStyle=1] join style (1 = round, 2 = mitre, 3 = bevel)
 * @param {number} [options.mitreLimit=5] mitre limit
 * @param {boolean} [options.singleSided=false] whether to generate a single-sided or double-sided buffer
 * @param {Object} [options.GEOS] GEOS library
 * @returns {FeatureCollection|Feature<Polygon|MultiPolygon>|undefined} buffered features
 * @example
 * const point = turf.point([-90.548630, 14.616599]);
 * const buffered = turf.buffer(point, 500, {units: 'miles'});
 *
 */
function buffer (geojson, radius, options) {
  // Optional params
  options = options || {}
  if (!GEOSFunctions) {
    GEOSFunctions = options.GEOS
  }
  // use user supplied options or default values
  const units = options.units || 'kilometers'
  const steps = options.steps || 8
  const endCapStyle = options.endCapStyle || 1
  const joinStyle = options.joinStyle || 1
  const mitreLimit = options.mitreLimit || 5
  const singleSided = options.singleSided || false

  // validation
  if (!geojson) throw new Error('geojson is required')
  if (typeof options !== 'object') throw new Error('options must be an object')
  if (typeof steps !== 'number') throw new Error('steps must be an number')

  // Allow negative buffers ("erosion") or zero-sized buffers ("repair geometry")
  if (radius === undefined) throw new Error('radius is required')
  if (steps <= 0) throw new Error('steps must be greater than 0')

  const results = []
  switch (geojson.type) {
    case 'GeometryCollection':
      geomEach(geojson, function (geometry) {
        const buffered = bufferFeature(
          geometry,
          radius,
          units,
          steps,
          endCapStyle,
          joinStyle,
          mitreLimit,
          singleSided
        )
        if (buffered) results.push(buffered)
      })
      return featureCollection(results)
    case 'FeatureCollection':
      featureEach(geojson, function (feature) {
        const multiBuffered = bufferFeature(
          feature,
          radius,
          units,
          steps,
          endCapStyle,
          joinStyle,
          mitreLimit,
          singleSided
        )
        if (multiBuffered) {
          featureEach(multiBuffered, function (buffered) {
            if (buffered) results.push(buffered)
          })
        }
      })
      return featureCollection(results)
  }
  return bufferFeature(
    geojson,
    radius,
    units,
    steps,
    endCapStyle,
    joinStyle,
    mitreLimit,
    singleSided
  )
}

/**
 * Buffer single Feature/Geometry
 *
 * @private
 * @param {Feature<any>} geojson input to be buffered
 * @param {number} radius distance to draw the buffer
 * @param {string} [units='kilometers'] any of the options supported by turf units
 * @param {number} [steps=8] number of steps
 * @param {number} [endCapStyle=1] end cap style (1 = round, 2 = flat, 3 = square)
 * @param {number} [joinStyle=1] join style (1 = round, 2 = mitre, 3 = bevel)
 * @param {number} [mitreLimit=5] mitre limit ratio
 * @param {boolean} [singleSided=false] whether to buffer just one side of the input line
 * @returns {Feature<Polygon|MultiPolygon>} buffered feature
 */
function bufferFeature (geojson, radius, units, steps, endCapStyle, joinStyle, mitreLimit, singleSided) {
  const properties = geojson.properties || {}
  const geometry = geojson.type === 'Feature' ? geojson.geometry : geojson

  // Geometry Types faster than jsts
  if (geometry.type === 'GeometryCollection') {
    const results = []
    geomEach(geojson, function (geometry) {
      const buffered = bufferFeature(
        geometry,
        radius,
        units,
        steps,
        endCapStyle,
        joinStyle,
        mitreLimit,
        singleSided
      )
      if (buffered) results.push(buffered)
    })
    return featureCollection(results)
  }

  // Project GeoJSON to Azimuthal Equidistant projection (convert to Meters)
  const projection = defineProjection(geometry)

  const projected = {
    type: geometry.type,
    coordinates: projectCoords(geometry.coordinates, projection)
  }

  // GEOS buffer operation
  const isBufferWithParams = endCapStyle || joinStyle || mitreLimit || singleSided
  let bufferParamsPtr
  if (isBufferWithParams) {
    bufferParamsPtr = GEOSFunctions.GEOSBufferParams_create()
    if (endCapStyle) {
      GEOSFunctions.GEOSBufferParams_setEndCapStyle(bufferParamsPtr, endCapStyle)
    }
    if (joinStyle) {
      GEOSFunctions.GEOSBufferParams_setJoinStyle(bufferParamsPtr, joinStyle)
    }
    if (mitreLimit) {
      GEOSFunctions.GEOSBufferParams_setMitreLimit(bufferParamsPtr, mitreLimit)
    }
    if (steps) {
      GEOSFunctions.GEOSBufferParams_setQuadrantSegments(bufferParamsPtr, steps)
    }
    if (singleSided) {
      GEOSFunctions.GEOSBufferParams_setSingleSided(bufferParamsPtr, singleSided)
    }
  }
  // create a GEOS object from the GeoJSON
  const geomPtr = geojsonToGeosGeom(projected, GEOSFunctions)
  const distance = radiansToLength(lengthToRadians(radius, units), 'meters')
  let bufferPtr
  if (isBufferWithParams) {
    bufferPtr = GEOSFunctions.GEOSBufferWithParams(geomPtr, bufferParamsPtr, distance)
  } else {
    bufferPtr = GEOSFunctions.GEOSBuffer(geomPtr, distance, steps)
  }
  // destroy the bufferParamsPtr if it exists
  if (bufferParamsPtr) {
    GEOSFunctions.GEOSBufferParams_destroy(bufferParamsPtr)
  }
  // update the original GeoJSON with the new geometry
  const buffered = geosGeomToGeojson(bufferPtr, GEOSFunctions)
  // destroy the GEOS objects
  GEOSFunctions.GEOSGeom_destroy(geomPtr)
  GEOSFunctions.GEOSGeom_destroy(bufferPtr)

  // Detect if empty geometries
  if (coordsIsNaN(buffered.coordinates)) return undefined

  // Unproject coordinates (convert to Degrees)
  const result = {
    type: buffered.type,
    coordinates: unprojectCoords(buffered.coordinates, projection)
  }

  return feature(result, properties)
}

/**
 * Coordinates isNaN
 *
 * @private
 * @param {Array<any>} coords GeoJSON Coordinates
 * @returns {boolean} if NaN exists
 */
function coordsIsNaN (coords) {
  if (Array.isArray(coords[0])) return coordsIsNaN(coords[0])
  return isNaN(coords[0])
}

/**
 * Project coordinates to projection
 *
 * @private
 * @param {Array<any>} coords to project
 * @param {GeoProjection} proj D3 Geo Projection
 * @returns {Array<any>} projected coordinates
 */
function projectCoords (coords, proj) {
  if (typeof coords[0] !== 'object') return proj(coords)
  return coords.map(function (coord) {
    return projectCoords(coord, proj)
  })
}

/**
 * Un-Project coordinates to projection
 *
 * @private
 * @param {Array<any>} coords to un-project
 * @param {GeoProjection} proj D3 Geo Projection
 * @returns {Array<any>} un-projected coordinates
 */
function unprojectCoords (coords, proj) {
  if (typeof coords[0] !== 'object') return proj.invert(coords)
  return coords.map(function (coord) {
    return unprojectCoords(coord, proj)
  })
}

/**
 * Define Azimuthal Equidistant projection
 *
 * @private
 * @param {Geometry|Feature<any>} geojson Base projection on center of GeoJSON
 * @returns {GeoProjection} D3 Geo Azimuthal Equidistant Projection
 */
function defineProjection (geojson) {
  const coords = center(geojson).geometry.coordinates
  const rotation = [-coords[0], -coords[1]]
  return geoAzimuthalEquidistant().rotate(rotation).scale(earthRadius)
}

export default buffer