protomaps-leaflet
Version:
Vector tile rendering and labeling for [Leaflet](https://github.com/Leaflet/Leaflet).
280 lines (279 loc) • 10.1 kB
JavaScript
import { CenteredTextSymbolizer, CircleSymbolizer, LineLabelSymbolizer, LineSymbolizer, PolygonSymbolizer, exp, } from "./../symbolizer";
function number(val, defaultValue) {
return typeof val === "number" ? val : defaultValue;
}
export function filterFn(arr) {
// hack around "$type"
if (arr.includes("$type")) {
return (z) => true;
}
if (arr[0] === "==") {
return (z, f) => f.props[arr[1]] === arr[2];
}
if (arr[0] === "!=") {
return (z, f) => f.props[arr[1]] !== arr[2];
}
if (arr[0] === "!") {
const sub = filterFn(arr[1]);
return (z, f) => !sub(z, f);
}
if (arr[0] === "<") {
return (z, f) => number(f.props[arr[1]], Infinity) < arr[2];
}
if (arr[0] === "<=") {
return (z, f) => number(f.props[arr[1]], Infinity) <= arr[2];
}
if (arr[0] === ">") {
return (z, f) => number(f.props[arr[1]], -Infinity) > arr[2];
}
if (arr[0] === ">=") {
return (z, f) => number(f.props[arr[1]], -Infinity) >= arr[2];
}
if (arr[0] === "in") {
return (z, f) => arr.slice(2, arr.length).includes(f.props[arr[1]]);
}
if (arr[0] === "!in") {
return (z, f) => !arr.slice(2, arr.length).includes(f.props[arr[1]]);
}
if (arr[0] === "has") {
return (z, f) => f.props.hasOwnProperty(arr[1]);
}
if (arr[0] === "!has") {
return (z, f) => !f.props.hasOwnProperty(arr[1]);
}
if (arr[0] === "all") {
const parts = arr.slice(1, arr.length).map((e) => filterFn(e));
return (z, f) => parts.every((p) => {
return p(z, f);
});
}
if (arr[0] === "any") {
const parts = arr.slice(1, arr.length).map((e) => filterFn(e));
return (z, f) => parts.some((p) => {
return p(z, f);
});
}
console.log("Unimplemented filter: ", arr[0]);
return (f) => false;
}
export function numberFn(obj) {
if (obj.base && obj.stops) {
return (z) => {
return exp(obj.base, obj.stops)(z - 1);
};
}
if (obj[0] === "interpolate" &&
obj[1][0] === "exponential" &&
obj[2][0] === "zoom") {
const slice = obj.slice(3);
const stops = [];
for (let i = 0; i < slice.length; i += 2) {
stops.push([slice[i], slice[i + 1]]);
}
return (z) => {
return exp(obj[1][1], stops)(z - 1);
};
}
if (obj[0] === "step" && obj[1][0] === "get") {
const slice = obj.slice(2);
const prop = obj[1][1];
return (z, f) => {
const val = f === null || f === void 0 ? void 0 : f.props[prop];
if (typeof val === "number") {
if (val < slice[1])
return slice[0];
for (let i = 1; i < slice.length; i += 2) {
if (val <= slice[i])
return slice[i + 1];
}
}
return slice[slice.length - 1];
};
}
console.log("Unimplemented numeric fn: ", obj);
return (z) => 1;
}
export function numberOrFn(obj, defaultValue = 0) {
if (!obj)
return defaultValue;
if (typeof obj === "number") {
return obj;
}
// If feature f is defined, use numberFn, otherwise use defaultValue
return (z, f) => (f ? numberFn(obj)(z, f) : defaultValue);
}
export function widthFn(width_obj, gap_obj) {
const w = numberOrFn(width_obj, 1);
const g = numberOrFn(gap_obj);
return (z, f) => {
const tmp = typeof w === "number" ? w : w(z, f);
if (g) {
return tmp + (typeof g === "number" ? g : g(z, f));
}
return tmp;
};
}
export function getFont(obj, fontsubmap) {
const fontfaces = [];
for (const wanted_face of obj["text-font"]) {
if (fontsubmap.hasOwnProperty(wanted_face)) {
fontfaces.push(fontsubmap[wanted_face]);
}
}
if (fontfaces.length === 0)
fontfaces.push({ face: "sans-serif" });
const text_size = obj["text-size"];
// for fallbacks, use the weight and style of the first font
let weight = "";
if (fontfaces.length && fontfaces[0].weight)
weight = `${fontfaces[0].weight} `;
let style = "";
if (fontfaces.length && fontfaces[0].style)
style = `${fontfaces[0].style} `;
if (typeof text_size === "number") {
return (z) => `${style}${weight}${text_size}px ${fontfaces
.map((f) => f.face)
.join(", ")}`;
}
if (text_size.stops) {
let base = 1.4;
if (text_size.base)
base = text_size.base;
else
text_size.base = base;
const t = numberFn(text_size);
return (z, f) => {
return `${style}${weight}${t(z, f)}px ${fontfaces
.map((f) => f.face)
.join(", ")}`;
};
}
if (text_size[0] === "step") {
const t = numberFn(text_size);
return (z, f) => {
return `${style}${weight}${t(z, f)}px ${fontfaces
.map((f) => f.face)
.join(", ")}`;
};
}
console.log("Can't parse font: ", obj);
return (z) => "12px sans-serif";
}
export function json_style(obj, fontsubmap) {
const paint_rules = [];
const label_rules = [];
const refs = new Map();
for (const layer of obj.layers) {
refs.set(layer.id, layer);
if (layer.layout && layer.layout.visibility === "none") {
continue;
}
if (layer.ref) {
const referenced = refs.get(layer.ref);
layer.type = referenced.type;
layer.filter = referenced.filter;
layer.source = referenced.source;
layer["source-layer"] = referenced["source-layer"];
}
const sourceLayer = layer["source-layer"];
let symbolizer;
let filter = undefined;
if (layer.filter) {
filter = filterFn(layer.filter);
}
// ignore background-color?
if (layer.type === "fill") {
paint_rules.push({
dataLayer: layer["source-layer"],
filter: filter,
symbolizer: new PolygonSymbolizer({
fill: layer.paint["fill-color"],
opacity: layer.paint["fill-opacity"],
}),
});
}
else if (layer.type === "fill-extrusion") {
// simulate fill-extrusion with plain fill
paint_rules.push({
dataLayer: layer["source-layer"],
filter: filter,
symbolizer: new PolygonSymbolizer({
fill: layer.paint["fill-extrusion-color"],
opacity: layer.paint["fill-extrusion-opacity"],
}),
});
}
else if (layer.type === "line") {
// simulate gap-width
if (layer.paint["line-dasharray"]) {
paint_rules.push({
dataLayer: layer["source-layer"],
filter: filter,
symbolizer: new LineSymbolizer({
width: widthFn(layer.paint["line-width"], layer.paint["line-gap-width"]),
dash: layer.paint["line-dasharray"],
dashColor: layer.paint["line-color"],
}),
});
}
else {
paint_rules.push({
dataLayer: layer["source-layer"],
filter: filter,
symbolizer: new LineSymbolizer({
color: layer.paint["line-color"],
width: widthFn(layer.paint["line-width"], layer.paint["line-gap-width"]),
}),
});
}
}
else if (layer.type === "symbol") {
if (layer.layout["symbol-placement"] === "line") {
label_rules.push({
dataLayer: layer["source-layer"],
filter: filter,
symbolizer: new LineLabelSymbolizer({
font: getFont(layer.layout, fontsubmap),
fill: layer.paint["text-color"],
width: layer.paint["text-halo-width"],
stroke: layer.paint["text-halo-color"],
textTransform: layer.layout["text-transform"],
label_props: layer.layout["text-field"]
? [layer.layout["text-field"]]
: undefined,
}),
});
}
else {
label_rules.push({
dataLayer: layer["source-layer"],
filter: filter,
symbolizer: new CenteredTextSymbolizer({
font: getFont(layer.layout, fontsubmap),
fill: layer.paint["text-color"],
stroke: layer.paint["text-halo-color"],
width: layer.paint["text-halo-width"],
textTransform: layer.layout["text-transform"],
label_props: layer.layout["text-field"]
? [layer.layout["text-field"]]
: undefined,
}),
});
}
}
else if (layer.type === "circle") {
paint_rules.push({
dataLayer: layer["source-layer"],
filter: filter,
symbolizer: new CircleSymbolizer({
radius: layer.paint["circle-radius"],
fill: layer.paint["circle-color"],
stroke: layer.paint["circle-stroke-color"],
width: layer.paint["circle-stroke-width"],
}),
});
}
}
label_rules.reverse();
return { paint_rules: paint_rules, label_rules: label_rules, tasks: [] };
}