protomaps-leaflet
Version:
Vector tile rendering and labeling for [Leaflet](https://github.com/Leaflet/Leaflet).
235 lines (234 loc) • 10.4 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import Point from "@mapbox/point-geometry";
import { labelRules, paintRules } from "../default_style/style";
import themes from "../default_style/themes";
import { Labelers } from "../labeler";
import { paint } from "../painter";
import { sourcesToViews } from "../view";
const timer = (duration) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, duration);
});
};
const reflect = (promise) => {
return promise.then((v) => {
return { status: "fulfilled", value: v };
}, (error) => {
return { status: "rejected", reason: error };
});
};
const leafletLayer = (options = {}) => {
class LeafletLayer extends L.GridLayer {
constructor(options = {}) {
if (options.noWrap && !options.bounds)
options.bounds = [
[-90, -180],
[90, 180],
];
if (options.attribution == null)
options.attribution =
'<a href="https://protomaps.com">Protomaps</a> © <a href="https://openstreetmap.org/copyright">OpenStreetMap</a>';
super(options);
if (options.theme) {
const theme = themes[options.theme];
this.paintRules = paintRules(theme);
this.labelRules = labelRules(theme);
this.backgroundColor = theme.background;
}
else {
this.paintRules = options.paintRules || [];
this.labelRules = options.labelRules || [];
this.backgroundColor = options.backgroundColor;
}
this.lastRequestedZ = undefined;
this.tasks = options.tasks || [];
this.views = sourcesToViews(options);
this.debug = options.debug;
const scratch = document.createElement("canvas").getContext("2d");
this.scratch = scratch;
this.onTilesInvalidated = (tiles) => {
for (const t of tiles) {
this.rerenderTile(t);
}
};
this.labelers = new Labelers(this.scratch, this.labelRules, 16, this.onTilesInvalidated);
this.tileSize = 256 * window.devicePixelRatio;
this.tileDelay = options.tileDelay || 3;
this.lang = options.lang;
}
renderTile(coords, element, key, done = () => { }) {
return __awaiter(this, void 0, void 0, function* () {
this.lastRequestedZ = coords.z;
const promises = [];
for (const [k, v] of this.views) {
const promise = v.getDisplayTile(coords);
promises.push({ key: k, promise: promise });
}
const tileResponses = yield Promise.all(promises.map((o) => {
return o.promise.then((v) => {
return { status: "fulfilled", value: v, key: o.key };
}, (error) => {
return { status: "rejected", reason: error, key: o.key };
});
}));
const preparedTilemap = new Map();
for (const tileResponse of tileResponses) {
if (tileResponse.status === "fulfilled") {
preparedTilemap.set(tileResponse.key, [tileResponse.value]);
}
else {
if (tileResponse.reason.name === "AbortError") {
// do nothing
}
else {
console.error(tileResponse.reason);
}
}
}
if (element.key !== key)
return;
if (this.lastRequestedZ !== coords.z)
return;
yield Promise.all(this.tasks.map(reflect));
if (element.key !== key)
return;
if (this.lastRequestedZ !== coords.z)
return;
const layoutTime = this.labelers.add(coords.z, preparedTilemap);
if (element.key !== key)
return;
if (this.lastRequestedZ !== coords.z)
return;
const labelData = this.labelers.getIndex(coords.z);
if (!this._map)
return; // the layer has been removed from the map
const center = this._map.getCenter().wrap();
const pixelBounds = this._getTiledPixelBounds(center);
const tileRange = this._pxBoundsToTileRange(pixelBounds);
const tileCenter = tileRange.getCenter();
const priority = coords.distanceTo(tileCenter) * this.tileDelay;
yield timer(priority);
if (element.key !== key)
return;
if (this.lastRequestedZ !== coords.z)
return;
const buf = 16;
const bbox = {
minX: 256 * coords.x - buf,
minY: 256 * coords.y - buf,
maxX: 256 * (coords.x + 1) + buf,
maxY: 256 * (coords.y + 1) + buf,
};
const origin = new Point(256 * coords.x, 256 * coords.y);
element.width = this.tileSize;
element.height = this.tileSize;
const ctx = element.getContext("2d");
if (!ctx) {
console.error("Failed to get Canvas context");
return;
}
ctx.setTransform(this.tileSize / 256, 0, 0, this.tileSize / 256, 0, 0);
ctx.clearRect(0, 0, 256, 256);
if (this.backgroundColor) {
ctx.save();
ctx.fillStyle = this.backgroundColor;
ctx.fillRect(0, 0, 256, 256);
ctx.restore();
}
let paintingTime = 0;
const paintRules = this.paintRules;
paintingTime = paint(ctx, coords.z, preparedTilemap, this.xray ? null : labelData, paintRules, bbox, origin, false, this.debug);
if (this.debug) {
ctx.save();
ctx.fillStyle = this.debug;
ctx.font = "600 12px sans-serif";
ctx.fillText(`${coords.z} ${coords.x} ${coords.y}`, 4, 14);
ctx.font = "12px sans-serif";
let ypos = 28;
for (const [k, v] of preparedTilemap) {
const dt = v[0].dataTile;
ctx.fillText(`${k + (k ? " " : "") + dt.z} ${dt.x} ${dt.y}`, 4, ypos);
ypos += 14;
}
ctx.font = "600 10px sans-serif";
if (paintingTime > 8) {
ctx.fillText(`${paintingTime.toFixed()} ms paint`, 4, ypos);
ypos += 14;
}
if (layoutTime > 8) {
ctx.fillText(`${layoutTime.toFixed()} ms layout`, 4, ypos);
}
ctx.strokeStyle = this.debug;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, 256);
ctx.stroke();
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(256, 0);
ctx.stroke();
ctx.restore();
}
done();
});
}
rerenderTile(key) {
for (const unwrappedK in this._tiles) {
const wrappedCoord = this._wrapCoords(this._keyToTileCoords(unwrappedK));
if (key === this._tileCoordsToKey(wrappedCoord)) {
this.renderTile(wrappedCoord, this._tiles[unwrappedK].el, key);
}
}
}
clearLayout() {
this.labelers = new Labelers(this.scratch, this.labelRules, 16, this.onTilesInvalidated);
}
rerenderTiles() {
for (const unwrappedK in this._tiles) {
const wrappedCoord = this._wrapCoords(this._keyToTileCoords(unwrappedK));
const key = this._tileCoordsToKey(wrappedCoord);
this.renderTile(wrappedCoord, this._tiles[unwrappedK].el, key);
}
}
createTile(coords, showTile) {
const element = L.DomUtil.create("canvas", "leaflet-tile");
element.lang = this.lang;
const key = this._tileCoordsToKey(coords);
element.key = key;
this.renderTile(coords, element, key, () => {
showTile(undefined, element);
});
return element;
}
_removeTile(key) {
const tile = this._tiles[key];
if (!tile) {
return;
}
tile.el.removed = true;
tile.el.key = undefined;
L.DomUtil.removeClass(tile.el, "leaflet-tile-loaded");
tile.el.width = tile.el.height = 0;
L.DomUtil.remove(tile.el);
delete this._tiles[key];
this.fire("tileunload", {
tile: tile.el,
coords: this._keyToTileCoords(key),
});
}
}
return new LeafletLayer(options);
};
export { leafletLayer };