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');
+ });
+ });
+ });
+ });
+});