ドルガバで学ぶ turf.js の論理演算機能

年に一度くらい魔が差す時があるので。
[元ネタ](https://twitter.com/KoalaEnglish180/status/1334812800154144768) と異なり、文字の輪郭を忠実にポリゴンとして扱います。つまり「D」は穴あきポリゴンです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8227/119fe7f1-70fc-1723-b14c-b84ddeee817e.png)

```javascript
const polygonD = {
  "type":"Feature",
  "properties":{},
  "geometry":{
    "type":"Polygon",
    "coordinates":[ 省略 ]
  }
};

const polygonG = {
  "type":"Feature",
  "properties":{ },
  "geometry":{
    "type":"Polygon",
    "coordinates":[ 省略 ]
  }
};

const polygonFrame = {
  "type": "Feature",
  "properties": {},
  "geometry": {
    "type": "Polygon",
    "coordinates": [ 省略 ]
  }
};

const map = new mapboxgl.Map({
  container: 'map',
  style: {
    version: 8,
    sources: {
      OSM: {
        type: "raster",
        tiles: [ "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png" ],
        tileSize: 256,
        attribution:
        "OpenStreetMap",
      },
      sources: {
        'type': 'geojson',
        'data': {
          "type": "FeatureCollection",
          "features": []    
        }
      },
      results: {
        'type': 'geojson',
        'data': {
          "type": "FeatureCollection",
          "features": []    
        }
      },
    },
    layers: [
      {
        id: "BASEMAP",
        type: "raster",
        source: "OSM",
        minzoom: 0,
        maxzoom: 18,
      },
      {
        id: 'sources',
        type: 'line',
        source: 'sources',
        paint: {
          'line-color': '#088',
          'line-width': 5,
          'line-opacity': 0.8,
        }
      },
      {
        id: 'results',
        type: 'fill',
        source: 'results',
        paint: {
          'fill-color': 'red',
          'fill-opacity': 1,
        }
      }      
    ],
  },      
});

map.once('load', () => {

  const sourceFeatures = [];

  // 元図形
  sourceFeatures.push({
    d: polygonD,
    g: polygonG,
    frame: polygonFrame,
  });

  // 上左
  sourceFeatures.push({
    d: turf.transformTranslate(polygonD, 70, 90, { units: 'kilometers' }),
    g: turf.transformTranslate(polygonG, 70, 90, { units: 'kilometers' }),
    frame: turf.transformTranslate(polygonFrame, 70, 90, { units: 'kilometers' }),
  });

  // 上中
  sourceFeatures.push({
    d: turf.transformTranslate(polygonD, 140, 90, { units: 'kilometers' }),
    g: turf.transformTranslate(polygonG, 140, 90, { units: 'kilometers' }),
    frame: turf.transformTranslate(polygonFrame, 140, 90, { units: 'kilometers' }),
  });

  // 上右
  sourceFeatures.push({
    d: turf.transformTranslate(polygonD, 210, 90, { units: 'kilometers' }),
    g: turf.transformTranslate(polygonG, 210, 90, { units: 'kilometers' }),
    frame: turf.transformTranslate(polygonFrame, 210, 90, { units: 'kilometers' }),
  });

  // 下左
  sourceFeatures.push({
    d: turf.transformTranslate(sourceFeatures[1].d, 60, 180, { units: 'kilometers' }),
    g: turf.transformTranslate(sourceFeatures[1].g, 60, 180, { units: 'kilometers' }),
    frame: turf.transformTranslate(sourceFeatures[1].frame, 60, 180, { units: 'kilometers' }),
  });

  // 下中
  sourceFeatures.push({
    d: turf.transformTranslate(sourceFeatures[2].d, 60, 180, { units: 'kilometers' }),
    g: turf.transformTranslate(sourceFeatures[2].g, 60, 180, { units: 'kilometers' }),
    frame: turf.transformTranslate(sourceFeatures[2].frame, 60, 180, { units: 'kilometers' }),
  });

  // 下右
  sourceFeatures.push({
    d: turf.transformTranslate(sourceFeatures[3].d, 60, 180, { units: 'kilometers' }),
    g: turf.transformTranslate(sourceFeatures[3].g, 60, 180, { units: 'kilometers' }),
    frame: turf.transformTranslate(sourceFeatures[3].frame, 60, 180, { units: 'kilometers' }),
  });

  sourceFeatureCollection = turf.featureCollection(sourceFeatures.reduce((acc, x) => {
    acc.push(x.d);
    acc.push(x.g);
    acc.push(x.frame);
    return acc;
  }, []));
  map.getSource('sources').setData(sourceFeatureCollection);


  // D AND G -> intersect
  const DandG = turf.intersect(sourceFeatures[1].d, sourceFeatures[1].g);

  // D OR G -> union
  const DorG = turf.union(sourceFeatures[2].d, sourceFeatures[2].g);

  // D XOR G -> (D - G) + (G - D)
  const DminusG = turf.difference(sourceFeatures[3].d, sourceFeatures[3].g);
  const GminusD = turf.difference(sourceFeatures[3].g, sourceFeatures[3].d);
  const DxorG = turf.union(DminusG, GminusD);

  // D NAND G -> frame - DandG
  const DnandG = turf.difference(sourceFeatures[4].frame, turf.intersect(sourceFeatures[4].d, sourceFeatures[4].g));

  // D NOR G -> frame - DorG
  const DnorG = turf.difference(sourceFeatures[5].frame, turf.union(sourceFeatures[5].d, sourceFeatures[5].g));

  // D NXOR G -> frame - DxorG
  const tmpDminusG = turf.difference(sourceFeatures[6].d, sourceFeatures[6].g);
  const tmpGminusD = turf.difference(sourceFeatures[6].g, sourceFeatures[6].d);
  const DnxorG = turf.difference(sourceFeatures[6].frame, turf.union(tmpDminusG, tmpGminusD));

  map.getSource('results').setData(turf.featureCollection([
    DandG, 
    DorG,
    DxorG,
    DnandG,
    DnorG,
    DnxorG,
  ]));

  map.fitBounds(turf.bbox(sourceFeatureCollection), { padding: { bottom: 200, top: 200, right: 200, left: 200 }});
});
```

また例によってだらだら長いコードになってしまったけど、論理演算を行っているのは、最下部のあたり。
turf.js の論理演算機能は、``intersect``, ``union``, ''difference'' のみです。

``intersect`` は、2つの図形が共に重なり合う領域、つまり AND です。
``union`` は、2つの図形のいずれかの領域、つまり OR。
``difference`` は差分、つまり第一引数の図形から、第二引数の図形の領域を引いた領域です。

この3つだけでも、組み合わせるとそれなりの事はできますねーという例でした。

## 完全なコード

- [Intersect/union/difference features - JSFiddle - Code Playground](https://jsfiddle.net/amay077/45sz06ub/)

## 参考

- https://turfjs.org/docs/#intersect
- https://turfjs.org/docs/#union
- https://turfjs.org/docs/#difference