I'm interested in trying to reproduce this kind of cartography in a Mapbox/Turf/vector tile stack:

enter image description here

I would start with the polygon for the lake edge, and generate a series of concentric polygons inside. Note that when the edges of the internal polygons meet, they split, so we will end up with multi-polygons.

Is there a way I can achieve this in Turf? Some possible candidates:

  • transformScale: just produces identical polygons, but smaller
  • lineOffset: would maybe solve part of the problem, but I'd need to manually detect when the lines are intersecting, then remove the intersecting bits.
  • buffer: sounds like the right tool, but the description is vague about how it would operate on lines or polygons.

To make matters more complicated, because the lake features might cross vector tile boundaries, I really want to first convert the polygon to a line, and remove the segment along the tile edge. This might limit options further.

If it can't be done in Turf, pointers to algorithms that can be implemented in JavaScript would still be valuable.


1 Answer 1


I have achieved the basic functionality using a combination of many Turf functions, particularly:

  • polygonToLine to transform the polygon into something we can call lineOffset on
  • lineOffset produces the offset itself
  • lineToPolygon converts the offset back into a polygon so we can start removing inside out pieces
  • unkinkPolygon breaks a polygon into pieces of different clockwiseness
  • booleanClockwise lets us filter out the inside-out pieces
  • booleanWithin lets us filter out "outliers"

Here's a basic implementation:

const turf = require('@turf/turf');

const waterPolygon = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[117.90527343750001,-41.90227704096369],[116.630859375,-44.33956524809713],[120.9375,-46.92025531537451],[124.23339843749999,-43.96119063892024],[129.9462890625,-43.771093817756494],[131.748046875,-46.58906908309183],[136.23046875,-44.902577996288855],[132.1435546875,-38.856820134743614],[129.8583984375,-42.293564192170074],[124.49707031249999,-42.45588764197166],[121.025390625,-39.30029918615028],[117.90527343750001,-41.90227704096369]]]}}]}.features[0];
const line = turf.polygonToLine(waterPolygon);

// when an offset intersects itself, it makes inside out pieces. We can strip them out
// like this
function removeInsideOuts(polygon) {
    const out = turf.unkinkPolygon(polygon)
    out.features = out.features.filter(p => !turf.booleanClockwise(turf.polygonToLine(p)));
    return out;

// offsets can get so extreme they make correctly oriented polygons that are no longer 
// inside the original feature. So we filter them out
function removeOutliers(polygons) {
    return polygons.filter(p => turf.booleanWithin(p, waterPolygon));

function makeOffset(line, distance) {
    return turf.lineOffset(line, distance);
const offsets = [50];
for (let i = 0; i < 10; i++) {
    offsets.push(offsets[i] * 1.3);

const features = [];
for (const offset of offsets) {
    const offsetPolygon = turf.lineToPolygon(makeOffset(line, -offset));
    const cleanPieces = removeInsideOuts(offsetPolygon).features;
const out = turf.featureCollection(features);

Sample output

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.