diff --git a/packages/turf-buffer/index.d.ts b/packages/turf-buffer/index.d.ts index 014186018..1408542dd 100644 --- a/packages/turf-buffer/index.d.ts +++ b/packages/turf-buffer/index.d.ts @@ -15,6 +15,7 @@ import { Units } from "@turf/helpers"; interface Options { units?: Units; steps?: number; + endCapStyle?: "round" | "flat" | "butt" | "square"; } /** diff --git a/packages/turf-buffer/index.js b/packages/turf-buffer/index.js index c059be84b..bc62bd488 100644 --- a/packages/turf-buffer/index.js +++ b/packages/turf-buffer/index.js @@ -42,6 +42,7 @@ function buffer(geojson, radius, options) { // use user supplied options or default values var units = options.units || "kilometers"; var steps = options.steps || 8; + var endCapStyle = options.endCapStyle || "round"; // validation if (!geojson) throw new Error("geojson is required"); @@ -56,13 +57,25 @@ function buffer(geojson, radius, options) { switch (geojson.type) { case "GeometryCollection": geomEach(geojson, function (geometry) { - var buffered = bufferFeature(geometry, radius, units, steps); + var buffered = bufferFeature( + geometry, + radius, + units, + steps, + endCapStyle + ); if (buffered) results.push(buffered); }); return featureCollection(results); case "FeatureCollection": featureEach(geojson, function (feature) { - var multiBuffered = bufferFeature(feature, radius, units, steps); + var multiBuffered = bufferFeature( + feature, + radius, + units, + steps, + endCapStyle + ); if (multiBuffered) { featureEach(multiBuffered, function (buffered) { if (buffered) results.push(buffered); @@ -71,7 +84,7 @@ function buffer(geojson, radius, options) { }); return featureCollection(results); } - return bufferFeature(geojson, radius, units, steps); + return bufferFeature(geojson, radius, units, steps, endCapStyle); } /** @@ -82,9 +95,10 @@ function buffer(geojson, radius, options) { * @param {number} radius distance to draw the buffer * @param {Units} [units='kilometers'] Supports all valid Turf {@link https://turfjs.org/docs/api/types/Units Units}. * @param {number} [steps=8] number of steps + * @param {'round'|'flat'|'butt'|'square'} [endCapStyle='round'] end cap style * @returns {Feature} buffered feature */ -function bufferFeature(geojson, radius, units, steps) { +function bufferFeature(geojson, radius, units, steps, endCapStyle) { var properties = geojson.properties || {}; var geometry = geojson.type === "Feature" ? geojson.geometry : geojson; @@ -92,12 +106,17 @@ function bufferFeature(geojson, radius, units, steps) { if (geometry.type === "GeometryCollection") { var results = []; geomEach(geojson, function (geometry) { - var buffered = bufferFeature(geometry, radius, units, steps); + var buffered = bufferFeature(geometry, radius, units, steps, endCapStyle); if (buffered) results.push(buffered); }); return featureCollection(results); } + // For point-type geometries, set the endCapStyle to undefined since they should not be affected by end cap styles + if (geometry.type === "Point" || geometry.type === "MultiPoint") { + endCapStyle = undefined; + } + // Project GeoJSON to Azimuthal Equidistant projection (convert to Meters) var projection = defineProjection(geometry); var projected = { @@ -109,7 +128,32 @@ function bufferFeature(geojson, radius, units, steps) { var reader = new GeoJSONReader(); var geom = reader.read(projected); var distance = radiansToLength(lengthToRadians(radius, units), "meters"); - var buffered = BufferOp.bufferOp(geom, distance, steps); + + var buffered; + + // Apply endCapStyle if valid - points are always round and polygons ignore endCapStyle + if (endCapStyle) { + var CAP_STYLE_MAP = { + round: BufferOp.CAP_ROUND, + flat: BufferOp.CAP_FLAT, + butt: BufferOp.CAP_BUTT, + square: BufferOp.CAP_SQUARE, + }; + + var capStyle = CAP_STYLE_MAP[endCapStyle]; + if (capStyle === undefined) { + throw new Error( + "Invalid endCapStyle: " + + endCapStyle + + " (expected one of 'round','flat','butt','square')" + ); + } + // Use the 4-arg overload when endCapStyle is specified + buffered = BufferOp.bufferOp(geom, distance, steps, capStyle); + } else { + // Otherwise use the 3-arg overload (like originally implemented) + buffered = BufferOp.bufferOp(geom, distance, steps); + } var writer = new GeoJSONWriter(); buffered = writer.write(buffered); diff --git a/packages/turf-buffer/package.json b/packages/turf-buffer/package.json index ab142ce44..719a1d6a9 100644 --- a/packages/turf-buffer/package.json +++ b/packages/turf-buffer/package.json @@ -6,7 +6,8 @@ "contributors": [ "Tom MacWright <@tmcw>", "Denis Carriere <@DenisCarriere>", - "Stefano Borghi <@stebogit>" + "Stefano Borghi <@stebogit>", + "Ryan Pimiskern <@plymer>" ], "license": "MIT", "bugs": { diff --git a/packages/turf-buffer/test.ts b/packages/turf-buffer/test.ts index d0ae67832..d777b7a10 100644 --- a/packages/turf-buffer/test.ts +++ b/packages/turf-buffer/test.ts @@ -7,6 +7,7 @@ import { writeJsonFileSync } from "write-json-file"; import { truncate } from "@turf/truncate"; import { featureEach } from "@turf/meta"; import { + lineString, featureCollection, point, polygon, @@ -182,6 +183,102 @@ test("turf-buffer - undefined return", (t) => { t.end(); }); +test("turf-buffer - endcap styles", (t) => { + const pt = point([-97, 49.8]); + + const pointFc = featureCollection([pt]); + + const pointDefault = buffer(pointFc, 10, { units: "miles" }); + const pointRound = buffer(pointFc, 10, { + units: "miles", + endCapStyle: "round", + }); + const pointButt = buffer(pointFc, 10, { + units: "miles", + endCapStyle: "butt", + }); + + t.deepEqual( + pointDefault, + pointRound, + "point - default and round produce the same result" + ); + t.deepEqual( + pointDefault, + pointButt, + "point - buffers are not affected by end cap style" + ); + + const poly = polygon([ + [ + [11, 0], + [22, 4], + [31, 0], + [31, 11], + [21, 15], + [11, 11], + [11, 0], + ], + ]); + + const polyDefault = buffer(poly, 10, { units: "miles" }); + const polyFlat = buffer(poly, 10, { + units: "miles", + endCapStyle: "flat", + }); + t.deepEqual( + polyDefault, + polyFlat, + "polygon - buffers are not affected by end cap style" + ); + + const ln = lineString([ + [-113.5, 53.5], + [-114, 51.1], + [-97, 49.8], + ]); + + const lineDefault = buffer(ln, 10, { units: "miles" }); + const lineRound = buffer(ln, 10, { + units: "miles", + endCapStyle: "round", + }); + const lineFlat = buffer(ln, 10, { units: "miles", endCapStyle: "flat" }); + const lineButt = buffer(ln, 10, { units: "miles", endCapStyle: "butt" }); + const lineSquare = buffer(ln, 10, { + units: "miles", + endCapStyle: "square", + }); + + t.deepEqual( + lineDefault, + lineRound, + "line - default and round produce the same result" + ); + t.deepEqual( + lineFlat, + lineButt, + "line - flat and butt produce the same result" + ); + t.isNotDeepEqual( + lineRound, + lineFlat, + "line - round and flat produce different results" + ); + t.isNotDeepEqual( + lineRound, + lineSquare, + "line - round and square produce different results" + ); + t.isNotDeepEqual( + lineSquare, + lineFlat, + "line - square and flat produce different results" + ); + + t.end(); +}); + function colorize(feature, color) { color = color || "#F00"; if (feature.properties) {