Public
Edited
Jun 2, 2023
Insert cell
Insert cell
Insert cell
Insert cell
mapSpec
Insert cell
Insert cell
worldMapImage.img
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
pixelTopology.arcs.flat().length
Insert cell
function populateMeta(topology) {
const geoPath = d3.geoPath(d3.geoIdentity());

const objectGeometries = Object.values(topology.objects).flatMap(gc => gc.geometries);
for (let geometry of objectGeometries) {
geometry.properties ??= {};
const feature = topojson.feature(topology, geometry);
geometry.properties.bbox = geoPath.bounds(feature).flat();
geometry.properties.area = Math.abs(d3.polygonArea(feature.geometry.coordinates[0]));
}
for (let [key, geometries] of Object.entries(_.groupBy(objectGeometries, g => g.properties.key))) {
geometries.forEach((g,i) => g.properties.id = (g.length == 1) ? key : `${key}-${i}`);
}

if (!topology.borders) return topology;

const borderBounds = new Map();
const borderGeometries = Object.values(topology.borders).flatMap(gc => gc.geometries);
for (let geometry of borderGeometries) {
geometry.properties ??= {};
const key = geometry.properties.key;
let bbox = borderBounds.get(key);
if (!bbox) {
const feature = topojson.feature(topology, geometry);
bbox = geoPath.bounds(feature).flat();
if (key !== undefined) borderBounds.set(key, bbox);
}
Object.assign(geometry.properties, { bbox, id: geometry.properties.key })
}
return topology;
}
Insert cell
Insert cell
pixelMapData.getGeoJson("land")
Insert cell
Insert cell
async function flattenTopology(topology) {
for (let [layerName, gc] of Object.entries(topology.objects)) {
const flat = topology.objects[layerName] = {
type: "GeometryCollection",
geometries: gc.geometries.flatMap(g => getChildrenFromMultiTopoJson(g))
};
debugger;
flat.geometries.forEach((g,i) => g.properties.id = `${layerName}-${i}`);
await pauseIfNeeded();
}
return topology;
}
Insert cell
pixelTopology
Insert cell
buildMapData(pixelTopology)
Insert cell
buildMapData(pixelTopology).getGeoJson("states", 2)
Insert cell
Insert cell
Insert cell
Insert cell
getChildrenFromMultiGeoJson
Insert cell
{
const topology = extractBorderTopology(pixelMapData.topology);
const fc = topojson.feature(topology, topology.objects.states);
return getChildrenFromMultiGeoJson(fc.features[0])
}

Insert cell
borders.land.geometries.filter(g => g.properties.isOutline)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
buildTopology(8)
Insert cell
async function buildTopology(downscaleFactor, cancellationToken) {
cancellationToken ??= getCancellationToken();
invalidation.then(() => cancellationToken.cancel = true);

const polygons = await tracePolygons(worldMapImage, colorTable, downscaleFactor, cancellationToken);
if (cancellationToken.cancel) return;
let topology = await buildTopologyFromPolygons(polygons);
if (cancellationToken.cancel) return;
await simplifyArcs(topology);
if (cancellationToken.cancel) return;
await smoothTopology(topology);
if (cancellationToken.cancel) return;
await upscaleTopology(topology, downscaleFactor);
if (cancellationToken.cancel) return;

if (topologyOptions.extend) {
await extendTopology(topology);
if (cancellationToken.cancel) return;
}
await splitTopology(topology);
if (cancellationToken.cancel) return;
await simplifyArcs(topology);
if (cancellationToken.cancel) return;
if (topologyOptions.round) {
const allCoords = new Set(topology.arcs.flat());
allCoords.forEach(c => { c[0] *= 4; c[1] *= 4; c[2] *= 4 });
topology.bbox = topology.bbox.map(c => c * 4);
}
topology = topojson.presimplify(topology);
if (cancellationToken.cancel) return;

if (topologyOptions.includeBorders) {
topology.borders = await extractBorderTopology(topology);
if (cancellationToken.cancel) return;
}

await populateMeta(topology);
if (cancellationToken.cancel) return;

topology.properties = { ...topologyOptions, generated: +new Date() }

return topology;
}
Insert cell
buildTopology(8)
Insert cell
Insert cell
Insert cell
Insert cell
async function splitTopology(topology) {
const tracedGeometries = topology.objects.traced.geometries;
for (let [layerName, { prop }] of Object.entries(mapSpec.layers)) {
topology.objects[layerName] = {
type: "GeometryCollection",
geometries: []
};
const layerGeometries = topology.objects[layerName].geometries;
const mergeGroups = _.groupBy(tracedGeometries, g => g.properties[prop]);

for (let [key, tracedObjects] of Object.entries(mergeGroups)) {
const tracedColorNum = tracedObjects[0].properties.colorNum;
const key = mapSpec.provinceProps.get(tracedColorNum)[prop];
if (key !== undefined) {
const mergedObject = topojson.mergeArcs(topology, tracedObjects);
mergedObject.properties = { layer: layerName, key };
if (topologyOptions.flatten) {
layerGeometries.push(...getChildrenFromMultiTopoJson(mergedObject));
}
else {
layerGeometries.push(mergedObject);
}
}
}
await pauseIfNeeded();
}

topology = topojson.presimplify(topology);
delete topology.objects.traced;
return topology;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function buildArcSegments(topology) {
const { COORD, SEGMENT, reverseDirections } = segmentConstants;
const arcSegments = topology.arcSegments = [];
function getArcCoords(arcIndex) {
let { arcIndex: positiveArcIndex, arc, isReversed } = topology.getArc(arcIndex);
if (isReversed) arc = arc.toReversed();
return { arcIndex: positiveArcIndex, arc };
}
function buildRingSegments(ringArcIndices) {
for (let i = 0; i < ringArcIndices.length; i++) {
const arcIndex = ringArcIndices[i];
const { arcIndex: positiveArcIndex, arc } = getArcCoords(arcIndex);
if (arcSegments[positiveArcIndex]) continue;
const { arc: prevArc } = getArcCoords(ringArcIndices[i-1] ?? ringArcIndices.at(-1));
const prevCoord = prevArc.at(-2);
const { arc: nextArc } = getArcCoords(ringArcIndices[i+1] ?? ringArcIndices[0]);
const nextCoord = nextArc[1];

arcSegments[arcIndex] = buildArcSegments(arc, prevCoord, nextCoord);
}
}
function buildArcSegments(arc, prevCoord, nextCoord) {
const segments = []
for (let coord of [...arc, nextCoord]) {
let segment = buildSegment(prevCoord, coord);
prevCoord = coord;
if (segment && segment.length > 0) segments.push(segment);
}
for (let i = 0; i < segments.length; i++) {
segments[i].prev = segments[i-1];
segments[i].next = segments[i+1];
}
segments.shift();
segments.pop();
return segments;
}

function buildSegment(fromCoord, toCoord) {
if (!fromCoord || !toCoord) return;
const axis = (fromCoord[0] != toCoord[0]) ? 0 : 1;
const delta = toCoord[axis] - fromCoord[axis];
const sign = Math.sign(delta);
const length = Math.abs(delta);
let direction;
if (axis == 0) direction = (sign == 1) ? "e" : "w";
if (axis == 1) direction = (sign == 1) ? "s" : "n";
return { fromCoord, toCoord, length, sign, axis, direction };
}

for (let geometry of topology.objects.traced.geometries) {
const arcs = (geometry.type == "Polygon") ? [geometry.arcs] : geometry.arcs;
for (let ring of arcs) {
for (let ringArcIndices of ring) {
buildRingSegments(ringArcIndices);
}
}
}
return topology;
}
Insert cell
Insert cell
Insert cell
pointsEqual = topojsonHashUtils.pointsEqual;
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
async function buildTopologyFromPolygons(polygons) {
const geometries = [];
for (let [colorNum, ringCoords] of polygons) {
let jstsPolygon = buildJstsPolygon(ringCoords);
jstsPolygon.setUserData({ colorNum });
geometries.push(jstsPolygon);
}
await pauseIfNeeded();
const features = geometries.map(g => {
const tracedColorNum = g.getUserData().colorNum;
const properties = colorSwap.traceColorProps.get(tracedColorNum);
return {
type: "Feature",
geometry: jstsWriter.write(g),
properties
}
});
await pauseIfNeeded();
const tracedGeoJSON = {
type: "FeatureCollection",
features
};
return topojson.topology({ traced: tracedGeoJSON });
}

/*
async function buildTopologyFromPolygons(polygons) {
const features = [];
for (let [tracedColorNum, ringCoords] of polygons) {
const turfPolygon = buildTurfPolygon(ringCoords);
turfPolygon.properties = colorSwap.traceColorProps.get(tracedColorNum);
features.push(turfPolygon);
await pauseIfNeeded();
}
const tracedGeoJSON = {
type: "FeatureCollection",
features
};
return topojson.topology({ traced: tracedGeoJSON });
}
*/
Insert cell
Insert cell
Insert cell
{
var multiPoly = turf.polygon([[[0,0],[0,10],[10,10],[10,0],[0,0]]]);
turf.rewind(multiPoly, { mutate: true, reverse: true });
return turf.booleanClockwise(multiPoly.geometry.coordinates[0]);
turf.rewind(multiPoly, { mutate: true });
turf.rewind(multiPoly, { mutate: true });
return multiPoly;
// return turf.polygonToLine(multiPoly);
// return getCleanRings([[0,0],[0,10],[10,10],[10,0],[0,0]]);
}

Insert cell
getCleanRings([[0,0],[0,10],[10,10],[10,0],[0,0]]);
Insert cell
{
return getCleanRings([[0, 0], [2, 0], [1, 1], [0, 2], [2, 2], [1, 1], [0, 0]]);
}
Insert cell
buildTurfPolygon([[[0,0],[0,10],[10,10],[10,0],[0,0]].toReversed()]);
Insert cell
{
var poly = turf.polygon([[[125, -30], [145, -30], [145, -20], [125, -20], [125, -30]], [[0,0],[0,10],[10,10],[10,0],[0,0]]]);
var line = turf.polygonToLine(poly);
return line;
}
Insert cell
{
const coords = [[0, 0], [2, 0], [1, 1], [0, 2], [2, 2], [1, 1]].toReversed();
const combos = [];
for (let i = 0; i < coords.length; i++) {
const left = coords.slice(i, coords.length);
const right = coords.slice(0, i);
const path = [...left, ...right, left[0]];
const ls = turf.lineString(path);
combos.push({path, ls, cw: turf.booleanClockwise(ls)});
}
return combos;
// splitOnDupCoords([[0, 0], [2, 0], [1, 1], [0, 2], [2, 2], [1, 1], [0, 0]])
}
Insert cell
buildTurfPolygon([[[0, 0], [2, 0], [1, 1], [2, 2], [0, 2], [1, 1], [0, 0]]])
Insert cell
function splitRing(ring) {
const splitpoints = createPointHashSet(ring.length);
{
const allPoints = createPointHashSet(ring.length);
for (let coord of ring.slice(0, -1)) {
if (allPoints.has(coord)) splitpoints.add(coord);
allPoints.add(coord);
}
}
if (splitpoints.values().length == 0) return [ring];

splitpoints.add(ring[0]);

let segment = [];
const segments = [segment];
const ringSegments = [];
const lastIndex = ring.length - 1;
for (let i = 0; i < ring.length; i++) {
const coord = ring[i];
segment.push(coord);
if (splitpoints.has(coord)) {
if (segment.length > 1) {
const ringStartIdx = segments.findLastIndex(s => pointsEqual(s[0], coord));
if (ringStartIdx >= 0) ringSegments.push(segments.splice(ringStartIdx));
}
if (i == 0 || i == lastIndex) continue;
segment = [coord];
segments.push(segment);
}
}
return ringSegments.map(rs => [rs[0][0], ...rs.flatMap(s => s.slice(1))]);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
var line = turf.lineString([[0, 0], [2, 0], [1, 1], [2, 2], [0, 2], [1, 1], [0, 0]].toReversed());
var polygon = turf.polygon(line);
return turf.booleanClockwise(line);
return polygon;
}
Insert cell
{
const sourceCoords = [[0, 0], [2, 0], [1, 1], [2, 2], [0, 2], [1, 1], [0, 0]];
const sourceRing = turf.lineString(sourceCoords);
const sourceRingReversed = turf.lineString(sourceCoords.toReversed());
// return [ splitRing(sourceCoords), splitRing(sourceCoords.toReversed()) ];
return turf.lineToPolygon(turf.featureCollection(splitRing(sourceCoords).map(c => turf.lineString(c))));
const mp = buildTurfPolygon([sourceCoords]);
const mpRings = turf.polygonToLine(mp).features;
const mpReversed = buildTurfPolygon([sourceCoords.toReversed()]);
const mpRingsReversed = turf.polygonToLine(mpReversed).features;
var clockwiseRing = turf.lineString([[0,0],[1,1],[1,0],[0,0]]);
var counterClockwiseRing = turf.lineString([[0,0],[1,0],[1,1],[0,0]]);

return {
mpRings,
mpRingsReversed,
mpRingsCW: mpRings.map(r => turf.booleanClockwise(r)),
mpRingsReversedCW: mpRingsReversed.map(r => turf.booleanClockwise(r))
}
}
Insert cell
{
const shell = [[0,0], [0,10], [10,10], [10,0], [0,0]].toReversed();
const hole = [[3,3], [10,5], [3,7], [3,3]].toReversed();
const ls = turf.lineString(shell);
// return turf.booleanClockwise(ls);
return buildTurfPolygon([shell, hole]);
}
Insert cell
buildTurfPolygon = {
function splitRing(ring) {
const splitpoints = createPointHashSet(ring.length);
{
const allPoints = createPointHashSet(ring.length);
for (let coord of ring.slice(0, -1)) {
if (allPoints.has(coord)) splitpoints.add(coord);
allPoints.add(coord);
}
}
if (splitpoints.values().length == 0) return [ring];
splitpoints.add(ring[0]);
let segment = [];
const segments = [segment];
const ringSegments = [];
const lastIndex = ring.length - 1;
for (let i = 0; i < ring.length; i++) {
const coord = ring[i];
segment.push(coord);
if (splitpoints.has(coord)) {
if (segment.length > 1) {
const ringStartIdx = segments.findLastIndex(s => pointsEqual(s[0], coord));
if (ringStartIdx >= 0) ringSegments.push(segments.splice(ringStartIdx));
}
if (i == 0 || i == lastIndex) continue;
segment = [coord];
segments.push(segment);
}
}
return ringSegments.map(rs => [rs[0][0], ...rs.flatMap(s => s.slice(1))]);
}

function buildTurfPolygon(rings, options) {
options = Object.assign({ ignoreHoles: false }, options);
const { ignoreHoles } = options;

const ringMeta = rings.flatMap(r => splitRing(r))
.map((ring,i) => {
const lineString = turf.lineString(ring);
turf.cleanCoords(lineString, { mutate: true });
const group = turf.booleanClockwise(lineString) ? "holeMeta" : "shellMeta";
if (ignoreHoles && group == "holeMeta") return undefined;
const polygon = turf.lineToPolygon(lineString);
polygon.properties = { index: i, key: "potato" };
return {
ring: turf.getCoords(lineString),
group,
bbox: turf.bbox(polygon),
area: Math.abs(d3.polygonArea(ring)),
polygon
};
})
.filter(rm => rm);

ringMeta.sort((a,b) => a.area - b.area);
let { shellMeta, holeMeta } = _.groupBy(ringMeta, rm => rm.group);
shellMeta ??= [];
holeMeta ??= [];
shellMeta.forEach(shell => shell.holes = []);

if (!ignoreHoles) {
for (let hole of holeMeta) {
let shell;
if (shellMeta.length == 1) {
shell = shellMeta[0];
}
else {
const candidates = shellMeta.filter(shell => shell.area > hole.area && Bbox.covers(shell.bbox, hole.bbox));
if (candidates.length == 1) {
shell = candidates[0];
}
else {
shell = candidates.find(shell => turf.booleanCovers(shell.polygon, hole.polygon));
console.log(`ran booleanCovers ${candidates.indexOf(shell) + 1} times`);
}
}
if (!shell) {
debugger;
throw "couldn't find shell for hole";
}
shell.polygon.geometry.coordinates.push(hole.ring);
}
}

const shells = shellMeta.map(s => s.polygon);
const fcPolygons = turf.dissolve(turf.featureCollection(shells))
const fcMultiPolygon = turf.combine(fcPolygons);
if (fcMultiPolygon.features.length != 1) {
debugger;
throw "Got multiple multipolygons";
}
const result = fcMultiPolygon.features[0];
result.properties = {};
return result;
}

return buildTurfPolygon;
}
Insert cell
buildJstsPolygon = {

function splitRing(ring) {
const splitpoints = createPointHashSet(ring.length);
{
const allPoints = createPointHashSet(ring.length);
for (let coord of ring.slice(0, -1)) {
if (allPoints.has(coord)) splitpoints.add(coord);
allPoints.add(coord);
}
}
if (splitpoints.values().length == 0) return [ring];
splitpoints.add(ring[0]);
let segment = [];
const segments = [segment];
const ringSegments = [];
const lastIndex = ring.length - 1;
for (let i = 0; i < ring.length; i++) {
const coord = ring[i];
segment.push(coord);
if (splitpoints.has(coord)) {
if (segment.length > 1) {
const ringStartIdx = segments.findLastIndex(s => pointsEqual(s[0], coord));
if (ringStartIdx >= 0) ringSegments.push(segments.splice(ringStartIdx));
}
if (i == 0 || i == lastIndex) continue;
segment = [coord];
segments.push(segment);
}
}
return ringSegments.map(rs => [rs[0][0], ...rs.flatMap(s => s.slice(1))]);
}
// This function converts the output of tracedPolygons (a big ol' pile of rings) into valid GeoJSON (multipolygons with holes).
// I'll be honest, I don't now how necessary it is since topo-server is going to eat the whole thing anyway
function buildJstsPolygon(ringCoords, ignoreHoles = false) {
const { Orientation, Coordinate } = jsts;
const sequences = ringCoords.flatMap(ring => splitRing(ring)).map(ring => ring.map(c => new Coordinate(...c)));
let { holes, shells } = _.groupBy(sequences, s => Orientation.isCCW(s) ? "shells" : "holes");
holes ??= [];
shells ??= [];
if (holes.length == 0 || ignoreHoles) {
const linearRings = shells.map(rc => jstsFactory.createLinearRing(rc));
const islands = linearRings.map(s => jstsFactory.createPolygon(s));
return jstsFactory.createMultiPolygon(islands);
}
for (let rings of [holes, shells]) {
for (let i = 0; i < rings.length; i++) {
const ring = jstsFactory.createLinearRing(rings[i]);
const polygon = jstsFactory.createPolygon(ring);
rings[i] = {
ring,
polygon,
area: polygon.getArea()
};
}
rings.sort((a,b) => a.area - b.area);
}
const shellHoles = new Map(shells.map(s => [s, []]));
for (let hole of holes) {
let holeAssigned = false;
for (let shell of shells) {
if (shell.area <= hole.area) continue;
if (shell.polygon.covers(hole.polygon)) {
shellHoles.get(shell).push(hole.ring);
break;
}
}
}
const islands = [];
for (let [shell, holes] of shellHoles) {
if (holes.length == 0) {
islands.push(shell.polygon);
}
else {
islands.push(jstsFactory.createPolygon(shell.ring, holes));
}
}
return jstsFactory.createMultiPolygon(islands);
}

return buildJstsPolygon
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
async function tracePolygons(worldMapImage, colorTable, downscaleFactor, cancellationToken) {
downscaleFactor = Math.max(downscaleFactor, 1);
cancellationToken ??= getCancellationToken();
const allPolygons = new Map();
const ingestPolygons = function(newPolygons) {
for (let [colorNum, newRings] of newPolygons) {
let currentRings = allPolygons.get(colorNum);
currentRings ? currentRings.push(...newRings) : allPolygons.set(colorNum, newRings);
}
}

const bitmap = await createImageBitmap(worldMapImage.canvas);
const ts = new TileSlicer(bitmap);
const tracer = new TileTracer(colorTable);
const destroy = () => { tracer.destroy(); ts.destroy(); };

invalidation.then(() => {
cancellationToken.cancel = true;
destroy();
});
const tileBatches = [[]];
const [tileCountX, tileCountY] = ts.getTileCounts(downscaleFactor);
for (let tileX = 0; tileX < tileCountX; tileX++) {
for (let tileY = 0; tileY < tileCountY; tileY++) {
if (cancellationToken.cancel) {
tracer.cancel();
destroy();
return;
}
const tile = await ts.getTileBitmap(tileX, tileY, downscaleFactor);
const polygons = await tracer.getPolygons(tile, worldMapImage.width);
ingestPolygons(polygons);
await pauseIfNeeded();
}
}
destroy();
return allPolygons;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function buildColorSwap(layerName) {
let selectedLayers = mapSpec.layers;
let getSelectedProps = (props) => props;
if (layerName) {
selectedLayers = _.pick(mapSpec.layers, [layerName]);
let selectedPropNames = Object.values(selectedLayers).map(l => l.prop);
getSelectedProps = (props) => _.pick(props, selectedPropNames);
}

const provincesFlat = [];
for (let [colorNum, props] of mapSpec.provinceProps) {
const traceId = Object.values(selectedLayers).map(p => props[p.prop]).join("|");
provincesFlat.push({ colorNum, traceId, props: getSelectedProps(props) });
}

const uniques = new Map();
for (let group of Object.values(_.groupBy(provincesFlat, "traceId"))) {
const props = group[0].props;
props.colorNum = group[0].colorNum;
const sourceColorNums = group.map(p => p.colorNum);
uniques.set(group[0].colorNum, { props, sourceColorNums });
}
const colorSwap = {};
const sourceColors = colorSwap.sourceColors = new Map();
const traceColorProps = colorSwap.traceColorProps = new Map();
for (let [traceColorNum, { props, sourceColorNums }] of uniques) {
sourceColorNums.forEach(sc => colorSwap.sourceColors.set(sc, traceColorNum));
colorSwap.traceColorProps.set(traceColorNum, props);
}
colorSwap.getLayerProp = function(layerName, traceColorNum) {
const props = traceColorProps.get(traceColorNum);
if (!props) return {};
const prop = selectedLayers[layerName].prop;
return { name: prop, value: props[prop] };
}
return colorSwap;
}
Insert cell
Insert cell
Insert cell
Insert cell
{
const shell = [[0,0], [0,10], [10,10], [10,0], [0,0]];
const hole = [[3,3], [10,5], [3,7], [3,3]];
return turf.buffer(turf.polygon([shell, hole]), 0)
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
var line1 = turf.lineString([[-2, 2], [4, 2]]);
var line2 = turf.lineString([[1, 1], [1, 2], [1, 3], [1, 4]]);

var cross = turf.booleanCrosses(line1, line2);
return de9im.crosses(line1, line2);
}
Insert cell
turf.boo
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
Insert cell
Insert cell
Insert cell
function getChildrenFromMultiGeometry(jstsMulti) {
if (!jstsMulti.getNumGeometries) return [jstsMulti];
const children = []
for (let i = 0; i < jstsMulti.getNumGeometries(); i++) {
children.push(jstsMulti.getGeometryN(i));
}
return children;
}
Insert cell
function getChildrenFromMultiGeoJson(geoMulti) {
if (!geoMulti.geometry.type.startsWith("Multi")) return [geoMulti];
const geometry = geoMulti.geometry;
const type = geometry.type.replace("Multi", "");
return geometry.coordinates.map(c => ({
type: geoMulti.type,
properties: Object.assign({}, geoMulti.properties),
geometry: {
type,
coordinates: c
}
}));
}
Insert cell
function getChildrenFromMultiTopoJson(geometry) {
if (!geometry) return [];
if (!geometry.type.startsWith("Multi")) return [geometry];
const type = geometry.type.replace("Multi", "");
return geometry.arcs.map(c => ({
type,
properties: Object.assign({}, geometry.properties),
arcs: c
}));
}
Insert cell
mergeFeatures(topojson.feature(pixelTopology, pixelTopology.objects.states).features, "key")
Insert cell
{
const features = topojson.feature(pixelTopology, pixelTopology.objects.states).features
const split = _.groupBy(features, f => f.geometry.type.startsWith("Multi"));
return split;
}
Insert cell
_.pick({potato: 7, tomato: 5}, undefined)
Insert cell
_.chain([{potato: 7, tomato: 5}, {potato: 7, tomato: 9}]).groupBy(f => ({ tomato: f.tomato })).value();
Insert cell
function mergeFeatures(features, fnGroupBy) {
const geoPath = d3.geoPath(d3.geoIdentity());

const splitByType = _.groupBy(features, f => f.geometry.type.startsWith("Multi"));
const flatFeatures = [
...(splitByType[false] ?? []),
...(splitByType[true] ?? []).flatMap(f => getChildrenFromMultiGeoJson(f))
];

const featuresByKey = Object.entries(_.groupBy(flatFeatures, groupBy));
const resultFeatures = new Map();
for (let featureSet of featuresByKey) {
// const key = groupBy(featureSet[0]);
// resultFeatures.set(key, featureSet);
}
return { featuresByKey, resultFeatures }
//return groupedByKey;
/*
for (let featureSet of Object.values(_.groupBy(edgeFeatures, f => f.properties[keyField]))) {
features.push(...unionPolygonFeatures(featureSet));
}
/*

const multiFeatures = split[true] ?? [];
const singleFeatures = split[false] ?? [];

const result = [
Object.values(_.groupBy(multiFeatures, f => f.properties[keyField]))
.flatMap(f => getChildrenFromMultiGeoJson(f)),
Object.values(_.groupBy(singleFeatures, f => f.properties[keyField]))
.flatMap(f => getChildrenFromMultiGeoJson(f)),
];
for (let featureSet of
results.push(...unionPolygonFeatures(featureSet));
}
const mpCoords =
return {
type: "Feature",
properties: features[0].properties,
geometry: {
type: "MultiPolygon",
coordinates: features.map(f => f.geometry.coordinates)
}
};
}
// Hemisphere wrapping
const extendedObjects = {};
for (let [layerName, gc] of Object.entries(topology.objects)) {
const geometries = topology.objects[layerName].geometries;
if (!geometries.length) return;

const eastGeometries = geometries.filter(g => {
const feature = topojson.feature(topology, g);
const gx1 = geoPath.bounds(feature)[1][0];
return (gx1 >= xMid);
});

const features = [
...(await getOffsetFeatures(eastGeometries, -x1)),
...(await getOffsetFeatures(geometries, 0)),
...(await getOffsetFeatures(geometries, x1))
];

const edgeFeatures = features.filter(f => {
const [gx0,,gx1] = geoPath.bounds(f).flat();
return (gx0 == x0 || gx0 == x1 || gx1 == x0 || gx1 == x1);
});
_.pull(features, ...edgeFeatures);
for (let featureSet of Object.values(_.groupBy(edgeFeatures, f => f.properties[keyField]))) {
features.push(...unionPolygonFeatures(featureSet));
}
extendedObjects[layerName] = { type: "FeatureCollection", features: features };
await pauseIfNeeded();
}
const extended = topojson.topology(extendedObjects);
topology.arcs = extended.arcs;
topology.objects = extended.objects;
return topology;

*/
}
Insert cell
function visitAll(object, fn) {
Object.keys(object).forEach(function(k) {
let halt = fn(object[k], k, object);
if (object[k] && typeof object[k] === 'object' && !halt) {
visitAll(object[k], fn);
}
});
}
Insert cell
Insert cell
turf.booleanCovers
Insert cell
de9imCounters = {
const de9im = await require('de9im@1.3.1/de9im.min.js');
const names = Object.keys(de9im).map(k => _.camelCase(["boolean", k]));
return Object.fromEntries(names.map(k => [k,0]));
}
Insert cell
Insert cell
turf.booleanCovers
Insert cell
Insert cell
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