mark_rhumbs.js

import { create } from "../container/create";
import { render } from "../container/render";
import { camelcasetodash, unique } from "../helpers/utils";

/**
 * @function rhumbs
 * @description The `rhumbs` function allows to display "rhumb lines" like on old portolan charts
 * @see {@link https://observablehq.com/@neocartocnrs/layout-marks}
 *
 * @property {string} [id] - id of the layer
 * @property {number} [nb = 16] - number of lines
 * @property {array} [pos = [10,10]] - position of the lines. If coords = "svg", pos values are in the svg document. If coords = "geo", pos values ar in latitude and longitude.
 * @property {string} [coords = "svg"] - See pos. If coords == "geo" and zoomable == true, then lines move with the zoom.
 * @property {string} [stroke = "#394a70"] - stroke color.
 * @property {number} [strokeWidth = 1] - stroke-width.
 * @property {number} [strokeOpacity = 0.3] - stroke-opacity.
 * @property {array|number} [strokeDasharray = [3,2]] - stroke-dasharray
 * @property {*} [*] - *other SVG attributes that can be applied (strokeDasharray, strokeWidth, opacity, strokeLinecap...)*
 * @property {*} [svg_*]  - *parameters of the svg container created if the layer is not called inside a container (e.g svg_width)*
 * @example
 * // There are several ways to use this function
 * geoviz.rhumbs(svg, { nb: 36 }) // where svg is the container
 * svg.rhumbs({ nb: 36 }) // where svg is the container
 * svg.plot({ type: "rhumbs", nb: 36 }) // where svg is the container
 * geoviz.rhumbs({ step: 36 }) // no container
 */

export function rhumbs(arg1, arg2) {
  // Test if new container
  let newcontainer =
    (arguments.length <= 1 || arguments[1] == undefined) &&
    !arguments[0]?._groups
      ? true
      : false;
  arg1 = newcontainer && arg1 == undefined ? {} : arg1;
  arg2 = arg2 == undefined ? {} : arg2;

  // Arguments by default
  const options = {
    mark: "rhumbs",
    id: unique(),
    nb: 16,
    pos: [10, 10],
    coords: "svg",
    stroke: "#394a70",
    strokeWidth: 1,
    strokeOpacity: 0.3,
    strokeDasharray: [3, 2],
  };
  let opts = { ...options, ...(newcontainer ? arg1 : arg2) };

  // The container
  let svgopts = {};
  Object.keys(opts)
    .filter((str) => str.slice(0, 4) == "svg_")
    .forEach((d) => {
      Object.assign(svgopts, {
        [d.slice(0, 4) == "svg_" ? d.slice(4) : d]: opts[d],
      });
      delete opts[d];
    });
  let svg = newcontainer ? create(svgopts) : arg1;

  // init a layer
  let layer = svg.selectAll(`#${opts.id}`).empty()
    ? svg.append("g").attr("id", opts.id)
    : svg.select(`#${opts.id}`);
  layer.selectAll("*").remove();

  // Some values
  const angles = getangle(opts.nb);
  const size = Math.max(svg.width, svg.height);
  const pos = opts.coords == "svg" ? opts.pos : svg.projection(opts.pos);

  // Draw
  const rhumbs = layer
    .selectAll("polyline")
    .data(angles)
    .join("polyline")
    .attr("points", function (d, i) {
      let x2 = pos[0] + Math.cos(d) * size;
      let y2 = pos[1] + Math.sin(d) * size;
      return pos[0] + "," + pos[1] + " " + x2 + "," + y2;
    });

  // Attributes
  let entries = Object.entries(opts).map((d) => d[0]);
  entries.forEach((d) => {
    rhumbs.attr(camelcasetodash(d), opts[d]);
  });

  // Zoom
  if (svg.zoomable && !svg.parent) {
    if (!svg.zoomablelayers.map((d) => d.id).includes(opts.id)) {
      svg.zoomablelayers.push(opts);
    } else {
      let i = svg.zoomablelayers.indexOf(
        svg.zoomablelayers.find((d) => d.id == opts.id)
      );
      svg.zoomablelayers[i] = opts;
    }
  }

  // Render
  if (newcontainer) {
    return render(svg);
  } else {
    return `#${opts.id}`;
  }
}

// An helper
function getangle(nb) {
  let angles = [];
  for (let i = 0; i < nb; i++) {
    angles[i] = (360 / nb) * i * (Math.PI / 180);
  }
  return angles;
}