• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

iTowns / itowns / 6979781676

24 Nov 2023 10:44AM UTC coverage: 77.111% (+0.1%) from 77.004%
6979781676

Pull #2223

github

web-flow
Merge 23836a3b7 into 1d10290b5
Pull Request #2223: Fix base alti for mesh 3d

4051 of 5992 branches covered (0.0%)

Branch coverage included in aggregate %.

216 of 238 new or added lines in 9 files covered. (90.76%)

9 existing lines in 4 files now uncovered.

7986 of 9618 relevant lines covered (83.03%)

1640.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.38
/src/Core/Style.js
1
import { FEATURE_TYPES } from 'Core/Feature';
1✔
2
import Cache from 'Core/Scheduler/Cache';
1✔
3
import Fetcher from 'Provider/Fetcher';
1✔
4
import * as mapbox from '@mapbox/mapbox-gl-style-spec';
1✔
5
import { Color } from 'three';
1✔
6
import { deltaE } from 'Renderer/Color';
1✔
7
import Coordinates from 'Core/Geographic/Coordinates';
189,958✔
8

9
import itowns_stroke_single_before from './StyleChunk/itowns_stroke_single_before.css';
10

11
export const cacheStyle = new Cache();
1✔
12

13
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1✔
14
const matrix = svg.createSVGMatrix();
1✔
15

16
const inv255 = 1 / 255;
1✔
17
const canvas = (typeof document !== 'undefined') ? document.createElement('canvas') : {};
1!
18

19
function baseAltitudeDefault(properties, ctx) {
20
    return ctx?.coordinates?.z || 0;
3,694!
21
}
22

23
export function readExpression(property, ctx) {
24
    if (property != undefined) {
7,239!
25
        if (property.expression) {
7,239!
26
            return property.expression.evaluate(ctx);
×
27
        } else if (property.stops) {
7,239!
UNCOV
28
            for (let i = property.stops.length - 1; i >= 0; i--) {
×
UNCOV
29
                const stop = property.stops[i];
×
30

NEW
31
                if (ctx.zoom >= stop[0]) {
×
UNCOV
32
                    return stop[1];
×
33
                }
34
            }
35
            return property.stops[0][1];
×
36
        }
37
        if (typeof property === 'string' || property instanceof String) {
7,239✔
38
            property = property.replace(/\{(.+?)\}/g, (a, b) => (ctx.properties[b] || '')).trim();
20!
39
            // return property.replace(/\{(.+?)\}/g, (a, b) => (ctx.properties[b] || '')).trim();
40
        }
41
        if (property instanceof Function) {
7,239!
42
            // TOBREAK: Pass the current `context` as a unique parameter.
43
            // In this proposal, metadata will be accessed in the callee by the
44
            // `context.properties` property.
UNCOV
45
            return property(ctx.properties, ctx);
×
46
        }
47
        return property;
7,239✔
48
    }
49
}
50

51
function rgba2rgb(orig) {
52
    if (!orig) {
16✔
53
        return {};
4✔
54
    } else if (orig.stops || orig.expression) {
12!
55
        return { color: orig };
×
56
    } else if (typeof orig == 'string') {
12!
57
        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);
12✔
58
        if (!result) {
12✔
59
            return { color: orig, opacity: 1.0 };
1✔
60
        } else if (result[7]) {
11✔
61
            let opacity = 1.0;
4✔
62
            if (result[9]) {
4!
63
                opacity = parseInt(result[9].length == 1 ? `${result[9]}${result[9]}` : result[9], 16) * inv255;
×
64
            }
65
            return { color: `#${result[8]}`, opacity };
4✔
66
        } else if (result[0]) {
7!
67
            return { color: `${result[2]}(${result[3]},${result[4]},${result[5]})`, opacity: (Number(result[6]) || 1.0) };
7✔
68
        }
69
    }
70
}
71

72
function readVectorProperty(property, options) {
73
    if (property != undefined) {
64✔
74
        if (mapbox.expression.isExpression(property)) {
19!
75
            return mapbox.expression.createExpression(property, options).value;
×
76
        } else {
77
            return property;
19✔
78
        }
79
    }
80
}
81

82
async function loadImage(source) {
14!
83
    let promise = cacheStyle.get(source, 'null');
2✔
84
    if (!promise) {
2!
85
        promise = Fetcher.texture(source, { crossOrigin: 'anonymous' });
2✔
86
        cacheStyle.set(promise, source, 'null');
2✔
87
    }
2✔
88
    return (await promise).image;
2✔
89
}
1✔
90

91
function cropImage(img, cropValues = { width: img.naturalWidth, height: img.naturalHeight }) {
3✔
92
    canvas.width = cropValues.width;
3✔
93
    canvas.height = cropValues.height;
3✔
94
    const ctx = canvas.getContext('2d', { willReadFrequently: true });
3✔
95
    ctx.drawImage(img,
3✔
96
        cropValues.x || 0, cropValues.y || 0, cropValues.width, cropValues.height,
12✔
97
        0, 0, cropValues.width, cropValues.height);
98
    return ctx.getImageData(0, 0, cropValues.width, cropValues.height);
3✔
99
}
100

101
function replaceWhitePxl(imgd, color, id) {
102
    if (!color) {
2✔
103
        return imgd;
1✔
104
    }
105
    const imgdColored = cacheStyle.get(id, color);
1✔
106
    if (!imgdColored) {
1!
107
        const pix = imgd.data;
1✔
108
        const newColor = new Color(color);
1✔
109
        const colorToChange = new Color('white');
1✔
110
        for (let i = 0, n = pix.length; i < n; i += 4) {
1✔
111
            const d = deltaE(pix.slice(i, i + 3), colorToChange) / 100;
100✔
112
            pix[i] = (pix[i] * d +  newColor.r * 255 * (1 - d));
100✔
113
            pix[i + 1] = (pix[i + 1] * d +  newColor.g * 255 * (1 - d));
100✔
114
            pix[i + 2] = (pix[i + 2] * d +  newColor.b * 255 * (1 - d));
100✔
115
        }
116
        cacheStyle.set(imgd, id, color);
1✔
117
        return imgd;
1✔
118
    }
119
    return imgdColored;
×
120
}
121

122
const textAnchorPosition = {
1✔
123
    left: [0, -0.5],
124
    right: [-1, -0.5],
125
    top: [-0.5, 0],
126
    bottom: [-0.5, -1],
127
    'top-right': [-1, 0],
128
    'bottom-left': [0, -1],
129
    'bottom-right': [-1, -1],
130
    center: [-0.5, -0.5],
131
    'top-left': [0, 0],
132
};
133

134
function defineStyleProperty(style, category, parameter, value, defaultValue) {
135
    let property;
136
    Object.defineProperty(
1,221✔
137
        style[category],
138
        parameter,
139
        {
140
            enumerable: true,
141
            get: () => {
142
                if (property) { return property; }
12,113✔
143
                if (value) { return readExpression(value, style.context); }
11,776✔
144
                const dataValue = style.context.fStyle?.[category]?.[parameter];
4,537!
145
                if (dataValue) { return readExpression(dataValue, style.context); }
4,537!
146
                if (defaultValue instanceof Function) {
4,537✔
147
                    return defaultValue(style.context.properties, style.context);
3,694✔
148
                }
149
                return defaultValue;
843✔
150
            },
151
            set: (v) => {
152
                property = v;
754✔
153
            },
154
        });
155
}
156

157
/**
5✔
158
 * @class
159
 * @classdesc StyleContext stores metadata of one FeatureGeometry that are needed for its style computation:
160
 * type of feature and what is needed (fill, stroke or draw a point, etc.) as well as where to get its
161
 * properties and its coordinates (for base_altitude).
162
 *
163
 * @property {number}           zoom Current zoom to display the FeatureGeometry.
164
 * @property {Object}           collection The FeatureCollection to which the FeatureGeometry is attached.
165
 * @property {Object}           properties Properties of the FeatureGeometry.
166
 * @property {string}           type Geometry type of the feature. Can be `point`, `line`, or `polygon`.
167
 * @property {Style}            style Style of the FeatureGeometry computed from Layer.style and user.style.
168
 * @property {Coordinates}      coordinates The coordinates (in world space) of the last vertex (x, y, z) set with
169
 *                                  setLocalCoordinatesFromArray().
170
 * private properties:
171
 * @property {Coordinates}      worldCoord @private Coordinates object to store coordinates in world space.
172
 * @property {Coordinates}      localCoordinates @private Coordinates object to store coordinates in local space.
173
 * @property {boolean}          worldCoordsComputed @private Have the world coordinates already been computed
174
 *                                  from the local coordinates?
175
 * @property {Feature}          feature  @private The itowns feature of interest.
176
 * @property {FeatureGeometry}  geometry  @private The FeatureGeometry to compute the style.
177
 */
178
export class StyleContext {
32✔
179
    #worldCoord = new Coordinates('EPSG:4326', 0, 0, 0);
32✔
180
    #localCoordinates = new Coordinates('EPSG:4326', 0, 0, 0);
32✔
181
    #worldCoordsComputed = true;
32✔
182
    #feature = {};
32✔
183
    #geometry = {};
1✔
184

185
    setZoom(zoom) {
186
        this.zoom = zoom;
3✔
187
    }
188

189
    setFeature(f) {
190
        this.#feature = f;
27✔
191
    }
192

193
    setGeometry(g) {
194
        this.#geometry = g;
31✔
195
    }
196

197
    setCollection(c) {
198
        this.collection = c;
14✔
199
        this.#localCoordinates.setCrs(c.crs);
14✔
200
    }
201

202
    setLocalCoordinatesFromArray(vertices, offset) {
203
        this.#worldCoordsComputed = false;
3,640✔
204
        return this.#localCoordinates.setFromArray(vertices, offset);
3,640✔
205
    }
206

207
    get properties() {
208
        return this.#geometry.properties;
7,427✔
209
    }
210

211
    get type() {
212
        return this.#feature.type;
3,731✔
213
    }
214
    get fStyle() {
215
        let featureStyle = this.#feature.style;
4,537✔
216
        if (featureStyle instanceof Function) {
4,537✔
217
            featureStyle = featureStyle(this.properties, this);
3,731✔
218
        }
219
        return featureStyle;
4,537✔
220
    }
221

222
    get coordinates() {
223
        if (!this.#worldCoordsComputed) {
3,694✔
224
            this.#worldCoordsComputed = true;
3,640✔
225
            this.#worldCoord.copy(this.#localCoordinates).applyMatrix4(this.collection.matrixWorld);
3,640✔
226
            if (this.#localCoordinates.crs == 'EPSG:4978') {
3,640✔
227
                return this.#worldCoord.as('EPSG:4326', this.#worldCoord);
1,783✔
228
            }
229
        }
230
        return this.#worldCoord;
1,911✔
231
    }
2✔
232
}
233

234
function _addIcon(icon, domElement, opt) {
235
    const cIcon = icon.cloneNode();
13✔
236

237
    cIcon.setAttribute('class', 'itowns-icon');
13✔
238

239
    // cIcon.width = icon.width * this.icon.size;
240
    // cIcon.height = icon.height * this.icon.size;
241
    // cIcon.style.color = this.icon.color;
242
    // cIcon.style.opacity = this.icon.opacity;
243
    cIcon.width = icon.width * opt.size;
13✔
244
    cIcon.height = icon.height * opt.size;
13✔
245
    cIcon.style.color = opt.color;
13✔
246
    cIcon.style.opacity = opt.opacity;
13✔
247
    cIcon.style.position = 'absolute';
13✔
248
    cIcon.style.top = '0';
13✔
249
    cIcon.style.left = '0';
13✔
250

251
    switch (opt.anchor) { // center by default
13✔
252
        case 'left':
253
            cIcon.style.top = `${-0.5 * cIcon.height}px`;
1✔
254
            break;
1✔
255
        case 'right':
256
            cIcon.style.top = `${-0.5 * cIcon.height}px`;
1✔
257
            cIcon.style.left = `${-cIcon.width}px`;
1✔
258
            break;
1✔
259
        case 'top':
260
            cIcon.style.left = `${-0.5 * cIcon.width}px`;
1✔
261
            break;
1✔
262
        case 'bottom':
263
            cIcon.style.top = `${-cIcon.height}px`;
1✔
264
            cIcon.style.left = `${-0.5 * cIcon.width}px`;
1✔
265
            break;
1✔
266
        case 'bottom-left':
267
            cIcon.style.top = `${-cIcon.height}px`;
1✔
268
            break;
1✔
269
        case 'bottom-right':
270
            cIcon.style.top = `${-cIcon.height}px`;
1✔
271
            cIcon.style.left = `${-cIcon.width}px`;
1✔
272
            break;
1✔
273
        case 'top-left':
274
            break;
1✔
275
        case 'top-right':
276
            cIcon.style.left = `${-cIcon.width}px`;
1✔
277
            break;
1✔
278
        case 'center':
279
        default:
280
            cIcon.style.top = `${-0.5 * cIcon.height}px`;
5✔
281
            cIcon.style.left = `${-0.5 * cIcon.width}px`;
5✔
282
            break;
5✔
283
    }
284

285
    cIcon.style['z-index'] = -1;
13✔
286
    domElement.appendChild(cIcon);
13✔
287
    return cIcon;
13✔
288
}
289

290
/**
291
 * @typedef {Object} StyleOptions
292
 * @memberof StyleOptions
293
 *
294
 * @property {Number} [order] - Order of the features that will be associated to
295
 * the style. It can helps sorting and prioritizing features if needed.
296
 *
297
 * @property {Object} [zoom] - Level on which to display the feature
298
 * @property {Number} [zoom.max] - max level
299
 * @property {Number} [zoom.min] - min level
300
 *
301
 * @property {Object} [fill] - Fill style for polygons.
302
 * @property {String|Function|THREE.Color} [fill.color] - Defines the main fill color. Can be
303
 * any [valid color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
304
 * Default is no value, which means no fill.
305
 * If the `Layer` is a `GeometryLayer` you can use `THREE.Color`.
306
 * @property {Image|Canvas|String|Object|Function} [fill.pattern] - Defines a pattern to fill the
307
 * surface with. It can be an `Image` to use directly, an url to fetch the pattern or an object containing
308
 * the url of the image to fetch and the transformation to apply.
309
 * from. See [this example] (http://www.itowns-project.org/itowns/examples/#source_file_geojson_raster)
310
 * for how to use.
311
 * @property {Image|String} [fill.pattern.source] - The image or the url to fetch the pattern image
312
 * @property {Object} [fill.pattern.cropValues] - The x, y, width and height (in pixel) of the sub image to use.
313
 * @property {THREE.Color} [fill.pattern.color] - Can be any [valid color string]
314
 * (https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
315
 * It will change the color of the white pixels of the source image.
316
 * @property {Number|Function} [fill.opacity] - The opacity of the color or of the
317
 * pattern. Can be between `0.0` and `1.0`. Default is `1.0`.
318
 * For a `GeometryLayer`, this opacity property isn't used.
319
 * @property {Number|Function} [fill.base_altitude] - `GeometryLayer` style option, defines altitude
320
 * for each coordinate.
321
 * If `base_altitude` is `undefined`, the original altitude is kept, and if it doesn't exist
322
 * then the altitude value is set to 0.
323
 * @property {Number|Function} [fill.extrusion_height] - `GeometryLayer` style option, if defined,
324
 * polygons will be extruded by the specified amount
325
 *
326
 * @property {Object} [stroke] - Lines and polygons edges.
327
 * @property {String|Function|THREE.Color} [stroke.color] The color of the line. Can be any [valid
328
 * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
329
 * Default is no value, which means no stroke.
330
 * If the `Layer` is a `GeometryLayer` you can use `THREE.Color`.
331
 * @property {Number|Function} [stroke.opacity] - The opacity of the line. Can be between
332
 * `0.0` and `1.0`. Default is `1.0`.
333
 * For a `GeometryLayer`, this opacity property isn't used.
334
 * @property {Number|Function} [stroke.width] - The width of the line. Default is `1.0`.
335
 * @property {Number|Function} [stroke.base_altitude] - `GeometryLayer` style option, defines altitude
336
 * for each coordinate.
337
 * If `base_altitude` is `undefined`, the original altitude is kept, and if it doesn't exist
338
 * then the altitude value is set to 0.
339
 *
340
 * @property {Object} [point] - Point style.
341
 * @property {String|Function} [point.color] - The color of the point. Can be any [valid
342
 * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
343
 * Default is no value, which means points won't be displayed.
344
 * @property {Number|Function} [point.radius] - The radius of the point, in pixel. Default
345
 * is `2.0`.
346
 * @property {String|Function} [point.line] - The color of the border of the point. Can be
347
 * any [valid color
348
 * string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
349
 * Not supported for a `GeometryLayer`.
350
 * @property {Number|Function} [point.width] - The width of the border, in pixel. Default
351
 * is `0.0` (no border).
352
 * @property {Number|Function} [point.opacity] - The opacity of the point. Can be between
353
 * `0.0` and `1.0`. Default is `1.0`.
354
 * Not supported for `GeometryLayer`.
355
 * @property {Number|Function} [point.base_altitude] - `GeometryLayer` style option, defines altitude
356
 * for each coordinate.
357
 * If `base_altitude` is `undefined`, the original altitude is kept, and if it doesn't exist
358
 * then the altitude value is set to 0.
359
 * @property {Object} [point.model] - 3D model to instantiate at each point position.
360
 *
361
 * @property {Object} [text] - All things {@link Label} related. (Supported for Points features, not yet
362
 * for Lines and Polygons features.)
363
 * @property {String|Function} [text.field] - A string representing a property key of
364
 * a `FeatureGeometry` enclosed in brackets, that will be replaced by the value of the
365
 * property for each geometry. For example, if each geometry contains a `name` property,
366
 * `text.field` can be set to `{name}`. Default is no value, indicating that no
367
 * text will be displayed.
368
 *
369
 * It's also possible to create more complex expressions. For example, you can combine
370
 * text that will always be displayed (e.g. `foo`) and variable properties (e.g. `{bar}`)
371
 * like the following: `foo {bar}`. You can also use multiple variables in one field.
372
 * Let's say for instance that you have two properties latin name and local name of a
373
 * place, you can write something like `{name_latin} - {name_local}` which can result
374
 * in `Marrakesh - مراكش` for example.
375
 * @property {String|Function} [text.color] - The color of the text. Can be any [valid
376
 * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
377
 * Default is `#000000`.
378
 * @property {String|Number[]|Function} [text.anchor] - The anchor of the text compared to its
379
 * position (see {@link Label} for the position). Can be one of the following values: `top`,
380
 * `left`, `bottom`, `right`, `center`, `top-left`, `top-right`, `bottom-left`
381
 * or `bottom-right`. Default is `center`.
382
 *
383
 * It can also be defined as an Array of two numbers. Each number defines an offset (in
384
 * fraction of the label width and height) between the label position and the top-left
385
 * corner of the text. The first value is the horizontal offset, and the second is the
386
 * vertical offset. For example, `[-0.5, -0.5]` will be equivalent to `center`.
387
 * @property {Array|Function} [text.offset] - The offset of the text, depending on its
388
 * anchor, in pixels. First value is from `left`, second is from `top`. Default
389
 * is `[0, 0]`.
390
 * @property {Number|Function} [text.padding] - The padding outside the text, in pixels.
391
 * Default is `2`.
392
 * @property {Number|Function} [text.size] - The size of the font, in pixels. Default is
393
 * `16`.
394
 * @property {Number|Function} [text.wrap] - The maximum width, in pixels, before the text
395
 * is wrapped, because the string is too long. Default is `10`.
396
 * @property {Number|Function} [text.spacing] - The spacing between the letters, in `em`.
397
 * Default is `0`.
398
 * @property {String|Function} [text.transform] - A value corresponding to the [CSS
399
 * property
400
 * `text-transform`](https://developer.mozilla.org/en-US/docs/Web/CSS/text-transform).
401
 * Default is `none`.
402
 * @property {String|Function} [text.justify] - A value corresponding to the [CSS property
403
 * `text-align`](https://developer.mozilla.org/en-US/docs/Web/CSS/text-align).
404
 * Default is `center`.
405
 * @property {Number|Function} [text.opacity] - The opacity of the text. Can be between
406
 * `0.0` and `1.0`. Default is `1.0`.
407
 * @property {Array|Function} [text.font] - A list (as an array of string) of font family
408
 * names, prioritized in the order it is set. Default is `Open Sans Regular,
409
 * Arial Unicode MS Regular, sans-serif`.
410
 * @property {String|Function} [text.haloColor] - The color of the halo. Can be any [valid
411
 * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
412
 * Default is `#000000`.
413
 * @property {Number|Function} [text.haloWidth] - The width of the halo, in pixels.
414
 * Default is `0`.
415
 * @property {Number|Function} [text.haloBlur] - The blur value of the halo, in pixels.
416
 * Default is `0`.
417
 *
418
 * @property {Object} [icon] - Defines the appearance of icons attached to label.
419
 * @property {String} [icon.source] - The url of the icons' image file.
420
 * @property {String} [icon.id] - The id of the icons' sub-image in a vector tile data set.
421
 * @property {String} [icon.cropValues] - the x, y, width and height (in pixel) of the sub image to use.
422
 * @property {String} [icon.anchor] - The anchor of the icon compared to the label position.
423
 * Can be `left`, `bottom`, `right`, `center`, `top-left`, `top-right`, `bottom-left`
424
 * or `bottom-right`. Default is `center`.
425
 * @property {Number} [icon.size] - If the icon's image is passed with `icon.source` and/or
426
 * `icon.id`, its size when displayed on screen is multiplied by `icon.size`. Default is `1`.
427
 * @property {String|Function} [icon.color] - The color of the icon. Can be any [valid
428
 * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
429
 * It will change the color of the white pixels of the icon source image.
430
 * @property {Number|Function} [icon.opacity] - The opacity of the icon. Can be between
431
 * `0.0` and `1.0`. Default is `1.0`.
432
*/
433

434
/**
435
 * @description An object that can contain any properties
436
 * (order, zoom, fill, stroke, point, text or/and icon)
437
 * and sub properties of a Style.<br/>
438
 * Used for the instanciation of a {@link Style}.
439
 * @hideconstructor
440
 */
441
export class StyleOptions {}
1✔
442

443
/**
1✔
444
 * @class
445
 * @classdesc A Style is a class that defines the visual appearance of {@link
446
 * FeatureCollection} and {@link Feature}. It is taken into account when drawing
447
 * them in textures that will be placed onto tiles.
448
 *
449
 * As there are five basic elements present in `Features`, there are also five
450
 * main components in a `Style` object:
451
 * - `fill` is for all fillings and polygons
452
 * - `stroke` is for all lines and polygons edges
453
 * - `point` is for all points
454
 * - `text` contains all {@link Label} related things
455
 * - `icon` defines the appearance of icons attached to label.
456
 *
457
 * Many style property can be set to functions. When that is the case, the function's
458
 * return type must necessarily be the same as the types (other than function) of the property.
459
 * For instance, if the `fill.pattern` property is set to a function, this function must return
460
 * an `Image`, a `Canvas`, or `String`.
461
 * The first parameter of functions used to set `Style` properties is always an object containing
462
 * the properties of the features displayed with the current `Style` instance.
463
 *
464
 * @property {Number} order - Order of the features that will be associated to
465
 * the style. It can helps sorting and prioritizing features if needed.
466
 * @property {Object} fill - Polygons and fillings style.
467
 * @property {String|Function|THREE.Color} fill.color - Defines the main color of the filling. Can be
468
 * any [valid color
469
 * string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
470
 * Default is no value, indicating that no filling needs to be done.
471
 * If the `Layer` is a `GeometryLayer` you can use `THREE.Color`.
472
 * @property {Image|Canvas|String|Object|Function} [fill.pattern] - Defines a pattern to fill the
473
 * surface with. It can be an `Image` to use directly, an url to fetch the pattern or an object containing
474
 * the url of the image to fetch and the transformation to apply.
475
 * from. See [this example] (http://www.itowns-project.org/itowns/examples/#source_file_geojson_raster)
476
 * for how to use.
477
 * @property {Image|String} [fill.pattern.source] - The image or the url to fetch the pattern image
478
 * @property {Object} [fill.pattern.cropValues] - The x, y, width and height (in pixel) of the sub image to use.
479
 * @property {THREE.Color} [fill.pattern.color] - Can be any [valid color string]
480
 * @property {Number|Function} fill.opacity - The opacity of the color or of the
481
 * pattern. Can be between `0.0` and `1.0`. Default is `1.0`.
482
 * For a `GeometryLayer`, this opacity property isn't used.
483
 * @property {Number|Function} fill.base_altitude - Only for {@link GeometryLayer}, defines altitude
484
 * for each coordinate.
485
 * If `base_altitude` is `undefined`, the original altitude is kept, and if it doesn't exist
486
 * then the altitude value is set to 0.
487
 * @property {Number|Function} [fill.extrusion_height] - Only for {@link GeometryLayer} and if user sets it.
488
 * If defined, polygons will be extruded by the specified amount.
489
 * @property {Object} stroke - Lines and polygons edges.
490
 * @property {String|Function|THREE.Color} stroke.color The color of the line. Can be any [valid
491
 * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
492
 * Default is no value, indicating that no stroke needs to be done.
493
 * If the `Layer` is a `GeometryLayer` you can use `THREE.Color`.
494
 * @property {Number|Function} stroke.opacity - The opacity of the line. Can be between
495
 * `0.0` and `1.0`. Default is `1.0`.
496
 * For a `GeometryLayer`, this opacity property isn't used.
497
 * @property {Number|Function} stroke.width - The width of the line. Default is `1.0`.
498
 * @property {Number|Function} stroke.base_altitude - Only for {@link GeometryLayer}, defines altitude
499
 * for each coordinate.
500
 * If `base_altitude` is `undefined`, the original altitude is kept, and if it doesn't exist
501
 * then the altitude value is set to 0.
502
 *
503
 * @property {Object} point - Point style.
504
 * @property {String|Function} point.color - The color of the point. Can be any [valid
505
 * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
506
 * Default is no value, indicating that no point will be shown.
507
 * @property {Number|Function} point.radius - The radius of the point, in pixel. Default
508
 * is `2.0`.
509
 * @property {String|Function} point.line - The color of the border of the point. Can be
510
 * any [valid color
511
 * string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
512
 * Not supported for `GeometryLayer`.
513
 * @property {Number|Function} point.width - The width of the border, in pixel. Default
514
 * is `0.0` (no border).
515
 * @property {Number|Function} point.opacity - The opacity of the point. Can be between
516
 * `0.0` and `1.0`. Default is `1.0`.
517
 * Not supported for `GeometryLayer`.
518
 * @property {Number|Function} point.base_altitude - Only for {@link GeometryLayer}, defines altitude
519
 * for each coordinate.
520
 * If `base_altitude` is `undefined`, the original altitude is kept, and if it doesn't exist
521
 * then the altitude value is set to 0.
522
 * @property {Object} point.model - 3D model to instantiate at each point position
523

524
 *
525
 * @property {Object} text - All things {@link Label} related.
526
 * @property {String|Function} text.field - A string representing a property key of
527
 * a `FeatureGeometry` enclosed in brackets, that will be replaced by the value of the
528
 * property for each geometry. For example, if each geometry contains a `name` property,
529
 * `text.field` can be set to `{name}`. Default is no value, indicating that no
530
 * text will be displayed.
531
 *
532
 * It's also possible to create more complex expressions. For example, you can combine
533
 * text that will always be displayed (e.g. `foo`) and variable properties (e.g. `{bar}`)
534
 * like the following: `foo {bar}`. You can also use multiple variables in one field.
535
 * Let's say for instance that you have two properties latin name and local name of a
536
 * place, you can write something like `{name_latin} - {name_local}` which can result
537
 * in `Marrakesh - مراكش` for example.
538
 * @property {String|Function} text.color - The color of the text. Can be any [valid
539
 * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
540
 * Default is `#000000`.
541
 * @property {String|Number[]|Function} text.anchor - The anchor of the text relative to its
542
 * position (see {@link Label} for the position). Can be one of the following values: `top`,
543
 * `left`, `bottom`, `right`, `center`, `top-left`, `top-right`, `bottom-left`
544
 * or `bottom-right`. Default is `center`.
545
 *
546
 * It can also be defined as an Array of two numbers. Each number defines an offset (in
547
 * fraction of the label width and height) between the label position and the top-left
548
 * corner of the text. The first value is the horizontal offset, and the second is the
549
 * vertical offset. For example, `[-0.5, -0.5]` will be equivalent to `center`.
550
 * @property {Array|Function} text.offset - The offset of the text, depending on its
551
 * anchor, in pixels. First value is from `left`, second is from `top`. Default
552
 * is `[0, 0]`.
553
 * @property {Number|Function} text.padding - The padding outside the text, in pixels.
554
 * Default is `2`.
555
 * @property {Number|Function} text.size - The size of the font, in pixels. Default is
556
 * `16`.
557
 * @property {Number|Function} text.wrap - The maximum width, in pixels, before the text
558
 * is wrapped, because the string is too long. Default is `10`.
559
 * @property {Number|Function} text.spacing - The spacing between the letters, in `em`.
560
 * Default is `0`.
561
 * @property {String|Function} text.transform - A value corresponding to the [CSS
562
 * property
563
 * `text-transform`](https://developer.mozilla.org/en-US/docs/Web/CSS/text-transform).
564
 * Default is `none`.
565
 * @property {String|Function} text.justify - A value corresponding to the [CSS property
566
 * `text-align`](https://developer.mozilla.org/en-US/docs/Web/CSS/text-align).
567
 * Default is `center`.
568
 * @property {Number|Function} text.opacity - The opacity of the text. Can be between
569
 * `0.0` and `1.0`. Default is `1.0`.
570
 * @property {Array|Function} text.font - A list (as an array of string) of font family
571
 * names, prioritized in the order it is set. Default is `Open Sans Regular,
572
 * Arial Unicode MS Regular, sans-serif`.
573
 * @property {String|Function} text.haloColor - The color of the halo. Can be any [valid
574
 * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
575
 * Default is `#000000`.
576
 * @property {Number|Function} text.haloWidth - The width of the halo, in pixels.
577
 * Default is `0`.
578
 * @property {Number|Function} text.haloBlur - The blur value of the halo, in pixels.
579
 * Default is `0`.
580
 *
581
 * @property {Object} icon - Defines the appearance of icons attached to label.
582
 * @property {String} icon.source - The url of the icons' image file.
583
 * @property {String} icon.id - The id of the icons' sub-image in a vector tile data set.
584
 * @property {String} icon.cropValues - the x, y, width and height (in pixel) of the sub image to use.
585
 * @property {String} icon.anchor - The anchor of the icon compared to the label position.
586
 * Can be `left`, `bottom`, `right`, `center`, `top-left`, `top-right`, `bottom-left`
587
 * or `bottom-right`. Default is `center`.
588
 * @property {Number} icon.size - If the icon's image is passed with `icon.source` and/or
589
 * `icon.id`, its size when displayed on screen is multiplied by `icon.size`. Default is `1`.
590
 * @property {String|Function} icon.color - The color of the icon. Can be any [valid
591
 * color string](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
592
 * It will change the color of the white pixels of the icon source image.
593
 * @property {Number|Function} icon.opacity - The opacity of the icon. Can be between
594
 * `0.0` and `1.0`. Default is `1.0`.
595
 *
596
 * @example
597
 * const style = new itowns.Style({
598
 *      stroke: { color: 'red' },
599
 *      point: { color: 'white', line: 'red' },
600
 * });
601
 *
602
 * const source = new itowns.FileSource(...);
603
 *
604
 * const layer = new itowns.ColorLayer('foo', {
605
 *      source: source,
606
 *      style: style,
607
 * });
608
 *
609
 * view.addLayer(layer);
610
 */
611

612
class Style {
1✔
613
    /**
614
     * @param {StyleOptions} [params={}] An object that contain any properties
615
     * (order, zoom, fill, stroke, point, text or/and icon)
616
     * and sub properties of a Style (@see {@link StyleOptions}).
617
     * @constructor
618
     */
619
    constructor(params = {}) {
29✔
620
        this.isStyle = true;
29✔
621
        this.context = new StyleContext();
29✔
622

623
        this.order = params.order || 0;
29✔
624

625
        params.zoom = params.zoom || {};
29✔
626
        params.fill = params.fill || {};
29✔
627
        params.stroke = params.stroke || {};
29✔
628
        params.point = params.point || {};
29✔
629
        params.text = params.text || {};
29✔
630
        params.icon = params.icon || {};
29✔
631

632
        this.zoom = {};
29✔
633
        defineStyleProperty(this, 'zoom', 'min', params.zoom.min);
29✔
634
        defineStyleProperty(this, 'zoom', 'max', params.zoom.max);
29✔
635

636
        this.fill = {};
29✔
637
        defineStyleProperty(this, 'fill', 'color', params.fill.color);
29✔
638
        defineStyleProperty(this, 'fill', 'opacity', params.fill.opacity, 1.0);
29✔
639
        defineStyleProperty(this, 'fill', 'pattern', params.fill.pattern);
29✔
640
        defineStyleProperty(this, 'fill', 'base_altitude', params.fill.base_altitude, baseAltitudeDefault);
29✔
641
        if (params.fill.extrusion_height) {
29✔
642
            defineStyleProperty(this, 'fill', 'extrusion_height', params.fill.extrusion_height);
3✔
643
        }
644

645
        this.stroke = {};
29✔
646
        defineStyleProperty(this, 'stroke', 'color', params.stroke.color);
29✔
647
        defineStyleProperty(this, 'stroke', 'opacity', params.stroke.opacity, 1.0);
29✔
648
        defineStyleProperty(this, 'stroke', 'width', params.stroke.width, 1.0);
29✔
649
        defineStyleProperty(this, 'stroke', 'dasharray', params.stroke.dasharray, []);
29✔
650
        defineStyleProperty(this, 'stroke', 'base_altitude', params.stroke.base_altitude, baseAltitudeDefault);
29✔
651

652
        this.point = {};
29✔
653
        defineStyleProperty(this, 'point', 'color', params.point.color);
29✔
654
        defineStyleProperty(this, 'point', 'line', params.point.line);
29✔
655
        defineStyleProperty(this, 'point', 'opacity', params.point.opacity, 1.0);
29✔
656
        defineStyleProperty(this, 'point', 'radius', params.point.radius, 2.0);
29✔
657
        defineStyleProperty(this, 'point', 'width', params.point.width, 0.0);
29✔
658
        defineStyleProperty(this, 'point', 'base_altitude', params.point.base_altitude, baseAltitudeDefault);
29✔
659
        if (params.point.model) {
29!
NEW
660
            defineStyleProperty(this, 'point', 'model', params.point.model);
×
661
        }
662

663
        this.text = {};
29✔
664
        defineStyleProperty(this, 'text', 'field', params.text.field);
29✔
665
        defineStyleProperty(this, 'text', 'zOrder', params.text.zOrder, 'auto');
29✔
666
        defineStyleProperty(this, 'text', 'color', params.text.color, '#000000');
29✔
667
        defineStyleProperty(this, 'text', 'anchor', params.text.anchor, 'center');
29✔
668
        defineStyleProperty(this, 'text', 'offset', params.text.offset, [0, 0]);
29✔
669
        defineStyleProperty(this, 'text', 'padding', params.text.padding, 2);
29✔
670
        defineStyleProperty(this, 'text', 'size', params.text.size, 16);
29✔
671
        defineStyleProperty(this, 'text', 'placement', params.text.placement, 'point');
29✔
672
        defineStyleProperty(this, 'text', 'rotation', params.text.rotation, 'auto');
29✔
673
        defineStyleProperty(this, 'text', 'wrap', params.text.wrap, 10);
29✔
674
        defineStyleProperty(this, 'text', 'spacing', params.text.spacing, 0);
29✔
675
        defineStyleProperty(this, 'text', 'transform', params.text.transform, 'none');
29✔
676
        defineStyleProperty(this, 'text', 'justify', params.text.justify, 'center');
29✔
677
        defineStyleProperty(this, 'text', 'opacity', params.text.opacity, 1.0);
29✔
678
        defineStyleProperty(this, 'text', 'font', params.text.font, ['Open Sans Regular', 'Arial Unicode MS Regular', 'sans-serif']);
29✔
679
        defineStyleProperty(this, 'text', 'haloColor', params.text.haloColor, '#000000');
29✔
680
        defineStyleProperty(this, 'text', 'haloWidth', params.text.haloWidth, 0);
29✔
681
        defineStyleProperty(this, 'text', 'haloBlur', params.text.haloBlur, 0);
29✔
682

683
        this.icon = {};
29✔
684
        defineStyleProperty(this, 'icon', 'source', params.icon.source);
29✔
685
        if (params.icon.key) {
29!
686
            console.warn("'icon.key' is deprecated: use 'icon.id' instead");
×
687
            params.icon.id = params.icon.key;
×
688
        }
689
        defineStyleProperty(this, 'icon', 'id', params.icon.id);
29✔
690
        defineStyleProperty(this, 'icon', 'cropValues', params.icon.cropValues);
29✔
691
        defineStyleProperty(this, 'icon', 'anchor', params.icon.anchor, 'center');
29✔
692
        defineStyleProperty(this, 'icon', 'size', params.icon.size, 1);
29✔
693
        defineStyleProperty(this, 'icon', 'color', params.icon.color);
29✔
694
        defineStyleProperty(this, 'icon', 'opacity', params.icon.opacity, 1.0);
29✔
695
    }
696

697
    /**
1✔
698
     * Copies the content of the target style into this style.
699
     * @param {Style} style - The style to copy.
700
     *
701
     * @return {Style} This style.
702
     */
703
    copy(style) {
704
        Object.assign(this.fill, style.fill);
18✔
705
        Object.assign(this.stroke, style.stroke);
18✔
706
        Object.assign(this.point, style.point);
18✔
707
        Object.assign(this.text, style.text);
18✔
708
        Object.assign(this.icon, style.icon);
18✔
709
        return this;
18✔
710
    }
711

NEW
712
    copy2(...style) {
×
NEW
713
        style.forEach((source) => {
×
NEW
714
            const descriptors = Object.keys(source).reduce((descriptors, key) => {
×
NEW
715
                descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
×
NEW
716
                return descriptors;
×
717
            }, {});
718
            // Par défaut, Object.assign copie également
719
            // les symboles énumérables
NEW
720
            Object.getOwnPropertySymbols(source).forEach((sym) => {
×
NEW
721
                const descriptor = Object.getOwnPropertyDescriptor(source, sym);
×
NEW
722
                if (descriptor.enumerable) {
×
NEW
723
                    descriptors[sym] = descriptor;
×
724
                }
725
            });
NEW
726
            Object.defineProperties(this, descriptors);
×
727
        });
NEW
728
        return this;
×
729
    }
730

731
    /**
732
         * Clones this style.
733
         *
734
         * @return {Style} The new style, cloned from this one.
735
         */
736
    clone() {
737
        const clone = new Style();
17✔
738
        return clone.copy(this);
17✔
739
    }
740

741
    setContext(ctx) {
742
        this.context = ctx;
3,646✔
743
    }
744

745
    /**
746
     * set Style from (geojson-like) properties.
747
     * @param {Object} properties (geojson-like) properties.
748
     * @param {FeatureContext} featCtx the context of the feature
749
     *
750
     * @returns {StyleOptions} containing all properties for itowns.Style
751
     */
752
    static setFromProperties(properties, featCtx) {
753
        const type = featCtx.type;
3,733✔
754
        const style = {};
3,733✔
755
        if (type === FEATURE_TYPES.POINT) {
3,733✔
756
            const point = {
49✔
757
                ...(properties.fill !== undefined && { color: properties.fill }),
49!
758
                ...(properties['fill-opacity'] !== undefined && { opacity: properties['fill-opacity'] }),
49!
759
                ...(properties.stroke !== undefined && { line: properties.stroke }),
49!
760
                ...(properties.radius !== undefined && { radius: properties.radius }),
50✔
761
            };
762
            if (Object.keys(point).length) {
49✔
763
                style.point = point;
1✔
764
            }
765
            const text = {
49✔
766
                ...(properties['label-color'] !== undefined && { color: properties['label-color'] }),
50✔
767
                ...(properties['label-opacity'] !== undefined && { opacity: properties['label-opacity'] }),
49!
768
                ...(properties['label-size'] !== undefined && { size: properties['label-size'] }),
49!
769
            };
770
            if (Object.keys(point).length) {
49✔
771
                style.text = text;
1✔
772
            }
773
            const icon = {
49✔
774
                ...(properties.icon !== undefined && { source: properties.icon }),
49!
775
                ...(properties['icon-scale'] !== undefined && { size: properties['icon-scale'] }),
49!
776
                ...(properties['icon-opacity'] !== undefined && { opacity: properties['icon-opacity'] }),
49!
777
                ...(properties['icon-color'] !== undefined && { color: properties['icon-color'] }),
50✔
778
            };
779
            if (Object.keys(icon).length) {
49✔
780
                style.icon = icon;
1✔
781
            }
782
        } else {
783
            const stroke = {
3,684✔
784
                ...(properties.stroke !== undefined && { color: properties.stroke }),
3,685✔
785
                ...(properties['stroke-width'] !== undefined && { width: properties['stroke-width'] }),
3,684!
786
                ...(properties['stroke-opacity'] !== undefined && { opacity: properties['stroke-opacity'] }),
3,684!
787
            };
788
            if (Object.keys(stroke).length) {
3,684✔
789
                style.stroke = stroke;
1✔
790
            }
791
            if (type !== FEATURE_TYPES.LINE) {
3,684✔
792
                const fill = {
3,678✔
793
                    ...(properties.fill !== undefined && { color: properties.fill }),
3,679✔
794
                    ...(properties['fill-opacity'] !== undefined && { opacity: properties['fill-opacity'] }),
3,678!
795
                };
796
                if (Object.keys(fill).length) {
3,678✔
797
                    style.fill = fill;
1✔
798
                }
799
            }
800
        }
801
        return style;
3,733✔
802
    }
803

804
    /**
805
     * set Style from vector tile layer properties.
806
     * @param {Object} layer vector tile layer.
807
     * @param {Object} sprites vector tile layer.
808
     * @param {Number} [order=0]
809
     * @param {Boolean} [symbolToCircle=false]
810
     *
811
     * @returns {StyleOptions} containing all properties for itowns.Style
812
     */
813
    static setFromVectorTileLayer(layer, sprites, order = 0, symbolToCircle = false) {
13!
814
        const style = {
13✔
815
            fill: {},
816
            stroke: {},
817
            point: {},
818
            text: {},
819
            icon: {},
820
        };
821

822
        layer.layout = layer.layout || {};
13✔
823
        layer.paint = layer.paint || {};
13✔
824

825
        style.order = order;
13✔
826

827
        if (layer.type === 'fill') {
13✔
828
            const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-color'] || layer.paint['fill-pattern'], { type: 'color' }));
9✔
829
            style.fill.color = color;
9✔
830
            style.fill.opacity = readVectorProperty(layer.paint['fill-opacity']) || opacity;
9✔
831
            if (layer.paint['fill-pattern']) {
9✔
832
                try {
1✔
833
                    style.fill.pattern = {
1✔
834
                        id: layer.paint['fill-pattern'],
835
                        source: sprites.source,
836
                        cropValues: sprites[layer.paint['fill-pattern']],
837
                    };
838
                } catch (err) {
839
                    err.message = `VTlayer '${layer.id}': argument sprites must not be null when using layer.paint['fill-pattern']`;
×
840
                    throw err;
×
841
                }
842
            }
843

844
            if (layer.paint['fill-outline-color']) {
9✔
845
                const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-outline-color'], { type: 'color' }));
2✔
846
                style.stroke.color = color;
2✔
847
                style.stroke.opacity = opacity;
2✔
848
                style.stroke.width = 1.0;
2✔
849
                style.stroke.dasharray = [];
2✔
850
            }
851
        } else if (layer.type === 'line') {
4✔
852
            const prepare = readVectorProperty(layer.paint['line-color'], { type: 'color' });
1✔
853
            const { color, opacity } = rgba2rgb(prepare);
1✔
854
            style.stroke.dasharray = readVectorProperty(layer.paint['line-dasharray']);
1✔
855
            style.stroke.color = color;
1✔
856
            style.stroke.lineCap = layer.layout['line-cap'];
1✔
857
            style.stroke.width = readVectorProperty(layer.paint['line-width']);
1✔
858
            style.stroke.opacity = readVectorProperty(layer.paint['line-opacity']) || opacity;
1✔
859
        } else if (layer.type === 'circle' || symbolToCircle) {
3✔
860
            const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['circle-color'], { type: 'color' }));
1✔
861
            style.point.color = color;
1✔
862
            style.point.opacity = opacity;
1✔
863
            style.point.radius = readVectorProperty(layer.paint['circle-radius']);
1✔
864
        } else if (layer.type === 'symbol') {
2!
865
            // overlapping order
866
            style.text.zOrder = readVectorProperty(layer.layout['symbol-z-order']);
2✔
867
            if (style.text.zOrder == 'auto') {
2✔
868
                style.text.zOrder = readVectorProperty(layer.layout['symbol-sort-key']) || 'Y';
1✔
869
            } else if (style.text.zOrder == 'viewport-y') {
1!
NEW
870
                style.text.zOrder = 'Y';
×
871
            } else if (style.text.zOrder == 'source') {
1!
NEW
872
                style.text.zOrder = 0;
×
873
            }
874

875
            // position
876
            style.text.anchor = readVectorProperty(layer.layout['text-anchor']);
2✔
877
            style.text.offset = readVectorProperty(layer.layout['text-offset']);
2✔
878
            style.text.padding = readVectorProperty(layer.layout['text-padding']);
2✔
879
            style.text.size = readVectorProperty(layer.layout['text-size']);
2✔
880
            style.text.placement = readVectorProperty(layer.layout['symbol-placement']);
2✔
881
            style.text.rotation = readVectorProperty(layer.layout['text-rotation-alignment']);
2✔
882

883
            // content
884
            style.text.field = readVectorProperty(layer.layout['text-field']);
2✔
885
            style.text.wrap = readVectorProperty(layer.layout['text-max-width']);
2✔
886
            style.text.spacing = readVectorProperty(layer.layout['text-letter-spacing']);
2✔
887
            style.text.transform = readVectorProperty(layer.layout['text-transform']);
2✔
888
            style.text.justify = readVectorProperty(layer.layout['text-justify']);
2✔
889

890
            // appearance
891
            const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['text-color'], { type: 'color' }));
2✔
892
            style.text.color = color;
2✔
893
            style.text.opacity = readVectorProperty(layer.paint['text-opacity']) || (opacity !== undefined && opacity);
2!
894

895
            style.text.font = readVectorProperty(layer.layout['text-font']);
2✔
896
            const haloColor = readVectorProperty(layer.paint['text-halo-color'], { type: 'color' });
2✔
897
            if (haloColor) {
2!
NEW
898
                style.text.haloColor = haloColor.color || haloColor;
×
NEW
899
                style.text.haloWidth = readVectorProperty(layer.paint['text-halo-width']);
×
NEW
900
                style.text.haloBlur = readVectorProperty(layer.paint['text-halo-blur']);
×
901
            }
902

903
            // additional icon
904
            const iconImg = readVectorProperty(layer.layout['icon-image']);
2✔
905
            if (iconImg) {
2✔
906
                try {
1✔
907
                    style.icon.id = iconImg;
1✔
908
                    style.icon.source = sprites.source;
1✔
909
                    style.icon.cropValues = sprites[iconImg];
1✔
910

911
                    style.icon.size = readVectorProperty(layer.layout['icon-size']) || 1;
1!
912
                    const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['icon-color'], { type: 'color' }));
1✔
913
                    style.icon.color = color;
1✔
914
                    style.icon.opacity = readVectorProperty(layer.paint['icon-opacity']) || (opacity !== undefined && opacity);
1!
915
                } catch (err) {
916
                    err.message = `VTlayer '${layer.id}': argument sprites must not be null when using layer.layout['icon-image']`;
×
917
                    throw err;
×
918
                }
919
            }
920
        }
921
        return style;
13✔
922
    }
1✔
923

924
    /**
925
     * Applies the style.fill to a polygon of the texture canvas.
926
     * @param {CanvasRenderingContext2D} txtrCtx The Context 2D of the texture canvas.
927
     * @param {Path2D} polygon The current texture canvas polygon.
928
     * @param {Number} invCtxScale The ratio to scale line width and radius circle.
929
     * @param {Boolean} canBeFilled - true if feature.type == FEATURE_TYPES.POLYGON.
930
     */
931
    applyToCanvasPolygon(txtrCtx, polygon, invCtxScale, canBeFilled) {
932
        const context = this.context;
3✔
933
        // draw line or edge of polygon
934
        if (this.stroke) {
3!
935
            // TO DO add possibility of using a pattern (https://github.com/iTowns/itowns/issues/2210)
936
            // Style.prototype._applyStrokeToPolygon.call(this, txtrCtx, invCtxScale, polygon);
937
            this._applyStrokeToPolygon(txtrCtx, invCtxScale, polygon, context);
3✔
938
        }
939

940
        // fill inside of polygon
941
        if (canBeFilled && this.fill) {
3!
942
            // canBeFilled can be move to StyleContext in the later PR
943
            // Style.prototype._applyFillToPolygon.call(this, txtrCtx, invCtxScale, polygon);
944
            this._applyFillToPolygon(txtrCtx, invCtxScale, polygon, context);
3✔
945
        }
946
    }
947

948
    _applyStrokeToPolygon(txtrCtx, invCtxScale, polygon) {
949
        if (txtrCtx.strokeStyle !== this.stroke.color) {
4✔
950
            txtrCtx.strokeStyle = this.stroke.color;
2✔
951
        }
952
        const width = (this.stroke.width || 2.0) * invCtxScale;
4!
953
        if (txtrCtx.lineWidth !== width) {
4✔
954
            txtrCtx.lineWidth = width;
2✔
955
        }
956
        const alpha = this.stroke.opacity == undefined ? 1.0 : this.stroke.opacity;
4!
957
        if (alpha !== txtrCtx.globalAlpha && typeof alpha == 'number') {
4!
958
            txtrCtx.globalAlpha = alpha;
4✔
959
        }
960
        if (txtrCtx.lineCap !== this.stroke.lineCap) {
4!
961
            txtrCtx.lineCap = this.stroke.lineCap;
×
962
        }
963
        txtrCtx.setLineDash(this.stroke.dasharray.map(a => a * invCtxScale * 2));
4✔
964
        txtrCtx.stroke(polygon);
4✔
965
    }
1✔
966

967
    async _applyFillToPolygon(txtrCtx, invCtxScale, polygon) {
21!
968
        // if (this.fill.pattern && txtrCtx.fillStyle.src !== this.fill.pattern.src) {
969
        // need doc for the txtrCtx.fillStyle.src that seems to always be undefined
970
        if (this.fill.pattern) {
8✔
971
            let img = this.fill.pattern;
1✔
972
            const cropValues = this.fill.pattern.cropValues;
1!
973
            if (this.fill.pattern.source) {
2✔
974
                img = await loadImage(this.fill.pattern.source);
×
975
            }
976
            cropImage(img, cropValues);
1✔
977

978
            txtrCtx.fillStyle = txtrCtx.createPattern(canvas, 'repeat');
1✔
979
            if (txtrCtx.fillStyle.setTransform) {
1!
980
                txtrCtx.fillStyle.setTransform(matrix.scale(invCtxScale));
1✔
981
            } else {
982
                console.warn('Raster pattern isn\'t completely supported on Ie and edge', txtrCtx.fillStyle);
×
983
            }
2✔
984
        } else if (txtrCtx.fillStyle !== this.fill.color) {
4✔
985
            txtrCtx.fillStyle = this.fill.color;
2✔
986
        }
987
        if (this.fill.opacity !== txtrCtx.globalAlpha) {
5!
988
            txtrCtx.globalAlpha = this.fill.opacity;
5✔
989
        }
990
        txtrCtx.fill(polygon);
5✔
991
    }
6✔
992

993
    /**
1✔
994
     * Applies this style to a DOM element. Limited to the `text` and `icon`
995
     * properties of this style.
996
     *
997
     * @param {Element} domElement - The element to set the style to.
998
     *
999
     * @returns {undefined|Promise<HTMLImageElement>}
1000
     *          for a text label: undefined.
1001
     *          for an icon: a Promise resolving with the HTMLImageElement containing the image.
1002
     */
1003
    async applyToHTML(domElement) {
140!
1004
        if (arguments.length > 1) {
22!
1005
            console.warn('Deprecated argument sprites. Sprites must be configured in style.');
×
1006
        }
1007
        domElement.style.padding = `${this.text.padding}px`;
22✔
1008
        domElement.style.maxWidth = `${this.text.wrap}em`;
22✔
1009

1010
        domElement.style.color = this.text.color;
22✔
1011
        if (this.text.size > 0) {
22!
1012
            domElement.style.fontSize = `${this.text.size}px`;
22✔
1013
        }
1014
        domElement.style.fontFamily = this.text.font.join(',');
22✔
1015
        domElement.style.textTransform = this.text.transform;
22✔
1016
        domElement.style.letterSpacing = `${this.text.spacing}em`;
22✔
1017
        domElement.style.textAlign = this.text.justify;
22✔
1018
        domElement.style['white-space'] = 'pre-line';
22✔
1019

1020
        if (this.text.haloWidth > 0) {
22✔
1021
            domElement.style.setProperty('--text_stroke_display', 'block');
14✔
1022
            domElement.style.setProperty('--text_stroke_width', `${this.text.haloWidth}px`);
14✔
1023
            domElement.style.setProperty('--text_stroke_color', this.text.haloColor);
14✔
1024
            domElement.setAttribute('data-before', domElement.textContent);
14✔
1025
        }
22✔
1026

1027
        if (!this.icon.source) {
35✔
1028
            return;
1029
        }
1030

1031
        const icon = document.createElement('img');
13✔
1032

1033
        const iconPromise  = new Promise((resolve, reject) => {
13✔
1034
            const opt = {
13✔
1035
                size: this.icon.size,
1036
                color: this.icon.color,
1037
                opacity: this.icon.opacity,
1038
                anchor: this.icon.anchor,
1039
            };
1040
            icon.onload = () => resolve(_addIcon(icon, domElement, opt));
13✔
1041
            icon.onerror = err => reject(err);
13✔
1042
        });
13✔
1043

1044
        if (!this.icon.cropValues && !this.icon.color) {
4✔
1045
            icon.src = this.icon.source;
22✔
1046
        } else {
1047
            const cropValues = this.icon.cropValues;
2✔
1048
            const color = this.icon.color;
2✔
1049
            const id = this.icon.id || this.icon.source;
2✔
1050
            const img = await loadImage(this.icon.source);
2✔
1051
            const imgd = cropImage(img, cropValues);
2✔
1052
            const imgdColored = replaceWhitePxl(imgd, color, id);
2✔
1053
            canvas.getContext('2d').putImageData(imgdColored, 0, 0);
2✔
1054
            icon.src = canvas.toDataURL('image/png');
13✔
1055
        }
1056
        return iconPromise;
22✔
1057
    }
23✔
1058

1059
    /**
1060
     * Gets the values corresponding to the anchor of the text. It is
1061
     * proportions, to use with a `translate()` and a `transform` property.
1062
     *
1063
     * @return {Number[]} Two percentage values, for x and y respectively.
1064
     */
1065
    getTextAnchorPosition() {
1066
        if (typeof this.text.anchor === 'string') {
8!
1067
            if (Object.keys(textAnchorPosition).includes(this.text.anchor)) {
8!
1068
                return textAnchorPosition[this.text.anchor];
8✔
1069
            } else {
1070
                console.error(`${this.text.anchor} is not a valid input for Style.text.anchor parameter.`);
×
1071
                return textAnchorPosition.center;
×
1072
            }
1073
        } else {
1074
            return this.text.anchor;
×
1075
        }
1076
    }
1077
}
1078

1079
// Add custom style sheet with iTowns specifics
1080
const CustomStyle = {
1✔
1081
    itowns_stroke_single_before,
1082
};
1083

1084
const customStyleSheet = (typeof document !== 'undefined') ? document.createElement('style') : {};
1!
1085
customStyleSheet.type = 'text/css';
1✔
1086

1087
Object.keys(CustomStyle).forEach((key) => {
1✔
1088
    customStyleSheet.innerHTML += `${CustomStyle[key]}\n\n`;
1✔
1089
});
1090

1091
if (typeof document !== 'undefined') {
1!
1092
    document.getElementsByTagName('head')[0].appendChild(customStyleSheet);
1✔
1093
}
1094

1095
export default Style;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc