tool_randompoints.js

import bbox from "@turf/bbox";
import area from "@turf/area";
import bboxPolygon from "@turf/bbox-polygon";
import { randomPoint } from "@turf/random";
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
import { sum } from "d3-array";

const d3 = Object.assign({}, { sum });

/**
 * @function tool/randompoints
 * @description
 * Generates random points inside polygons or multipolygons using a dot-density approach.
 * Each point is returned as a valid GeoJSON Feature with `properties` containing
 * `{ geom_id, data, var, id }`.
 *
 * @param {GeoJSON} data - FeatureCollection of polygons/multipolygons
 * @param {string} var - property name to convert into dots
 * @param {number} [dotval] - number of units per dot, default = total / 5000
 * @returns {GeoJSON} FeatureCollection<Point>
 */
export function randompoints({ data, var: varName, dotval } = {}) {
  if (!data || !data.features || !varName) {
    return { type: "FeatureCollection", features: [] };
  }

  const total = d3.sum(data.features, (d) =>
    Number(d.properties?.[varName] || 0),
  );
  dotval = dotval ?? Math.round(total / 5000);

  const points = [];
  let pointCounter = 0;

  data.features.forEach((feature, fIdx) => {
    const geomType = feature.geometry.type;
    const coords = feature.geometry.coordinates;
    const value = Number(feature.properties?.[varName] || 0);
    const totalDots = Math.round(value / dotval);
    const totalArea = area(feature) || 0;
    const geom_id = fIdx;

    // ---------- POLYGON ----------
    if (geomType === "Polygon") {
      const pts = dotsInPoly(feature, totalDots);
      pts.forEach((pt) => {
        points.push({
          type: "Feature",
          geometry: pt.geometry,
          properties: {
            geom_id,
            data: value,
            var: varName,
            id: pointCounter++,
          },
        });
      });
    }

    // ---------- MULTIPOLYGON ----------
    else if (geomType === "MultiPolygon") {
      // calcul surfaces des parties
      const parts = coords.map((c, i) => {
        const geom = { type: "Polygon", coordinates: c };
        const partArea = area({ type: "Feature", geometry: geom }) || 0;
        const ratio = totalArea ? partArea / totalArea : 0;
        return { geom, area: partArea, ratio, part_id: i };
      });

      // allocation des dots proportionnellement
      const exactDots = parts.map((p) => totalDots * p.ratio);
      const baseDots = exactDots.map(Math.floor);

      let remaining = totalDots - d3.sum(baseDots);
      const order = exactDots
        .map((v, i) => ({ i, r: v - baseDots[i] }))
        .sort((a, b) => b.r - a.r);

      for (let k = 0; k < remaining; k++) baseDots[order[k].i]++;

      // génération points pour chaque partie
      parts.forEach((p) => {
        if (baseDots[p.part_id] === 0) return;
        const pts = dotsInPoly(
          { type: "Feature", geometry: p.geom },
          baseDots[p.part_id],
        );
        pts.forEach((pt) => {
          points.push({
            type: "Feature",
            geometry: pt.geometry,
            properties: {
              geom_id: `${geom_id}_${p.part_id}`,
              data: value,
              var: varName,
              id: pointCounter++,
            },
          });
        });
      });
    }
  });

  return { type: "FeatureCollection", dotvalue: dotval, features: points };
}

function dotsInPoly(feature, nbdots) {
  const box = bbox(feature);
  const polyArea = area(feature);
  const bboxArea = area(bboxPolygon(box));
  const efficiency = polyArea / bboxArea;
  const maxIter = Math.ceil((nbdots / efficiency) * 1.2);

  const points = [];
  let attempts = 0;

  while (points.length < nbdots && attempts < maxIter) {
    const pt = randomPoint(1, { bbox: box }).features[0];
    if (booleanPointInPolygon(pt, feature)) points.push(pt);
    attempts++;
  }

  return points;
}