Polygon – Mask GeoJSON with Nested Polygons/Nested Donuts

donut-polygonsgeojsonmaskingpolygonturf

How would I mask a geojson that looks like this

screenshot of map

{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[102,2],[103,2],[103,3],[102,3],[102,2]]],[[[100,0],[101,0],[101,1],[100,1],[100,0]],[[100.2,0.2],[100.2,0.8],[100.8,0.8],[100.8,0.2],[100.2,0.2]]],[[[100.3,0.3],[100.7,0.3],[100.7,0.7],[100.3,0.7],[100.3,0.3]],[[100.4305160192801,0.67817296142852],[100.43926588878527,0.6777431305923907],[100.44793148784716,0.6764577778174132],[100.45642935778174,0.6743292824257617],[100.46467765559704,0.6713781440787653],[100.47259694234847,0.667632785305691],[100.48011094831709,0.6631292777150927],[100.48714730763562,0.6579109945286341],[100.49363825528266,0.6520281927874464],[100.49952127973005,0.6455375292587178],[100.50473972495757,0.6385015147088778],[100.50924333603814,0.6309879118031904],[100.5129887430422,0.6230690824341719],[100.51593987860606,0.6148212907677792],[100.51806832514795,0.6063239687221135],[100.51935358839506,0.5976589509544159],[100.51978329459449,0.5889096867249723],[100.51935330951578,0.5801604362283926],[100.51806777810651,0.5714954591315431],[100.51593908442491,0.56299820313176],[100.51298773224117,0.5547505003481902],[100.50924214746165,0.5468317792832317],[100.50473840428187,0.5393182999407914],[100.49951987770788,0.5322824194649319],[100.4936368257929,0.5257918953685365],[100.48714590561279,0.5199092330597317],[100.48010962764015,0.5146910839473947],[100.47259575377038,0.5101876999202546],[100.46467664479428,0.5064424494514895],[100.45642856359898,0.503491399987565],[100.4479309408045,0.5013629706420072],[100.43926560990533,0.5000776585380188],[100.4305160192801,0.49964784143480295],[100.42176642865488,0.5000776585380188],[100.41310109775573,0.5013629706420072],[100.40460347496123,0.503491399987565],[100.39635539376593,0.5064424494514895],[100.38843628478985,0.5101876999202546],[100.38092241092006,0.5146910839473947],[100.37388613294743,0.5199092330597317],[100.3673952127673,0.5257918953685367],[100.36151216085236,0.5322824194649319],[100.35629363427834,0.5393182999407915],[100.35178989109856,0.5468317792832317],[100.34804430631903,0.5547505003481902],[100.34509295413531,0.56299820313176],[100.34296426045371,0.5714954591315431],[100.34167872904445,0.5801604362283926],[100.34124874396572,0.5889096867249723],[100.34167845016515,0.5976589509544159],[100.34296371341229,0.6063239687221135],[100.34509215995416,0.6148212907677792],[100.34804329551801,0.6230690824341718],[100.35178870252209,0.6309879118031904],[100.35629231360264,0.6385015147088778],[100.36151075883018,0.6455375292587178],[100.36739378327755,0.6520281927874465],[100.37388473092459,0.6579109945286342],[100.3809210902431,0.6631292777150927],[100.38843509621175,0.667632785305691],[100.39635438296318,0.6713781440787653],[100.40460268077847,0.6743292824257617],[100.41310055071305,0.6764577778174132],[100.42176614977494,0.6777431305923907],[100.4305160192801,0.67817296142852]]],[[[101.2,0.2],[101.8,0.2],[101.8,0.8],[101.2,0.8],[101.2,0.2]],[[101.3,0.3],[101.3,0.7],[101.7,0.7],[101.7,0.3],[101.3,0.3]]]]},"properties":{"name":"Dinagat Islands"}}]}

As you can see, the bottom left polygon is in the shape of a 'nested donut'.
This is what I have so far (which I have adapted from this answere)

const maskWithPossibleInnerRings = (geometry, cover) => {
  const coords = turf.getCoords(geometry);
  const outerRings = [];
  const innerRings = [];
  coords.forEach(function (polyCoords) {
    polyCoords.forEach(function (linearRing, i) {
      if (i == 0) {
        outerRings.push([linearRing]);
      } else {
        var poly = turf.rewind(turf.polygon([linearRing]));
        innerRings.push(turf.getCoords(poly)[0]);
      }
    });
  });
  const outerRingsPoly = turf.multiPolygon(outerRings);
  const maskedOuterRingsPoly = turf.mask(outerRingsPoly, cover);
  const maskedOuterRings = turf.getCoords(maskedOuterRingsPoly);
  const allRings = [maskedOuterRings];
  innerRings.forEach(innerRing => allRings.push([innerRing]));
  const finalPoly = turf.multiPolygon(allRings);
  console.log('RESULT')
  console.log(JSON.stringify(finalPoly))
  return finalPoly;
};

The result looks like this

result map screenshot

Not quite what I want. I think the problem starts here:

  const outerRingsPoly = turf.multiPolygon(outerRings);

outerRingsPoly looks like this

polygon creation

which is a problem for the next line

const maskedOuterRingsPoly = turf.mask(outerRingsPoly, cover);

maskedOuterRingsPoly looks like this

polygon mask result

So only the largest polygon gets used for mask(). I want the result to look like this though

result screen shot

{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"MultiPolygon","coordinates":[[[[180,90],[-180,90],[-180,-90],[180,-90],[180,90]],[[100,0],[101,0],[101,1],[100,1],[100,0]],[[101.2,0.2],[101.8,0.2],[101.8,0.8],[101.2,0.8],[101.2,0.2]],[[102,2],[103,2],[103,3],[102,3],[102,2]]],[[[100.2,0.2],[100.2,0.8],[100.8,0.8],[100.8,0.2],[100.2,0.2]],[[100.3,0.3],[100.7,0.3],[100.7,0.7],[100.3,0.7],[100.3,0.3]]],[[[100.4305160192801,0.67817296142852],[100.43926588878527,0.6777431305923907],[100.44793148784716,0.6764577778174132],[100.45642935778174,0.6743292824257617],[100.46467765559704,0.6713781440787653],[100.47259694234847,0.667632785305691],[100.48011094831709,0.6631292777150927],[100.48714730763562,0.6579109945286341],[100.49363825528266,0.6520281927874464],[100.49952127973005,0.6455375292587178],[100.50473972495757,0.6385015147088778],[100.50924333603814,0.6309879118031904],[100.5129887430422,0.6230690824341719],[100.51593987860606,0.6148212907677792],[100.51806832514795,0.6063239687221135],[100.51935358839506,0.5976589509544159],[100.51978329459449,0.5889096867249723],[100.51935330951578,0.5801604362283926],[100.51806777810651,0.5714954591315431],[100.51593908442491,0.56299820313176],[100.51298773224117,0.5547505003481902],[100.50924214746165,0.5468317792832317],[100.50473840428187,0.5393182999407914],[100.49951987770788,0.5322824194649319],[100.4936368257929,0.5257918953685365],[100.48714590561279,0.5199092330597317],[100.48010962764015,0.5146910839473947],[100.47259575377038,0.5101876999202546],[100.46467664479428,0.5064424494514895],[100.45642856359898,0.503491399987565],[100.4479309408045,0.5013629706420072],[100.43926560990533,0.5000776585380188],[100.4305160192801,0.49964784143480295],[100.42176642865488,0.5000776585380188],[100.41310109775573,0.5013629706420072],[100.40460347496123,0.503491399987565],[100.39635539376593,0.5064424494514895],[100.38843628478985,0.5101876999202546],[100.38092241092006,0.5146910839473947],[100.37388613294743,0.5199092330597317],[100.3673952127673,0.5257918953685367],[100.36151216085236,0.5322824194649319],[100.35629363427834,0.5393182999407915],[100.35178989109856,0.5468317792832317],[100.34804430631903,0.5547505003481902],[100.34509295413531,0.56299820313176],[100.34296426045371,0.5714954591315431],[100.34167872904445,0.5801604362283926],[100.34124874396572,0.5889096867249723],[100.34167845016515,0.5976589509544159],[100.34296371341229,0.6063239687221135],[100.34509215995416,0.6148212907677792],[100.34804329551801,0.6230690824341718],[100.35178870252209,0.6309879118031904],[100.35629231360264,0.6385015147088778],[100.36151075883018,0.6455375292587178],[100.36739378327755,0.6520281927874465],[100.37388473092459,0.6579109945286342],[100.3809210902431,0.6631292777150927],[100.38843509621175,0.667632785305691],[100.39635438296318,0.6713781440787653],[100.40460268077847,0.6743292824257617],[100.41310055071305,0.6764577778174132],[100.42176614977494,0.6777431305923907],[100.4305160192801,0.67817296142852]]],[[[101.3,0.3],[101.3,0.7],[101.7,0.7],[101.7,0.3],[101.3,0.3]]]]}}]}

How would I go on about masking polygons with a varying deep number of outer- and innerrings when I must do it in GeoJson? What kind of "rule" should I follow?

  1. Find outer most rings (how)?
  2. Apply proposed code (see above) to all most outer rings
  3. Do some winding to all the other outer- and inner rings
  4. Put everything together

Here is a fiddle to play with: http://jsfiddle.net/86x0bj5t/2/

Best Answer

Problem when masking complex multipolygon is with polygons which are inside hole of another polygon. These polygons have to be treated a bit differently:

  • Outer ring of such polygon has to become inner ring (hole) of the enclosing polygon.
  • Inner ring (hole) of such a polygon should be added to multipolygon collection as a regular polygon.

Proof of concept code for the multipolygon from the question could then look something like this:

const outerRings = [];
const innerRings = [];
const outerToInnerLink = [];
const enclosedOuterRings = [];

coords.forEach(function (polyCoords, j) {
  polyCoords.forEach(function (linearRing, i) {
    if (i == 0) {
      outerRings.push([linearRing]);
    } else {
      let poly = turf.polygon([linearRing]);
      if (turf.booleanClockwise(linearRing)) {
        poly = turf.rewind(poly);
      }
      outerToInnerLink[j] = innerRings.length;
      innerRings.push(turf.getCoords(poly));
    }
  });
});

innerRings.forEach(function(innerRing, i) {
  outerRings.forEach(function(outerRing, j) {
    if (turf.booleanContains(turf.polygon(innerRing), turf.polygon(outerRing))) {
      enclosedOuterRings.push([i, j]);
    }
  });
});

function rewindLinearRing(linearRing) {
  let poly = turf.polygon([linearRing]);
  poly = turf.rewind(poly);
  return(turf.getCoords(poly)[0]);
}

const addedRings = [];
enclosedOuterRings.slice().reverse().forEach(function(enclosedOuterRing) {
  const outerRingInd = enclosedOuterRing[1];
  const enclosedRing = outerRings[outerRingInd];
  const enclosingRing = innerRings[enclosedOuterRing[0]];
  enclosingRing.push(rewindLinearRing(enclosedRing[0]));
  outerRings.splice(outerRingInd, 1);
  if (outerToInnerLink[outerRingInd]) {
    addedRings.push([rewindLinearRing(innerRings[outerToInnerLink[outerRingInd]][0])]);
    innerRings.splice(outerToInnerLink[outerRingInd], 1);
  }
});

const outerRingsPoly = turf.multiPolygon(outerRings);
const maskedOuterRingsPoly = turf.mask(outerRingsPoly, null);
const maskedOuterRings = turf.getCoords(maskedOuterRingsPoly);
const allRings = [maskedOuterRings];
allRings.push(...innerRings);
allRings.push(...addedRings);
const finalPoly = turf.multiPolygon(allRings);

This is the result (using Leaflet for display):

enter image description here