// Imports
import { range, max } from "d3-array";
import { Delaunay } from "d3-delaunay";
import { geoProject } from "d3-geo-projection";
const d3 = Object.assign({}, { range, max, Delaunay, geoProject });
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
/**
* @function tool/dotstogrid
* @description `dotstogrid` is a function to create a regular grid in the SVG plan count the number of dots inside
* @see {@link https://observablehq.com/@neocartocnrs/bees}
* @property {object} svg - A geoviz SVG container
* @property {string} [options.type = "hex"] - type of grid ("hex", "square", "triangle","random")
* @property {number} [options.step = 50] - grid resolution (in pixels)
* @property {geoJSON} [options.data = undefined] - dots to count in the grid
* @property {string} [options.var = undefined] - field to sum
* @example
* geoviz.tool.dotstogrid(svg, {type:"triangle", step:30, data: dots, var: "mayvar"})
*/
export function dotstogrid(
svg,
options = {
data,
type,
step,
var: undefined,
}
) {
options.type = options.type ? options.type : "hex";
options.step = options.step ? options.step : 50;
let grid;
switch (options.type) {
case "hex":
case "hexbin":
grid = hexbin(options.step, svg.width, svg.height);
break;
case "square":
grid = square(options.step, svg.width, svg.height);
break;
case "triangle":
grid = triangle(options.step, svg.width, svg.height);
break;
case "random":
grid = random(options.step, svg.width, svg.height);
break;
default:
grid = hexbin(options.step, svg.width, svg.height);
}
if (options.data !== undefined) {
// TODO improve this for faster calculation
let data = d3.geoProject(options.data, svg.projection);
const features = data.features.filter(
(d) => d.geometry?.coordinates != undefined
);
let count = 0;
let sum = 0;
let result = [];
grid.forEach((g) => {
features.forEach((d) => {
if (booleanPointInPolygon(d, g)) {
count += 1;
sum += d.properties[options.var];
}
});
if (count > 0) {
let prop = options.var
? { id: g.properties.index, count, sum }
: { id: g.properties.index, count };
result.push({
type: "Feature",
geometry: g.geometry,
properties: prop,
});
}
count = 0;
});
return { type: "FeatureCollection", features: result };
} else {
return { type: "FeatureCollection", features: grid };
}
}
// Squares
function square(step, width, height) {
// build grid
let y = d3.range(0 + step / 2, height, step).reverse();
let x = d3.range(0 + step / 2, width, step);
let grid = x.map((x, i) => y.map((y) => [x, y])).flat();
let s = step / 2;
// build object
let result = grid.map((d, i) => {
return {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [
[
[d[0] - s, d[1] + s],
[d[0] + s, d[1] + s],
[d[0] + s, d[1] - s],
[d[0] - s, d[1] - s],
[d[0] - s, d[1] + s],
],
],
},
properties: {
index: i,
},
};
});
return result;
}
// Hexagons
function hexbin(step, width, height) {
let w = step;
let size = w / Math.sqrt(3);
let h = 2 * size * (3 / 4);
// build grid
let y = d3.range(0, height + size, h).reverse();
if (y.length % 2) {
y.unshift(d3.max(y) + h);
}
let x = d3.range(0, width + size, w);
let grid = x.map((x, i) => y.map((y) => [x, y])).flat();
grid = grid.map((d, i) => {
return i % 2 == 1 ? [d[0] + w / 2, d[1]] : d;
});
let s = step / 2;
// build object
let result = grid.map((d, i) => {
let hex = [];
for (let i = 0; i < 6; i++) {
let ang = (Math.PI / 180) * (60 * i - 30);
hex.push([d[0] + size * Math.cos(ang), d[1] + size * Math.sin(ang)]);
}
return {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [[hex[0], hex[1], hex[2], hex[3], hex[4], hex[5], hex[0]]],
},
properties: {
index: i,
},
};
});
return result;
}
// Triangles
function triangle(step, width, height) {
let triangletop = (p, size) => {
let h = (Math.sqrt(3) / 2) * size;
let p1 = [p[0] + size / 2, p[1]];
let p2 = [p[0], p[1] - h];
let p3 = [p[0] - size / 2, p[1]];
return [p1, p2, p3, p1];
};
let trianglebottom = (p, size) => {
let h = (Math.sqrt(3) / 2) * size;
let p1 = [p[0] + size / 2, p[1]];
let p2 = [p[0], p[1] + h];
let p3 = [p[0] - size / 2, p[1]];
return [p1, p2, p3, p1];
};
let size = step / Math.sqrt(3);
let h = (Math.sqrt(3) / 2) * step;
// build grid
let y = d3.range(0, height + size, h).reverse();
if (y.length % 2) {
y.unshift(d3.max(y) + h);
}
let x = d3.range(0, width + size, step);
let grid = x.map((x, i) => y.map((y) => [x, y])).flat();
grid = grid.map((d, i) => {
return i % 2 == 1 ? [d[0] + step / 2, d[1]] : d;
});
let nb = grid.length;
grid = grid.concat(grid);
// build object
let result = grid.map((d, i) => {
return {
type: "Feature",
geometry: {
type: "Polygon",
coordinates:
i < nb ? [triangletop(d, step)] : [trianglebottom(d, step)],
},
properties: {
index: i,
},
};
});
return result;
}
// Random
function random(step, width, height) {
let grid = [];
let nb = Math.round((width / step) * (height / step));
for (let i = 0; i < nb; i++) {
grid.push([Math.random() * width, Math.random() * height]);
}
let voronoi = d3.Delaunay.from(
grid,
(d) => d[0],
(d) => d[1]
).voronoi([0, 0, width, height]);
// build object
let result = grid.map((d, i) => {
return {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [voronoi.cellPolygon(i)],
},
properties: {
index: i,
},
};
});
return result;
}