UNPKG

tile-stencil

Version:

Load a MapLibre style document and parse it into Javascript functions

962 lines (850 loc) 27.3 kB
function expandStyleURL(url, token) { const prefix = /^mapbox:\/\/styles\//; if (!url.match(prefix)) return url; const apiRoot = "https://api.mapbox.com/styles/v1/"; return url.replace(prefix, apiRoot) + "?access_token=" + token; } function expandSpriteURLs(url, pixRatio, token) { // Returns an array containing urls to .png and .json files const ratio = Math.floor(Math.min(Math.max(1.0, pixRatio), 4.0)); const ratioStr = (ratio > 1) ? "@" + ratio + "x" : ""; const prefix = /^mapbox:\/\/sprites\//; if (!url.match(prefix)) return { image: url + ratioStr + ".png", meta: url + ratioStr + ".json", }; // We have a Mapbox custom url. Expand to an absolute URL, as per the spec const apiRoot = "https://api.mapbox.com/styles/v1/"; url = url.replace(prefix, apiRoot) + "/sprite"; const tokenString = "?access_token=" + token; return { image: url + ratioStr + ".png" + tokenString, meta: url + ratioStr + ".json" + tokenString, }; } function expandTileURL(url, token) { const prefix = /^mapbox:\/\//; if (!url.match(prefix)) return url; const apiRoot = "https://api.mapbox.com/v4/"; return url.replace(prefix, apiRoot) + ".json?secure&access_token=" + token; } function expandGlyphURL(url, token) { const prefix = /^mapbox:\/\/fonts\//; if (!url.match(prefix)) return url; const apiRoot = "https://api.mapbox.com/fonts/v1/"; return url.replace(prefix, apiRoot) + "?access_token=" + token; } function getGeoJSON(data) { const dataPromise = (typeof data === "object" && data !== null) ? Promise.resolve(data) : getJSON(data); // data may be a URL. Try loading it return dataPromise.then(json => { // Is it valid GeoJSON? For now, just check for a .type property return (json.type) ? json : Promise.reject(Error("invalid GeoJSON: " + JSON.stringify(json))); }); } function getJSON(href) { return (typeof href === "string" && href.length) ? fetch(href).then(checkFetch) : Promise.reject(Error("invalid URL: " + JSON.stringify(href))); } function checkFetch(response) { if (!response.ok) { const { status, statusText, url } = response; const message = `HTTP ${status} ${statusText} for URL ${url}`; return Promise.reject(Error(message)); } return response.json(); } function getImage(href) { const img = new Image(); return new Promise( (resolve, reject) => { img.onerror = () => reject(Error("Failed to retrieve image from " + href)); img.onload = () => (img.complete && img.naturalWidth !== 0) ? resolve(img) : reject(Error("Incomplete image received from " + href)); img.crossOrigin = "anonymous"; img.src = href; }); } function warn(message) { console.log("tile-stencil had a problem loading part of the style document"); console.log(" " + message); console.log(" Not a fatal error. Proceeding with the rest of the style..."); } function define(constructor, factory, prototype) { constructor.prototype = factory.prototype = prototype; prototype.constructor = constructor; } function extend(parent, definition) { var prototype = Object.create(parent.prototype); for (var key in definition) prototype[key] = definition[key]; return prototype; } function Color() {} var darker = 0.7; var brighter = 1 / darker; var reI = "\\s*([+-]?\\d+)\\s*", reN = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)\\s*", reP = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)%\\s*", reHex = /^#([0-9a-f]{3,8})$/, reRgbInteger = new RegExp("^rgb\\(" + [reI, reI, reI] + "\\)$"), reRgbPercent = new RegExp("^rgb\\(" + [reP, reP, reP] + "\\)$"), reRgbaInteger = new RegExp("^rgba\\(" + [reI, reI, reI, reN] + "\\)$"), reRgbaPercent = new RegExp("^rgba\\(" + [reP, reP, reP, reN] + "\\)$"), reHslPercent = new RegExp("^hsl\\(" + [reN, reP, reP] + "\\)$"), reHslaPercent = new RegExp("^hsla\\(" + [reN, reP, reP, reN] + "\\)$"); var named = { aliceblue: 0xf0f8ff, antiquewhite: 0xfaebd7, aqua: 0x00ffff, aquamarine: 0x7fffd4, azure: 0xf0ffff, beige: 0xf5f5dc, bisque: 0xffe4c4, black: 0x000000, blanchedalmond: 0xffebcd, blue: 0x0000ff, blueviolet: 0x8a2be2, brown: 0xa52a2a, burlywood: 0xdeb887, cadetblue: 0x5f9ea0, chartreuse: 0x7fff00, chocolate: 0xd2691e, coral: 0xff7f50, cornflowerblue: 0x6495ed, cornsilk: 0xfff8dc, crimson: 0xdc143c, cyan: 0x00ffff, darkblue: 0x00008b, darkcyan: 0x008b8b, darkgoldenrod: 0xb8860b, darkgray: 0xa9a9a9, darkgreen: 0x006400, darkgrey: 0xa9a9a9, darkkhaki: 0xbdb76b, darkmagenta: 0x8b008b, darkolivegreen: 0x556b2f, darkorange: 0xff8c00, darkorchid: 0x9932cc, darkred: 0x8b0000, darksalmon: 0xe9967a, darkseagreen: 0x8fbc8f, darkslateblue: 0x483d8b, darkslategray: 0x2f4f4f, darkslategrey: 0x2f4f4f, darkturquoise: 0x00ced1, darkviolet: 0x9400d3, deeppink: 0xff1493, deepskyblue: 0x00bfff, dimgray: 0x696969, dimgrey: 0x696969, dodgerblue: 0x1e90ff, firebrick: 0xb22222, floralwhite: 0xfffaf0, forestgreen: 0x228b22, fuchsia: 0xff00ff, gainsboro: 0xdcdcdc, ghostwhite: 0xf8f8ff, gold: 0xffd700, goldenrod: 0xdaa520, gray: 0x808080, green: 0x008000, greenyellow: 0xadff2f, grey: 0x808080, honeydew: 0xf0fff0, hotpink: 0xff69b4, indianred: 0xcd5c5c, indigo: 0x4b0082, ivory: 0xfffff0, khaki: 0xf0e68c, lavender: 0xe6e6fa, lavenderblush: 0xfff0f5, lawngreen: 0x7cfc00, lemonchiffon: 0xfffacd, lightblue: 0xadd8e6, lightcoral: 0xf08080, lightcyan: 0xe0ffff, lightgoldenrodyellow: 0xfafad2, lightgray: 0xd3d3d3, lightgreen: 0x90ee90, lightgrey: 0xd3d3d3, lightpink: 0xffb6c1, lightsalmon: 0xffa07a, lightseagreen: 0x20b2aa, lightskyblue: 0x87cefa, lightslategray: 0x778899, lightslategrey: 0x778899, lightsteelblue: 0xb0c4de, lightyellow: 0xffffe0, lime: 0x00ff00, limegreen: 0x32cd32, linen: 0xfaf0e6, magenta: 0xff00ff, maroon: 0x800000, mediumaquamarine: 0x66cdaa, mediumblue: 0x0000cd, mediumorchid: 0xba55d3, mediumpurple: 0x9370db, mediumseagreen: 0x3cb371, mediumslateblue: 0x7b68ee, mediumspringgreen: 0x00fa9a, mediumturquoise: 0x48d1cc, mediumvioletred: 0xc71585, midnightblue: 0x191970, mintcream: 0xf5fffa, mistyrose: 0xffe4e1, moccasin: 0xffe4b5, navajowhite: 0xffdead, navy: 0x000080, oldlace: 0xfdf5e6, olive: 0x808000, olivedrab: 0x6b8e23, orange: 0xffa500, orangered: 0xff4500, orchid: 0xda70d6, palegoldenrod: 0xeee8aa, palegreen: 0x98fb98, paleturquoise: 0xafeeee, palevioletred: 0xdb7093, papayawhip: 0xffefd5, peachpuff: 0xffdab9, peru: 0xcd853f, pink: 0xffc0cb, plum: 0xdda0dd, powderblue: 0xb0e0e6, purple: 0x800080, rebeccapurple: 0x663399, red: 0xff0000, rosybrown: 0xbc8f8f, royalblue: 0x4169e1, saddlebrown: 0x8b4513, salmon: 0xfa8072, sandybrown: 0xf4a460, seagreen: 0x2e8b57, seashell: 0xfff5ee, sienna: 0xa0522d, silver: 0xc0c0c0, skyblue: 0x87ceeb, slateblue: 0x6a5acd, slategray: 0x708090, slategrey: 0x708090, snow: 0xfffafa, springgreen: 0x00ff7f, steelblue: 0x4682b4, tan: 0xd2b48c, teal: 0x008080, thistle: 0xd8bfd8, tomato: 0xff6347, turquoise: 0x40e0d0, violet: 0xee82ee, wheat: 0xf5deb3, white: 0xffffff, whitesmoke: 0xf5f5f5, yellow: 0xffff00, yellowgreen: 0x9acd32 }; define(Color, color, { copy: function(channels) { return Object.assign(new this.constructor, this, channels); }, displayable: function() { return this.rgb().displayable(); }, hex: color_formatHex, // Deprecated! Use color.formatHex. formatHex: color_formatHex, formatHsl: color_formatHsl, formatRgb: color_formatRgb, toString: color_formatRgb }); function color_formatHex() { return this.rgb().formatHex(); } function color_formatHsl() { return hslConvert(this).formatHsl(); } function color_formatRgb() { return this.rgb().formatRgb(); } function color(format) { var m, l; format = (format + "").trim().toLowerCase(); return (m = reHex.exec(format)) ? (l = m[1].length, m = parseInt(m[1], 16), l === 6 ? rgbn(m) // #ff0000 : l === 3 ? new Rgb((m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), ((m & 0xf) << 4) | (m & 0xf), 1) // #f00 : l === 8 ? rgba(m >> 24 & 0xff, m >> 16 & 0xff, m >> 8 & 0xff, (m & 0xff) / 0xff) // #ff000000 : l === 4 ? rgba((m >> 12 & 0xf) | (m >> 8 & 0xf0), (m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), (((m & 0xf) << 4) | (m & 0xf)) / 0xff) // #f000 : null) // invalid hex : (m = reRgbInteger.exec(format)) ? new Rgb(m[1], m[2], m[3], 1) // rgb(255, 0, 0) : (m = reRgbPercent.exec(format)) ? new Rgb(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, 1) // rgb(100%, 0%, 0%) : (m = reRgbaInteger.exec(format)) ? rgba(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1) : (m = reRgbaPercent.exec(format)) ? rgba(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, m[4]) // rgb(100%, 0%, 0%, 1) : (m = reHslPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, 1) // hsl(120, 50%, 50%) : (m = reHslaPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1) : named.hasOwnProperty(format) ? rgbn(named[format]) // eslint-disable-line no-prototype-builtins : format === "transparent" ? new Rgb(NaN, NaN, NaN, 0) : null; } function rgbn(n) { return new Rgb(n >> 16 & 0xff, n >> 8 & 0xff, n & 0xff, 1); } function rgba(r, g, b, a) { if (a <= 0) r = g = b = NaN; return new Rgb(r, g, b, a); } function rgbConvert(o) { if (!(o instanceof Color)) o = color(o); if (!o) return new Rgb; o = o.rgb(); return new Rgb(o.r, o.g, o.b, o.opacity); } function rgb(r, g, b, opacity) { return arguments.length === 1 ? rgbConvert(r) : new Rgb(r, g, b, opacity == null ? 1 : opacity); } function Rgb(r, g, b, opacity) { this.r = +r; this.g = +g; this.b = +b; this.opacity = +opacity; } define(Rgb, rgb, extend(Color, { brighter: function(k) { k = k == null ? brighter : Math.pow(brighter, k); return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity); }, darker: function(k) { k = k == null ? darker : Math.pow(darker, k); return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity); }, rgb: function() { return this; }, displayable: function() { return (-0.5 <= this.r && this.r < 255.5) && (-0.5 <= this.g && this.g < 255.5) && (-0.5 <= this.b && this.b < 255.5) && (0 <= this.opacity && this.opacity <= 1); }, hex: rgb_formatHex, // Deprecated! Use color.formatHex. formatHex: rgb_formatHex, formatRgb: rgb_formatRgb, toString: rgb_formatRgb })); function rgb_formatHex() { return "#" + hex(this.r) + hex(this.g) + hex(this.b); } function rgb_formatRgb() { var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a)); return (a === 1 ? "rgb(" : "rgba(") + Math.max(0, Math.min(255, Math.round(this.r) || 0)) + ", " + Math.max(0, Math.min(255, Math.round(this.g) || 0)) + ", " + Math.max(0, Math.min(255, Math.round(this.b) || 0)) + (a === 1 ? ")" : ", " + a + ")"); } function hex(value) { value = Math.max(0, Math.min(255, Math.round(value) || 0)); return (value < 16 ? "0" : "") + value.toString(16); } function hsla(h, s, l, a) { if (a <= 0) h = s = l = NaN; else if (l <= 0 || l >= 1) h = s = NaN; else if (s <= 0) h = NaN; return new Hsl(h, s, l, a); } function hslConvert(o) { if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity); if (!(o instanceof Color)) o = color(o); if (!o) return new Hsl; if (o instanceof Hsl) return o; o = o.rgb(); var r = o.r / 255, g = o.g / 255, b = o.b / 255, min = Math.min(r, g, b), max = Math.max(r, g, b), h = NaN, s = max - min, l = (max + min) / 2; if (s) { if (r === max) h = (g - b) / s + (g < b) * 6; else if (g === max) h = (b - r) / s + 2; else h = (r - g) / s + 4; s /= l < 0.5 ? max + min : 2 - max - min; h *= 60; } else { s = l > 0 && l < 1 ? 0 : h; } return new Hsl(h, s, l, o.opacity); } function hsl(h, s, l, opacity) { return arguments.length === 1 ? hslConvert(h) : new Hsl(h, s, l, opacity == null ? 1 : opacity); } function Hsl(h, s, l, opacity) { this.h = +h; this.s = +s; this.l = +l; this.opacity = +opacity; } define(Hsl, hsl, extend(Color, { brighter: function(k) { k = k == null ? brighter : Math.pow(brighter, k); return new Hsl(this.h, this.s, this.l * k, this.opacity); }, darker: function(k) { k = k == null ? darker : Math.pow(darker, k); return new Hsl(this.h, this.s, this.l * k, this.opacity); }, rgb: function() { var h = this.h % 360 + (this.h < 0) * 360, s = isNaN(h) || isNaN(this.s) ? 0 : this.s, l = this.l, m2 = l + (l < 0.5 ? l : 1 - l) * s, m1 = 2 * l - m2; return new Rgb( hsl2rgb(h >= 240 ? h - 240 : h + 120, m1, m2), hsl2rgb(h, m1, m2), hsl2rgb(h < 120 ? h + 240 : h - 120, m1, m2), this.opacity ); }, displayable: function() { return (0 <= this.s && this.s <= 1 || isNaN(this.s)) && (0 <= this.l && this.l <= 1) && (0 <= this.opacity && this.opacity <= 1); }, formatHsl: function() { var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a)); return (a === 1 ? "hsl(" : "hsla(") + (this.h || 0) + ", " + (this.s || 0) * 100 + "%, " + (this.l || 0) * 100 + "%" + (a === 1 ? ")" : ", " + a + ")"); } })); /* From FvD 13.37, CSS Color Module Level 3 */ function hsl2rgb(h, m1, m2) { return (h < 60 ? m1 + (m2 - m1) * h / 60 : h < 180 ? m2 : h < 240 ? m1 + (m2 - m1) * (240 - h) / 60 : m1) * 255; } function buildInterpolator(stops, base = 1) { if (!stops || stops.length < 2 || stops[0].length !== 2) return; // Confirm stops are all the same type, and convert colors to arrays const type = getType(stops[0][1]); if (!stops.every(s => getType(s[1]) === type)) return; stops = stops.map(([x, y]) => [x, convertIfColor(y)]); const izm = stops.length - 1; const scale = getScale(base); const interpolate = getInterpolator(type); return function(x) { const iz = stops.findIndex(stop => stop[0] > x); if (iz === 0) return stops[0][1]; // x is below first stop if (iz < 0) return stops[izm][1]; // x is above last stop const [x0, y0] = stops[iz - 1]; const [x1, y1] = stops[iz]; return interpolate(y0, scale(x0, x, x1), y1); }; } function getType(v) { return color(v) ? "color" : typeof v; } function convertIfColor(val) { // Convert CSS color strings to clamped RGBA arrays for WebGL if (!color(val)) return val; const c = rgb(val); return [c.r / 255, c.g / 255, c.b / 255, c.opacity]; } function getScale(base) { // Return a function to find the relative position of x between a and b // Exponential scale follows mapbox-gl-js, style-spec/function/index.js // NOTE: https://github.com/mapbox/mapbox-gl-js/issues/2698 not addressed! const scale = (base === 1) ? (a, x, b) => (x - a) / (b - a) // Linear scale : (a, x, b) => (Math.pow(base, x - a) - 1) / (Math.pow(base, b - a) - 1); // Add check for zero range return (a, x, b) => (a === b) ? 0 : scale(a, x, b); } function getInterpolator(type) { // Return a function to find an interpolated value between end values v1, v2, // given relative position t between the two end positions switch (type) { case "number": // Linear interpolator return (v1, t, v2) => v1 + t * (v2 - v1); case "color": // Interpolate RGBA return (v1, t, v2) => v1.map((v, i) => v + t * (v2[i] - v)); default: // Assume step function return (v1) => v1; } } function autoGetters(properties = {}, defaults) { return Object.entries(defaults).reduce((d, [key, val]) => { d[key] = buildStyleFunc(properties[key], val); return d; }, {}); } function buildStyleFunc(style, defaultVal) { if (style === undefined) { return getConstFunc(defaultVal); } else if (typeof style !== "object" || Array.isArray(style)) { return getConstFunc(style); } else { return getStyleFunc(style); } // NOT IMPLEMENTED: zoom-and-property functions } function getConstFunc(rawVal) { const val = convertIfColor(rawVal); const func = () => val; return Object.assign(func, { type: "constant" }); } function getStyleFunc(style) { const { type, property = "zoom", base = 1, stops } = style; const getArg = (property === "zoom") ? (zoom) => zoom : (zoom, feature) => feature.properties[property]; const getVal = (type === "identity") ? convertIfColor : buildInterpolator(stops, base); if (!getVal) return console.log("style: " + JSON.stringify(style) + "\nERROR in tile-stencil: unsupported style!"); const styleFunc = (zoom, feature) => getVal(getArg(zoom, feature)); return Object.assign(styleFunc, { type: (property === "zoom") ? "zoom" : "property", property, }); } const layoutDefaults = { "background": { "visibility": "visible", }, "fill": { "visibility": "visible", }, "line": { "visibility": "visible", "line-cap": "butt", "line-join": "miter", "line-miter-limit": 2, "line-round-limit": 1.05, }, "symbol": { "visibility": "visible", "symbol-placement": "point", "symbol-spacing": 250, "symbol-avoid-edges": false, "symbol-sort-key": undefined, "symbol-z-order": "auto", "icon-allow-overlap": false, "icon-ignore-placement": false, "icon-optional": false, "icon-rotation-alignment": "auto", "icon-size": 1, "icon-text-fit": "none", "icon-text-fit-padding": [0, 0, 0, 0], "icon-image": undefined, "icon-rotate": 0, "icon-padding": 2, "icon-keep-upright": false, "icon-offset": [0, 0], "icon-anchor": "center", "icon-pitch-alignment": "auto", "text-pitch-alignment": "auto", "text-rotation-alignment": "auto", "text-field": "", "text-font": ["Open Sans Regular", "Arial Unicode MS Regular"], "text-size": 16, "text-max-width": 10, "text-line-height": 1.2, "text-letter-spacing": 0, "text-justify": "center", "text-radial-offset": 0, "text-variable-anchor": undefined, "text-anchor": "center", "text-max-angle": 45, "text-rotate": 0, "text-padding": 2.0, "text-keep-upright": true, "text-transform": "none", "text-offset": [0, 0], "text-allow-overlap": false, "text-ignore-placement": false, "text-optional": false, }, "raster": { "visibility": "visible", }, "circle": { "visibility": "visible", }, "fill-extrusion": { "visibility": "visible", }, "heatmap": { "visibility": "visible", }, "hillshade": { "visibility": "visible", }, }; const paintDefaults = { "background": { "background-color": "#000000", "background-opacity": 1, "background-pattern": undefined, }, "fill": { "fill-antialias": true, "fill-opacity": 1, "fill-color": "#000000", "fill-outline-color": undefined, "fill-outline-width": 1, // non-standard! "fill-translate": [0, 0], "fill-translate-anchor": "map", "fill-pattern": undefined, }, "line": { "line-opacity": 1, "line-color": "#000000", "line-translate": [0, 0], "line-translate-anchor": "map", "line-width": 1, "line-gap-width": 0, "line-offset": 0, "line-blur": 0, "line-dasharray": [0, 0, 0, 0], "line-pattern": undefined, "line-gradient": undefined, }, "symbol": { "icon-opacity": 1, "icon-color": "#000000", "icon-halo-color": "rgba(0, 0, 0, 0)", "icon-halo-width": 0, "icon-halo-blur": 0, "icon-translate": [0, 0], "icon-translate-anchor": "map", "text-opacity": 1, "text-color": "#000000", "text-halo-color": "rgba(0, 0, 0, 0)", "text-halo-width": 0, "text-halo-blur": 0, "text-translate": [0, 0], "text-translate-anchor": "map", }, "raster": { "raster-opacity": 1, "raster-hue-rotate": 0, "raster-brighness-min": 0, "raster-brightness-max": 1, "raster-saturation": 0, "raster-contrast": 0, "raster-resampling": "linear", "raster-fade-duration": 300, }, "circle": { "circle-radius": 5, "circle-color": "#000000", "circle-blur": 0, "circle-opacity": 1, "circle-translate": [0, 0], "circle-translate-anchor": "map", "circle-pitch-scale": "map", "circle-pitch-alignment": "viewport", "circle-stroke-width": 0, "circle-stroke-color": "#000000", "circle-stroke-opacity": 1, }, "fill-extrusion": { "fill-extrusion-opacity": 1, "fill-extrusion-color": "#000000", "fill-extrusion-translate": [0, 0], "fill-extrusion-translate-anchor": "map", "fill-extrusion-height": 0, "fill-extrusion-base": 0, "fill-extrusion-vertical-gradient": true, }, "heatmap": { "heatmap-radius": 30, "heatmap-weight": 1, "heatmap-intensity": 1, "heatmap-color": [ "interpolate", ["linear"], ["heatmap-density"], 0, "rgba(0, 0, 255,0)", 0.1, "royalblue", 0.3, "cyan", 0.5, "lime", 0.7, "yellow", 1, "red" ], "heatmap-opacity": 1, }, "hillshade": { "hillshade-illumination-direction": 335, "hillshade-illumination-anchor": "viewport", "hillshade-exaggeration": 0.5, "hillshade-shadow-color": "#000000", "hillshade-highlight-color": "#FFFFFF", "hillshade-accent-color": "#000000", }, }; const refProperties = ["type", "minzoom", "maxzoom", "source", "source-layer", "filter", "layout"]; function derefLayers(layers) { // Some layers in Mapbox styles contain a non-standard "ref" property, // pointing to the "id" of another layer. // Augment these layers with properties from the referenced layer const map = layers.reduce((m, l) => (m[l.id] = l, m), {}); return layers.map(l => ("ref" in l) ? deref(l, map[l.ref]) : l); } function deref(layer, parent) { const result = Object.assign({}, layer); delete result.ref; refProperties.forEach(k => { if (k in parent) result[k] = parent[k]; }); return result; } function loadLinks(styleDoc, mapboxToken) { const { sources: rawSources, glyphs, sprite, layers } = styleDoc; styleDoc.layers = derefLayers(layers); if (glyphs) { styleDoc.glyphs = expandGlyphURL(glyphs, mapboxToken); } return Promise.all([ expandSources(rawSources, mapboxToken), loadSprite(sprite, mapboxToken), ]).then(([sources, spriteData]) => { return Object.assign(styleDoc, { sources, spriteData }); }); } function expandSources(rawSources, token) { const expandPromises = Object.entries(rawSources).map(expandSource); function expandSource([key, source]) { const { type, url } = source; const infoPromise = (type === "geojson") ? getGeoJSON(source.data).then(data => ({ data })) : (url) ? getJSON(expandTileURL(url, token)) : // Get linked TileJSON Promise.resolve({}); // No linked info return infoPromise.then( val => ({ [key]: Object.assign({}, source, val, { type }) }), err => (warn("sources." + key + ": " + err.message), ({})) ); } return Promise.all(expandPromises).then(results => { return results.reduce((a, c) => Object.assign(a, c), {}); }); } function loadSprite(sprite, token) { if (!sprite) return; const notWorker = (window && window.devicePixelRatio); const pixRatio = (notWorker) ? window.devicePixelRatio : 1.0; const urls = expandSpriteURLs(sprite, pixRatio, token); return Promise.all([getImage(urls.image), getJSON(urls.meta)]).then( ([image, meta]) => ({ image, meta }), err => warn("sprite: " + err.message) ); } function buildFeatureFilter(filterObj) { // filterObj is a filter definition following the 'deprecated' syntax: // https://maplibre.org/maplibre-gl-js-docs/style-spec/other/#other-filter if (!filterObj) return () => true; const [type, ...vals] = filterObj; // If this is a combined filter, the vals are themselves filter definitions switch (type) { case "all": { const filters = vals.map(buildFeatureFilter); // Iteratively recursive! return (d) => filters.every( filt => filt(d) ); } case "any": { const filters = vals.map(buildFeatureFilter); return (d) => filters.some( filt => filt(d) ); } case "none": { const filters = vals.map(buildFeatureFilter); return (d) => filters.every( filt => !filt(d) ); } default: return getSimpleFilter(filterObj); } } function getSimpleFilter(filterObj) { const [type, key, ...vals] = filterObj; const getVal = initFeatureValGetter(key); switch (type) { // Existential Filters case "has": return d => !!getVal(d); // !! forces a Boolean return case "!has": return d => !getVal(d); // Comparison Filters case "==": return d => getVal(d) === vals[0]; case "!=": return d => getVal(d) !== vals[0]; case ">": return d => getVal(d) > vals[0]; case ">=": return d => getVal(d) >= vals[0]; case "<": return d => getVal(d) < vals[0]; case "<=": return d => getVal(d) <= vals[0]; // Set Membership Filters case "in" : return d => vals.includes( getVal(d) ); case "!in" : return d => !vals.includes( getVal(d) ); default: console.log("prepFilter: unknown filter type = " + filterObj[0]); } // No recognizable filter criteria. Return a filter that is always true return () => true; } function initFeatureValGetter(key) { switch (key) { case "$type": // NOTE: data includes MultiLineString, MultiPolygon, etc-NOT IN SPEC return f => { const t = f.geometry.type; if (t === "MultiPoint") return "Point"; if (t === "MultiLineString") return "LineString"; if (t === "MultiPolygon") return "Polygon"; return t; }; case "$id": return f => f.id; default: return f => f.properties[key]; } } function getStyleFuncs(inputLayer) { const layer = Object.assign({}, inputLayer); // Leave input unchanged // Replace rendering properties with functions layer.layout = autoGetters(layer.layout, layoutDefaults[layer.type]); layer.paint = autoGetters(layer.paint, paintDefaults[layer.type] ); return layer; } function loadStyle(style, mapboxToken) { // Loads a style document and any linked information const getStyle = (typeof style === "object") ? Promise.resolve(style) // style is JSON already : getJSON( expandStyleURL(style, mapboxToken) ); // Get from URL return getStyle.then(checkStyle) .then( styleDoc => loadLinks(styleDoc, mapboxToken) ); } function checkStyle(doc) { const { version, sources, layers } = doc; const noSource = typeof sources !== "object" || sources === null || Array.isArray(sources); const error = noSource ? "missing sources object" : (!Array.isArray(layers)) ? "missing layers array" : (version !== 8) ? "unsupported version number" : null; return (error) ? Promise.reject(error) : doc; } export { buildFeatureFilter, getStyleFuncs, loadStyle };