Source: rewind.js

import { geoContains, geoArea, geoStream, geoTransform } from "d3-geo";
import { check } from "./helpers/check.js";

/**
 * @function rewind
 * @summary Rewind a geoJSON (fil recipe). The function allows to rewind the winding order of a GeoJSON object. The winding order of a polygon is the order in which the vertices are visited by the path that defines the polygon. The winding order of a polygon is significant because it determines the interior of the polygon. The winding order of a polygon is typically either clockwise or counterclockwise.
 * @description Based on https://observablehq.com/@fil/rewind
 * @param {object|array} data - A GeoJSON FeatureCollection, an array of features, an array of geometries, a single feature or a single geometry.
 * @param {object} options - Optional parameters
 * @param {number} [options.simple = true] - Rewind simple polygons larger than a hemisphere
 * @returns {object|array} - A GeoJSON FeatureCollection, an array of features, an array of geometries, a single feature or a single geometry (it depends on what you've set as `data`)
 * @example
 * geotoolbox.rewind(*a geojson*)
 */
export function rewind(data, { simple = true } = {}) {
  const handle = check(data);
  let x = handle.import(data);

  let result = x?.stream
    ? geoRewindProjection(x, simple)
    : x?.type
    ? geoRewindFeature(x, simple)
    : Array.isArray(x)
    ? Array.from(x, (d) => rewind(d, simple))
    : x;

  return handle.export(result);
}

const geoRewindFeature = (feature, simple) =>
  geoProjectSimple(feature, geoRewindStream(simple));

function geoRewindStream(simple = true) {
  let ring, polygon;
  return geoTransform({
    polygonStart() {
      this.stream.polygonStart();
      polygon = [];
    },
    lineStart() {
      if (polygon) polygon.push((ring = []));
      else this.stream.lineStart();
    },
    lineEnd() {
      if (!polygon) this.stream.lineEnd();
    },
    point(x, y) {
      if (polygon) ring.push([x, y]);
      else this.stream.point(x, y);
    },
    polygonEnd() {
      for (let [i, ring] of polygon.entries()) {
        ring.push(ring[0].slice());
        if (
          i
            ? // a hole must contain the first point of the polygon
              !geoContains(
                { type: "Polygon", coordinates: [ring] },
                polygon[0][0]
              )
            : polygon[1]
            ? // the outer ring must contain the first point of its first hole (if any)
              !geoContains(
                { type: "Polygon", coordinates: [ring] },
                polygon[1][0]
              )
            : // a single ring polygon must be smaller than a hemisphere (optional)
              simple &&
              geoArea({ type: "Polygon", coordinates: [ring] }) > 2 * Math.PI
        ) {
          ring.reverse();
        }

        this.stream.lineStart();
        ring.pop();
        for (const [x, y] of ring) this.stream.point(x, y);
        this.stream.lineEnd();
      }
      this.stream.polygonEnd();
      polygon = null;
    },
  });
}

const geoProjectSimple = function (object, projection) {
  const stream = projection.stream;
  let project;
  if (!stream) throw new Error("invalid projection");
  switch (object && object.type) {
    case "Feature":
      project = projectFeature;
      break;
    case "FeatureCollection":
      project = projectFeatureCollection;
      break;
    default:
      project = projectGeometry;
      break;
  }
  return project(object, stream);
};

function projectFeatureCollection(o, stream) {
  return { ...o, features: o.features.map((f) => projectFeature(f, stream)) };
}

function projectFeature(o, stream) {
  return { ...o, geometry: projectGeometry(o.geometry, stream) };
}

function projectGeometryCollection(o, stream) {
  return {
    ...o,
    geometries: o.geometries.map((o) => projectGeometry(o, stream)),
  };
}

function projectGeometry(o, stream) {
  return !o
    ? null
    : o.type === "GeometryCollection"
    ? projectGeometryCollection(o, stream)
    : o.type === "Polygon" || o.type === "MultiPolygon"
    ? projectPolygons(o, stream)
    : o;
}

function projectPolygons(o, stream) {
  let coordinates = [];
  let polygon, line;
  geoStream(
    o,
    stream({
      polygonStart() {
        coordinates.push((polygon = []));
      },
      polygonEnd() {},
      lineStart() {
        polygon.push((line = []));
      },
      lineEnd() {
        line.push(line[0].slice());
      },
      point(x, y) {
        line.push([x, y]);
      },
    })
  );
  if (o.type === "Polygon") coordinates = coordinates[0];
  return { ...o, coordinates };
}