Published
Edited
Apr 17, 2020
2 stars
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
{
// Create a parser instance
const parser = new pbftile.PbfTileParser();
// This handler will generate GeoJson Features from individual handler calls.
class GeoJsonHandler extends pbftile.TileHandler {
constructor() {
super();
this.info = {};
}

beginLayer(layer) { this.info[layer.name] = []; }

// Points
onPoint(x, y, feature, layer) {
this.info[layer.name].push({
type : 'Feature',
geometry : {
type : 'Point',
coordinates : [x, y]
},
properties : feature.properties
});
}

// Lines
beginLine(feature, layer) { this.line = []; }
onLinePoint(x, y, feature, layer) { this.line.push([x, y]); }
endLine(feature, layer) {
if (!this.line.length) return ;
this.info[layer.name].push({
type : 'Feature',
geometry : {
type : 'LineString',
coordinates : this.line
},
properties : feature.properties
});
}

// Polygons
beginPolygon(feature, layer) { this.polygon = []; }
beginPolygonRing(feature, layer) { this.line = []; }
onPolygonPoint(x, y, feature, layer) { this.line.push([x, y]); }
endPolygonRing(feature, layer) { if (this.line.length) this.polygon.push(this.line); }
endPolygon(feature, layer) {
if (!this.polygon.length) return ;
this.info[layer.name].push({
type : 'Feature',
geometry : {
type : 'Polygon',
coordinates : this.polygon
},
properties : feature.properties
});
}
}
// Instantiate our handler
const handler = new GeoJsonHandler();
// Parse the tile
parser.parseTile(tileBlob, handler);
// Return the generated object with features separated by layers
return handler.info;
}
Insert cell
Insert cell
riverTileSize=256
Insert cell
riverRenderCommands = {
// Create a parser instance
const parser = new pbftile.PbfTileParser();

const commands = [];
const riverColor = "#9ecae1";
// const riverBorderColor = "#3182bd";
const provider = (layer) => {

// Return different handlers for 'water' and 'waterway'
if (layer.name === 'water') {
return (feature, layer) => {
// Handler only rivers
if (feature.properties.class !== 'river') return ;
return (geometryType, feature, layer) => {
// Handle only polygons here
if (geometryType !== 'Polygon') return ;
return {
beginPolygon(feature, layer) { commands.push(['beginPath']); },
beginPolygonRing(feature, layer) { this.start = true; },
onPolygonPoint(x, y, feature, layer) {
commands.push([this.start ? 'moveTo' : 'lineTo', x, y]);
this.start = false;
},
endPolygon(feature, layer) {
commands.push(['closePath']);
// Add a command to define styles
commands.push(['setStyle', {
// Styles for lines
strokeStyle : riverColor, lineWidth : 1, lineJoin : 'round', lineCap : 'round',
// Styles for filling
fillStyle : riverColor
}]);
commands.push(['fill']);
commands.push(['stroke']);
}
}
}
}
}
if (layer.name === 'waterway') {
return (feature, layer) => {
// Handler only rivers
if (feature.properties.class !== 'river') return ;
return (geometryType, feature, layer) => {
// Handle only lines here
if (geometryType !== 'LineString') return ;
return {
beginLine(feature, layer) { commands.push(['beginPath']); this.start = true; },
onLinePoint(x, y, feature, layer) {
commands.push([this.start ? 'moveTo' : 'lineTo', x, y]);
this.start = false;
},
endLine(feature, layer) {
// Add a command to define styles
commands.push(['setStyle', {
// Styles for lines
strokeStyle : riverColor, lineWidth : 3, lineJoin : 'round', lineCap : 'round'
}]);
commands.push(['stroke']);
}
}
}
}
}
}
let handler;
// Dispatch calls to river handlers:
handler = new pbftile.DispatchHandler({ provider });
// Transform coordinates from tile extent bounding box to our tile size:
handler = new pbftile.CoordsTransform({ handler, tileSize : riverTileSize });
// Parse the tile
parser.parseTile(tileBlob, handler);
// Return the generated list of commands to apply to a canvas
return commands;
}
Insert cell
Insert cell
{
const tileSize = riverTileSize;
const context = DOM.context2d(tileSize, tileSize, 1);
context.fillStyle = '#eee';
context.fillRect(0, 0, tileSize, tileSize);
riverRenderCommands.forEach(([command, ...args]) => {
// We need to handle the "setStyle" command separately.
if (command === 'setStyle') {
const style = {...args[0]};
context.lineWidth = (style.strokeStyle) ? (+style.lineWidth || 1) : 0;
delete style.lineWidth;
// Copy all parameters from the style to the canvas
Object.assign(context, style);
} else {
context[command](...args);
}
});
return context.canvas;
}
Insert cell
Insert cell
Insert cell
Insert cell
// This method transform styles to a sequence of commands to apply to canvas contexts.
function prepareStyles(styles, commands = []) {

let lastStyle, lastStyleCommands, lastDrawCommands;
const setCurrentStyle = (style) => {
if (!style) return false;
if (lastStyle !== style) {
lastStyle = style;
const commands = getStyleCommands(style);
lastStyleCommands = commands[0];
lastDrawCommands = commands[1];
}
return lastDrawCommands && lastDrawCommands.length > 0;
}

return Object.assign((layer) => {
const layerStyles = styles[layer.name] || styles['default'];
if (!layerStyles) return ;

return (feature, layer) => {
// Apply filters if they are defined in the layer
if (layerStyles.filter && !layerStyles.filter(feature, layer)) return ;

return (geometryType, feature, layer) => {
if (geometryType === 'Point') {
const pointStyle = layerStyles.points;
if (!setCurrentStyle(pointStyle)) return ;
const radius = pointStyle.radius || 1;
commands.push(...lastStyleCommands);
return {
onPoint(x, y, feature, layer) {
commands.push(['beginPath'], ['arc', x, y, radius, 0, 2 * Math.PI], ['closePath']);
commands.push(...lastDrawCommands);
}
}
}
if (geometryType === 'LineString') {
const lineStyle = layerStyles.lines;
if (!setCurrentStyle(lineStyle)) return ;
commands.push(...lastStyleCommands);
return {
beginLine(feature, layer) {
commands.push(['beginPath']);
this.start = true;
},
onLinePoint(x, y, feature, layer) {
commands.push([this.start ? 'moveTo' : 'lineTo', x, y]);
this.start = false;
},
endLine(feature, layer) {
commands.push(...lastDrawCommands); }
}
}
if (geometryType === 'Polygon') {
const polygonStyle = layerStyles.polygons;
if (!setCurrentStyle(polygonStyle)) return ;
commands.push(...lastStyleCommands);
return {
beginPolygon(feature, layer) { commands.push(['beginPath']); },
beginPolygonRing(feature, layer) { this.start = true; },
onPolygonPoint(x, y, feature, layer) {
commands.push([this.start ? 'moveTo' : 'lineTo', x, y]);
this.start = false;
},
endPolygon(feature, layer) {
commands.push(['closePath']);
commands.push(...lastDrawCommands);
}
}
}
}
}
}, { commands });

function getStyleCommands(style) {
const styleCommands = [];
const drawCommands = [];
let styleArgs = {};
let lineDash = [];
for (let [key, value] of Object.entries(style)) {
if (key === 'lineDash') { lineDash = value; }
if (key === 'id' || key === 'radius') continue;
else { styleArgs[key] = value; }
}
let fill = !!styleArgs.fillStyle;
let stroke = false;
if (styleArgs.strokeStyle && styleArgs.lineWidth !== 0) {
styleArgs.lineWidth = styleArgs.lineWidth || 1; // Set 1 if lineWidth is not defined
styleCommands.push(['setLineDash', lineDash]);
stroke = true;
}
if (fill || stroke) { styleCommands.push(['setStyle', styleArgs]); }
if (fill) drawCommands.push(['fill']);
if (stroke) drawCommands.push(['stroke']);
return [styleCommands, drawCommands];
}

}
Insert cell
Insert cell
function applyCommands(commands, context, params = {}) {
commands.forEach(([command, ...args]) => {
if (command === 'setStyle') {
let style = args[0];
if (!style) return ;
const paramsPrefix = `$params.`;
for (let [key, value] of Object.entries(style)) {
if (typeof value === 'function') { value = value(params, style, context); }
if (typeof value === 'string' && value.indexOf(paramsPrefix) === 0) {
value = value.substring(paramsPrefix.length)
value = params[value];
}
if (value) context[key] = value;
}
}
else context[command](...args);
});
}
Insert cell
Insert cell
function compileCommands(commands) {
let code = ['params = params || {};'];
commands.forEach(([command, ...args]) => {
if (command === 'setStyle') {
const style = args[0];
for (let [key, value] of Object.entries(style)) {
if (typeof value === 'string' && value[0] === '$') value = value.substring(1);
else value = JSON.stringify(value);
code.push(`context.${key} = ${value};`);
}
} else {
code.push(`context.${command}(${args.map(JSON.stringify).join(',')});`);
}
});
const f = new Function(['context', 'params'], `${code.join('\n')}`);
return f;
}
Insert cell
class StyleHandler extends pbftile.DispatchHandler {
constructor(options) {
const commands = [];
const provider = prepareStyles(options.styles, commands);
super({ ...options, provider });
this.commands = commands;
}
compile() { return compileCommands(this.commands); }
apply(context, params) { return applyCommands(this.commands, context, params); }
}
Insert cell
Insert cell
params = ({
routeColor : 'red'
})
Insert cell
Insert cell
function applySimpleStylesToCanvas(context, styles, log = console.log) {
const tileSize = context.canvas.width;
const parser = new pbftile.PbfTileParser();
const handler = new StyleHandler({ styles });
const tranform = new pbftile.CoordsTransform({ tileSize, handler });

// Step 1: generate commands to apply to canvas:
{
const start = Date.now();
parser.parseTile(tileBlob, tranform);
const end = Date.now();
log(`Step 1: commands generation: ${end - start}ms`);
}

// // Step 2: compile commands to the canvas
// let f;
// {
// const start = Date.now();
// f = handler.compile();
// const end = Date.now();
// log(`Step 2: compile commands to canvas: ${end - start}ms`);
// }
// // Step 3: execute generated function to the canvas
// {
// const start = Date.now();
// f(context, params);
// const end = Date.now();
// log(`Step 3: execute generated function to the canvas: ${end - start}ms`);
// }
// Step 2: apply generated commands to the canvas
{
const start = Date.now();
handler.apply(context, params);
const end = Date.now();
log(`Step 2: apply generated commands to the canvas: ${end - start}ms`);
}
return context.canvas;
}

Insert cell
Insert cell
function newTileContext() {
const tileSize = width;
// const tileSize = 256;
const w = tileSize, h = tileSize;
const context = DOM.context2d(w, h, 1);
return context;
}
Insert cell
Insert cell
simpleStyle=({
transportation : {
filter : (feature) => true, // Optional filter. All features are accepted
lines : { strokeStyle : 'gray', lineWidth : 2, lineJoin : 'round' },
polygons : { strokeStyle : 'silver', lineWidth : 1, lineJoin : 'round', fillStyle : 'black' },
points : { },
},
})
Insert cell
applySimpleStylesToCanvas(newTileContext(), simpleStyle, (msg) => console.log(`[SimpleStyle] ${msg}`));

Insert cell
complexStyle=({
transportation : {
filter : (feature) => feature.properties.class === 'rail', // Show only rails
lines : { strokeStyle : 'gray', lineWidth : 2, lineJoin : 'round' },
},
poi : {
points : { strokeStyle : 'maroon', lineWidth : 1, lineJoin : 'round', fillStyle : 'red', radius : 5 }
},
building : {
filter : (feature) => true, // Optional filter. All features are accepted
polygons : { strokeStyle : 'gray', lineWidth : 1, lineJoin : 'round', fillStyle : 'orange' },
},
})
Insert cell
applySimpleStylesToCanvas(newTileContext(), complexStyle, (msg) => console.log(`[ComplexStyle] ${msg}`));
Insert cell
Insert cell
Insert cell
function drawTile({
tile = tileBlob,
layers,
tileSize,
shiftX = 0,
shiftY = 0,
log = console.log
}) {
const parser = new pbftile.PbfTileParser();
layers = Array.isArray(layers) ? layers : [layers];

// Get list of style layers
const styleHandlers = layers.map(styles => new StyleHandler({ styles }));
// Dispatch individual calls to multiple layers
const handler = new pbftile.CompositeTileHandler(styleHandlers);
// Transform coordinates before sending them to underlying listeners
const tranform = new pbftile.CoordsTransform({ tileSize, handler, shiftX, shiftY });

const start = Date.now();
parser.parseTile(tile, tranform);
const end = Date.now();
log(`Commands generation: ${end - start}ms`);

return handler.map(({ handler }) => handler);
}
Insert cell
// See https://observablehq.com/@d3/color-schemes
palette = ["#6e40aa","#bf3caf","#fe4b83","#ff7847","#e2b72f","#aff05b","#52f667","#1ddfa3","#23abd8","#4c6edb","#6e40aa"];

Insert cell
async function* applyStylesToCanvas(context, layers, log = console.log) {
const tileSize = context.canvas.width * 0.75;
const delta = (context.canvas.width - tileSize) / 2;
const l = log;
const logView = html`<pre />`;
log = (msg) => {
logView.innerHTML += msg + "\n";
l(msg);
}
const layerStyles = drawTile({
tileSize,
log : (msg) => log(`Step1: ${msg}`),
layers,
shiftX : delta,
shiftY : delta
});
let compile = !!tileConfig.compile;
let draw;
if (compile) {
// Step 2: compile commands to the canvas
const start = Date.now();
const commands = layerStyles.map((s) => s.compile());
draw = (context, params) => commands.forEach(c => c(context, params));
const end = Date.now();
log(`Step 2: compile commands to canvas: ${end - start}ms`);
} else {
draw = (context, params) => layerStyles.forEach((s) => s.apply(context, params));
}

log(`Size of the full generated style: ${layerStyles.map(JSON.stringify).join('\n').length}`);
// Step 3: execute generated function to the canvas
let counter = 1;
function redraw(params) {
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
const start = Date.now();
draw(context, params);
const end = Date.now();
log(`- Drawing ${counter++}: apply commands to the canvas: ${end - start}ms`);
}
let N = 10;
const min = 32;
const max = 255;
for (let i = 0; i < N; i++) {
logView.innerHTML = '';
redraw({ routeColor : palette[i % palette.length] });
yield html`<div>
${context.canvas}
${logView}
</div>`;
await Promises.delay(500);
}
// return html`<div>
// ${context.canvas}
// ${logView}
// </div>`;
}
Insert cell
Insert cell
multilayerRoadsStyle=([{
// Outlines for all transport lines except rails
transportation : {
filter : (feature) => feature.properties.class !== 'rail',
lines : { strokeStyle : 'black', lineWidth : 4, lineJoin : 'round', lineCap : 'round' },
}
}, {
// All transport lines except rails
transportation : {
filter : (feature) => feature.properties.class !== 'rail',
lines : { strokeStyle : '$params.routeColor', lineWidth : 2, lineJoin : 'round' },
}
}, {
// Only rails outline - draw them on top
transportation : {
filter : (feature) => feature.properties.class === 'rail', // Show only rails
lines : { strokeStyle : 'black', lineWidth : 4, lineJoin : 'round' },
}
}, {
// Only rails main line - draw them on top
transportation : {
filter : (feature) => feature.properties.class === 'rail', // Show only rails
lines : { strokeStyle : 'yellow', lineWidth : 1.5, lineJoin : 'round', lineDash : [5, 3] },
}
}])
Insert cell
applyStylesToCanvas(newTileContext(), multilayerRoadsStyle, (msg) => console.log(`[CompositeHandler] ${msg}`));
Insert cell
multilayerFullStyle=([
{
default : { // This style is applied to all layers
// filter : (feature, layer) => true,
polygons : { fillStyle : '#eee', strokeStyle : 'silver', lineWidth : 0.5 },
},
landuse : { polygons : { fillStyle : 'silver' } },
landcover : { polygons : { fillStyle : '#eee' } },
waterway : {
lines : {
lineWidth : 3, strokeStyle : 'blue', lineJoin : 'round', lineCap : 'round', lineDash : [5, 12, 1, 12]
},
polygons : { fillStyle : 'blue' }
},
water : {
polygons : { fillStyle : 'cyan' },
}
},
...multilayerRoadsStyle,
{
building : {
polygons : { fillStyle : 'orange', strokeStyle : 'black', lineWidth : 0.5 }
},
}, {
poi : {
points : { fillStyle : 'red', strokeStyle : 'maroon', lineWidth : 2, radius : 6 }
},
place : {
points : { fillStyle : 'rgba(0,0,255,0.4)', strokeStyle : 'rgba(255,255,255,0.8)', lineWidth : 3, radius : 20 }
}
}
])
Insert cell
applyStylesToCanvas(newTileContext(), multilayerFullStyle, (msg) => console.log(`[FullLayersHandler] ${msg}`));
Insert cell
Insert cell
Protobuf = require('pbf');
Insert cell
VectorTile = (await require("https://bundle.run/@mapbox/vector-tile@1.3.1")).VectorTile

Insert cell
import {form} from "@mbostock/form-input"
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