Public
Edited
Jan 3
Importers
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
worldMapInfo
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
Insert cell
Insert cell
Insert cell
proj = getMapGeoProjection(d3.geoMiller, [sampleImage.naturalWidth, sampleImage.naturalHeight], mapGeoExtent)
Insert cell
Insert cell
sampleTopology = buildTopology(sampleTraceResult, { allowExpansion: false, /* wrapX: sampleOptions.wrapX /*, projection: proj */});
Insert cell
_sampleTopology = sampleTopology
Insert cell
({
type: sampleTopology.type,
bbox: sampleTopology.bbox.map(c => 4*c),
objects: sampleTopology.objects,
arcs: sampleTopology.arcs.map(a => a.map(c => [4*c[0], 4*c[1]]))
})
Insert cell
sampleTopology
Insert cell
topojson.merge(sampleTopology, sampleTopology.objects.traced.geometries)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
async function gpuProcessTile(bitmap, tileRect, mapRect, reglProgram) {
const { regl, commands, buffers } = reglProgram;
const tileTexture = regl.texture({ data: bitmap });
const tileDimensions = [ bitmap.width, bitmap.height ];

const mapEdgesAbs = {
x0: mapRect.x,
y0: mapRect.y,
x1: mapRect.x + mapRect.width,
y1: mapRect.y + mapRect.height
};
const mapBboxVec = [
mapEdgesAbs.x0 - tileRect.x,
mapEdgesAbs.y0 - tileRect.y,
mapEdgesAbs.x1 - tileRect.x - 1,
mapEdgesAbs.y1 - tileRect.y - 1
]
const tileBboxVec = [
TILE_PADDING,
TILE_PADDING,
TILE_PADDING + tileRect.width - 1,
TILE_PADDING + tileRect.height - 1
]
Object.values(buffers).forEach(b => b.resize(...tileDimensions));

commands.replaceColors({ uSampler: tileTexture, framebuffer: buffers.color });
await glClientWaitAsync(regl);
tileTexture.destroy();

commands.traceImage({ uSampler: buffers.color, mapBboxVec, tileBboxVec, framebuffer: buffers.traced });
await glClientWaitAsync(regl);
const results = [];
const rect = Bbox.dims(tileBboxVec);
if (mapRect.width == tileRect.x + tileRect.width) rect.width++;
if (mapRect.height == tileRect.y + tileRect.height) rect.height++;
const data = await readFramebufferAsync(regl, buffers.traced, rect);
return new PixelIO(rect.width, rect.height, data);
}
Insert cell
Insert cell
sampleMapSpec
Insert cell
Insert cell
Insert cell
sampleTopology
Insert cell
topojson.mergeArcs(sampleTopology, sampleTopology.objects.traced.geometries.filter(d => colorTableLayerMap.replacedColorProps.get(d.properties.id)?.region == "region_andes"))
Insert cell
sampleTopology.objects.traced.geometries.filter(d => colorTableLayerMap.replacedColorProps.get(d.properties.id)?.region == "region_andes")
Insert cell
sampleTopology.objects
Insert cell
sampleMapSpec
Insert cell
colorTableLayerMap
Insert cell
splitLayers(sampleTopology, colorTableLayerMap)
Insert cell
function splitLayers(topology, layerMap) {
topology = {...topology};
topology.objects = {...topology.objects};
const { layerObjectColors, replacedColorProps } = layerMap;
const keyColors = new Map();
// debugger;
replacedColorProps.forEach((props, colorNum) => keyColors.set(props.key, colorNum));
const tracedGeometries = topology.objects.traced.geometries;
for (let [layerName, layerObjects] of Object.entries(layerObjectColors)) {
const layerGeometries = [];
for (let [id, keys] of Object.entries(layerObjects)) {
// debugger;
const colorNums = keys.map(k => keyColors.get(k));
const matches = tracedGeometries.filter(g => colorNums.includes(g.properties.colorNum));
const merged = (matches.length == 1) ? {...matches[0]} : topojson.mergeArcs(topology, matches);
merged.properties = { id };
layerGeometries.push(merged);
}
topology.objects[layerName] = turf.geometryCollection(layerGeometries).geometry;
}
return topology;
}
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
parseGpuTileResult(sampleTileGpuResult)
Insert cell
Insert cell
getMapEdgeCoords({n: true, e: true, s: true, w: true}, { x: 5, y: 5, width: 10, height: 10 })
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function buildGeoJsonForPolygon(ringCoords) {
const { Orientation, Coordinate } = jsts;

const polygonMeta = ringCoords.map(rc => {
const coords = rc.map(c => new Coordinate(...c));
const group = Orientation.isCCW(coords) ? "shellMeta" : "holeMeta";
const ring = jstsFactory.createLinearRing(coords);
const polygon = jstsFactory.createPolygon(ring);
return {
coords: rc,
ring,
polygon,
group,
bbox: Bbox.fromPoints(rc),
area: polygon.getArea()
};
});
polygonMeta.sort((a,b) => a.area - b.area);
let { shellMeta, holeMeta } = _.groupBy(polygonMeta, rm => rm.group);
shellMeta ??= [];
holeMeta ??= [];
shellMeta.forEach(shell => shell.holeCoords = []);

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 => shell.polygon.covers(hole.polygon));
}
}
if (!shell) {
throw "couldn't find shell for hole";
}
shell.holeCoords.push(hole.coords);
}

const islandCoords = shellMeta.map(shell => [shell.coords, ...shell.holeCoords]);
const polygon = (islandCoords.length == 1) ? turf.polygon(islandCoords[0]) : turf.multiPolygon(islandCoords);
/*
let buffered;
try {
buffered = turf.buffer(polygon, 0);
return buffered.geometry;
}
catch (e) {
debugger;
turf.buffer(polygon, 0);
console.log("Couldn't buffer", {polygon, buffered, e});
return polygon.geometry;
}
*/
return polygon.geometry;
/*
const islands = [];
for (let shell of shellMeta) {
const holeRings = shell.holeRings;
if (holeRings.length == 0) {
islands.push(shell.polygon);
}
else {
islands.push(jstsFactory.createPolygon(shell.ring, shell.holeRings));
}
}
const geometry = jstsFactory.createMultiPolygon(islands); //.buffer(0);
return jstsWriter.write(geometry);
*/
}
Insert cell
Insert cell
_.max(sampleTopology.properties.junctions.map(j => j[0]))
Insert cell
sampleTopology.arcs.flat().length
// 97012
Insert cell
CoordMask.fromArray([0.5, 0.5])
Insert cell
function presimplifyProtectJunctions(topology) {
const coordTypes = topology.properties.coordTypes;
const junctionCoordNums = [...coordTypes.numericMap.entries()].filter(e => e[1] == "junction").map(e => e[0]);
const junctions = MaskSet.around(new Set(junctionCoordNums), CoordMask);

function weight(triangle) {
if (junctions.has(triangle[1])) return Infinity;
return topojson.planarTriangleArea(triangle);
}

const result = topojson.presimplify(topology, weight);
result.properties = topology.properties;
return result;
}
Insert cell
{
let a = [1,2,3,4,5,1];
rotateRing(a, -2)
return a;
}
Insert cell
function coordsEqual(a,b) {
if (a === b) return true;
if (a === undefined || b === undefined) return false;
return a[0] == b[0] && a[1] == b[1];
}
Insert cell
function rotateRing(ring, newStartIndex) {
if (newStartIndex <= 0) return;
const newRing = [
...ring.slice(newStartIndex, -1),
...ring.slice(0, newStartIndex),
ring[newStartIndex]
];
ring.splice(0, ring.length, ...newRing);
}
Insert cell
_.min([1,2,3,undefined])
Insert cell
sampleTopology
Insert cell
sampleGeoJson
Insert cell
weightCounts = {
const topology = sampleTopology;
const usedArcStats = [];

for (let geometry of sampleTopology.objects.traced.geometries) {
const arcIds = geometry.arcs.flat().map(a => (a < 0) ? ~a : a).flat();
arcIds.forEach(i => usedArcStats.push(arcStats[i]));
}
let weightCounts = _.groupBy(usedArcStats.flat(), "value");
weightCounts = Object.entries(weightCounts).map(([k,v]) => ({ value: parseFloat(k), coordCount: _.sumBy(v, "coordCount") }));
weightCounts = _.sortBy(weightCounts, v => -v.value);
for (let i = 1; i < weightCounts.length; i++) {
weightCounts[i].coordCount += weightCounts[i-1].coordCount;
}
weightCounts = _.sortBy(weightCounts, v => v.value);
return weightCounts
/*
const arcs = arcIds.map(id => topology.arcs[id]);
return { topology, geometry, arcIds, arcs };
const coords = turf.coordAll(sampleGeoJson);
let weights = coords.map(a => a[2]);
let valueCounts = Object.entries(_.groupBy(weights)).map(e => ({ value: parseFloat(e[0]), count: e[1].length }));
valueCounts = _.sortBy(valueCounts, v => -v.value);

valueCounts.forEach((vc, i) => {
const sum = (i == 0) ? 0 : valueCounts[i-1].total;
vc.total = vc.count + sum;
})
return valueCounts.at(-1);
return valueCounts;
return Plot.plot({
width,
marks: [
Plot.rectY(valueCounts, Plot.binX({y: "count"}, {x: { cumulative: true, thresholds: 200, value: "value"}})),
Plot.ruleY([0])
]
})
*/
}
Insert cell
JSON.stringify(arcStatsMetaFull).replaceAll("\"", "")
Insert cell
arcStatsMetaFull.filter(m => !m.x)
Insert cell
arcStatsMetaFull = {
const allStats = arcStats.flat();
const values = _.sortBy(_.uniq(allStats.map(s => s.value)));
const coordCounts = [];
for (let value of values) {
const stats = allStats.filter(s => s.value >= value);
coordCounts.push({ x: parseFloat(Math.log2(value).toFixed(4)), y: _.sumBy(stats, "coordCount") });
}
return coordCounts;
}
Insert cell
arcStatsMeta = {
const allStats = arcStats.flat();
const values = _.uniq(allStats.map(s => s.value));
const minLogValue = Math.floor(Math.log2(_.min(values)));
const maxLogValue = Math.ceil(Math.log2(_.max(values.filter(v => v < Infinity))));
const coordCounts = [];
for (let value of d3.range(minLogValue, maxLogValue).map(v => 2**v)) {
const stats = allStats.filter(s => s.value >= value);
coordCounts.push({ value, coordCounts: _.sumBy(stats, "coordCount") });
}
return coordCounts;
}
Insert cell
arcStats = {
const topology = sampleTopology;
const arcStats = topology.arcs.map(arc => {
const weightCounts = new Map();
for (let weight of arc.map(a => a[2])) {
weightCounts.set(weight, (weightCounts.get(weight) ?? 0) + 1);
}
return weightCounts;
/*
_.uniq(arc.map(a => a[2]))
let weightCounts = Object.entries(_.groupBy(weights)).map(e => ({ value: parseFloat(e[0]), coordCount: e[1].length }));
weightCounts = _.sortBy(weightCounts, v => -v.value);
/*
for (let i = 1; i < weightCounts.length; i++) {
weightCounts[i].coordCount += weightCounts[i-1].coordCount;
}
weightCounts.push({ value: 0, coordCount: arc.length });
*/
return weightCounts;
});
return arcStats;
}
Insert cell
sumWeightCounts(arcStats)
Insert cell
{
const ts = performance.now();

const topology = sampleTopology;
const arcWeightCounts = topology.arcs.map(arc => {
const weightCounts = new Map();
for (let weight of arc.map(a => a[2])) {
weightCounts.set(weight, (weightCounts.get(weight) ?? 0) + 1);
}
return weightCounts;
});

const result = sumWeightCounts(arcWeightCounts);
const runtime = performance.now() - ts;
return { result, runtime };
}
Insert cell
function sumWeightCounts(weightCounts) {
const sums = new Map();
weightCounts.forEach(wc => {
for (const [weight, count] of wc.entries()) {
sums.set(weight, (sums.get(weight) ?? 0) + count);
}
});
return sums;
}
Insert cell
sampleTopology
Insert cell
sampleGeoJson
Insert cell
function isRing(arc) {
return arc.length > 3 && coordsEqual(arc[0], arc.at(-1));
}
Insert cell
Object.keys(d3.scaleLinear())
Insert cell
{
const traceResult = sampleTraceResult;
const [x0, y0, x1, y1] = traceResult.bbox;
const scaleX = d3.scaleLinear().domain([x0, x1]).range([-180, 180]);
const scaleY = d3.scaleLinear().domain([y0, y1]).range([60, -60]);
// debugger;
const scale = {
transform: c => {
c[0] = scaleX(c[0]);
c[1] = scaleY(c[1]);
},
untransform: c => {
c[0] = Math.round(scaleX.invert(c[0])),
c[1] = Math.round(scaleY.invert(c[1]))
}
}
const geojson = _.cloneDeep(await buildGeoJsonRaw(traceResult));
turf.coordEach(geojson, c => scale.transform(c));
// turf.coordEach(geojson, c => scale.untransform(c));
// const untransformed = transformed.map(c => scale.untransform(c));
const stitched = d3.geoStitch(geojson);
turf.coordEach(stitched, c => scale.untransform(c));
return { stitched: stitched.features.filter(f => {
const coords = turf.coordAll(f);
return (_.min(coords.map(c => c[0])) < 10 && _.max(coords.map(c => c[0])) > 2040)
}) };
}
Insert cell
async function buildTopology(traceResult, options) {
options = Object.assign({ wrapX: false, smooth: true, allowExpansion: true }, options);

/*
const edges = {
type: "Feature",
geometry: { type: "MultiLineString" },
properties: { id: NaN }
}
{
let bboxEdges = Bbox.edges(traceResult.bbox);
if (options.wrapX) bboxEdges = _.pick(bboxEdges, ["N", "S"]);
edges.geometry.coordinates = Object.values(bboxEdges);
}
*/
// Create topology
const geojson = await buildGeoJsonRaw(traceResult);
// geojson.features.push(edges);
let topology = topojson.topology({traced: geojson});
topology.bbox = [...traceResult.bbox];
// topology.arcs = topology.arcs.map(a => a.map(c => [...c]));
// Separate ring and line arcs
const ringArcs = [];
const lineArcs = [];
topology.arcs.forEach(arc => (isRing(arc)) ? ringArcs.push(arc) : lineArcs.push(arc));

// Add endpoints of line arcs to coordTypes as junctions
const coordTypes = traceResult.coordTypes.clone();
for (let arc of lineArcs) {
coordTypes.set(arc[0], "junction");
coordTypes.set(arc.at(-1), "junction");
}
// Rotate ring arcs to start on a junction if possible, or the closest point to the polygon center otherwise
const fnFindNewStart = (options.allowExpansion) ? (c => coordTypes.get(c) == "junction") : (c => !!coordTypes.get(c))
for (let arc of ringArcs) {
let newStart = fnFindNewStart(arc);
if (newStart < 0) {
if (Math.abs(d3.polygonArea(arc)) == 1) continue;
const centroid = d3.polygonCentroid(arc);
const distancesSquared = arc.map(c => (centroid[0] - c[0])**2 + (centroid[1] - c[1])**2);
const min = _.min(distancesSquared);
newStart = distancesSquared.indexOf(min);
}
if (newStart > 0) rotateRing(arc, newStart);
}

// Smooth topology
if (options.smooth) smoothTopology(topology, coordTypes, options.allowExpansion);
// Build topology.properties
topology.properties = {
options,
coordTypes
};

// nodeEdges(topology);

// Remove unneeded points
topology = presimplifyProtectJunctions(topology);
topology.arcs = topology.arcs.map(arc => arc.filter(c => c[2] !== 0));
return topology;
}
Insert cell
sampleTopology.arcs[0].slice(-2)
Insert cell
topojson
Insert cell
{
let st2 = {...sampleTopology};
st2.properties = {...st2.properties};
delete st2.properties.coordTypes;
st2 = _.cloneDeep(st2);

const tas = new TopoArcUtil(st2);
return tas.findArcId(st2.arcs[12].toReversed());
tas.splitAtIndex(0, 2);
return st2.arcs[0];
}
Insert cell
sampleTopology
Insert cell
TopoArcUtil = {
function isRing(arc) {
return eq(arc[0], arc.at(-1));
}
function eq(xy1, xy2) {
return (xy1 == xy2) || (xy1[0] == xy2[0] && xy1[1] == xy2[1]);
}
function isValueBetween(value, a, b) {
const min = (a < b) ? a : b;
const max = (min == a) ? b : a;
return value >= min && value <= max;
}
function isPointBetween(point, xy1, xy2) {
const [px, py] = point;
const [x1, y1] = xy1;
const [x2, y2] = xy2;

// Slope check
if ((py - y1) * (x2 - x1) != (y2 - y1) * (py - x1)) return false;

// Bounds check
if (!isValueBetween(px, x1, x2)) return false;
if (!isValueBetween(py, y1, y2)) return false;

return true;
}

class TopoArcUtil {
constructor(topology) {
this.topology = topology;
this.shiftedRingArcIds = new Set();
this.arcs = this.topology.arcs;
const allGeometries = [];
Object.values(topology.objects).forEach(o => turf.geomEach(o, g => allGeometries.push(g)));
const allArcArrays = [];
for (let geometry of allGeometries) {
switch (geometry.type) {
case "Polygon": allArcArrays.push(...geometry.arcs); break;
case "MultiPolygon": geometry.arcs.forEach(a => allArcArrays.push(...a)); break;
}
}
this._arcUsageIndex = new Map();
for (let arcArray of allArcArrays) {
for (let arcId of arcArray) {
this._addArcUsageToIndex(arcArray, arcId);
}
}
}
_addArcUsageToIndex(arcArray, arcId) {
if (arcId < 0) arcId = ~arcId;
if (!this._arcUsageIndex.has(arcId)) this._arcUsageIndex.set(arcId, []);
this._arcUsageIndex.get(arcId).push(arcArray);
}

findArcId(points) {
const [coord1, coord2] = points;
let foundArcId = undefined;
for (let arcId = 0; arcId < this.arcs.length; arcId++) {
let arc = this.arcs[arcId];
if (!arc) continue;
if (eq(coord1, arc[0]) && eq(coord2, arc[1])) foundArcId = arcId;
if (eq(coord1, arc.at(-1)) && eq(coord2, arc.at(-2))) foundArcId = ~arcId;
if (foundArcId !== undefined) break;
}
const foundArc = (foundArcId >= 0) ? this.arcs[foundArcId] : this.arcs[~foundArcId];
if (foundArc.length != points.length) throw "wyd";
return foundArcId;
}
shiftRing(arcId, index) {
if (this.shiftedRingArcIds.has(arcId)) throw "can't shift twice";

const arc = this.arcs[arcId];
if (!isRing(arc)) throw "not a ring";
arc.pop();
const newLeft = arc.splice(index);
arc.splice(0, 0, ...newLeft);
arc.push(arc[0]);
this.shiftedRingArcIds.add(arcId);
}
splitAtIndex(arcId, index) {
if (index == 0) return;
const arcs = this.arcs;
const arc = arcs[arcId];
if (index == arc.length -1) return;

if (isRing(arc) && !this.shiftedRingArcIds.has(arcId)) {
this.shiftRing(arcId, index);
return;
}
const [arcLeftId, arcRightId] = [arcId, arcs.length];
const [arcLeft, arcRight] = [arc.splice(0, index+1), arc.splice(index)]; // arcRight[0] == arcLeft.at(-1) == arc[index];
arcs[arcLeftId] = arcLeft;
arcs[arcRightId] = arcRight;
const negArcId = ~arcId;
for (let arcArray of this._arcUsageIndex.get(arcId) ?? []) {
this._addArcToIndex(arcArray, arcRightId);
const posIndex = arcArray.indexOf(arcLeftId);
if (posIndex >= 0) {
arcArray.splice(posIndex, 1, arcLeftId, arcRightId);
}
else {
const negIndex = arcArray.indexOf(~arcLeftId);
if (negIndex >= 0) {
arcArray.splice(negIndex, 1, ~arcRightId, ~arcLeftId);
}
}
}
}
splitAtPoint(arcId, point) {
const arc = this.arcs[arcId];
if (eq(arc[0], point)) return;
if (eq(arc.at(-1), point)) return;
for (let i = 1; i < arc.length-1; i++) {
if (eq(arc[i], point)) {
// exact match
this.splitAtIndex(arcId, i);
return;
}
const prev = arc[i-1];
if (isPointBetween(point, prev, arc[i])) {
// between two existing points
arc.splice(i, 0, point);
this.splitAtIndex(arcId, i);
return;
}
}
throw "couldn't splitAtPoint";
}
}
return TopoArcUtil;
}
Insert cell
function nodeEdges(topology) {
const yValueSet = new Set();
const edgeArcs = new Set();
const x0 = topology.bbox[0];
const x1 = topology.bbox[2];
for (let i = 0; i < topology.arcs.length; i++) {
const arc = topology.arcs[i];
for (let [x, y] of arc) {
if (x == x0 || x == x1) {
yValueSet.add(y);
edgeArcs.add(arc);
}
}
}
const yValues = _.sortBy([...yValueSet]);
return yValues;
function getIntermediateValues(fromY, toY) {
if (fromY > toY) return getIntermediateValues(toY, fromY).toReversed();
return yValues.filter(y => y > fromY && y < toY);
}

for (let arc of edgeArcs) {
let oldArc = [...arc];
for (let i = 0; i < arc.length-1; i++) {
const [fromX, fromY] = arc[i];
if (fromX != x0 && fromX != x1) continue;
const [toX, toY] = arc[i+1];
if (toX != x0 && toX != x1) continue;
if (fromX != toX || fromY == toY) continue;

const newValues = getIntermediateValues(fromY, toY).map(y => [fromX, y]);
if (!newValues.length) continue;
arc.splice(i+1, 0, ...newValues);
}
}
}
Insert cell
smoothTopology = {
function coordsEqual(c1, c2) {
return (c1[0] == c2[0] && c1[1] == c2[1]);
}
function maybePush(arc, coord) {
const prevCoord = arc.at(-1);
if (!prevCoord || !coordsEqual(prevCoord, coord)) arc.push(coord);
}

function getMaxShiftAmount(coordType, allowExpansion) {
if (!coordType) return Infinity;
if (!allowExpansion) return 0;
if (coordType == "corner" || coordType == "intrusion") return 0.25;
return 0;
}
function smoothArc(arc, coordTypes, allowExpansion) {
const arcCoordTypes = arc.map(c => coordTypes.get(c));
const smoothArc = [];

function getSegmentToNext(fromCoordIndex) {
const fromCoord = arc[fromCoordIndex];
if (!fromCoord) return;
const toCoord = arc[fromCoordIndex + 1];
if (!toCoord) return;

const axis = (fromCoord[0] != toCoord[0]) ? 0 : 1;
return {
fromCoordIndex, axis,
delta: toCoord[axis] - fromCoord[axis]
}
}
let coord, coordType,
prevSegment, nextSegment,
axis, delta, maxShiftAmount, shiftAmount;
for (let i = 0; i < arc.length; i++) {
coord = arc[i];
coordType = arcCoordTypes[i];
const maxShiftAmount = getMaxShiftAmount(coordType, allowExpansion);

if (!prevSegment || prevSegment.fromCoordIndex != i-1) {
prevSegment = getSegmentToNext(i-1);
}
if (prevSegment) {
let { delta, axis } = prevSegment;
delta *= -1;
shiftAmount = Math.sign(delta) * Math.min(maxShiftAmount, Math.abs(delta)/2);
const newCoord = [coord[0], coord[1]];
newCoord[axis] += shiftAmount;
maybePush(smoothArc, newCoord);
}

nextSegment = getSegmentToNext(i);
if (nextSegment) {
let { delta, axis } = nextSegment;
shiftAmount = Math.sign(delta) * Math.min(maxShiftAmount, Math.abs(delta)/2);
const newCoord = [coord[0], coord[1]];
newCoord[axis] += shiftAmount;
maybePush(smoothArc, newCoord);
}

prevSegment = nextSegment;
}
const first = arc[0];
const last = arc.at(-1);
if (first[0] == last[0] && first[1] == last[1]) {
maybePush(smoothArc, [...smoothArc[0]]);
}
return smoothArc;
}
function smoothTopology(topology, coordTypes, allowExpansion = true) {
// If any coord has a decimal part, don't smooth
for (let arc of topology.arcs) {
for (let coord of arc) {
if (~~coord[0] != coord[0] || ~~coord[1] != coord[1]) throw "topology has already been smoothed"
}
}
// Smooth
topology.arcs = topology.arcs.map(a => smoothArc(a, coordTypes, allowExpansion));
}

return smoothTopology;
}
Insert cell
Insert cell
Insert cell
sampleTopology
Insert cell
{

function getSvgPath(polygon) {
const path = d3.path();
for (let ring of polygon.geometry.coordinates) {
path.moveTo(...ring[0]);
ring.slice(1).forEach(c => path.lineTo(...c));
path.closePath();
}
return svg`<path d="${path.toString()}">`;
}
const allCoords = Object.values(gcMergeResults).flat().map(f => turf.getCoords(f).flat()).flat();
const vb = Bbox.fromPoints(allCoords);

const result = svg`<svg
width=${vb[2]}
height=${vb[3]}
viewBox="${vb.join(",")}"
style="max-width:100%;height:auto;">`

for (let polygon of gcMergeResults.newPolygons) {
const path = getSvgPath(polygon);
path.style.fill = "#ff000044";
result.append(path);
}
for (let polygon of gcMergeResults.oldPolygons) {
const path = getSvgPath(polygon);
path.style.fill = "#0000ff44";
result.append(path);
}
return result;
}
Insert cell
gcMergeResults = {
let newPolygons = geoContiguous.newMultiPolygons[0].geometry.coordinates.map(c => turf.polygon(c));
let oldPolygons = geoContiguous.oldMultiPolygons[0].geometry.coordinates.map(c => turf.polygon(c));
[...newPolygons, ...oldPolygons].forEach(p => {
p.properties.bbox = turf.bbox(p);
});
newPolygons = _.sortBy(newPolygons, [p => p.properties.bbox[0], p => p.properties.bbox[1]]);
oldPolygons = _.sortBy(oldPolygons, [p => p.properties.bbox[0], p => p.properties.bbox[1]]);
return { newPolygons, oldPolygons }
}

Insert cell
geoContiguous = {
let st2 = {...sampleTopology};
st2.properties = {...st2.properties};
delete st2.properties.coordTypes;
st2 = _.cloneDeep(st2);
return makeContiguous(st2, st2.objects.traced);
}
Insert cell
function makeContiguous(topology, layer) {
const geoJson = topojson.feature(topology, layer);
turf.rewind(geoJson, { mutate: true });
const x0 = topology.bbox[0];
const x1 = topology.bbox[2];
const multiPolygons = geoJson.features.filter(f => f.geometry.type == "MultiPolygon");
const newMultiPolygons = [];
for (let feature of multiPolygons) {
const groups = { left: [], right: [], both: [] };
for (let child of feature.geometry.coordinates.map(c => turf.polygon(c, {...feature.properties}))) {
const bbox = turf.bbox(child);
const isLeft = bbox[0] == x0;
const isRight = bbox[2] == x1;
if (isLeft && isRight) groups.both.push(child);
else if (isLeft) groups.left.push(child);
else if (isRight) groups.right.push(child);
}

let didMerge = false;
const newPolygons = [];
if (groups.both.length) {
throw "TODO"
didMerge = true;
}

if (groups.left.length * groups.right.length > 0) {
didMerge = true;
const jstsRight = groups.right.map(p => jstsReader.read(p).geometry.buffer(0));
const jstsLeft = groups.left.map(p => jstsReader.read(p).geometry.buffer(0));
// debugger;
const jstsShifted = jstsLeft.map(p => jsts.translate(p, [x1, 0]));

// debugger;
let union = jstsRight[0];
jstsRight.slice(1).forEach(p => union = union.union(p));
jstsShifted.forEach(p => union = union.union(p));

const newPolygons = [];
for (let i = 0; i < union.getNumGeometries(); i++) {
let p = union.getGeometryN(i);
const envelope = p.getEnvelopeInternal();
if (envelope.getMinX() >= x1) p = jsts.translate(p, [-x1, 0]);
newPolygons.push(p);
}
const newMultiPolygon = jstsFactory.createMultiPolygon(newPolygons);
newMultiPolygons.push(turf.feature(jstsWriter.write(newMultiPolygon), {...feature.properties, modified: true, old: feature }));
/*
newPolygons.push(jstsRight);
for (let child of groups.left) {
debugger;
const jstsShifted = jsts.translate(jstsReader.read(child).geometry, [x1, 0]);
const polygonToKeep = (jstsRight.intersects(jstsShifted)) ? jstsShifted : jstsReader.read(child).geometry;
if (polygonToKeep == jstsShifted) needsMerge = true;;
newPolygons.push(polygonToKeep);
}
*/
}

if (!didMerge) {
// newMultiPolygons.push(feature);
}
// const bbox = turf.bbox(f);
// return bbox[0] <= topology.bbox[0] && bbox[1] >= topology.bbox[1];
// })
}
return { multiPolygons, newMultiPolygons, oldMultiPolygons: newMultiPolygons.map(p => p.properties.id).map(id => multiPolygons.find(p => p.properties.id == id)) };
}
//turf.coordAll(geoJson)
Insert cell
function _makeContiguous(topology, layer) {
const geoJson = topojson.feature(topology, layer);
const x0 = topology.bbox[0];
const x1 = topology.bbox[2];
const multiPolygons = geoJson.features.filter(f => f.geometry.type == "MultiPolygon");
for (let feature of multiPolygons) {
const groups = { left: [], right: [], both: [] };
for (let child of feature.geometry.coordinates.map(c => turf.polygon(c, {...feature.properties}))) {
const bbox = turf.bbox(child);
const isLeft = bbox[0] == x0;
const isRight = bbox[2] == x1;
if (isLeft && isRight) groups.both.push(child);
else if (isLeft) groups.left.push(child);
else if (isRight) groups.right.push(child);
}

let needsMerge = false;
const newPolygons = [];
if (groups.both.length) {
needsMerge = true;
}

if (groups.left.length * groups.right.length > 0) {
const jstsRight = jstsReader.read(turf.multiPolygon(groups.right.map(p => p.geometry.coordinates))).geometry;
newPolygons.push(jstsRight);
for (let child of groups.left) {
debugger;
const jstsShifted = jsts.translate(jstsReader.read(child).geometry, [x1, 0]);
const polygonToKeep = (jstsRight.intersects(jstsShifted)) ? jstsShifted : jstsReader.read(child).geometry;
if (polygonToKeep == jstsShifted) needsMerge = true;;
newPolygons.push(polygonToKeep);
}
}
else {
continue;
}

if (needsMerge) {
let result = newPolygons[0];
debugger;
newPolygons.slice(1).forEach(p => result = result.union(p));
return result;
}
// const bbox = turf.bbox(f);
// return bbox[0] <= topology.bbox[0] && bbox[1] >= topology.bbox[1];
// })
}
return;
}
//turf.coordAll(geoJson)
Insert cell
jsts.valid.IsValidOp
Insert cell
Insert cell
Insert cell
li = new LoadIndicator(sampleImage);
Insert cell
li.canvas
Insert cell
Insert cell
Insert cell
Insert cell
{
const sourceColorNum = ColorMask.fromArray([10, 20, 30, 83]);
const obj = GpuColorMask.toObject(sourceColorNum);
const obj2 = _.cloneDeep(obj);

obj2.colorNum = ColorMask.fromArray([...ColorMask.toArray(obj2.colorNum).slice(0,3), 6]);
obj2.neighbors.e = true;
obj2.coordType = "junction";
const reverse = GpuColorMask.fromObject(obj2);
const rereverse = GpuColorMask.toObject(reverse);
return { sourceColorNum, obj, reverse, rereverse, finalColor: ColorMask.toObject(rereverse.colorNum) }
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function getClampedX(x, width) {
if (width <= 0) throw "Invalid width";
while (x < 0) x += width;
x %= width;
return x;
}
Insert cell
tmp(1,2,3)
Insert cell
function tmp() {
return [...arguments];
}
Insert cell
{
const mm = new MaskMap(CoordMask);
mm.set([1,1], 10001);
mm.set([3,3], 30003);
mm.set([1001,1], 10001);
mm.set([1003,3], 10030003);

function fnMax(values) {
if (values.length <= 1) return values[0];
return _.max(values);
}
const mr = new CylReader(mm, 1000, _.max);
return mr.get([-4997,3]);
}
Insert cell
-1999 % 1000
Insert cell
class CylReader {
constructor(maskCollection, width, fnCoalesce) {
if (!width || width < 0 || !Number.isFinite(width)) throw "CylMaskMap: Invalid width";
this.collection = maskCollection;
this.width = width;
this.fnCoalesce = fnCoalesce ?? this._defaultFnCoalesce;
}

_defaultFnCoalesce(values) {
const uniq = _.uniq(values);
if (uniq.length <= 1) return uniq[0];
throw "CylMaskMap: default fnCoalesce: Multiple values detected."
}
_getXValues(x) {
// debugger;
const width = this.width;
x %= width;
if (x < 0) x += width;
if (x == 0) {
return [x, x + width, x + width + width];
}
else {
return [x, x + width];
}
}
get(key) {
const [keyX, y] = key;
// debugger;
const keys = this._getXValues(keyX).map(x => [x,y]);
const values = keys.filter(k => this.collection.has(k))
.map(k => this.collection.get(k));
return this.fnCoalesce(values);
}
}

Insert cell
{
const m1 = new MaskMap(CoordMask);
m1.set([1,1], 1);
const m2 = m1.clone();
m2.set([3,3], 3);
return {m1, m2, eq: [...m1.keys()][0] == [...m2.keys()][0] };
}
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
Bbox.fromDims({x: 5, y: 0, width: 105, height: 110})
Insert cell
class Bbox {
static isEqual(bbox1, bbox2) {
if (bbox1 == bbox2) return true;
return (bbox1?.length == 4 &&
bbox1?.length == 4 &&
bbox1[0] == bbox2[0] &&
bbox1[1] == bbox2[1] &&
bbox1[2] == bbox2[2] &&
bbox1[3] == bbox2[3]);
}
static contains(bbox1, bbox2) {
if (bbox1 == bbox2) return false;
return (bbox1?.length === bbox2?.length &&
bbox1[0] < bbox2[0] &&
bbox1[1] < bbox2[1] &&
bbox1[2] > bbox2[2] &&
bbox1[3] > bbox2[3]);
}
static covers(bbox1, bbox2) {
if (bbox1 == bbox2) return true;
return (bbox1?.length === bbox2?.length &&
bbox1[0] <= bbox2[0] &&
bbox1[1] <= bbox2[1] &&
bbox1[2] >= bbox2[2] &&
bbox1[3] >= bbox2[3]);
}
static containsPoint(bbox, point) {
return Bbox.contains(bbox, [...point, ...point]);
}
static coversPoint(bbox, point) {
return Bbox.covers(bbox, [...point, ...point]);
}
static isPointOnEdge(bbox, point) {
return bbox[0] == point[0] ||
bbox[1] == point[1] ||
bbox[2] == point[0] ||
bbox[3] == point[1];
}
static width(bbox) {
return bbox[2] - bbox[0] + 1;
}
static height(bbox) {
return bbox[3] - bbox[1] + 1;
}
static dims(bbox) {
return { x: bbox[0], y: bbox[1], width: Bbox.width(bbox), height: Bbox.height(bbox) };
}
static fromDims(dims) {
return [
dims.x,
dims.y,
dims.width + dims.x - 1,
dims.height + dims.y - 1
];
}
static fromPoints(points) {
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
for (let [x, y] of points) {
if (x < x0) x0 = x;
if (x > x1) x1 = x;
if (y < y0) y0 = y;
if (y > y1) y1 = y;
}
return [x0, y0, x1, y1];
}
static corners(bbox) {
const [x0, y0, x1, y1] = bbox;
return [ [x0,y0], [x0,y1], [x1,y1], [x1,y0] ];
}
static edges(bbox) {
const [x0, y0, x1, y1] = bbox;
return {
N: [[x0,y0], [x1,y0]],
E: [[x1,y0], [x1,y1]],
S: [[x0,y1], [x1,y1]],
W: [[x0,y0], [x0,y1]]
}
}
static area(bbox) {
return Bbox.width(bbox) * Bbox.height(bbox);
}
static buffer(bbox, amount) {
return [bbox[0] - amount, bbox[1] - amount, bbox[2] + amount, bbox[3] + amount];
}
}
Insert cell
async function pause(timeout = 10) {
await new Promise(r => requestIdleCallback(r, { timeout }));
}
Insert cell
pauseIfNeeded = {
const pauseIncrement = 1000/15;
let lastPause = performance.now();

async function pauseIfNeeded(_pauseIncrement) {
_pauseIncrement ??= pauseIncrement;
if (lastPause + _pauseIncrement < performance.now()) {
await new Promise(r => requestAnimationFrame(r));
lastPause = performance.now();
}
}

return pauseIfNeeded;
}
Insert cell
Insert cell
viewof highlightSyntax = Inputs.toggle({ label: "GLSL syntax highlighting" })
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
topojson = require("topojson-client", "topojson-simplify", "topojson-server")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import { mapInfo as worldMapInfo } from "2e90991e4c98c8a7"
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
sampleTopology
Insert cell
selectedColors = {
let hexes = ["#D78C29", "#F78BB7", "#2B18E0", "#B5760C", "#553B3F"];
let kvps = hexes.map((h,i) => [ColorMask.fromHex(h), Math.max(i-2, 0)]);
return new Map(kvps);
}
Insert cell
sampleTraceResult
Insert cell
selectedTopology = {
let selectedTraceResult = {...sampleTraceResult};
selectedTraceResult.polygonRings = new Map(selectedColors.keys().map(k => [k, sampleTraceResult.polygonRings.get(k)]));
let topology = await buildTopology(selectedTraceResult, { allowExpansion: false });
for (let g of topology.objects.traced.geometries) {
g.properties.complexity = selectedColors.get(g.properties.colorNum);
}
topology.bbox = Bbox.fromPoints(topology.arcs.flat());

let uniquePoints = new PointSet(topology.arcs.flat());
topology.properties.coordTypes = { "junction": new PointSet(), "corner": new PointSet(), "intrusion": new PointSet() };
for (let [coord, type] of selectedTraceResult.coordTypes.entries()) {
if (uniquePoints.has(coord)) {
topology.properties.coordTypes[type].maybeAdd(coord);
}
}
for (let [type, set] of [...Object.entries(topology.properties.coordTypes)]) {
topology.properties.coordTypes[type] = [...set.values()];
}
return topology;
}
/*
sampleTopology = buildTopology(sampleTraceResult, { allowExpansion: false, /* wrapX: sampleOptions.wrapX /*, projection: proj });
let countries = sampleTopology.objects.traced.geometries.filter(g => keys.has(g.properties.colorNum));
let selectedArcs = new Set(countries.flatMap(c => c.arcs.flat(Infinity).map(a => (a < 0 ? ~a : a))));
return topojson.filter(sampleTopology, arcIds => arcIds.some(id => selectedArcs.has(id)));
}

/*
{
let features =

sampleTopology.objects.traced
return topojson.feature(sampleTopology, sampleTopology.objects.traced)
topojson.feature(
colorNums.map(({ colorNum, complexity }) => [colorNum, complexity, sampleGeoJson.features.find(f => f.properties.colorNum == colorNum)]);
*/
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