Public
Edited
Nov 14, 2023
8 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const container = html`<div style="height:500px;">`;
yield container;
mapboxgl.accessToken =
"pk.eyJ1IjoidG9mZmkiLCJhIjoiY2lvMDBxMzR3MDB1eHZza2x4NGI2YjI5OSJ9.IEaNA05pWbT92nOu-lEOYw";

let initialPolygon = viewof selectedCountry.value;
if (customGeojsonString) {
initialPolygon = JSON.parse(customGeojsonString);
} else {
initialPolygon = countries.features.find(
({ properties: { name } }) => name === initialPolygon
);
}
const map = new mapboxgl.Map({
container: container,
style: "mapbox://styles/toffi/clnx5216j006b01qm3ijohntw",
center: turf.center(initialPolygon).geometry.coordinates,
zoom: 10
});

map.fitBounds(turf.bbox(initialPolygon), {
padding: 20,
duration: 0
});
const options = {
displayControlsDefault: false,
controls: {
polygon: true,
trash: true
},
userProperties: true
};

const Draw = new MapboxDraw(options);
map.addControl(Draw, "top-left");

let fc = turf.featureCollection([]);

function calcLabelPositions(polygon) {
// make sure polygon is a feature
if (polygon.type === "FeatureCollection") polygon = polygon.features[0];
// Turf.js
// Centroid
const startCentroid = performance.now();
const centroid = turf.centroid(polygon);
centroid.properties = {
type: "Centroid",
name: polygon.properties?.name || "Unnamed",
ID: polygon.ID
};
const endCentroid = performance.now();
mutable times.Centroid += ~~(endCentroid - startCentroid);

// Point on surface
const startPos = performance.now();
const pos = turf.pointOnSurface(polygon);
pos.properties = {
type: "Point on surface (POS)",
name: polygon.properties?.name || "Unnamed",
ID: polygon.ID
};
const endPos = performance.now();
mutable times.POS += ~~(endPos - startPos);

// Polylabel
const startPolylabel = performance.now();
const polylabelPos = findPolylabel(
polygon,
viewof polylabelPrecision.value
);
const polylab = turf.point(polylabelPos);
polylab.properties = {
type: "Polylabel",
name: polygon.properties?.name || "Unnamed",
ID: polygon.ID
};
const endPolylabel = performance.now();
mutable times.Polylabel += ~~(endPolylabel - startPolylabel);

// GEOS
// Maximum inscribed circle
const startMic = performance.now();
const geomPtr = geojsonToGeosGeom(polygon);
const micPtr = geos.GEOSMaximumInscribedCircle(
geomPtr,
viewof micTolerance.value
);
// the mic is a GEOSGeom linestring with two points,
// the first point is the center of the mic, the second
// point is the distance (tolerance) of the mic
const centerPtr = geos.GEOSGeomGetPointN(micPtr, 0);
const mic = turf.feature(geosGeomToGeojson(centerPtr));
mic.properties = {
type: "Maximum inscribed circle (MIC)",
name: polygon.properties?.name || "Unnamed",
ID: polygon.ID
};
// free memory
geos.GEOSFree(centerPtr);
geos.GEOSFree(micPtr);
geos.GEOSFree(geomPtr);
const endMic = performance.now();
mutable times.MIC += ~~(endMic - startMic);

fc.features.push(centroid, pos, mic, polylab);
}

viewof selectedCountry.addEventListener("change", () => {
const newSelectedCountryName = viewof selectedCountry.value;
const newSelectedCountryBoundary = countries.features.find(
({ properties: { name } }) => name === newSelectedCountryName
);
map.fitBounds(turf.bbox(newSelectedCountryBoundary), {
padding: 20
});
});

function updateLabels() {
// reset performance times
mutable times = {
Centroid: 0,
POS: 0,
Polylabel: 0,
MIC: 0
};
// reset fc
fc = turf.featureCollection([]);
countries.features.forEach((country) => {
calcLabelPositions(country);
});
console.log(mutable times);
map.getSource("label-points").setData(fc);
}
// create a minimal debounced update labels function, since Observable sliders
// fire change events constantly while sliding
const debouncedUpdateLabels = _.debounce(updateLabels, 10);

viewof micTolerance.addEventListener("change", debouncedUpdateLabels);
viewof polylabelPrecision.addEventListener("change", debouncedUpdateLabels);

map.on("load", function () {
if (customGeojsonString) {
const ID = Draw.add(initialPolygon);
initialPolygon.ID = ID[0];
calcLabelPositions(initialPolygon);
}
countries.features.forEach((country) => {
const ID = Draw.add(country);
country.ID = ID[0];
calcLabelPositions(country);
});
console.log(mutable times);

map.on("draw.create", (e) => {
// create labels for new poly, mutates fc
calcLabelPositions(e.features[0]);
map.getSource("label-points").setData(fc);
});

map.on("draw.delete", (e) => {
console.log(e.features[0], fc);
fc.features = fc.features.filter(
({ properties: { ID, id } }) =>
ID !== e.features[0].ID &&
ID !== e.features[0].id &&
id !== e.features[0].id
);
map.getSource("label-points").setData(fc);
});

map.addSource("label-points", {
type: "geojson",
data: fc
});

map.addLayer({
id: "label-position",
type: "circle",
source: "label-points",
paint: {
// Make circles larger as the user zooms from z12 to z22.
"circle-radius": 5,
// Color circles by ethnicity, using a `match` expression.
"circle-color": [
"match",
["get", "type"],
"Centroid",
"#fbb03b",
"Maximum inscribed circle (MIC)",
"#223b53",
"Polylabel",
"#e55e5e",
"Point on surface (POS)",
"#21a373",
/* other */ "#ccc"
]
}
});

map.addLayer({
id: "label-points-labels",
type: "symbol",
source: "label-points",
layout: {
"text-field": ["get", "name"],
"text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
"text-size": 18
},
paint: {
"text-color": "#fff",
"text-halo-width": 1,
"text-halo-color": "grey"
},
...(viewof radios.value !== "All"
? { filter: ["==", "type", viewof radios.value] }
: {})
});
});

function changeLabelPos() {
const curValue = viewof radios.value;
if (curValue === "All") {
map.setFilter("label-points-labels", null);
} else {
map.setFilter("label-points-labels", ["==", "type", curValue]);
}
}
viewof radios.addEventListener("change", changeLabelPos);

invalidation.then(() => {
map.remove();
viewof micTolerance.removeEventListener("change", debouncedUpdateLabels);
viewof polylabelPrecision.removeEventListener(
"change",
debouncedUpdateLabels
);
viewof radios.removeEventListener("change", changeLabelPos);
});
}
Insert cell
bermuda = countries.features.find(
({ properties: { name } }) => name === "Bermuda"
)
Insert cell
countries = FileAttachment("world.geojson").json()
Insert cell
Insert cell
function findPolylabel(feature, precision) {
if (feature.type === "FeatureCollection") feature = feature.features[0];
let output = [];
if (feature.geometry.type === "Polygon") {
output = polylabel(feature.geometry.coordinates, precision);
} else {
let maxArea = 0,
maxPolygon = [];
for (let i = 0, l = feature.geometry.coordinates.length; i < l; i++) {
const p = feature.geometry.coordinates[i];
const area = d3.geoArea({ type: "Polygon", coordinates: p });
if (area > maxArea) {
maxPolygon = p;
maxArea = area;
}
}
output = polylabel(maxPolygon);
}
return output;
}
Insert cell
mutable times = ({
Centroid: 0,
POS: 0,
Polylabel: 0,
MIC: 0
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import { Swatches } from "@d3/color-legend"
Insert cell
polylabel = require("https://bundle.run/polylabel@1.1.0")
Insert cell
geos = {
const initGeosJs = (
await import("https://cdn.skypack.dev/geos-wasm@1.1.6?min")
).default;
const geos = await initGeosJs();
return geos;
}
Insert cell
turf = require("@turf/turf")
Insert cell
MapboxDraw = {
const MapboxDraw = await require("@mapbox/mapbox-gl-draw");
{
const href = await require.resolve(
"@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css"
);
document.head.appendChild(html`<link href=${href} rel=stylesheet>`);
}
return MapboxDraw;
}
Insert cell
mapboxgl = {
const gl = await require("mapbox-gl@2.15.0");
if (!gl.accessToken) {
// token allowed only this Observable
gl.accessToken =
"pk.eyJ1IjoidG9mZmkiLCJhIjoiY2lvMDBxMzR3MDB1eHZza2x4NGI2YjI5OSJ9.IEaNA05pWbT92nOu-lEOYw";
const href = await require.resolve("mapbox-gl@2.15.0/dist/mapbox-gl.css");
document.head.appendChild(html`<link href=${href} rel=stylesheet>`);
}
return gl;
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more