Public
Edited
Aug 13, 2023
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
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
Insert cell
sampleTopology.arcs.flat().length
// 97357
Insert cell
Insert cell
JSON.stringify(sampleTopology).length
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);
const data = await readFramebufferAsync(regl, buffers.traced, rect);
return new PixelIO(rect.width, rect.height, data);
}
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
parseGpuTileResult(sampleTileGpuResult)
Insert cell
async function _gpuProcessImage(image, options) {
const allCoordTypes = new Map();
const allPixelVectors = new Map();
allPixelVectors.getOrCreate = (key) => {
let maskSet = allPixelVectors.get(key);
if (!maskSet) allPixelVectors.set(key, maskSet = new Set());
return maskSet;
}

function ingestParsedResult(parsedTileResult) {
const { coordTypes, pixelVectors } = parsedTileResult;
for (let [coordNum, coordType] of parsedTileResult.coordTypes) {
allCoordTypes.set(coordNum, coordType)
}
for (let [colorNum, tilePixelVectors] of parsedTileResult.pixelVectors) {
const allVectors = allPixelVectors.getOrCreate(colorNum);
tilePixelVectors.forEach(v => allVectors.add(v));
}
}

const { colorTable, wrapX, maxTileSize, fnOnTileProcessed } = (options ?? {});

const tileSlicer = new TileSlicerPadded(image, { wrapX, maxTileSize });
const reglProgram = getReglProgram(tileSlicer.baseTileSize, colorTable);
for (let rect of tileSlicer.getTileRects()) {
const bitmap = await tileSlicer.getTileBitmap(rect);
const gpuTileResult = await gpuProcessTile(bitmap, rect, reglProgram, { fnOnTileProcessed, wrapX });
bitmap.close();
const parsedTileResult = parseGpuTileResult(gpuTileResult);
ingestParsedResult(parsedTileResult);
}

return { allCoordTypes, allPixelVectors };
/*

reglProgram.destroy();
const polygonRings = await pixelVectorsToRings(pixelVectors, coordClasses, [tileSlicer.imageWidth, tileSlicer.imageHeight]);
const bbox = [0, 0, image.naturalWidth ?? image.width, image.naturalHeight ?? image.height];
return { bbox, polygonRings, coordClasses, wrapX: !!wrapX };
*/
}
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
Insert cell
Insert cell
sampleTopology
Insert cell
sampleTopology.arcs.flat().length
// 97012
Insert cell
async function buildTopology(traceResult, options) {
options = Object.assign({ smooth: true, allowExpansion: true, round: false }, options);
options.round ??= (options.smooth && !options.projection);
const geojson = await buildGeoJsonRaw(traceResult);
let topology = topojson.topology({traced: geojson});
topology.properties = { options };
topology.bbox = [...traceResult.bbox];
topology.arcs = topology.arcs.map(a => a.map(c => [...c]));

// Add endpoints of non-loop arcs to coordTypes as junctions
const coordTypes = traceResult.coordTypes.clone();
topology.arcs.forEach(a => {
const first = a[0];
const last = a.at(-1);
if (first[0] != last[0] || first[1] != last[1]) {
coordTypes.set(first, "junction");
coordTypes.set(last, "junction");
}
});
if (options.smooth) {
smoothTopology(topology, coordTypes, options.allowExpansion);
// Round to int
if (options.round) {
scaleTopology(topology, smoothTopology.roundFactor);
}
}

// Remove unneeded points
const simplified = topojson.presimplify(topology);
simplified.arcs.flat()
.filter(c => coordTypes.get(c) == "junction")
.forEach(c => c[2] = Infinity);
topology = topojson.simplify(simplified, 0.001);
if (options.projection) {
const proj = options.projection;
topology.arcs = topology.arcs.map(a => a.map(c => proj.invert(c)));
}
return topology;
}
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];
debugger;
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 (topology.properties?.isSmoothed) return;

topology.arcs = topology.arcs.map(a => smoothArc(a, coordTypes, allowExpansion));
topology.properties ??= {};
topology.properties.isSmoothed = true;
}

smoothTopology.roundFactor = 4;
return smoothTopology;
}
Insert cell
_smoothTopology = {
function smoothArc(arc, coordTypes) {
const arcCoordTypes = arc.map(c => coordTypes.get(c));

const smoothArc = [];
smoothArc.maybePush = function (coord) {
const prevCoord = smoothArc.at(-1);
if (prevCoord && prevCoord[0] == coord[0] && prevCoord[1] == coord[1]) return;
smoothArc.push(coord);
}

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];
maxShiftAmount = Infinity;
switch (coordType) {
case "junction": maxShiftAmount = 0; break;
case "intrusion":
case "corner": maxShiftAmount = 0.25; break;
}
if (maxShiftAmount == 0) {
smoothArc.maybePush([...coord]);
continue;
}

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;
smoothArc.maybePush(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;
smoothArc.maybePush(newCoord);
}

prevSegment = nextSegment;
}
const first = arc[0];
const last = arc.at(-1);
if (first[0] == last[0] && first[1] == last[1]) {
smoothArc.maybePush([...smoothArc[0]]);
}
delete smoothArc.maybePush;
return smoothArc;
}
function smoothTopology(topology, coordTypes) {
if (topology.properties?.isSmoothed) return;

topology.arcs = topology.arcs.map(a => smoothArc(a, coordTypes));
topology.properties ??= {};
topology.properties.isSmoothed = true;
}

smoothTopology.roundFactor = 4;
return smoothTopology;
}
Insert cell
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
class MaskMap extends Map {
constructor(maskType) {
super();
this.maskType = maskType;
this._fnFromArray = maskType.fromArray;
this._fnToArray = maskType.toArray;
this._keyValues = new Map();
this._numericMap = new Map();

for (let name of ["_fnFromArray", "_fnToArray", "_keyValues", "_numericMap"]) {
const prop = Object.getOwnPropertyDescriptor(this, name);
prop.enumerable = false;
Object.defineProperty(this, name, prop);
}
}
static around(numericMap, maskType) {
const m = new MaskMap(maskType);
m._numericMap = numericMap;
return m;
}
get numericMap() {
return this._numericMap;
}
clear() {
this._keyValues.clear();
this._numericMap.clear();
}
set(key, value) {
const numKey = this._fnFromArray(key);
this._keyValues.set(numKey, key);
this._numericMap.set(numKey, value);
return value;
}
maybeSet(key, value) {
const numKey = this._fnFromArray(key);
const currentValue = this._numericMap.get(numKey);
return currentValue ?? this.set(key, value);
}
get(key) {
const numKey = this._fnFromArray(key);
return this._numericMap.get(numKey);
}
delete(key) {
const numKey = this._fnFromArray(key);
this._keyMap.delete(numKey);
this._numericMap.delete(numKey);
}
has(key) {
const numKey = this._fnFromArray(key);
return this._numericMap.has(numKey);
}
get size() {
return this._numericMap.size;
}
*[Symbol.iterator]() {
for (let [numKey, value] of this._numericMap) {
if (!this._keyValues.has(numKey)) this._keyValues.set(numKey, this._fnToArray(numKey));
yield [this._keyValues.get(numKey), value];
}
}
entries() {
return wrapIterator('Map Iterator', this);
}
keys() {
return wrapIterator('Map Iterator', this, e => e[0]);
}
values() {
return wrapIterator('Map Iterator', this, e => e[1]);
}
forEach(callbackFn, thisArg) {
if (arguments.length > 1) throw "Not implemented"
for (let [key, value] of this) {
callbackFn(value, key, this);
}
}
clone() {
return MaskMap.around(new Map(this._numericMap), this.maskType);
}
}

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 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
Insert cell
Insert cell
Insert cell
viewof traceImageFragShader = {
let neighborsDefines = Object.entries(neighborsEncoding.values)
.map(([key, value]) => `float f${_.startCase(key)} = ${(value / 255).toFixed(32)};`)
.join("\n");
const coordDefines = Object.entries(coordTypeEncoding.values)
.map(([key, value]) => `const float f${_.startCase(key)} = ${(value / 255).toFixed(32)};`)
.join("\n");

// Starts with north and goes clockwise
const dirKeys4 = ["N", "E", "S", "W"];;
const dirKeys = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
const cAndDirKeys = ["C", ...dirKeys];
const compassPointsN = [[0,-1],[1,-1],[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1]];
const compassPoints = {
N: compassPointsN,
E: [...compassPointsN.slice(2), ...compassPointsN.slice(0, 2)],
S: [...compassPointsN.slice(4), ...compassPointsN.slice(0, 4)],
W: [...compassPointsN.slice(6), ...compassPointsN.slice(0, 6)]
}
const compassCenters = {
N: [ 0, 0],
E: [-1, 0],
S: [-1, -1],
W: [ 0, -1]
}
return highlightGlsl.fragment(`#version 300 es
precision mediump float;
// traceImageFragShader

uniform sampler2D uSampler;
uniform ivec4 mapBboxVec;
uniform ivec4 tileBboxVec;
out vec4 fragColor;



/* Common ------------------------------------------------------------------------------ */
struct bbox {
ivec2 xy0;
ivec2 xy1;
};
struct edges {
bool N;
bool E;
bool S;
bool W;
};
bbox getBbox(ivec4 mapBboxVec) {
return bbox(mapBboxVec.xy, mapBboxVec.zw);
}
bool isInsideBbox(ivec2 xy, bbox box) {
return !(any(lessThan(xy, box.xy0)) || any(greaterThan(xy, box.xy1)));
}
edges getEdges(ivec2 xy, bbox mapBbox) {
edges mapEdges = edges(false, false, false, false);

mapEdges.N = (xy.y == mapBbox.xy0.y);
mapEdges.E = (xy.x == mapBbox.xy1.x);
mapEdges.S = (xy.y == mapBbox.xy1.y);
mapEdges.W = (xy.x == mapBbox.xy0.x);
return mapEdges;
}

struct directions {
${cAndDirKeys.map(k => ` ivec2 ${k};`).join("\n").trim() /* ivec2 C, ivec2 N...NW */}
};
struct directionColors {
${cAndDirKeys.map(k => ` vec4 ${k};`).join("\n").trim() /* vec4 C, vec4 N...NW */}
};
${Object.entries(compassPoints) /* const directions directions[N..W] = ... */
.map(([key, values]) => {
const params = [compassCenters[key], ...values].map(([x,y]) => `ivec2(${x}, ${y})`).join(", ");
return `const directions directions${key} = directions(${params});`
})
.join("\n")}

directionColors readColors(ivec2 xyC, directions dirs) {
ivec2 newXyC = xyC + dirs.C;
vec4 cC = texelFetch(uSampler, newXyC, 0);
return directionColors(cC, ${dirKeys.map(dir => `texelFetch(uSampler, newXyC + dirs.${dir}, 0)`).join(", ") /* texelFetch(uSampler, newXyC + dirs.[N..NW], 0) */});
}


/* Neighbor finder --------------------------------------------------------------------- */
${neighborsDefines.trim()}

edges getNeighbors(directionColors dirColors) {
edges neighbors = edges(false, false, false, false);
neighbors.N = dirColors.C != dirColors.N;
neighbors.E = dirColors.C != dirColors.E;
neighbors.S = dirColors.C != dirColors.S;
neighbors.W = dirColors.C != dirColors.W;
return neighbors;
}

float getEncodedNeighbors(directionColors dirColors, edges mapEdges) {
if (dirColors.C.a == 0.0) return 0.0;

edges neighbors = getNeighbors(dirColors);
float encodedNeighbors = 0.0;
if (mapEdges.N || neighbors.N) encodedNeighbors += fN;
if (mapEdges.E || neighbors.E) encodedNeighbors += fE;
if (mapEdges.S || neighbors.S) encodedNeighbors += fS;
if (mapEdges.W || neighbors.W) encodedNeighbors += fW;
return encodedNeighbors;
}



/* coordType detector ------------------------------------------------------------------ */
${coordDefines.trim()}

int countUniqueColors(directionColors dirColors) {
// Count the number of unique colors in the 4px range [C, W, NW, N]
int colorCount = 1;
vec4 uniqueColor1 = dirColors.C;
vec4 uniqueColor2 = vec4(-1.0);
vec4 uniqueColor3 = vec4(-1.0);
if (dirColors.W != uniqueColor1) {
colorCount++;
uniqueColor2 = dirColors.W;
}
if (dirColors.NW != uniqueColor1 && dirColors.NW != uniqueColor2) {
colorCount++;
uniqueColor3 = dirColors.NW;
}
if (dirColors.N != uniqueColor1 && dirColors.N != uniqueColor2 && dirColors.N != uniqueColor3) {
colorCount++;
}
return colorCount;
}

/*
bool isEdgeJunction(directionColors dirColors, edges mapEdges) {
if (mapEdges.N && dirColors.C != dirColors.W) return true;
if (mapEdges.S && dirColors.N != dirColors.NW) return true;

if (!mapEdges.E && !mapEdges.W) return false;

int colorCount = countUniqueColors(dirColors);
if (colorCount == 1) return false;

edges neighbors = getNeighbors(
if (mapEdges.W) {
}

if (mapEdges.W

edges neighbors = edges(
dirColors.C.a + dirColors.W.a > 0.0,
dirColors.W.a + dirColors.SW.a > 0.0,
dirColors.N.a + dirColors.NW.a > 0.0,
dirColors.C.a + dirColors.N.a > 0.0
);
// All
if (dirColors.C == dirColors.W && dirColors.C == dirColors.NW && dirColors.C == dirColors.N) return false;

edges neighbors = edges(
false,
dirColors.W.a + dirColors.SW.a > 0.0,
false,
dirColors.C.a + dirColors.N.a > 0.0
);
return ${dirKeys4.map(dir => `(mapEdges.${dir} && neighbors.${dir})`).join(" || ")};
}
*/

bool isJunction(directionColors dirColors, edges mapEdges) {
if (mapEdges.N && dirColors.C != dirColors.W) return true;
if (mapEdges.S && dirColors.N != dirColors.NW) return true;

int colorCount = countUniqueColors(dirColors);
if (colorCount == 1) {
return false;
}
if (colorCount >= 3) {
// Standard junction: 3 distinct colors touch at this coord
return true;
}

// if (isEdgeJunction(dirColors, mapEdges)) return true;

if (colorCount == 2) {
if (mapEdges.W || mapEdges.E) {
return true;
}
if (dirColors.C == dirColors.NW && dirColors.N == dirColors.W) {
// Special case junction: checkerboard
return true;
}
}


return false;
}

bool isIntrusion(directionColors dirColors, ivec2 xyC, directions dirs) {
// Assumption: isMaybeCorner test already passed

// Horizontal check
for (int i = 1; i < 5; i++) {
ivec2 xyE = xyC + i * dirs.E;
vec4 cE = texelFetch(uSampler, xyE, 0);
vec4 cNE = texelFetch(uSampler, xyE + dirs.N, 0);
if (cNE == dirColors.C) {
// It advanced north again. This isn't an intrusion.
break;
}
if (cE != dirColors.C) {
// It retreated south without advancing north
return true;
}
}

// Vertical check
for (int i = 1; i < 5; i++) {
ivec2 xyS = xyC + i * dirs.S;
vec4 cS = texelFetch(uSampler, xyS, 0);
vec4 cSW = texelFetch(uSampler, xyS + dirs.W, 0);
if (cSW == dirColors.C) {
// It advanced west again. Not an intrusion.
break;
}
if (cS != dirColors.C) {
// It retreated east without advancing west.
return true;
}
}

return false;
}

bool isMaybeCorner(directionColors dirColors) {
return (dirColors.C != dirColors.W && dirColors.C != dirColors.NW && dirColors.C != dirColors.N);
}
bool isCorner(directionColors dirColors) {
// Assumption: isMaybeCorner test already passed
if (dirColors.C == dirColors.NE || dirColors.C == dirColors.SW) return false;
return (dirColors.C == dirColors.E && dirColors.C == dirColors.SE && dirColors.C == dirColors.S);
}

float getEncodedCoordType(ivec2 xyC, edges mapEdges) {
directions[4] allDirs = directions[4](${dirKeys4.map(k => `directions${k}`).join(", ")});
directionColors dirColorsN = readColors(xyC, directionsN);

// Edge junction test
// if (isEdgeJunction(dirColorsN, mapEdges)) return fJunction;

// Junction test
if (isJunction(dirColorsN, mapEdges)) return fJunction;

// If we're on the south or west edge of the map, only junctions matter
if (mapEdges.S || mapEdges.W) return 0.0;


// Begin more expensive tests
directionColors[4] allDirColors = directionColors[4](dirColorsN, ${d3.range(1,4).map(i => `readColors(xyC, allDirs[${i}])`).join(", ")});
bool[4] allDirMaybes = bool[4](${d3.range(0,4).map(i => `isMaybeCorner(allDirColors[${i}])`).join(", ")});

// Intrusion test
for (int i = 0; i < 4; i++) {
if (!allDirMaybes[i]) continue;
directionColors dirColors = allDirColors[i];
directions dirs = allDirs[i];
ivec2 xyCOffset = xyC + dirs.C;
if (isIntrusion(dirColors, xyCOffset, dirs)) return fIntrusion;
}

// Corner test
for (int i = 0; i < 4; i++) {
if (!allDirMaybes[i]) continue;
directionColors dirColors = allDirColors[i];
if (isCorner(dirColors)) return fCorner;
}

// Default value
return 0.0;
}



/* main -------------------------------------------------------------------------------- */
const float fOpaque = ${(colorOpacityEncoding.values[255] / 255).toFixed(32)};

void main () {
fragColor = vec4(0.0);
ivec2 xyC = ivec2(round(gl_FragCoord.xy - 0.5));
vec4 cC = texelFetch(uSampler, xyC, 0);

bbox mapBbox = getBbox(mapBboxVec);
bbox tileBbox = getBbox(tileBboxVec);

directionColors dirColorsN = readColors(xyC, directionsN);
// neighbors
float encodedNeighbors = 0.0;
edges mapEdges = getEdges(xyC, mapBbox);
if (cC.a != 0.0 && isInsideBbox(xyC, tileBbox)) encodedNeighbors = getEncodedNeighbors(dirColorsN, mapEdges);

// coordType
float encodedCoordType = 0.0;
mapBbox.xy1 += ivec2(1);
mapEdges = getEdges(xyC, mapBbox);
if (mapEdges.S || mapEdges.W) tileBbox.xy1 += ivec2(1);
if (isInsideBbox(xyC, tileBbox)) encodedCoordType = getEncodedCoordType(xyC, mapEdges);

// If there's no trace data, exit
if (encodedNeighbors + encodedCoordType == 0.0) return;

// combine
float encodedColorOpacity = 0.0;
if (encodedNeighbors > 0.0) {
fragColor.rgb = cC.rgb;
encodedColorOpacity = fOpaque;
}

fragColor.a = encodedNeighbors + encodedCoordType + encodedColorOpacity;
}`);
}
Insert cell
Insert cell
Insert cell
topojson = require("topojson-client", "topojson-simplify", "topojson-server")
Insert cell
Insert cell
idb = await import('https://unpkg.com/idb@7.1.1/with-async-ittr.js?module')
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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