From 99afbfa20f743d5e60871731389775e1e8d1347a Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 15 Oct 2024 16:31:37 +0200 Subject: [PATCH] refactor(StyleOptions): move StyleOptions from Style to its own file --- src/Core/Feature.js | 4 +- src/Core/Style.js | 246 -------------------- src/Core/StyleOptions.js | 399 ++++++++++++++++++++++++++++++++ src/Source/VectorTilesSource.js | 4 +- test/unit/label.js | 3 +- test/unit/style.js | 31 +-- 6 files changed, 421 insertions(+), 266 deletions(-) create mode 100644 src/Core/StyleOptions.js diff --git a/src/Core/Feature.js b/src/Core/Feature.js index 82980a747d..9e869be16f 100644 --- a/src/Core/Feature.js +++ b/src/Core/Feature.js @@ -2,7 +2,7 @@ import * as THREE from 'three'; import Extent from 'Core/Geographic/Extent'; import Coordinates from 'Core/Geographic/Coordinates'; import CRS from 'Core/Geographic/Crs'; -import Style from 'Core/Style'; +import StyleOptions from 'Core/StyleOptions'; function defaultExtent(crs) { return new Extent(crs, Infinity, -Infinity, Infinity, -Infinity); @@ -251,7 +251,7 @@ class Feature { } this._pos = 0; this._pushValues = (this.size === 3 ? push3DValues : push2DValues).bind(this); - this.style = Style.setFromProperties; + this.style = StyleOptions.setFromProperties; } /** * Instance a new {@link FeatureGeometry} and push in {@link Feature}. diff --git a/src/Core/Style.js b/src/Core/Style.js index 3e716bd26a..929d3fa0a9 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -1,7 +1,5 @@ -import { FEATURE_TYPES } from 'Core/Feature'; import Cache from 'Core/Scheduler/Cache'; import Fetcher from 'Provider/Fetcher'; -import * as mapbox from '@mapbox/mapbox-gl-style-spec'; import { Color } from 'three'; import { deltaE } from 'Renderer/Color'; import Coordinates from 'Core/Geographic/Coordinates'; @@ -13,8 +11,6 @@ const cacheStyle = new Cache(); const matrix = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix(); const canvas = document.createElement('canvas'); -const inv255 = 1 / 255; - function baseAltitudeDefault(properties, ctx) { return ctx?.coordinates?.z || 0; } @@ -47,37 +43,6 @@ export function readExpression(property, ctx) { return property; } -function rgba2rgb(orig) { - if (!orig) { - return {}; - } else if (orig.stops || orig.expression) { - return { color: orig }; - } else if (typeof orig == 'string') { - const result = orig.match(/(?:((hsl|rgb)a? *\(([\d.%]+(?:deg|g?rad|turn)?)[ ,]*([\d.%]+)[ ,]*([\d.%]+)[ ,/]*([\d.%]*)\))|(#((?:[\d\w]{3}){1,2})([\d\w]{1,2})?))/i); - if (result === null) { - return { color: orig, opacity: 1.0 }; - } else if (result[7]) { - let opacity = 1.0; - if (result[9]) { - opacity = parseInt(result[9].length == 1 ? `${result[9]}${result[9]}` : result[9], 16) * inv255; - } - return { color: `#${result[8]}`, opacity }; - } else if (result[1]) { - return { color: `${result[2]}(${result[3]},${result[4]},${result[5]})`, opacity: (result[6] ? Number(result[6]) : 1.0) }; - } - } -} - -function readVectorProperty(property, options) { - if (property != undefined) { - if (mapbox.expression.isExpression(property)) { - return mapbox.expression.createExpression(property, options).value; - } else { - return property; - } - } -} - async function loadImage(source) { let promise = cacheStyle.get(source, 'null'); if (!promise) { @@ -700,217 +665,6 @@ class Style { this.context = ctx; } - /** - * set Style from (geojson-like) properties. - * @param {Object} properties (geojson-like) properties. - * @param {FeatureContext} featCtx the context of the feature - * - * @returns {StyleOptions} containing all properties for itowns.Style - */ - static setFromProperties(properties, featCtx) { - const type = featCtx.type; - const style = {}; - if (type === FEATURE_TYPES.POINT) { - const point = { - ...(properties.fill !== undefined && { color: properties.fill }), - ...(properties['fill-opacity'] !== undefined && { opacity: properties['fill-opacity'] }), - ...(properties.stroke !== undefined && { line: properties.stroke }), - ...(properties.radius !== undefined && { radius: properties.radius }), - }; - if (Object.keys(point).length) { - style.point = point; - } - const text = { - ...(properties['label-color'] !== undefined && { color: properties['label-color'] }), - ...(properties['label-opacity'] !== undefined && { opacity: properties['label-opacity'] }), - ...(properties['label-size'] !== undefined && { size: properties['label-size'] }), - }; - if (Object.keys(point).length) { - style.text = text; - } - const icon = { - ...(properties.icon !== undefined && { source: properties.icon }), - ...(properties['icon-scale'] !== undefined && { size: properties['icon-scale'] }), - ...(properties['icon-opacity'] !== undefined && { opacity: properties['icon-opacity'] }), - ...(properties['icon-color'] !== undefined && { color: properties['icon-color'] }), - }; - if (Object.keys(icon).length) { - style.icon = icon; - } - } else { - const stroke = { - ...(properties.stroke !== undefined && { color: properties.stroke }), - ...(properties['stroke-width'] !== undefined && { width: properties['stroke-width'] }), - ...(properties['stroke-opacity'] !== undefined && { opacity: properties['stroke-opacity'] }), - }; - if (Object.keys(stroke).length) { - style.stroke = stroke; - } - if (type !== FEATURE_TYPES.LINE) { - const fill = { - ...(properties.fill !== undefined && { color: properties.fill }), - ...(properties['fill-opacity'] !== undefined && { opacity: properties['fill-opacity'] }), - }; - if (Object.keys(fill).length) { - style.fill = fill; - } - } - } - return style; - } - - /** - * set Style from vector tile layer properties. - * @param {Object} layer vector tile layer. - * @param {Object} sprites vector tile layer. - * @param {Number} [order=0] - * @param {Boolean} [symbolToCircle=false] - * - * @returns {StyleOptions} containing all properties for itowns.Style - */ - static setFromVectorTileLayer(layer, sprites, order = 0, symbolToCircle = false) { - const style = { - fill: {}, - stroke: {}, - point: {}, - text: {}, - icon: {}, - }; - - layer.layout = layer.layout || {}; - layer.paint = layer.paint || {}; - - style.order = order; - - if (layer.type === 'fill') { - const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-color'] || layer.paint['fill-pattern'], { type: 'color' })); - style.fill.color = color; - style.fill.opacity = readVectorProperty(layer.paint['fill-opacity']) || opacity; - if (layer.paint['fill-pattern']) { - try { - style.fill.pattern = { - id: layer.paint['fill-pattern'], - source: sprites.source, - cropValues: sprites[layer.paint['fill-pattern']], - }; - } catch (err) { - err.message = `VTlayer '${layer.id}': argument sprites must not be null when using layer.paint['fill-pattern']`; - throw err; - } - } - - if (layer.paint['fill-outline-color']) { - const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-outline-color'], { type: 'color' })); - style.stroke.color = color; - style.stroke.opacity = opacity; - style.stroke.width = 1.0; - style.stroke.dasharray = []; - } - } else if (layer.type === 'line') { - const prepare = readVectorProperty(layer.paint['line-color'], { type: 'color' }); - const { color, opacity } = rgba2rgb(prepare); - style.stroke.dasharray = readVectorProperty(layer.paint['line-dasharray']); - style.stroke.color = color; - style.stroke.lineCap = layer.layout['line-cap']; - style.stroke.width = readVectorProperty(layer.paint['line-width']); - style.stroke.opacity = readVectorProperty(layer.paint['line-opacity']) || opacity; - } else if (layer.type === 'circle' || symbolToCircle) { - const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['circle-color'], { type: 'color' })); - style.point.color = color; - style.point.opacity = opacity; - style.point.radius = readVectorProperty(layer.paint['circle-radius']); - } else if (layer.type === 'symbol') { - // overlapping order - style.text.zOrder = readVectorProperty(layer.layout['symbol-z-order']); - if (style.text.zOrder == 'auto') { - style.text.zOrder = readVectorProperty(layer.layout['symbol-sort-key']) || 'Y'; - } else if (style.text.zOrder == 'viewport-y') { - style.text.zOrder = 'Y'; - } else if (style.text.zOrder == 'source') { - style.text.zOrder = 0; - } - - // position - style.text.anchor = readVectorProperty(layer.layout['text-anchor']); - style.text.offset = readVectorProperty(layer.layout['text-offset']); - style.text.padding = readVectorProperty(layer.layout['text-padding']); - style.text.size = readVectorProperty(layer.layout['text-size']); - style.text.placement = readVectorProperty(layer.layout['symbol-placement']); - style.text.rotation = readVectorProperty(layer.layout['text-rotation-alignment']); - - // content - style.text.field = readVectorProperty(layer.layout['text-field']); - style.text.wrap = readVectorProperty(layer.layout['text-max-width']); - style.text.spacing = readVectorProperty(layer.layout['text-letter-spacing']); - style.text.transform = readVectorProperty(layer.layout['text-transform']); - style.text.justify = readVectorProperty(layer.layout['text-justify']); - - // appearance - const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['text-color'], { type: 'color' })); - style.text.color = color; - style.text.opacity = readVectorProperty(layer.paint['text-opacity']) || (opacity !== undefined && opacity); - - style.text.font = readVectorProperty(layer.layout['text-font']); - const haloColor = readVectorProperty(layer.paint['text-halo-color'], { type: 'color' }); - if (haloColor) { - style.text.haloColor = haloColor.color || haloColor; - style.text.haloWidth = readVectorProperty(layer.paint['text-halo-width']); - style.text.haloBlur = readVectorProperty(layer.paint['text-halo-blur']); - } - - // additional icon - const iconImg = readVectorProperty(layer.layout['icon-image']); - if (iconImg) { - try { - style.icon.id = iconImg; - if (iconImg.stops) { - const iconCropValue = { - ...(iconImg.base !== undefined && { base: iconImg.base }), - stops: iconImg.stops.map((stop) => { - let cropValues = sprites[stop[1]]; - if (stop[1].includes('{')) { - cropValues = function _(p) { - const id = stop[1].replace(/\{(.+?)\}/g, (a, b) => (p[b] || '')).trim(); - cropValues = sprites[id]; - return sprites[id]; - }; - } - return [stop[0], cropValues]; - }), - }; - style.icon.cropValues = iconCropValue; - } else { - style.icon.cropValues = sprites[iconImg]; - if (iconImg[0].includes('{')) { - style.icon.cropValues = function _(p) { - const id = iconImg.replace(/\{(.+?)\}/g, (a, b) => (p[b] || '')).trim(); - style.icon.cropValues = sprites[id]; - return sprites[id]; - }; - } - } - style.icon.source = sprites.source; - style.icon.size = readVectorProperty(layer.layout['icon-size']) || 1; - const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['icon-color'], { type: 'color' })); - style.icon.color = color; - style.icon.opacity = readVectorProperty(layer.paint['icon-opacity']) || (opacity !== undefined && opacity); - } catch (err) { - err.message = `VTlayer '${layer.id}': argument sprites must not be null when using layer.layout['icon-image']`; - throw err; - } - } - } - // VectorTileSet: by default minZoom = 0 and maxZoom = 24 - // https://docs.mapbox.com/style-spec/reference/layers/#maxzoom and #minzoom - // Should be move to layer properties, when (if) one mapBox layer will be considered as several itowns layers. - // issue https://github.com/iTowns/itowns/issues/2153 (last point) - style.zoom = { - min: layer.minzoom || 0, - max: layer.maxzoom || 24, - }; - return style; - } - /** * Applies the style.fill to a polygon of the texture canvas. * @param {CanvasRenderingContext2D} txtrCtx The Context 2D of the texture canvas. diff --git a/src/Core/StyleOptions.js b/src/Core/StyleOptions.js new file mode 100644 index 0000000000..44319d8a27 --- /dev/null +++ b/src/Core/StyleOptions.js @@ -0,0 +1,399 @@ +import { FEATURE_TYPES } from 'Core/Feature'; +import * as mapbox from '@mapbox/mapbox-gl-style-spec'; + +const inv255 = 1 / 255; + +/** + * An object that can contain any properties (order, zoom, fill, stroke, point, + * text or/and icon) and sub properties of a Style.
+ * Used for the instanciation of a {@link Style}. + * + * @typedef {Object} StyleOptions + * + * @property {Number} [order] - Order of the features that will be associated to + * the style. It can helps sorting and prioritizing features if needed. + * + * @property {Object} [zoom] - Level on which to display the feature + * @property {Number} [zoom.max] - max level + * @property {Number} [zoom.min] - min level + * + * @property {Object} [fill] - Fill style for polygons. + * @property {String|Function|THREE.Color} [fill.color] - Defines the main fill color. Can be + * any [valid color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). + * Default is no value, which means no fill. + * If the `Layer` is a `GeometryLayer` you can use `THREE.Color`. + * @property {Image|Canvas|String|Object|Function} [fill.pattern] - Defines a pattern to fill the + * surface with. It can be an `Image` to use directly, an url to fetch the pattern or an object containing + * the url of the image to fetch and the transformation to apply. + * from. See [this example](http://www.itowns-project.org/itowns/examples/#source_file_geojson_raster) + * for how to use. + * @property {Image|String} [fill.pattern.source] - The image or the url to fetch the pattern image + * @property {Object} [fill.pattern.cropValues] - The x, y, width and height (in pixel) of the sub image to use. + * @property {THREE.Color} [fill.pattern.color] - Can be any + * [valid color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). + * It will change the color of the white pixels of the source image. + * @property {Number|Function} [fill.opacity] - The opacity of the color or of the + * pattern. Can be between `0.0` and `1.0`. Default is `1.0`. + * For a `GeometryLayer`, this opacity property isn't used. + * @property {Number|Function} [fill.base_altitude] - `GeometryLayer` style option, defines altitude + * for each coordinate. + * If `base_altitude` is `undefined`, the original altitude is kept, and if it doesn't exist + * then the altitude value is set to 0. + * @property {Number|Function} [fill.extrusion_height] - `GeometryLayer` style option, if defined, + * polygons will be extruded by the specified amount + * + * @property {Object} [stroke] - Lines and polygons edges. + * @property {String|Function|THREE.Color} [stroke.color] The color of the line. Can be any [valid + * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). + * Default is no value, which means no stroke. + * If the `Layer` is a `GeometryLayer` you can use `THREE.Color`. + * @property {Number|Function} [stroke.opacity] - The opacity of the line. Can be between + * `0.0` and `1.0`. Default is `1.0`. + * For a `GeometryLayer`, this opacity property isn't used. + * @property {Number|Function} [stroke.width] - The width of the line. Default is `1.0`. + * @property {Number|Function} [stroke.base_altitude] - `GeometryLayer` style option, defines altitude + * for each coordinate. + * If `base_altitude` is `undefined`, the original altitude is kept, and if it doesn't exist + * then the altitude value is set to 0. + * + * @property {Object} [point] - Point style. + * @property {String|Function} [point.color] - The color of the point. Can be any [valid + * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). + * Default is no value, which means points won't be displayed. + * @property {Number|Function} [point.radius] - The radius of the point, in pixel. Default + * is `2.0`. + * @property {String|Function} [point.line] - The color of the border of the point. Can be + * any [valid color + * string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). + * Not supported for a `GeometryLayer`. + * @property {Number|Function} [point.width] - The width of the border, in pixel. Default + * is `0.0` (no border). + * @property {Number|Function} [point.opacity] - The opacity of the point. Can be between + * `0.0` and `1.0`. Default is `1.0`. + * Not supported for `GeometryLayer`. + * @property {Number|Function} [point.base_altitude] - `GeometryLayer` style option, defines altitude + * for each coordinate. + * If `base_altitude` is `undefined`, the original altitude is kept, and if it doesn't exist + * then the altitude value is set to 0. + * @property {Object} [point.model] - 3D model to instantiate at each point position. + * + * @property {Object} [text] - All things {@link Label} related. (Supported for Points features, not yet + * for Lines and Polygons features.) + * @property {String|Function} [text.field] - A string representing a property key of + * a `FeatureGeometry` enclosed in brackets, that will be replaced by the value of the + * property for each geometry. For example, if each geometry contains a `name` property, + * `text.field` can be set to `{name}`. Default is no value, indicating that no + * text will be displayed. + * + * It's also possible to create more complex expressions. For example, you can combine + * text that will always be displayed (e.g. `foo`) and variable properties (e.g. `{bar}`) + * like the following: `foo {bar}`. You can also use multiple variables in one field. + * Let's say for instance that you have two properties latin name and local name of a + * place, you can write something like `{name_latin} - {name_local}` which can result + * in `Marrakesh - مراكش` for example. + * @property {String|Function} [text.color] - The color of the text. Can be any [valid + * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). + * Default is `#000000`. + * @property {String|Number[]|Function} [text.anchor] - The anchor of the text compared to its + * position (see {@link Label} for the position). Can be one of the following values: `top`, + * `left`, `bottom`, `right`, `center`, `top-left`, `top-right`, `bottom-left` + * or `bottom-right`. Default is `center`. + * + * It can also be defined as an Array of two numbers. Each number defines an offset (in + * fraction of the label width and height) between the label position and the top-left + * corner of the text. The first value is the horizontal offset, and the second is the + * vertical offset. For example, `[-0.5, -0.5]` will be equivalent to `center`. + * @property {Array|Function} [text.offset] - The offset of the text, depending on its + * anchor, in pixels. First value is from `left`, second is from `top`. Default + * is `[0, 0]`. + * @property {Number|Function} [text.padding] - The padding outside the text, in pixels. + * Default is `2`. + * @property {Number|Function} [text.size] - The size of the font, in pixels. Default is + * `16`. + * @property {Number|Function} [text.wrap] - The maximum width, in pixels, before the text + * is wrapped, because the string is too long. Default is `10`. + * @property {Number|Function} [text.spacing] - The spacing between the letters, in `em`. + * Default is `0`. + * @property {String|Function} [text.transform] - A value corresponding to the [CSS + * property + * `text-transform`](https://developer.mozilla.org/en-US/docs/Web/CSS/text-transform). + * Default is `none`. + * @property {String|Function} [text.justify] - A value corresponding to the [CSS property + * `text-align`](https://developer.mozilla.org/en-US/docs/Web/CSS/text-align). + * Default is `center`. + * @property {Number|Function} [text.opacity] - The opacity of the text. Can be between + * `0.0` and `1.0`. Default is `1.0`. + * @property {Array|Function} [text.font] - A list (as an array of string) of font family + * names, prioritized in the order it is set. Default is `Open Sans Regular, + * Arial Unicode MS Regular, sans-serif`. + * @property {String|Function} [text.haloColor] - The color of the halo. Can be any [valid + * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). + * Default is `#000000`. + * @property {Number|Function} [text.haloWidth] - The width of the halo, in pixels. + * Default is `0`. + * @property {Number|Function} [text.haloBlur] - The blur value of the halo, in pixels. + * Default is `0`. + * + * @property {Object} [icon] - Defines the appearance of icons attached to label. + * @property {String} [icon.source] - The url of the icons' image file. + * @property {String} [icon.id] - The id of the icons' sub-image in a vector tile data set. + * @property {String} [icon.cropValues] - the x, y, width and height (in pixel) of the sub image to use. + * @property {String} [icon.anchor] - The anchor of the icon compared to the label position. + * Can be `left`, `bottom`, `right`, `center`, `top-left`, `top-right`, `bottom-left` + * or `bottom-right`. Default is `center`. + * @property {Number} [icon.size] - If the icon's image is passed with `icon.source` and/or + * `icon.id`, its size when displayed on screen is multiplied by `icon.size`. Default is `1`. + * @property {String|Function} [icon.color] - The color of the icon. Can be any [valid + * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). + * It will change the color of the white pixels of the icon source image. + * @property {Number|Function} [icon.opacity] - The opacity of the icon. Can be between + * `0.0` and `1.0`. Default is `1.0`. +*/ + +/** + * generate a StyleOptions from (geojson-like) properties. + * @param {Object} properties (geojson-like) properties. + * @param {FeatureContext} featCtx the context of the feature + * + * @returns {StyleOptions} containing all properties for itowns.Style + */ +function setFromProperties(properties, featCtx) { + const type = featCtx.type; + const style = {}; + if (type === FEATURE_TYPES.POINT) { + const point = { + ...(properties.fill !== undefined && { color: properties.fill }), + ...(properties['fill-opacity'] !== undefined && { opacity: properties['fill-opacity'] }), + ...(properties.stroke !== undefined && { line: properties.stroke }), + ...(properties.radius !== undefined && { radius: properties.radius }), + }; + if (Object.keys(point).length) { + style.point = point; + } + const text = { + ...(properties['label-color'] !== undefined && { color: properties['label-color'] }), + ...(properties['label-opacity'] !== undefined && { opacity: properties['label-opacity'] }), + ...(properties['label-size'] !== undefined && { size: properties['label-size'] }), + }; + if (Object.keys(point).length) { + style.text = text; + } + const icon = { + ...(properties.icon !== undefined && { source: properties.icon }), + ...(properties['icon-scale'] !== undefined && { size: properties['icon-scale'] }), + ...(properties['icon-opacity'] !== undefined && { opacity: properties['icon-opacity'] }), + ...(properties['icon-color'] !== undefined && { color: properties['icon-color'] }), + }; + if (Object.keys(icon).length) { + style.icon = icon; + } + } else { + const stroke = { + ...(properties.stroke !== undefined && { color: properties.stroke }), + ...(properties['stroke-width'] !== undefined && { width: properties['stroke-width'] }), + ...(properties['stroke-opacity'] !== undefined && { opacity: properties['stroke-opacity'] }), + }; + if (Object.keys(stroke).length) { + style.stroke = stroke; + } + if (type !== FEATURE_TYPES.LINE) { + const fill = { + ...(properties.fill !== undefined && { color: properties.fill }), + ...(properties['fill-opacity'] !== undefined && { opacity: properties['fill-opacity'] }), + }; + if (Object.keys(fill).length) { + style.fill = fill; + } + } + } + return style; +} + +function readVectorProperty(property, options) { + if (property != undefined) { + if (mapbox.expression.isExpression(property)) { + return mapbox.expression.createExpression(property, options).value; + } else { + return property; + } + } +} + +function rgba2rgb(orig) { + if (!orig) { + return {}; + } else if (orig.stops || orig.expression) { + return { color: orig }; + } else if (typeof orig == 'string') { + const result = orig.match(/(?:((hsl|rgb)a? *\(([\d.%]+(?:deg|g?rad|turn)?)[ ,]*([\d.%]+)[ ,]*([\d.%]+)[ ,/]*([\d.%]*)\))|(#((?:[\d\w]{3}){1,2})([\d\w]{1,2})?))/i); + if (result === null) { + return { color: orig, opacity: 1.0 }; + } else if (result[7]) { + let opacity = 1.0; + if (result[9]) { + opacity = parseInt(result[9].length == 1 ? `${result[9]}${result[9]}` : result[9], 16) * inv255; + } + return { color: `#${result[8]}`, opacity }; + } else if (result[1]) { + return { color: `${result[2]}(${result[3]},${result[4]},${result[5]})`, opacity: (result[6] ? Number(result[6]) : 1.0) }; + } + } +} + +/** + * set Style from vector tile layer properties. + * @param {Object} layer vector tile layer. + * @param {Object} sprites vector tile layer. + * @param {Number} [order=0] + * @param {Boolean} [symbolToCircle=false] + * + * @returns {StyleOptions} containing all properties for itowns.Style + */ +function setFromVectorTileLayer(layer, sprites, order = 0, symbolToCircle = false) { + const style = { + fill: {}, + stroke: {}, + point: {}, + text: {}, + icon: {}, + }; + + layer.layout = layer.layout || {}; + layer.paint = layer.paint || {}; + + style.order = order; + + if (layer.type === 'fill') { + const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-color'] || layer.paint['fill-pattern'], { type: 'color' })); + style.fill.color = color; + style.fill.opacity = readVectorProperty(layer.paint['fill-opacity']) || opacity; + if (layer.paint['fill-pattern']) { + try { + style.fill.pattern = { + id: layer.paint['fill-pattern'], + source: sprites.source, + cropValues: sprites[layer.paint['fill-pattern']], + }; + } catch (err) { + err.message = `VTlayer '${layer.id}': argument sprites must not be null when using layer.paint['fill-pattern']`; + throw err; + } + } + + if (layer.paint['fill-outline-color']) { + const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-outline-color'], { type: 'color' })); + style.stroke.color = color; + style.stroke.opacity = opacity; + style.stroke.width = 1.0; + style.stroke.dasharray = []; + } + } else if (layer.type === 'line') { + const prepare = readVectorProperty(layer.paint['line-color'], { type: 'color' }); + const { color, opacity } = rgba2rgb(prepare); + style.stroke.dasharray = readVectorProperty(layer.paint['line-dasharray']); + style.stroke.color = color; + style.stroke.lineCap = layer.layout['line-cap']; + style.stroke.width = readVectorProperty(layer.paint['line-width']); + style.stroke.opacity = readVectorProperty(layer.paint['line-opacity']) || opacity; + } else if (layer.type === 'circle' || symbolToCircle) { + const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['circle-color'], { type: 'color' })); + style.point.color = color; + style.point.opacity = opacity; + style.point.radius = readVectorProperty(layer.paint['circle-radius']); + } else if (layer.type === 'symbol') { + // overlapping order + style.text.zOrder = readVectorProperty(layer.layout['symbol-z-order']); + if (style.text.zOrder == 'auto') { + style.text.zOrder = readVectorProperty(layer.layout['symbol-sort-key']) || 'Y'; + } else if (style.text.zOrder == 'viewport-y') { + style.text.zOrder = 'Y'; + } else if (style.text.zOrder == 'source') { + style.text.zOrder = 0; + } + + // position + style.text.anchor = readVectorProperty(layer.layout['text-anchor']); + style.text.offset = readVectorProperty(layer.layout['text-offset']); + style.text.padding = readVectorProperty(layer.layout['text-padding']); + style.text.size = readVectorProperty(layer.layout['text-size']); + style.text.placement = readVectorProperty(layer.layout['symbol-placement']); + style.text.rotation = readVectorProperty(layer.layout['text-rotation-alignment']); + + // content + style.text.field = readVectorProperty(layer.layout['text-field']); + style.text.wrap = readVectorProperty(layer.layout['text-max-width']); + style.text.spacing = readVectorProperty(layer.layout['text-letter-spacing']); + style.text.transform = readVectorProperty(layer.layout['text-transform']); + style.text.justify = readVectorProperty(layer.layout['text-justify']); + + // appearance + const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['text-color'], { type: 'color' })); + style.text.color = color; + style.text.opacity = readVectorProperty(layer.paint['text-opacity']) || (opacity !== undefined && opacity); + + style.text.font = readVectorProperty(layer.layout['text-font']); + const haloColor = readVectorProperty(layer.paint['text-halo-color'], { type: 'color' }); + if (haloColor) { + style.text.haloColor = haloColor.color || haloColor; + style.text.haloWidth = readVectorProperty(layer.paint['text-halo-width']); + style.text.haloBlur = readVectorProperty(layer.paint['text-halo-blur']); + } + + // additional icon + const iconImg = readVectorProperty(layer.layout['icon-image']); + if (iconImg) { + try { + style.icon.id = iconImg; + if (iconImg.stops) { + const iconCropValue = { + ...(iconImg.base !== undefined && { base: iconImg.base }), + stops: iconImg.stops.map((stop) => { + let cropValues = sprites[stop[1]]; + if (stop[1].includes('{')) { + cropValues = function _(p) { + const id = stop[1].replace(/\{(.+?)\}/g, (a, b) => (p[b] || '')).trim(); + cropValues = sprites[id]; + return sprites[id]; + }; + } + return [stop[0], cropValues]; + }), + }; + style.icon.cropValues = iconCropValue; + } else { + style.icon.cropValues = sprites[iconImg]; + if (iconImg[0].includes('{')) { + style.icon.cropValues = function _(p) { + const id = iconImg.replace(/\{(.+?)\}/g, (a, b) => (p[b] || '')).trim(); + style.icon.cropValues = sprites[id]; + return sprites[id]; + }; + } + } + style.icon.source = sprites.source; + style.icon.size = readVectorProperty(layer.layout['icon-size']) || 1; + const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['icon-color'], { type: 'color' })); + style.icon.color = color; + style.icon.opacity = readVectorProperty(layer.paint['icon-opacity']) || (opacity !== undefined && opacity); + } catch (err) { + err.message = `VTlayer '${layer.id}': argument sprites must not be null when using layer.layout['icon-image']`; + throw err; + } + } + } + // VectorTileSet: by default minZoom = 0 and maxZoom = 24 + // https://docs.mapbox.com/style-spec/reference/layers/#maxzoom and #minzoom + // Should be move to layer properties, when (if) one mapBox layer will be considered as several itowns layers. + // issue https://github.com/iTowns/itowns/issues/2153 (last point) + style.zoom = { + min: layer.minzoom || 0, + max: layer.maxzoom || 24, + }; + return style; +} + + +export default { + setFromProperties, + setFromVectorTileLayer, +}; diff --git a/src/Source/VectorTilesSource.js b/src/Source/VectorTilesSource.js index d0f4fd4241..ed4f9f9aef 100644 --- a/src/Source/VectorTilesSource.js +++ b/src/Source/VectorTilesSource.js @@ -1,5 +1,5 @@ import { featureFilter } from '@mapbox/mapbox-gl-style-spec'; -import Style from 'Core/Style'; +import StyleOptions from 'Core/StyleOptions'; import TMSSource from 'Source/TMSSource'; import URLBuilder from 'Provider/URLBuilder'; import Fetcher from 'Provider/Fetcher'; @@ -102,7 +102,7 @@ class VectorTilesSource extends TMSSource { if (layer.type === 'background') { this.backgroundLayer = layer; } else if (ffilter(layer)) { - const style = Style.setFromVectorTileLayer(layer, this.sprites, order, this.symbolToCircle); + const style = StyleOptions.setFromVectorTileLayer(layer, this.sprites, order, this.symbolToCircle); this.styles[layer.id] = style; if (!this.layers[layer['source-layer']]) { diff --git a/test/unit/label.js b/test/unit/label.js index b0fb2a4f61..151fbebd24 100644 --- a/test/unit/label.js +++ b/test/unit/label.js @@ -2,6 +2,7 @@ import assert from 'assert'; import * as THREE from 'three'; import Label from 'Core/Label'; import Style from 'Core/Style'; +import StyleOptions from 'Core/StyleOptions'; import { FeatureCollection, FEATURE_TYPES } from 'Core/Feature'; import Coordinates from 'Core/Geographic/Coordinates'; import Extent from 'Core/Geographic/Extent'; @@ -64,7 +65,7 @@ describe('Label', function () { }; before('init style', function () { - style = new Style(Style.setFromVectorTileLayer(layerVT, sprites)); + style = new Style(StyleOptions.setFromVectorTileLayer(layerVT, sprites)); }); it('should throw errors for bad Label construction', function () { diff --git a/test/unit/style.js b/test/unit/style.js index a30f1e5183..541cbb788b 100644 --- a/test/unit/style.js +++ b/test/unit/style.js @@ -1,5 +1,6 @@ import { FEATURE_TYPES } from 'Core/Feature'; import Style from 'Core/Style'; +import StyleOptions from 'Core/StyleOptions'; import assert from 'assert'; import { TextureLoader } from 'three'; import Fetcher from 'Provider/Fetcher'; @@ -337,7 +338,7 @@ describe('Style', function () { 'label-color': '#eba55f', 'icon-color': '#eba55f', }; - const style = Style.setFromProperties(properties, { type: FEATURE_TYPES.POINT }); + const style = StyleOptions.setFromProperties(properties, { type: FEATURE_TYPES.POINT }); assert.equal(style.point.radius, 2); assert.equal(style.text.color, '#eba55f'); assert.equal(style.icon.color, '#eba55f'); @@ -347,7 +348,7 @@ describe('Style', function () { fill: '#eba55f', stroke: '#eba55f', }; - const style = Style.setFromProperties(properties, { type: FEATURE_TYPES.POLYGON }); + const style = StyleOptions.setFromProperties(properties, { type: FEATURE_TYPES.POLYGON }); assert.equal(style.stroke.color, '#eba55f'); assert.equal(style.fill.color, '#eba55f'); }); @@ -359,7 +360,7 @@ describe('Style', function () { const vectorTileLayer = { type: 'fill', }; - let style = Style.setFromVectorTileLayer(vectorTileLayer); + let style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); // origin is undefined assert.equal(style.fill.color, undefined); // origin has stops or expression @@ -369,7 +370,7 @@ describe('Style', function () { }, 'fill-outline-color': ['string', 'blue'], }; - style = Style.setFromVectorTileLayer(vectorTileLayer); + style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); assert.equal(style.fill.color, vectorTileLayer.paint['fill-color']); assert.equal(style.stroke.color.constructor.name, 'StyleExpression'); assert.equal(style.stroke.color.evaluate().constructor.name, 'Color'); @@ -378,7 +379,7 @@ describe('Style', function () { 'fill-color': 'red', 'fill-outline-color': '#aabbccdd', }; - style = Style.setFromVectorTileLayer(vectorTileLayer); + style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); assert.equal(style.fill.color, vectorTileLayer.paint['fill-color']); assert.equal(style.fill.opacity, 1); assert.equal(style.stroke.color, '#aabbcc'); @@ -388,7 +389,7 @@ describe('Style', function () { 'fill-color': 'rgba(120, 130, 140, 12)', 'fill-outline-color': 'hsl(220, 230, 240)', }; - style = Style.setFromVectorTileLayer(vectorTileLayer); + style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); assert.equal(style.fill.color, 'rgb(120,130,140)'); assert.equal(style.fill.opacity, 12); assert.equal(style.stroke.color, 'hsl(220,230,240)'); @@ -406,7 +407,7 @@ describe('Style', function () { 'fill-outline-color': '#eba55f', 'fill-opacity': 0.5, }; - const style = Style.setFromVectorTileLayer(vectorTileLayer); + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); // fill-outline-color assert.equal(style.stroke.color, '#eba55f'); // fill-opacity @@ -419,7 +420,7 @@ describe('Style', function () { filler: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, source: 'ImgUrl', }; - const style = Style.setFromVectorTileLayer(vectorTileLayer, sprites); + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer, sprites); // fill-pattern assert.equal(style.fill.pattern.id, imgId); assert.equal(style.fill.pattern.cropValues, sprites[imgId]); @@ -433,7 +434,7 @@ describe('Style', function () { 'line-color': '#eba55f', }, }; - const style = Style.setFromVectorTileLayer(vectorTileLayer); + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); assert.equal(style.stroke.color, '#eba55f'); }); @@ -444,7 +445,7 @@ describe('Style', function () { 'circle-color': '#eba55f', }, }; - const style = Style.setFromVectorTileLayer(vectorTileLayer); + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); assert.equal(style.point.color, '#eba55f'); }); @@ -457,7 +458,7 @@ describe('Style', function () { 'symbol-z-order': 'auto', 'text-justify': 'center', }; - const style = Style.setFromVectorTileLayer(vectorTileLayer); + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); // symbol-z-order assert.equal(style.text.zOrder, 'Y'); // text-justify @@ -474,7 +475,7 @@ describe('Style', function () { [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, source: 'ImgUrl', }; - const style = Style.setFromVectorTileLayer(vectorTileLayer, sprites); + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer, sprites); assert.equal(style.icon.id, vectorTileLayer.layout['icon-image']); assert.equal(style.icon.cropValues, sprites[vectorTileLayer.layout['icon-image']]); }); @@ -488,7 +489,7 @@ describe('Style', function () { [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, source: 'ImgUrl', }; - const style = Style.setFromVectorTileLayer(vectorTileLayer, sprites); + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer, sprites); assert.equal(style.icon.id, vectorTileLayer.layout['icon-image']); assert.equal(typeof style.icon.cropValues, 'function'); }); @@ -505,7 +506,7 @@ describe('Style', function () { [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, source: 'ImgUrl', }; - const style = Style.setFromVectorTileLayer(vectorTileLayer, sprites); + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer, sprites); assert.equal(style.icon.id, vectorTileLayer.layout['icon-image']); assert.equal(style.icon.cropValues.stops[0][1], sprites[vectorTileLayer.layout['icon-image'].stops[0][1]]); }); @@ -522,7 +523,7 @@ describe('Style', function () { [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, source: 'ImgUrl', }; - const style = Style.setFromVectorTileLayer(vectorTileLayer, sprites); + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer, sprites); assert.equal(style.icon.id, vectorTileLayer.layout['icon-image']); assert.equal(typeof style.icon.cropValues.stops[0][1], 'function'); });