protomaps-leaflet
Version:
Vector tile rendering and labeling for [Leaflet](https://github.com/Leaflet/Leaflet).
173 lines (172 loc) • 7.97 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 { Labeler } from "../labeler";
import { paint } from "../painter";
import { sourcesToViews } from "../view";
const R = 6378137;
const MAX_LATITUDE = 85.0511287798;
const MAXCOORD = R * Math.PI;
const project = (latlng) => {
const d = Math.PI / 180;
const constrainedLat = Math.max(Math.min(MAX_LATITUDE, latlng.y), -MAX_LATITUDE);
const sin = Math.sin(constrainedLat * d);
return new Point(R * latlng.x * d, (R * Math.log((1 + sin) / (1 - sin))) / 2);
};
const unproject = (point) => {
const d = 180 / Math.PI;
return {
lat: (2 * Math.atan(Math.exp(point.y / R)) - Math.PI / 2) * d,
lng: (point.x * d) / R,
};
};
const instancedProject = (origin, displayZoom) => {
return (latlng) => {
const projected = project(latlng);
const normalized = new Point((projected.x + MAXCOORD) / (MAXCOORD * 2), 1 - (projected.y + MAXCOORD) / (MAXCOORD * 2));
return normalized.mult((1 << displayZoom) * 256).sub(origin);
};
};
const instancedUnproject = (origin, displayZoom) => {
return (point) => {
const normalized = new Point(point.x, point.y)
.add(origin)
.div((1 << displayZoom) * 256);
const projected = new Point(normalized.x * (MAXCOORD * 2) - MAXCOORD, (1 - normalized.y) * (MAXCOORD * 2) - MAXCOORD);
return unproject(projected);
};
};
export const getZoom = (degreesLng, cssPixels) => {
const d = cssPixels * (360 / degreesLng);
return Math.log2(d / 256);
};
export class Static {
constructor(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.views = sourcesToViews(options);
this.debug = options.debug || "";
}
drawContext(ctx, width, height, latlng, displayZoom) {
return __awaiter(this, void 0, void 0, function* () {
const center = project(latlng);
const normalizedCenter = new Point((center.x + MAXCOORD) / (MAXCOORD * 2), 1 - (center.y + MAXCOORD) / (MAXCOORD * 2));
// the origin of the painter call in global Z coordinates
const origin = normalizedCenter
.clone()
.mult(Math.pow(2, displayZoom) * 256)
.sub(new Point(width / 2, height / 2));
// the bounds of the painter call in global Z coordinates
const bbox = {
minX: origin.x,
minY: origin.y,
maxX: origin.x + width,
maxY: origin.y + height,
};
const promises = [];
for (const [k, v] of this.views) {
const promise = v.getBbox(displayZoom, bbox);
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", value: [], reason: error, key: o.key };
});
}));
const preparedTilemap = new Map();
for (const tileResponse of tileResponses) {
if (tileResponse.status === "fulfilled") {
preparedTilemap.set(tileResponse.key, tileResponse.value);
}
}
const start = performance.now();
const labeler = new Labeler(displayZoom, ctx, this.labelRules, 16, undefined); // because need ctx to measure
const layoutTime = labeler.add(preparedTilemap);
if (this.backgroundColor) {
ctx.save();
ctx.fillStyle = this.backgroundColor;
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
const paintRules = this.paintRules;
const p = paint(ctx, displayZoom, preparedTilemap, labeler.index, paintRules, bbox, origin, true, this.debug);
if (this.debug) {
ctx.save();
ctx.translate(-origin.x, -origin.y);
ctx.strokeStyle = this.debug;
ctx.fillStyle = this.debug;
ctx.font = "12px sans-serif";
let idx = 0;
for (const [k, v] of preparedTilemap) {
for (const preparedTile of v) {
ctx.strokeRect(preparedTile.origin.x, preparedTile.origin.y, preparedTile.dim, preparedTile.dim);
const dt = preparedTile.dataTile;
ctx.fillText(`${k + (k ? " " : "") + dt.z} ${dt.x} ${dt.y}`, preparedTile.origin.x + 4, preparedTile.origin.y + 14 * (1 + idx));
}
idx++;
}
ctx.restore();
}
// TODO this API isn't so elegant
return {
elapsed: performance.now() - start,
project: instancedProject(origin, displayZoom),
unproject: instancedUnproject(origin, displayZoom),
};
});
}
drawCanvas(canvas, latlng, displayZoom, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
const dpr = window.devicePixelRatio;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
if (!(canvas.width === width * dpr && canvas.height === height * dpr)) {
canvas.width = width * dpr;
canvas.height = height * dpr;
}
if (options.lang)
canvas.lang = options.lang;
const ctx = canvas.getContext("2d");
if (!ctx) {
console.error("Failed to initialize canvas2d context.");
return;
}
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
return this.drawContext(ctx, width, height, latlng, displayZoom);
});
}
drawContextBounds(ctx, topLeft, bottomRight, width, height) {
return __awaiter(this, void 0, void 0, function* () {
const deltaDegrees = bottomRight.x - topLeft.x;
const center = new Point((topLeft.x + bottomRight.x) / 2, (topLeft.y + bottomRight.y) / 2);
return this.drawContext(ctx, width, height, center, getZoom(deltaDegrees, width));
});
}
drawCanvasBounds(canvas, topLeft, bottomRight, width, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
const deltaDegrees = bottomRight.x - topLeft.x;
const center = new Point((topLeft.x + bottomRight.x) / 2, (topLeft.y + bottomRight.y) / 2);
return this.drawCanvas(canvas, center, getZoom(deltaDegrees, width), options);
});
}
}