diff --git a/src/Core/Feature.js b/src/Core/Feature.js index 018bb45c95..770637536d 100644 --- a/src/Core/Feature.js +++ b/src/Core/Feature.js @@ -1,7 +1,7 @@ import * as THREE from 'three'; import Extent from 'Core/Geographic/Extent'; import Coordinates from 'Core/Geographic/Coordinates'; -import Style from 'Core/Style'; +import StyleOptions from 'Core/StyleOptions'; function defaultExtent(crs) { return new Extent(crs, Infinity, -Infinity, Infinity, -Infinity); @@ -250,7 +250,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 0083a0574d..58fa90cfde 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 maplibre from '@maplibre/maplibre-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 (maplibre.expression.isExpression(property)) { - return maplibre.expression.createExpression(property, options).value; - } else { - return property; - } - } -} - async function loadImage(source) { let promise = cacheStyle.get(source, 'null'); if (!promise) { @@ -301,150 +266,6 @@ function _addIcon(icon, domElement, opt) { return cIcon; } -/** - * An object that can contain any properties (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 {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`. -*/ - /** * A Style is a class that defines the visual appearance of {@link * FeatureCollection} and {@link Feature}. It is taken into account when drawing @@ -697,238 +518,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 {Boolean} [symbolToCircle=false] - * - * @returns {StyleOptions} containing all properties for itowns.Style - */ - static setFromVectorTileLayer(layer, sprites, symbolToCircle = false) { - const style = { - fill: {}, - stroke: {}, - point: {}, - text: {}, - icon: {}, - }; - - layer.layout = layer.layout || {}; - layer.paint = layer.paint || {}; - - 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; - } else { - style.stroke.width = 0.0; - } - } 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') { - // if symbol we shouldn't draw stroke but defaut value is 1. - style.stroke.width = 0.0; - // 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']);// Units ems - 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) { - const cropValueDefault = { - x: 0, - y: 0, - width: 1, - height: 1, - }; - 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(); - if (cropValues === undefined) { - // const warning = `WARNING: "${id}" not found in sprite file`; - sprites[id] = cropValueDefault;// or return cropValueDefault; - } - return sprites[id]; - }; - } else if (cropValues === undefined) { - // const warning = `WARNING: "${stop[1]}" not found in sprite file`; - cropValues = cropValueDefault; - } - return [stop[0], cropValues]; - }), - }; - style.icon.cropValues = iconCropValue; - } else { - style.icon.cropValues = sprites[iconImg]; - if (iconImg.includes('{')) { - style.icon.cropValues = function _(p) { - const id = iconImg.replace(/\{(.+?)\}/g, (a, b) => (p[b] || '')).trim(); - if (sprites[id] === undefined) { - // const warning = `WARNING: "${id}" not found in sprite file`; - sprites[id] = cropValueDefault;// or return cropValueDefault; - } - return sprites[id]; - }; - } else if (sprites[iconImg] === undefined) { - // const warning = `WARNING: "${iconImg}" not found in sprite file`; - style.icon.cropValues = cropValueDefault; - } - } - 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' })); - // https://docs.mapbox.com/style-spec/reference/layers/#paint-symbol-icon-color - if (iconImg.sdf) { - 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..abfb8277fe --- /dev/null +++ b/src/Core/StyleOptions.js @@ -0,0 +1,416 @@ +import { FEATURE_TYPES } from 'Core/Feature'; +import * as maplibre from '@maplibre/maplibre-gl-style-spec'; + +/** + * An object that can contain any properties (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 {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`. +*/ + +/** + * 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 + */ +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 (maplibre.expression.isExpression(property)) { + return maplibre.expression.createExpression(property, options).value; + } else { + return property; + } + } +} + +const inv255 = 1 / 255; + +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 {Boolean} [symbolToCircle=false] + * + * @returns {StyleOptions} containing all properties for itowns.Style + */ +function setFromVectorTileLayer(layer, sprites, symbolToCircle = false) { + const style = { + fill: {}, + stroke: {}, + point: {}, + text: {}, + icon: {}, + }; + + layer.layout = layer.layout || {}; + layer.paint = layer.paint || {}; + + 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; + } else { + style.stroke.width = 0.0; + } + } 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') { + // if symbol we shouldn't draw stroke but defaut value is 1. + style.stroke.width = 0.0; + // 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']);// Units ems + 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) { + const cropValueDefault = { + x: 0, + y: 0, + width: 1, + height: 1, + }; + 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(); + if (cropValues === undefined) { + // const warning = `WARNING: "${id}" not found in sprite file`; + sprites[id] = cropValueDefault;// or return cropValueDefault; + } + return sprites[id]; + }; + } else if (cropValues === undefined) { + // const warning = `WARNING: "${stop[1]}" not found in sprite file`; + cropValues = cropValueDefault; + } + return [stop[0], cropValues]; + }), + }; + style.icon.cropValues = iconCropValue; + } else { + style.icon.cropValues = sprites[iconImg]; + if (iconImg.includes('{')) { + style.icon.cropValues = function _(p) { + const id = iconImg.replace(/\{(.+?)\}/g, (a, b) => (p[b] || '')).trim(); + if (sprites[id] === undefined) { + // const warning = `WARNING: "${id}" not found in sprite file`; + sprites[id] = cropValueDefault;// or return cropValueDefault; + } + return sprites[id]; + }; + } else if (sprites[iconImg] === undefined) { + // const warning = `WARNING: "${iconImg}" not found in sprite file`; + style.icon.cropValues = cropValueDefault; + } + } + 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' })); + // https://docs.mapbox.com/style-spec/reference/layers/#paint-symbol-icon-color + if (iconImg.sdf) { + 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 360b7448d3..067db89796 100644 --- a/src/Source/VectorTilesSource.js +++ b/src/Source/VectorTilesSource.js @@ -1,5 +1,5 @@ import { featureFilter } from '@maplibre/maplibre-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'; @@ -117,7 +117,7 @@ class VectorTilesSource extends TMSSource { if (layer['source-layer'] === undefined) { getPropertiesFromRefLayer(mvtStyle.layers, layer); } - const style = Style.setFromVectorTileLayer(layer, this.sprites, this.symbolToCircle); + const style = StyleOptions.setFromVectorTileLayer(layer, this.sprites, 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..7e338e1ecf 100644 --- a/test/unit/label.js +++ b/test/unit/label.js @@ -58,13 +58,19 @@ describe('Label', function () { 'text-field': 'label', }, }; - const sprites = { - img: '', - icon: { x: 0, y: 0, width: 10, height: 10 }, - }; before('init style', function () { - style = new Style(Style.setFromVectorTileLayer(layerVT, sprites)); + const styleOptions = { + text: { + field: 'label', + }, + icon: { + id: 'icon', + cropValues: { x: 0, y: 0, width: 10, height: 10 }, + size: 1, + }, + }; + style = new Style(styleOptions); }); it('should throw errors for bad Label construction', function () { diff --git a/test/unit/style.js b/test/unit/style.js index a30f1e5183..75239efa86 100644 --- a/test/unit/style.js +++ b/test/unit/style.js @@ -1,4 +1,3 @@ -import { FEATURE_TYPES } from 'Core/Feature'; import Style from 'Core/Style'; import assert from 'assert'; import { TextureLoader } from 'three'; @@ -329,204 +328,4 @@ describe('Style', function () { }); }); }); - - describe('setFromProperties', () => { - it('FEATURE_TYPES.POINT', () => { - const properties = { - radius: 2, - 'label-color': '#eba55f', - 'icon-color': '#eba55f', - }; - const style = Style.setFromProperties(properties, { type: FEATURE_TYPES.POINT }); - assert.equal(style.point.radius, 2); - assert.equal(style.text.color, '#eba55f'); - assert.equal(style.icon.color, '#eba55f'); - }); - it('FEATURE_TYPES.POLYGON', () => { - const properties = { - fill: '#eba55f', - stroke: '#eba55f', - }; - const style = Style.setFromProperties(properties, { type: FEATURE_TYPES.POLYGON }); - assert.equal(style.stroke.color, '#eba55f'); - assert.equal(style.fill.color, '#eba55f'); - }); - }); - - describe('setFromVectorTileLayer', () => { - describe('test sub-function', () => { - it('rgba2rgb(color)', () => { - const vectorTileLayer = { - type: 'fill', - }; - let style = Style.setFromVectorTileLayer(vectorTileLayer); - // origin is undefined - assert.equal(style.fill.color, undefined); - // origin has stops or expression - vectorTileLayer.paint = { - 'fill-color': { - stops: [[10, '#eba55f']], - }, - 'fill-outline-color': ['string', 'blue'], - }; - style = Style.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'); - // origin is string (named or hex) - vectorTileLayer.paint = { - 'fill-color': 'red', - 'fill-outline-color': '#aabbccdd', - }; - style = Style.setFromVectorTileLayer(vectorTileLayer); - assert.equal(style.fill.color, vectorTileLayer.paint['fill-color']); - assert.equal(style.fill.opacity, 1); - assert.equal(style.stroke.color, '#aabbcc'); - assert.equal(style.stroke.opacity, 221 / 255); - // origin is string (rgba or hsl) - vectorTileLayer.paint = { - 'fill-color': 'rgba(120, 130, 140, 12)', - 'fill-outline-color': 'hsl(220, 230, 240)', - }; - style = Style.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)'); - assert.equal(style.stroke.opacity, 1); - }); - }); - - describe("layer.type==='fill'", () => { - const imgId = 'filler'; - const vectorTileLayer = { - type: 'fill', - }; - it('without fill-pattern (or sprites)', () => { - vectorTileLayer.paint = { - 'fill-outline-color': '#eba55f', - 'fill-opacity': 0.5, - }; - const style = Style.setFromVectorTileLayer(vectorTileLayer); - // fill-outline-color - assert.equal(style.stroke.color, '#eba55f'); - // fill-opacity - assert.equal(style.fill.opacity, vectorTileLayer.paint['fill-opacity']); - }); - - it('with fill-pattern (and sprites)', () => { - vectorTileLayer.paint['fill-pattern'] = imgId; - const sprites = { - filler: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, - source: 'ImgUrl', - }; - const style = Style.setFromVectorTileLayer(vectorTileLayer, sprites); - // fill-pattern - assert.equal(style.fill.pattern.id, imgId); - assert.equal(style.fill.pattern.cropValues, sprites[imgId]); - }); - }); - - it("layer.type==='line'", () => { - const vectorTileLayer = { - type: 'line', - paint: { - 'line-color': '#eba55f', - }, - }; - const style = Style.setFromVectorTileLayer(vectorTileLayer); - assert.equal(style.stroke.color, '#eba55f'); - }); - - it("layer.type==='circle'", () => { - const vectorTileLayer = { - type: 'circle', - paint: { - 'circle-color': '#eba55f', - }, - }; - const style = Style.setFromVectorTileLayer(vectorTileLayer); - assert.equal(style.point.color, '#eba55f'); - }); - - describe("layer.type==='symbol'", () => { - const vectorTileLayer = { - type: 'symbol', - }; - it('without icon-image', () => { - vectorTileLayer.layout = { - 'symbol-z-order': 'auto', - 'text-justify': 'center', - }; - const style = Style.setFromVectorTileLayer(vectorTileLayer); - // symbol-z-order - assert.equal(style.text.zOrder, 'Y'); - // text-justify - assert.equal(style.text.justify, vectorTileLayer.layout['text-justify']); - }); - - describe('with icon-image (and sprites)', () => { - it("with icon-image = 'icon-13'", () => { - const imgId = 'icon-13'; - vectorTileLayer.layout = { - 'icon-image': imgId, - }; - const sprites = { - [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, - source: 'ImgUrl', - }; - const style = Style.setFromVectorTileLayer(vectorTileLayer, sprites); - assert.equal(style.icon.id, vectorTileLayer.layout['icon-image']); - assert.equal(style.icon.cropValues, sprites[vectorTileLayer.layout['icon-image']]); - }); - - it("with icon-image = '{name}'", () => { - const imgId = '{name}'; - vectorTileLayer.layout = { - 'icon-image': imgId, - }; - const sprites = { - [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, - source: 'ImgUrl', - }; - const style = Style.setFromVectorTileLayer(vectorTileLayer, sprites); - assert.equal(style.icon.id, vectorTileLayer.layout['icon-image']); - assert.equal(typeof style.icon.cropValues, 'function'); - }); - - it("with icon-image = {stops: [$zoom, 'icon-13']", () => { - const imgId = 'icon-13'; - vectorTileLayer.layout = { - 'icon-image': { - base: 1, - stops: [[13, imgId]], - }, - }; - const sprites = { - [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, - source: 'ImgUrl', - }; - const style = Style.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]]); - }); - - it("with icon-image = {stops: [$zoom, '{name}']", () => { - const imgId = '{name}'; - vectorTileLayer.layout = { - 'icon-image': { - base: 1, - stops: [[13, imgId]], - }, - }; - const sprites = { - [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, - source: 'ImgUrl', - }; - const style = Style.setFromVectorTileLayer(vectorTileLayer, sprites); - assert.equal(style.icon.id, vectorTileLayer.layout['icon-image']); - assert.equal(typeof style.icon.cropValues.stops[0][1], 'function'); - }); - }); - }); - }); }); diff --git a/test/unit/styleoptions.js b/test/unit/styleoptions.js new file mode 100644 index 0000000000..fb65e81691 --- /dev/null +++ b/test/unit/styleoptions.js @@ -0,0 +1,235 @@ +import { FEATURE_TYPES } from 'Core/Feature'; +import StyleOptions from 'Core/StyleOptions'; +import assert from 'assert'; +// import { TextureLoader } from 'three'; +// import Fetcher from 'Provider/Fetcher'; +// import sinon from 'sinon'; + +describe('StyleOptions', function () { + // const textureLoader = new TextureLoader(); + // let stubFetcherTexture; + // before(function () { + // stubFetcherTexture = sinon.stub(Fetcher, 'texture') + // .callsFake((url, options = {}) => { + // let res; + // let rej; + + // textureLoader.crossOrigin = options.crossOrigin; + + // const promise = new Promise((resolve, reject) => { + // res = resolve; + // rej = reject; + // }); + + // textureLoader.load(url, (x) => { + // x.image = document.createElement('img'); + // return res(x); + // }, () => {}, rej); + // return promise; + // }); + // }); + + // after(function () { + // stubFetcherTexture.restore(); + // }); + + describe('setFromProperties', () => { + it('FEATURE_TYPES.POINT', () => { + const properties = { + radius: 2, + 'label-color': '#eba55f', + 'icon-color': '#eba55f', + }; + 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'); + }); + it('FEATURE_TYPES.POLYGON', () => { + const properties = { + fill: '#eba55f', + stroke: '#eba55f', + }; + const style = StyleOptions.setFromProperties(properties, { type: FEATURE_TYPES.POLYGON }); + assert.equal(style.stroke.color, '#eba55f'); + assert.equal(style.fill.color, '#eba55f'); + }); + }); + + describe('setFromVectorTileLayer', () => { + describe('test sub-function', () => { + it('rgba2rgb(color)', () => { + const vectorTileLayer = { + type: 'fill', + }; + let style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); + // origin is undefined + assert.equal(style.fill.color, undefined); + // origin has stops or expression + vectorTileLayer.paint = { + 'fill-color': { + stops: [[10, '#eba55f']], + }, + 'fill-outline-color': ['string', 'blue'], + }; + 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'); + // origin is string (named or hex) + vectorTileLayer.paint = { + 'fill-color': 'red', + 'fill-outline-color': '#aabbccdd', + }; + 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'); + assert.equal(style.stroke.opacity, 221 / 255); + // origin is string (rgba or hsl) + vectorTileLayer.paint = { + 'fill-color': 'rgba(120, 130, 140, 12)', + 'fill-outline-color': 'hsl(220, 230, 240)', + }; + 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)'); + assert.equal(style.stroke.opacity, 1); + }); + }); + + describe("layer.type==='fill'", () => { + const imgId = 'filler'; + const vectorTileLayer = { + type: 'fill', + }; + it('without fill-pattern (or sprites)', () => { + vectorTileLayer.paint = { + 'fill-outline-color': '#eba55f', + 'fill-opacity': 0.5, + }; + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); + // fill-outline-color + assert.equal(style.stroke.color, '#eba55f'); + // fill-opacity + assert.equal(style.fill.opacity, vectorTileLayer.paint['fill-opacity']); + }); + + it('with fill-pattern (and sprites)', () => { + vectorTileLayer.paint['fill-pattern'] = imgId; + const sprites = { + filler: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, + source: 'ImgUrl', + }; + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer, sprites); + // fill-pattern + assert.equal(style.fill.pattern.id, imgId); + assert.equal(style.fill.pattern.cropValues, sprites[imgId]); + }); + }); + + it("layer.type==='line'", () => { + const vectorTileLayer = { + type: 'line', + paint: { + 'line-color': '#eba55f', + }, + }; + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); + assert.equal(style.stroke.color, '#eba55f'); + }); + + it("layer.type==='circle'", () => { + const vectorTileLayer = { + type: 'circle', + paint: { + 'circle-color': '#eba55f', + }, + }; + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); + assert.equal(style.point.color, '#eba55f'); + }); + + describe("layer.type==='symbol'", () => { + const vectorTileLayer = { + type: 'symbol', + }; + it('without icon-image', () => { + vectorTileLayer.layout = { + 'symbol-z-order': 'auto', + 'text-justify': 'center', + }; + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer); + // symbol-z-order + assert.equal(style.text.zOrder, 'Y'); + // text-justify + assert.equal(style.text.justify, vectorTileLayer.layout['text-justify']); + }); + + describe('with icon-image (and sprites)', () => { + it("with icon-image = 'icon-13'", () => { + const imgId = 'icon-13'; + vectorTileLayer.layout = { + 'icon-image': imgId, + }; + const sprites = { + [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, + source: 'ImgUrl', + }; + 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']]); + }); + + it("with icon-image = '{name}'", () => { + const imgId = '{name}'; + vectorTileLayer.layout = { + 'icon-image': imgId, + }; + const sprites = { + [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, + source: 'ImgUrl', + }; + const style = StyleOptions.setFromVectorTileLayer(vectorTileLayer, sprites); + assert.equal(style.icon.id, vectorTileLayer.layout['icon-image']); + assert.equal(typeof style.icon.cropValues, 'function'); + }); + + it("with icon-image = {stops: [$zoom, 'icon-13']", () => { + const imgId = 'icon-13'; + vectorTileLayer.layout = { + 'icon-image': { + base: 1, + stops: [[13, imgId]], + }, + }; + const sprites = { + [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, + source: 'ImgUrl', + }; + 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]]); + }); + + it("with icon-image = {stops: [$zoom, '{name}']", () => { + const imgId = '{name}'; + vectorTileLayer.layout = { + 'icon-image': { + base: 1, + stops: [[13, imgId]], + }, + }; + const sprites = { + [imgId]: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, + source: 'ImgUrl', + }; + 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'); + }); + }); + }); + }); +});