Published
Edited
May 26, 2020
2 forks
28 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
painters = parsedStyle.layers.map(tileStencil.getStyleFuncs).map(layer => {
let source = parsedStyle.sources[layer.source];
let tileSize = source ? source.tileSize : 512;
return tilePainter.initMapPainter({
context: map.context,
styleLayer: layer,
spriteObject: parsedStyle.spriteData,
tileSize
});
})
Insert cell
Insert cell
function drawLayers(ctx) {
ctx.clearRect(0, 0, width, reactiveHeight);
const tilesets = sources.getTilesets([width, reactiveHeight], transform);
const zoom = Math.log2(transform.k) - 9;

painters.forEach(painter => {
if (zoom < painter.minzoom || painter.maxzoom < zoom) return;
drawLayer(painter, zoom, tilesets[painter.source]);
});
}
Insert cell
Insert cell
Insert cell
function drawLayer(painter, zoom, tileset) {
// No tiles for background layers
if (!tileset) return painter({ zoom });

for (const tileBox of tileset) {
if (!tileBox) continue;

let position = {
x: (tileBox.x + tileset.translate[0]) * tileset.scale,
y: (tileBox.y + tileset.translate[1]) * tileset.scale,
w: tileset.scale
};

painter({
source: tileBox.tile.data,
position,
crop: { x: tileBox.sx, y: tileBox.sy, w: tileBox.sw },
zoom,
boxes: []
});
}
}
Insert cell
Insert cell
Insert cell
sources = initSources(parsedStyle)
Insert cell
function initSources(style) {
const queue = chunkedQueue.init();
const { sources, layers } = style;
const getters = {};
const workerMonitors = [];
const listenerDiv = DOM.element("div");

Object.entries(sources).forEach(([key, source]) => {
let loader =
source.type === "vector"
? initVectorLoader(key, source)
: initRasterLoader(source);
let tileFactory = buildFactory(source, loader, listenerDiv);
getters[key] = initSource({ source, tileFactory });
});

function initVectorLoader(key, source) {
let subset = layers.filter(
l => l.source === key && l.type !== "fill-extrusion"
);
let loader = tileMixer.initTileMixer({ source, layers: subset, queue });
workerMonitors.push(loader.workerTasks);
return loader;
}

function getTilesets(viewpt, transfm) {
const tilesets = {};
Object.entries(getters).forEach(([key, getter]) => {
tilesets[key] = getter.getTiles(viewpt, transfm);
});
queue.sortTasks();
return tilesets;
}

return {
getTilesets,
workerTasks: () =>
workerMonitors.reduce((sum, counter) => sum + counter(), 0),
queuedTasks: () => queue.countTasks()
};
}
Insert cell
Insert cell
function initSource({ source, tileFactory }) {
const { type, tileSize = 512, minzoom = 0, maxzoom = 30 } = source;
const tileCache = tileRack.initCache(tileSize, tileFactory);
var numTiles = 0;

// Set up the tile layout
const layout = d3tileAlternate
.tile()
.tileSize(tileSize * Math.sqrt(2)) // Don't let d3-tile squeeze the tiles
.maxZoom(maxzoom)
.clampX(false); // Allow panning across the antimeridian

function getTiles(viewport, transform) {
// Get the grid of tiles needed for the current viewport
// TODO: Don't order tiles if they won't be displayed at the current zoom!!
layout.size(viewport);
let tiles = layout(transform);
let metric = getTileMetric(layout, tiles, 1.5);
let dropCondition = tile => metric(tile) > 0.9;

// Retrieve a tile box for every tile in the grid
const grid = tiles.map(([x, y, z]) => {
const [xw, yw, zw] = d3.tileWrap([x, y, z]);
const box = tileCache.retrieve([zw, xw, yw], dropCondition);
if (!box) return;
// Add tile indices to returned box
return Object.assign(box, { x, y, z });
});

// Avoid seams between tiles: round coordinate transform to the nearest pixel
// TODO: Problematic if different sources have different tile sizes!
let tilePixels = Math.round(tiles.scale * devicePixelRatio);
grid.scale = tilePixels / devicePixelRatio;
grid.translate = tiles.translate.map(
x => Math.round(x * tilePixels) / tilePixels
);

// Prune the cache
numTiles = tileCache.trim(metric, 0.9);

return grid;
}

return { getTiles, numTiles: () => numTiles };
}
Insert cell
function buildFactory(source, loader, listenerDiv) {
const {
minzoom = 0,
maxzoom = 30,
bounds = [-180, -90, 180, 90],
scheme = "xyz"
} = source;

// Convert bounds to Web Mercator (the projection ASSUMED by tilejson-spec)
let [xmin, ymax] = projMercator.lonLatToXY([], bounds.slice(0, 2));
let [xmax, ymin] = projMercator.lonLatToXY([], bounds.slice(2, 4));
if (scheme === "tms") [ymin, ymax] = [ymax, ymin];

return { create };

function create(z, x, y) {
// Exit if out of bounds
if (z < minzoom || maxzoom < z) return;
let zFac = 1 / 2 ** z;
if ((x + 1) * zFac < xmin || x * zFac > xmax) return;
if ((y + 1) * zFac < ymin || y * zFac > ymax) return;

let id = [z, x, y].join("/");
const tile = { z, x, y, id, priority: 0 };

function callback(err, data) {
if (err) return; // console.log(err);
tile.data = data;
tile.rendered = true; // Should be .ready -- requires change to tile-rack
listenerDiv.dispatchEvent(new Event("tileLoaded"));
}

const getPriority = () => tile.priority;
const loadTask = loader.request({ z, x, y, getPriority, callback });

tile.cancel = () => {
loadTask.abort();
tile.canceled = true;
};

return tile;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3
.select(map.context.canvas)
.call(zoomer)
.call(zoomer.transform, mutable transform)
Insert cell
zoomer = d3
.zoom()
.scaleExtent([1 << 10, 1 << 26])
.extent([[0, 0], [width, reactiveHeight]])
.translateExtent([[-Infinity, -0.5], [Infinity, 0.5]])
.on("zoom", () => {
mutable transform = d3.event.transform;
})
Insert cell
mutable transform = d3.zoomIdentity
.translate(projection([0, 0])[0], projection([0, 0])[1])
.scale(projection.scale() * 2 * Math.PI)
Insert cell
projection = d3
.geoMercator()
.center([-73.886, 40.745])
.scale(Math.pow(2, 18) / (2 * Math.PI))
.translate([width / 2, reactiveHeight / 2])
Insert cell
Insert cell
parsedStyle = tileStencil.loadStyle(styleDoc)
Insert cell
styleDoc = {
//let doc = await FileAttachment("klokantech-basic-style.json").json();
let doc = await styleChoice.file.json();

const maptilerKey = "mrAq6zQEFxOkanukNbGm"; // Get your own key: maptiler.com
Object.values(doc.sources).forEach(src => {
if (src.url) src.url = src.url.replace(/{key}/, maptilerKey);
});

return doc;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3tileAlternate = {
// Alternate version of d3-tile, that adds maxZoom behavior
const response = await fetch(
"https://raw.githubusercontent.com/GlobeletJS/d3-tile/master/dist/d3-tile.min.js"
);
const blob = await response.blob();
return require(URL.createObjectURL(blob)).catch(() => window.tileMaxZoom);
}
Insert cell
import { getTileMetric } from "@jjhembd/map-tile-priorities"
Insert cell
d3 = require("d3@5", "d3-geo@1", "d3-tile@1")
Insert cell
tileRack = import("tile-rack@0.2.2")
Insert cell
tilePainter = import("tile-painter@0.3.4")
Insert cell
chunkedQueue = import("chunked-queue@0.1.2")
Insert cell
tileMixer = import("tile-mixer@0.0.8")
Insert cell
tileStencil = import('tile-stencil@0.2.1')
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