UNPKG

protomaps-leaflet

Version:

Vector tile rendering and labeling for [Leaflet](https://github.com/Leaflet/Leaflet).

235 lines (234 loc) 10.4 kB
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 };