Public
Edited
Dec 11, 2023
6 forks
1 star
Insert cell
Insert cell
Insert cell
container = html `<div style="height:450px;"></div>`
Insert cell
grossCountry = _.sortBy(countries.features.filter(c => c.geometry.type == "Polygon"), [f => -f.geometry.coordinates.length, f => -f.geometry.coordinates.reduce((sum, r) => sum + r.length, 0)])
Insert cell
JSON.stringify(grossCountry[0])
Insert cell
path = {
const result = [];
const p = d3.path();
for (let ring of grossCountry[0].geometry.coordinates) {
p.moveTo(...ring[0]);
ring.slice(1).forEach(c => p.lineTo(...c));
p.closePath();
result.push(p._);
}
return result.join("")
}
Insert cell
turf.bbox(grossCountry[0]).join(" ")
Insert cell
path._
Insert cell
Insert cell
new MaskedPathLayer()
Insert cell
class MaskedPathLayer extends deck.PathLayer {
static {
MaskedPathLayer.layerName = 'MaskedPathLayer';
MaskedPathLayer.defaultProps = {
getVisibleColor: { type: 'accessor', value: [255, 0, 0] },
maskTexture: { type: 'image' },
};
}
initializeState(params) {
const GL = WebGL2RenderingContext;
super.initializeState(params);
this.getAttributeManager().addInstanced({
visibleColor: {
size: this.props.colorFormat.length,
type: GL.UNSIGNED_BYTE,
normalized: true,
accessor: 'getVisibleColor',
defaultValue: [255, 0, 0, 255],
shaderAttributes: {
instanceFillColors: {
divisor: 1
}
}
},
});
}
draw({ uniforms }) {
super.draw({
uniforms: {
...uniforms,
maskTexture: this.props.maskTexture
}
})
}
}
Insert cell
Insert cell
Insert cell
Insert cell
class PickingMaskPass extends deck._LayersPass {
constructor(device, props) {
super(device, props);
const { mapSize = 2048 } = props;

this.maskMap = device.createTexture({
width: mapSize,
height: mapSize,
sampler: {
minFilter: 'linear',
magFilter: 'linear',
addressModeU: 'clamp-to-edge',
addressModeV: 'clamp-to-edge'
}
});

this.fbo = device.createFramebuffer({
id: 'maskmap',
width: mapSize,
height: mapSize,
colorAttachments: [this.maskMap]
});
}

render(options) {
const colorMask = [false, false, false, false];
colorMask[options.channel] = true;
const clearColor = [255, 255, 255, 255];

const GL = WebGL2RenderingContext;
return luma.withParameters(
this.device,
{
blend: true,
blendFunc: [GL.ZERO, GL.ONE],
blendEquation: GL.FUNC_SUBTRACT,
colorMask,
depthTest: false
},
() => super.render({...options, clearColor, target: this.fbo, pass: 'pickingMask'})
);
}

shouldDrawLayer(layer) {
return layer.props.operation.includes('pickingMask');
}

delete() {
this.fbo.delete();
this.maskMap.delete();
}
}
Insert cell
PickingMaskEffect = {
const { Layer, Viewport, log } = deck;
const { Texture, Buffer } = luma;
const { equals } = mathgl;
const { joinLayerBounds, getRenderBounds, makeViewport } = ProjectionUtils;

/*
type Mask = {
index: number;
bounds: Bounds;
coordinateOrigin: [number, number, number];
coordinateSystem: CoordinateSystem;
};
type Channel = {
id: string;
index: number;
layers: Layer[];
bounds: Bounds | null;
maskBounds: Bounds;
layerBounds: Bounds[];
coordinateOrigin: [number, number, number];
coordinateSystem: CoordinateSystem;
};
export type MaskPreRenderStats = {
didRender: boolean;
};
*/
// Class to manage mask effect
class PickingMaskEffect {
id = 'mask-effect';
props = null;
useInPicking = true;
order = 0;
private dummyMaskMap?: Texture;
private channels: (Channel | null)[] = [];
private masks: Record<string, Mask> | null = null;
private maskPass?: MaskPass;
private maskMap?: Texture;
private lastViewport?: Viewport;
preRender(
device: Device,
{layers, layerFilter, viewports, onViewportActive, views, isPicking}: PreRenderOptions
): MaskPreRenderStats {
let didRender = false;
if (!this.dummyMaskMap) {
this.dummyMaskMap = device.createTexture({
width: 1,
height: 1
});
}
if (isPicking) {
// Do not update on picking pass
return {didRender};
}
const maskLayers = layers.filter(l => l.props.visible && l.props.operation.includes('mask'));
if (maskLayers.length === 0) {
this.masks = null;
this.channels.length = 0;
return {didRender};
}
this.masks = {};
if (!this.maskPass) {
this.maskPass = new MaskPass(device, {id: 'default-mask'});
this.maskMap = this.maskPass.maskMap;
}
// Map layers to channels
const channelMap = this._sortMaskChannels(maskLayers);
// TODO - support multiple views
const viewport = viewports[0];
const viewportChanged = !this.lastViewport || !this.lastViewport.equals(viewport);
if (viewport.resolution !== undefined) {
log.warn('MaskExtension is not supported in GlobeView')();
return {didRender};
}
for (const maskId in channelMap) {
const result = this._renderChannel(channelMap[maskId], {
layerFilter,
onViewportActive,
views,
viewport,
viewportChanged
});
didRender ||= result;
}
// debugFBO(this.maskMap, {opaque: true});
return {didRender};
}
/* eslint-disable-next-line complexity */
private _renderChannel(
channelInfo: Channel,
{
layerFilter,
onViewportActive,
views,
viewport,
viewportChanged
}: {
layerFilter: PreRenderOptions['layerFilter'];
onViewportActive: PreRenderOptions['onViewportActive'];
views: PreRenderOptions['views'];
viewport: Viewport;
viewportChanged: boolean;
}
): boolean {
let didRender = false;
const oldChannelInfo = this.channels[channelInfo.index];
if (!oldChannelInfo) {
return didRender;
}
const maskChanged =
// If a channel is new
channelInfo === oldChannelInfo ||
// If sublayers have changed
channelInfo.layers.length !== oldChannelInfo.layers.length ||
channelInfo.layers.some(
(layer, i) =>
// Layer instance is updated
// Layer props might have changed
// Undetermined props could have an effect on the output geometry of a mask layer,
// for example getRadius+updateTriggers, radiusScale, modelMatrix
layer !== oldChannelInfo.layers[i] ||
// Some prop is in transition
layer.props.transitions
) ||
// If a sublayer's positions have been updated, the cached bounds will change shallowly
channelInfo.layerBounds.some((b, i) => b !== oldChannelInfo.layerBounds[i]);
channelInfo.bounds = oldChannelInfo.bounds;
channelInfo.maskBounds = oldChannelInfo.maskBounds;
this.channels[channelInfo.index] = channelInfo;
if (maskChanged || viewportChanged) {
// Recalculate mask bounds
this.lastViewport = viewport;
const layerBounds = joinLayerBounds(channelInfo.layers, viewport);
channelInfo.bounds = layerBounds && getRenderBounds(layerBounds, viewport);
if (maskChanged || !equals(channelInfo.bounds, oldChannelInfo.bounds)) {
// Rerender mask FBO
const {maskPass, maskMap} = this;
const maskViewport =
layerBounds &&
makeViewport({
bounds: channelInfo.bounds!,
viewport,
width: maskMap!.width,
height: maskMap!.height,
border: 1
});
channelInfo.maskBounds = maskViewport ? maskViewport.getBounds() : [0, 0, 1, 1];
// @ts-ignore (2532) This method is only called from preRender where maskPass is defined
maskPass.render({
pass: 'mask',
channel: channelInfo.index,
layers: channelInfo.layers,
layerFilter,
viewports: maskViewport ? [maskViewport] : [],
onViewportActive,
views,
moduleParameters: {
devicePixelRatio: 1
}
});
didRender = true;
}
}
// @ts-ignore (2532) This method is only called from preRender where masks is defined
this.masks[channelInfo.id] = {
index: channelInfo.index,
bounds: channelInfo.maskBounds,
coordinateOrigin: channelInfo.coordinateOrigin,
coordinateSystem: channelInfo.coordinateSystem
};
return didRender;
}
/**
* Find a channel to render each mask into
* If a maskId already exists, diff and update the existing channel
* Otherwise replace a removed mask
* Otherwise create a new channel
* Returns a map from mask layer id to channel info
*/
private _sortMaskChannels(maskLayers: Layer[]): Record<string, Channel> {
const channelMap = {};
let channelCount = 0;
for (const layer of maskLayers) {
const {id} = layer.root;
let channelInfo = channelMap[id];
if (!channelInfo) {
if (++channelCount > 4) {
log.warn('Too many mask layers. The max supported is 4')();
continue; // eslint-disable-line no-continue
}
channelInfo = {
id,
index: this.channels.findIndex(c => c?.id === id),
layers: [],
layerBounds: [],
coordinateOrigin: layer.root.props.coordinateOrigin,
coordinateSystem: layer.root.props.coordinateSystem
};
channelMap[id] = channelInfo;
}
channelInfo.layers.push(layer);
channelInfo.layerBounds.push(layer.getBounds());
}
for (let i = 0; i < 4; i++) {
const channelInfo = this.channels[i];
if (!channelInfo || !(channelInfo.id in channelMap)) {
// The mask id at this channel no longer exists
this.channels[i] = null;
}
}
for (const maskId in channelMap) {
const channelInfo = channelMap[maskId];
if (channelInfo.index < 0) {
channelInfo.index = this.channels.findIndex(c => !c);
this.channels[channelInfo.index] = channelInfo;
}
}
return channelMap;
}
getModuleParameters(): {
maskMap: Texture;
maskChannels: Record<string, Mask> | null;
} {
return {
maskMap: this.masks ? this.maskMap! : this.dummyMaskMap!,
maskChannels: this.masks
};
}
cleanup(): void {
if (this.dummyMaskMap) {
this.dummyMaskMap.delete();
this.dummyMaskMap = undefined;
}
if (this.maskPass) {
this.maskPass.delete();
this.maskPass = undefined;
this.maskMap = undefined;
}
this.lastViewport = undefined;
this.masks = null;
this.channels.length = 0;
}
}
}
Insert cell
deckgl.deckRenderer.l
Insert cell
sdfShaders = ({
inject: {
'vs:#decl': `\
attribute vec4 instancePerimeterSegmentXyzw;
out float distanceToEdge;
`,
'vs:#main-end': `\
vec2 perimeterCommonCoord1 = project_position(instancePerimeterSegmentXyzw.xy);
vec2 perimeterCommonCoord2 = project_position(instancePerimeterSegmentXyzw.zw);

if (perimeterCommonCoord1 == perimeterCommonCoord2) {
distanceToEdge = distance(geometry.position.xy, perimeterCommonCoord1.xy);
}
else {
float x0 = geometry.position.x;
float y0 = geometry.position.y;
float x1 = perimeterCommonCoord1.x;
float y1 = perimeterCommonCoord1.y;
float x2 = perimeterCommonCoord2.x;
float y2 = perimeterCommonCoord2.y;
float numerator = (x2-x1)*(y1-y0)-(x1-x0)*(y2-y1);
float denominator = distance(perimeterCommonCoord1, perimeterCommonCoord2);
distanceToEdge = abs(numerator / denominator);
}
`,
'fs:#decl': `
varying float distanceToEdge;
`,
'fs:DECKGL_FILTER_COLOR': `\
// vec2 pixelCoord = gl_FragCoord.xy; // * pixelsPerClip;
// vec2 commonCoord = pixelCoord * commonUnitsPerPixel;
// float commonDistance = commonDistanceToSegment(commonCoord);
// float pixelDistance = commonDistance / commonUnitsPerPixel;

color.a = distanceToEdge / 255.0;
// color.a = commonCoord.x / 255.0;
// gl_FragCoord.x * pixelsPerClip.x;
// color.a = gl_FragCoord.x / 256.0;
`,
},
})
Insert cell
skel
Insert cell
SdfPolygonLayer = {
class SdfPolygonLayer extends deck.SolidPolygonLayer {
static {
SdfPolygonLayer.layerName = 'SdfPolygonLayer';
SdfPolygonLayer.defaultProps = _.cloneDeep(deck.SolidPolygonLayer.defaultProps);
SdfPolygonLayer.defaultProps.getPerimeterSegment = () => [0,0,0,0];
}

initializeState(params) {
super.initializeState(params);
this.getAttributeManager().addInstanced({
instancePerimeterSegmentXyzw: {
size: 4,
accessor: 'getPerimeterSegment'
},
});
}
getShaders(type) {
// use object.assign to make sure we don't overwrite existing fields like `vs`, `modules`...
let shaders = super.getShaders(type);
shaders = Object.assign({}, shaders, sdfShaders);
return shaders;
}
}

return SdfPolygonLayer;
}
Insert cell
deck.SolidPolygonLayer.defaultProps
Insert cell
MaskedGeoJsonLayer = {

const MASK_LAYER_DEFAULT_PROPS = _.cloneDeep(deck.SolidPolygonLayer.defaultProps);
delete MASK_LAYER_DEFAULT_PROPS.getLineColor;
const MASK_LAYER_MAPPING = {
type: deck.SolidPolygonLayer,
props: {
extruded: 'extruded',
filled: 'filled',
wireframe: 'wireframe',
elevationScale: 'elevationScale',
material: 'material',
_full3d: '_full3d',
getElevation: 'getElevation',
}
};

function forwardProps(layer, mapping) {
const {transitions, updateTriggers} = layer.props;
const result = {
updateTriggers: {},
transitions: transitions && {
getPosition: transitions.geometry
}
};
for (const sourceKey in mapping) {
const targetKey = mapping[sourceKey];
let value = layer.props[sourceKey];
if (sourceKey.startsWith('get')) {
// isAccessor
value = layer.getSubLayerAccessor(value);
result.updateTriggers[targetKey] = updateTriggers[sourceKey];
if (transitions) {
result.transitions[targetKey] = transitions[sourceKey];
}
}
result[targetKey] = value;
}
return result;
}
class MaskedGeoJsonLayer extends deck.GeoJsonLayer {
static {
MaskedGeoJsonLayer.layerName = 'MaskedGeoJsonLayer';
MaskedGeoJsonLayer.defaultProps = _.cloneDeep(deck.GeoJsonLayer.defaultProps);;
MaskedGeoJsonLayer.defaultProps._subLayerProps = {
"polygons-stroke": {
extensions: [ new deck.MaskExtension() ],
maskByInstance: false,
maskId: "polygons-mask"
},
"polygons-mask": {
operation: 'mask',
}
}
}
_renderMaskLayer() {
const {extruded, wireframe} = this.props;
const {layerProps} = this.state;
const id = 'polygons-mask';
const PolygonMaskLayer =
this.shouldRenderSubLayer(id, layerProps.polygons?.data) &&
this.getSubLayerClass(id, MASK_LAYER_MAPPING.type);
if (PolygonMaskLayer) {
const forwardedProps = forwardProps(this, MASK_LAYER_MAPPING.props);
return new PolygonMaskLayer(
forwardedProps,
this.getSubLayerProps({
id,
updateTriggers: forwardedProps.updateTriggers
}),
layerProps.polygons
);
}
return null;
}
renderLayers() {
debugger;
const {extruded} = this.props;
const polygonFillLayer = this._renderPolygonLayer();
const lineLayers = this._renderLineLayers();
const pointLayers = this._renderPointLayers();
const maskLayer = (lineLayers || pointLayers) ? this._renderMaskLayer() : null;
/*
const nonFillLayers = [lineLayers, pointLayers].filter(a => a).flat();
nonFillLayers.forEach(l => {
if (l.extensions?.length) {
debugger;
}
else {
const extension = new deck.MaskExtension();
extension.initializeState.call(l, l.context, extension);
l.extensions.push(extension);
l.setChangeFlags({ extensionsChanged: true });
l.maskId = 'polygons-mask';
}
});*/
return [
// If not extruded: flat fill layer is drawn below outlines
!extruded && polygonFillLayer,
maskLayer,
lineLayers,
pointLayers,
// If extruded: draw fill layer last for correct blending behavior
extruded && polygonFillLayer
];
}
}

return MaskedGeoJsonLayer;
}
Insert cell
spl = new deck.SolidPolygonLayer()
Insert cell
class PolygonBorderLayer extends deck.SolidPolygonLayer {
static {
PolygonBorderLayer.layerName = 'PolygonBorderLayer';
PolygonBorderLayer.POSITIONS = {
CENTER: 0,
INSIDE: 1,
OUTSIDE: 2
}
PolygonBorderLayer.defaultProps = _.cloneDeep(deck.PathLayer.defaultProps);
PolygonBorderLayer.defaultProps.getPosition = { type: 'accessor', value: 0, min: 0, max: 2 };
updateTriggers: {
getFillColor: [maleColor, femaleColor]
}
}

}
Insert cell
Insert cell
class FeaturesLayer extends deck.CompositeLayer {
static {
FeaturesLayer.layerName = "FeaturesLayer";
FeaturesLayer.defaultProps = Object.assign({}, deck.GeoJsonLayer.defaultProps, {
stroked: true,
filled: true,
extruded: false,
// wrapLongitude: true,
autoHighlight: true,
lineWidthUnits: 'common',
lineJointRounded: true,
lineWidthMinPixels: 1,
lineWidthMaxPixels: 10,
getLineWidth: 1,
getFillColor: f => [128, 128, 128],
getLineColor: f => [255, 0, 0],
getOffset: f => 0.5,
extensions: [new deck.PathStyleExtension({offset: true})],
getFeatureId: f => f.properties.name
});
}
getForwardedProps() {
const forwarded = { updateTriggers: {} };
for (let key of Object.keys(deck.GeoJsonLayer.defaultProps)) {
if (key in this.props) forwarded[key] = this.props[key];
if (key in this.props.updateTriggers) forwarded.updateTriggers[key] = this.props.updateTriggers[key];
}
return forwarded;
}
renderLayers() {
const layers = [];
for (let feature of this.props.data ?? []) {
const id = this.props.getFeatureId(feature);
const layer = new deck.GeoJsonLayer(this.getForwardedProps(), this.getSubLayerProps({ id }), { data: feature });
layers.push(layer);
}
return layers;
}
}
Insert cell
feature = countries.features[0]
Insert cell
countries = fetch('https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_scale_rank.geojson')
.then(resp => resp.json())
.then(geo => d3.geoStitch(geo))
.then(result => {
// Join results
const countries = new Map();
for (const feature of result.features) {
const name = feature.properties.sr_subunit;
if (!countries.has(name)) countries.set(name, []);
countries.get(name).push(feature);
}
const features = Array.from(countries.entries())
.map(([name, features], i) => {
// const color = [55 + i / 200, 55 + i % 200, 128];
return (features.length === 1)
? turf.polygon(features[0].geometry.coordinates, {name})
: turf.multiPolygon(features.map(f => f.geometry.coordinates), {name});
// turf.coordEach(feature, c => c[2] = i);
});
const fc = turf.featureCollection(features);
return d3.geoStitch(fc);
});
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