diff --git a/examples/css/example.css b/examples/css/example.css index 4bdc1c8553..384c6af523 100644 --- a/examples/css/example.css +++ b/examples/css/example.css @@ -253,7 +253,7 @@ h3 { .tooltip { display: none; - background-image: linear-gradient(rgba(80, 80, 80,0.95), rgba(60, 60, 60,0.95)); + background-image: linear-gradient(rgba(167, 164, 164, 0.95), rgba(60, 60, 60,0.95)); box-shadow: -1px 2px 5px 1px rgba(0, 0, 0, 0.5); margin-top: 20px; margin-left: 20px; diff --git a/examples/js/plugins/FeatureToolTip.js b/examples/js/plugins/FeatureToolTip.js index 0dc0fcad2b..f19446ebca 100644 --- a/examples/js/plugins/FeatureToolTip.js +++ b/examples/js/plugins/FeatureToolTip.js @@ -64,42 +64,38 @@ const FeatureToolTip = (function _() { } } - function getGeometryProperties(geometry) { - return function properties() { return geometry.properties; }; - } - function fillToolTip(features, layer, options) { let content = ''; let feature; let geometry; - let style; + const style = layer.style; let fill; let stroke; let symb = ''; let prop; + const context = style.context; + for (let p = 0; p < features.length; p++) { feature = features[p]; geometry = feature.geometry; - style = (geometry.properties && geometry.properties.style) || feature.style || layer.style; - const context = { globals: {}, properties: getGeometryProperties(geometry) }; - style = style.applyContext(context); + + context.setFeature(feature); + context.setGeometry(geometry); if (feature.type === itowns.FEATURE_TYPES.POLYGON) { symb = '◼'; - if (style) { - fill = style.fill && style.fill.color; - stroke = style.stroke && ('1.25px ' + style.stroke.color); - } + fill = style.fill && style.fill.color; + stroke = style.stroke && ('1.25px ' + style.stroke.color); } else if (feature.type === itowns.FEATURE_TYPES.LINE) { symb = '━'; - fill = style && style.stroke && style.stroke.color; + fill = style.stroke && style.stroke.color; stroke = '0px'; } else if (feature.type === itowns.FEATURE_TYPES.POINT) { symb = '●'; - if (style && style.point) { // Style and style.point can be undefined if no style options were passed - fill = style.point.color; - stroke = '1.25px ' + style.point.line; + if (style.point || style.icon) { // Style and style.point can be undefined if no style options were passed + fill = (style.point && style.point.color) || (style.icon && style.icon.color); + stroke = '1.25px ' + ((style.point && style.point.line) || 'black'); } } @@ -109,10 +105,10 @@ const FeatureToolTip = (function _() { content += ''; if (geometry.properties) { - content += (geometry.properties.description || geometry.properties.name || geometry.properties.nom || layer.name || ''); + content += (geometry.properties.description || geometry.properties.name || geometry.properties.nom || geometry.properties.title || layer.name || ''); } - if (feature.type === itowns.FEATURE_TYPES.POINT) { + if (feature.type === itowns.FEATURE_TYPES.POINT && options.writeLatLong) { content += '
long ' + feature.coordinates[0].toFixed(4) + ''; content += '
lat ' + feature.coordinates[1].toFixed(4) + ''; } @@ -231,8 +227,8 @@ const FeatureToolTip = (function _() { } const opts = options || { filterAllProperties: true }; - opts.filterProperties = opts.filterProperties == undefined ? [] : opts.filterProperties; - opts.filterProperties.concat(['name', 'nom', 'style', 'description']); + opts.filterProperties = opts.filterProperties === undefined ? [] : opts.filterProperties; + opts.writeLatLong = opts.writeLatLong || false; layers.push({ layer: layer, options: opts }); layersId.push(layer.id); diff --git a/examples/source_file_gpx_3d.html b/examples/source_file_gpx_3d.html index 86016cac62..c968daea5d 100644 --- a/examples/source_file_gpx_3d.html +++ b/examples/source_file_gpx_3d.html @@ -61,6 +61,15 @@ var waypointGeometry = new itowns.THREE.BoxGeometry(1, 1, 80); var waypointMaterial = new itowns.THREE.MeshBasicMaterial({ color: 0xffffff }); + const style = { + stroke: { + color: 'red', + width: 2, + }, + point: { + color: 'white', + } + }; // Listen for globe full initialisation event view.addEventListener(itowns.GLOBE_VIEW_EVENTS.GLOBE_INITIALIZED, function () { console.info('Globe initialized'); @@ -72,18 +81,9 @@ out: { crs: view.referenceCrs, structure: '3d', - style: new itowns.Style({ - stroke: { - color: 'red', - width: 2, - }, - point: { - color: 'white', - } - }), } })) - .then(itowns.Feature2Mesh.convert()) + .then(itowns.Feature2Mesh.convert({style})) .then(function (mesh) { if (mesh) { mesh.updateMatrixWorld(); diff --git a/src/Converter/Feature2Mesh.js b/src/Converter/Feature2Mesh.js index 8a66e700bd..e057ad6687 100644 --- a/src/Converter/Feature2Mesh.js +++ b/src/Converter/Feature2Mesh.js @@ -7,10 +7,12 @@ import Extent from 'Core/Geographic/Extent'; import Crs from 'Core/Geographic/Crs'; import OrientationUtils from 'Utils/OrientationUtils'; import Coordinates from 'Core/Geographic/Coordinates'; -import { StyleContext } from 'Core/Style'; +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(); @@ -191,7 +193,9 @@ function featureToPoint(feature, options) { const vertices = new Float32Array(ptsIn); inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse); normal.set(0, 0, 1).multiply(inverseScale); - context.globals = { point: true }; + + const pointMaterialSize = []; + context.setFeature(feature); for (const geometry of feature.geometries) { const start = geometry.indices[0].offset; @@ -206,10 +210,14 @@ function featureToPoint(feature, options) { } coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, v)); - const style = feature.style.applyContext(context); - const { base_altitude, color } = style.point; + style.setContext(context); + const { base_altitude, color, radius } = style.point; coord.z = 0; + if (!pointMaterialSize.includes(radius)) { + pointMaterialSize.push(radius); + } + // populate vertices base.copy(normal).multiplyScalar(base_altitude).add(coord).toArray(vertices, v); toColor(color).multiplyScalar(255).toArray(colors, v); @@ -223,7 +231,11 @@ function featureToPoint(feature, options) { geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true)); geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1)); - options.pointMaterial.size = feature.style.point.radius; + options.pointMaterial.size = pointMaterialSize[0]; + if (pointMaterialSize.length > 1) { + // TODO CREATE material for each feature + console.warn('Too many differents point.radius, only the first one will be used'); + } return new THREE.Points(geom, options.pointMaterial); } @@ -241,9 +253,8 @@ function featureToLine(feature, options) { const geom = new THREE.BufferGeometry(); geom.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); - // TODO CREATE material for each feature - options.lineMaterial.linewidth = feature.style.stroke.width; - context.globals = { stroke: true }; + const lineMaterialWidth = []; + context.setFeature(feature); const countIndices = (count - feature.geometries.length) * 2; const indices = getIntArrayFromSize(countIndices, count); @@ -279,18 +290,26 @@ function featureToLine(feature, options) { } coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, v)); - const style = feature.style.applyContext(context); - const { base_altitude, color } = style.stroke; + style.setContext(context); + const { base_altitude, color, width } = style.stroke; coord.z = 0; + if (!lineMaterialWidth.includes(width)) { + lineMaterialWidth.push(width); + } + // populate geometry buffers base.copy(normal).multiplyScalar(base_altitude).add(coord).toArray(vertices, v); toColor(color).multiplyScalar(255).toArray(colors, v); batchIds[j] = id; } - featureId++; } + options.lineMaterial.linewidth = lineMaterialWidth[0]; + if (lineMaterialWidth.length > 1) { + // TODO CREATE material for each feature + console.warn('Too many differents stroke.width, only the first one will be used'); + } geom.setAttribute('color', new THREE.BufferAttribute(colors, 3, true)); geom.setAttribute('batchId', new THREE.BufferAttribute(batchIds, 1)); geom.setIndex(new THREE.BufferAttribute(indices, 1)); @@ -304,7 +323,7 @@ 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); normal.set(0, 0, 1).multiply(inverseScale); @@ -332,7 +351,7 @@ function featureToPolygon(feature, options) { } coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, i)); - const style = feature.style.applyContext(context); + style.setContext(context); const { base_altitude, color } = style.fill; coord.z = 0; @@ -392,7 +411,7 @@ 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); coord.setCrs(context.collection.crs); @@ -418,7 +437,7 @@ function featureToExtrudedPolygon(feature, options) { coord.copy(context.setLocalCoordinatesFromArray(ptsIn, i)); - const style = feature.style.applyContext(context); + style.setContext(context); const { base_altitude, extrusion_height, color } = style.fill; coord.z = 0; @@ -513,7 +532,7 @@ function createInstancedMesh(mesh, count, ptsIn) { function pointsToInstancedMeshes(feature) { const ptsIn = feature.vertices; const count = feature.geometries.length; - const modelObject = feature.style.point.model.object; + const modelObject = style.point.model.object; if (modelObject instanceof THREE.Mesh) { return createInstancedMesh(modelObject, count, ptsIn); @@ -524,15 +543,15 @@ function pointsToInstancedMeshes(feature) { meshes.forEach(mesh => group.add(createInstancedMesh(mesh, count, ptsIn))); return group; } else { - throw new Error('The format of the model object provided in the feature style (feature.style.point.model.object) is not supported. Only THREE.Mesh or THREE.Object3D are supported.'); + throw new Error('The format of the model object provided in the style (layer.style.point.model.object) is not supported. Only THREE.Mesh or THREE.Object3D are supported.'); } } /** * 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) { @@ -543,7 +562,7 @@ function featureToMesh(feature, options) { let mesh; switch (feature.type) { case FEATURE_TYPES.POINT: - if (feature.style.point?.model?.object) { + if (style.point?.model?.object) { try { mesh = pointsToInstancedMeshes(feature); mesh.isInstancedMesh = true; @@ -558,7 +577,7 @@ function featureToMesh(feature, options) { mesh = featureToLine(feature, options); break; case FEATURE_TYPES.POLYGON: - if (feature.style.fill.extrusion_height) { + if (style.fill && Object.keys(style.fill).includes('extrusion_height')) { mesh = featureToExtrudedPolygon(feature, options); } else { mesh = featureToPolygon(feature, options); @@ -573,10 +592,6 @@ function featureToMesh(feature, options) { } mesh.feature = feature; - if (options.layer) { - mesh.layer = options.layer; - } - return mesh; } @@ -592,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({ @@ -624,20 +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 = this; } + // 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 77dcaa0d65..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,27 +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(); @@ -49,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) { @@ -73,36 +63,32 @@ function drawPoint(ctx, x, y, style = {}, invCtxScale) { const coord = new Coordinates('EPSG:4326', 0, 0, 0); -function drawFeature(ctx, feature, extent, style, invCtxScale) { +function drawFeature(ctx, feature, extent, invCtxScale) { const extentDim = extent.planarDimensions(); const scaleRadius = extentDim.x / ctx.canvas.width; for (const geometry of feature.geometries) { if (Extent.intersectsExtent(geometry.extent, extent)) { context.setGeometry(geometry); - const contextStyle = (geometry.properties.style || 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)); } } } @@ -121,7 +107,9 @@ 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, style = {}, backgroundColor) { + createTextureFromFeature(collection, extent, sizeTexture, layerStyle, backgroundColor) { + style = layerStyle || defaultStyle; + style.setContext(context); let texture; if (collection) { @@ -139,7 +127,9 @@ export default { ctx.fillStyle = backgroundColor.getStyle(); ctx.fillRect(0, 0, sizeTexture, sizeTexture); } - ctx.globalCompositeOperation = style.globalCompositeOperation || 'source-over'; + + // Documentation needed !! + ctx.globalCompositeOperation = layerStyle.globalCompositeOperation || 'source-over'; ctx.imageSmoothingEnabled = false; ctx.lineJoin = 'round'; @@ -168,16 +158,12 @@ 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) { - drawFeature(ctx, feature, featureExtent, feature.style || style, invCtxScale); + context.setFeature(feature); + drawFeature(ctx, feature, featureExtent, invCtxScale); } texture = new THREE.CanvasTexture(c); diff --git a/src/Core/Feature.js b/src/Core/Feature.js index 26bf4a69cd..197572b859 100644 --- a/src/Core/Feature.js +++ b/src/Core/Feature.js @@ -251,7 +251,7 @@ class Feature { } this._pos = 0; this._pushValues = (this.size === 3 ? push3DValues : push2DValues).bind(this); - this.style = new Style({}, collection.style); + this.style = Style.setFromProperties; } /** * Instance a new {@link FeatureGeometry} and push in {@link Feature}. @@ -424,7 +424,7 @@ export class FeatureCollection extends THREE.Object3D { /** * Updates the global transform of the object and its descendants. * - * @param {booolean} force The force + * @param {boolean} force The force */ updateMatrixWorld(force) { super.updateMatrixWorld(force); @@ -498,12 +498,4 @@ export class FeatureCollection extends THREE.Object3D { this.features.push(ref); return ref; } - - setParentStyle(style) { - if (style) { - this.features.forEach((f) => { - f.style.parent = style; - }); - } - } } diff --git a/src/Core/Label.js b/src/Core/Label.js index d1818b0518..019185443f 100644 --- a/src/Core/Label.js +++ b/src/Core/Label.js @@ -83,7 +83,7 @@ class Label extends THREE.Object3D { if (typeof content === 'string') { this.content = document.createElement('div'); - this.content.textContent = content; + this.content.textContent = style.text.field; } else { this.content = content.cloneNode(true); } diff --git a/src/Core/Style.js b/src/Core/Style.js index 8856e58ede..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 base_altitudeDefault(properties, ctx) { +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,19 +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 { - return property; } + return property; } } @@ -138,27 +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: () => { - if (property === undefined) { - return style.parent[category][name] || defaultValue; - } else { - return property; + // != 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; } /** @@ -167,28 +174,35 @@ 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 {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 {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); #localCoordinates = new Coordinates('EPSG:4326', 0, 0, 0); #worldCoordsComputed = true; + #feature = {}; #geometry = {}; - /** - * @constructor - */ - constructor() { - this.globals = {}; + + setZoom(zoom) { + this.zoom = zoom; + } + + setFeature(f) { + this.#feature = f; } setGeometry(g) { @@ -209,6 +223,17 @@ export class StyleContext { return this.#geometry.properties; } + get type() { + return this.#feature.type; + } + get featureStyle() { + let featureStyle = this.#feature.style; + if (featureStyle instanceof Function) { + featureStyle = featureStyle(this.properties, this); + } + return featureStyle; + } + get coordinates() { if (!this.#worldCoordsComputed) { this.#worldCoordsComputed = true; @@ -221,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 @@ -418,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). @@ -548,24 +625,14 @@ class Style { * @param {StyleOptions} [params={}] An object that contain any properties * (order, zoom, fill, stroke, point, text or/and icon) * and sub properties of a Style (@see {@link StyleOptions}). - * @param {Style} [parent] The parent style, that is looked onto if a value - * is missing. * @constructor */ - constructor(params = {}, parent) { + constructor(params = {}) { this.isStyle = true; + this.context = new StyleContext(); this.order = params.order || 0; - this.parent = parent || { - zoom: {}, - fill: {}, - stroke: {}, - point: {}, - text: {}, - icon: {}, - }; - params.zoom = params.zoom || {}; params.fill = params.fill || {}; params.stroke = params.stroke || {}; @@ -581,15 +648,17 @@ class Style { defineStyleProperty(this, 'fill', 'color', params.fill.color); 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, base_altitudeDefault); - defineStyleProperty(this, 'fill', 'extrusion_height', params.fill.extrusion_height); + defineStyleProperty(this, 'fill', 'base_altitude', params.fill.base_altitude, baseAltitudeDefault); + if (params.fill.extrusion_height) { + defineStyleProperty(this, 'fill', 'extrusion_height', params.fill.extrusion_height); + } this.stroke = {}; defineStyleProperty(this, 'stroke', 'color', params.stroke.color); defineStyleProperty(this, 'stroke', 'opacity', params.stroke.opacity, 1.0); defineStyleProperty(this, 'stroke', 'width', params.stroke.width, 1.0); defineStyleProperty(this, 'stroke', 'dasharray', params.stroke.dasharray, []); - defineStyleProperty(this, 'stroke', 'base_altitude', params.stroke.base_altitude, base_altitudeDefault); + defineStyleProperty(this, 'stroke', 'base_altitude', params.stroke.base_altitude, baseAltitudeDefault); this.point = {}; defineStyleProperty(this, 'point', 'color', params.point.color); @@ -597,8 +666,10 @@ class Style { defineStyleProperty(this, 'point', 'opacity', params.point.opacity, 1.0); 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, base_altitudeDefault); - defineStyleProperty(this, 'point', 'model', params.point.model); + defineStyleProperty(this, 'point', 'base_altitude', params.point.base_altitude, baseAltitudeDefault); + if (params.point.model) { + defineStyleProperty(this, 'point', 'model', params.point.model); + } this.text = {}; defineStyleProperty(this, 'text', 'field', params.text.field); @@ -650,62 +721,47 @@ class Style { } /** - * Clones this style. - * - * @return {Style} The new style, cloned from this one. - */ + * Clones this style. + * + * @return {Style} The new style, cloned from this one. + */ clone() { const clone = new 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 {Object} context The context of the FeatureGeometry that we want to get the Style. - * - * @return {Style} mapped style depending on context. - */ - applyContext(context) { - const style = {}; - if (this.fill.color || this.fill.pattern || context.globals.fill) { - mapPropertiesFromContext('fill', this, style, context); - } - if (this.stroke.color || context.globals.stroke) { - mapPropertiesFromContext('stroke', this, style, context); - } - if (this.point.color || this.point.model || context.globals.point) { - mapPropertiesFromContext('point', this, style, context); - } - if (this.text || context.globals.text) { - mapPropertiesFromContext('text', this, style, context); - } - if (this.icon || context.globals.icon) { - mapPropertiesFromContext('icon', this, style, context); - } - style.order = this.order; - return new Style(style); + setContext(ctx) { + this.context = ctx; } /** * set Style from (geojson-like) properties. * @param {Object} properties (geojson-like) properties. - * @param {Number} type + * @param {FeatureContext} featCtx the context of the feature * * @returns {StyleOptions} containing all properties for itowns.Style */ - setFromGeojsonProperties(properties, type) { + static setFromProperties(properties, featCtx) { + const type = featCtx.type; + const style = {}; if (type === FEATURE_TYPES.POINT) { - this.point.color = properties.fill; - this.point.opacity = properties['fill-opacity']; - this.point.line = properties.stroke; - this.point.radius = properties.radius; - - this.text.color = properties['label-color']; - this.text.opacity = properties['label-opacity']; - this.text.size = properties['label-size']; - + const point = { + ...(properties.fill !== undefined && { color: properties.fill }), + ...(properties['fill-opacity'] !== undefined && { opacity: properties['fill-opacity'] }), + ...(properties.stroke !== undefined && { line: properties.stroke }), + ...(properties.radius !== undefined && { radius: properties.radius }), + }; + if (Object.keys(point).length) { + style.point = point; + } + const text = { + ...(properties['label-color'] !== undefined && { color: properties['label-color'] }), + ...(properties['label-opacity'] !== undefined && { opacity: properties['label-opacity'] }), + ...(properties['label-size'] !== undefined && { size: properties['label-size'] }), + }; + if (Object.keys(point).length) { + style.text = text; + } const icon = { ...(properties.icon !== undefined && { source: properties.icon }), ...(properties['icon-scale'] !== undefined && { size: properties['icon-scale'] }), @@ -713,19 +769,28 @@ class Style { ...(properties['icon-color'] !== undefined && { color: properties['icon-color'] }), }; if (Object.keys(icon).length) { - this.icon = icon; + style.icon = icon; } } else { - this.stroke.color = properties.stroke; - this.stroke.width = properties['stroke-width']; - this.stroke.opacity = properties['stroke-opacity']; - + const stroke = { + ...(properties.stroke !== undefined && { color: properties.stroke }), + ...(properties['stroke-width'] !== undefined && { width: properties['stroke-width'] }), + ...(properties['stroke-opacity'] !== undefined && { opacity: properties['stroke-opacity'] }), + }; + if (Object.keys(stroke).length) { + style.stroke = stroke; + } if (type !== FEATURE_TYPES.LINE) { - this.fill.color = properties.fill; - this.fill.opacity = properties['fill-opacity']; + const fill = { + ...(properties.fill !== undefined && { color: properties.fill }), + ...(properties['fill-opacity'] !== undefined && { opacity: properties['fill-opacity'] }), + }; + if (Object.keys(fill).length) { + style.fill = fill; + } } } - return this; + return style; } /** @@ -737,19 +802,27 @@ class Style { * * @returns {StyleOptions} containing all properties for itowns.Style */ - setFromVectorTileLayer(layer, sprites, order = 0, symbolToCircle = false) { + static setFromVectorTileLayer(layer, sprites, order = 0, symbolToCircle = false) { + const style = { + fill: {}, + stroke: {}, + point: {}, + text: {}, + icon: {}, + }; + layer.layout = layer.layout || {}; layer.paint = layer.paint || {}; - this.order = order; + style.order = order; - if (layer.type === 'fill' && !this.fill.color) { + if (layer.type === 'fill') { const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-color'] || layer.paint['fill-pattern'], { type: 'color' })); - this.fill.color = color; - this.fill.opacity = readVectorProperty(layer.paint['fill-opacity']) || opacity; + style.fill.color = color; + style.fill.opacity = readVectorProperty(layer.paint['fill-opacity']) || opacity; if (layer.paint['fill-pattern']) { try { - this.fill.pattern = { + style.fill.pattern = { id: layer.paint['fill-pattern'], source: sprites.source, cropValues: sprites[layer.paint['fill-pattern']], @@ -762,82 +835,90 @@ class Style { if (layer.paint['fill-outline-color']) { const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['fill-outline-color'], { type: 'color' })); - this.stroke.color = color; - this.stroke.opacity = opacity; - this.stroke.width = 1.0; - this.stroke.dasharray = []; + style.stroke.color = color; + style.stroke.opacity = opacity; + style.stroke.width = 1.0; + style.stroke.dasharray = []; } - } else if (layer.type === 'line' && !this.stroke.color) { + } else if (layer.type === 'line') { const prepare = readVectorProperty(layer.paint['line-color'], { type: 'color' }); const { color, opacity } = rgba2rgb(prepare); - this.stroke.dasharray = readVectorProperty(layer.paint['line-dasharray']); - this.stroke.color = color; - this.stroke.lineCap = layer.layout['line-cap']; - this.stroke.width = readVectorProperty(layer.paint['line-width']); - this.stroke.opacity = readVectorProperty(layer.paint['line-opacity']) || opacity; + style.stroke.dasharray = readVectorProperty(layer.paint['line-dasharray']); + style.stroke.color = color; + style.stroke.lineCap = layer.layout['line-cap']; + style.stroke.width = readVectorProperty(layer.paint['line-width']); + style.stroke.opacity = readVectorProperty(layer.paint['line-opacity']) || opacity; } else if (layer.type === 'circle' || symbolToCircle) { const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['circle-color'], { type: 'color' })); - this.point.color = color; - this.point.opacity = opacity; - this.point.radius = readVectorProperty(layer.paint['circle-radius']); + style.point.color = color; + style.point.opacity = opacity; + style.point.radius = readVectorProperty(layer.paint['circle-radius']); } else if (layer.type === 'symbol') { // overlapping order - this.text.zOrder = readVectorProperty(layer.layout['symbol-z-order']); - if (this.text.zOrder == 'auto') { - this.text.zOrder = readVectorProperty(layer.layout['symbol-sort-key']) || 'Y'; - } else if (this.text.zOrder == 'viewport-y') { - this.text.zOrder = 'Y'; - } else if (this.text.zOrder == 'source') { - this.text.zOrder = 0; + style.text.zOrder = readVectorProperty(layer.layout['symbol-z-order']); + if (style.text.zOrder == 'auto') { + style.text.zOrder = readVectorProperty(layer.layout['symbol-sort-key']) || 'Y'; + } else if (style.text.zOrder == 'viewport-y') { + style.text.zOrder = 'Y'; + } else if (style.text.zOrder == 'source') { + style.text.zOrder = 0; } // position - this.text.anchor = readVectorProperty(layer.layout['text-anchor']); - this.text.offset = readVectorProperty(layer.layout['text-offset']); - this.text.padding = readVectorProperty(layer.layout['text-padding']); - this.text.size = readVectorProperty(layer.layout['text-size']); - this.text.placement = readVectorProperty(layer.layout['symbol-placement']); - this.text.rotation = readVectorProperty(layer.layout['text-rotation-alignment']); + style.text.anchor = readVectorProperty(layer.layout['text-anchor']); + style.text.offset = readVectorProperty(layer.layout['text-offset']); + style.text.padding = readVectorProperty(layer.layout['text-padding']); + style.text.size = readVectorProperty(layer.layout['text-size']); + style.text.placement = readVectorProperty(layer.layout['symbol-placement']); + style.text.rotation = readVectorProperty(layer.layout['text-rotation-alignment']); // content - this.text.field = readVectorProperty(layer.layout['text-field']); - this.text.wrap = readVectorProperty(layer.layout['text-max-width']); - this.text.spacing = readVectorProperty(layer.layout['text-letter-spacing']); - this.text.transform = readVectorProperty(layer.layout['text-transform']); - this.text.justify = readVectorProperty(layer.layout['text-justify']); + style.text.field = readVectorProperty(layer.layout['text-field']); + style.text.wrap = readVectorProperty(layer.layout['text-max-width']); + style.text.spacing = readVectorProperty(layer.layout['text-letter-spacing']); + style.text.transform = readVectorProperty(layer.layout['text-transform']); + style.text.justify = readVectorProperty(layer.layout['text-justify']); // appearance const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['text-color'], { type: 'color' })); - this.text.color = color; - this.text.opacity = readVectorProperty(layer.paint['text-opacity']) || (opacity !== undefined && opacity); + style.text.color = color; + style.text.opacity = readVectorProperty(layer.paint['text-opacity']) || (opacity !== undefined && opacity); - this.text.font = readVectorProperty(layer.layout['text-font']); + style.text.font = readVectorProperty(layer.layout['text-font']); const haloColor = readVectorProperty(layer.paint['text-halo-color'], { type: 'color' }); if (haloColor) { - this.text.haloColor = haloColor.color || haloColor; - this.text.haloWidth = readVectorProperty(layer.paint['text-halo-width']); - this.text.haloBlur = readVectorProperty(layer.paint['text-halo-blur']); + style.text.haloColor = haloColor.color || haloColor; + style.text.haloWidth = readVectorProperty(layer.paint['text-halo-width']); + style.text.haloBlur = readVectorProperty(layer.paint['text-halo-blur']); } // additional icon const iconImg = readVectorProperty(layer.layout['icon-image']); if (iconImg) { try { - this.icon.id = iconImg; - this.icon.source = sprites.source; - this.icon.cropValues = sprites[iconImg]; + style.icon.id = iconImg; + style.icon.source = sprites.source; + style.icon.cropValues = sprites[iconImg]; - this.icon.size = readVectorProperty(layer.layout['icon-size']) || 1; + style.icon.size = readVectorProperty(layer.layout['icon-size']) || 1; const { color, opacity } = rgba2rgb(readVectorProperty(layer.paint['icon-color'], { type: 'color' })); - this.icon.color = color; - this.icon.opacity = readVectorProperty(layer.paint['icon-opacity']) || (opacity !== undefined && opacity); + style.icon.color = color; + style.icon.opacity = readVectorProperty(layer.paint['icon-opacity']) || (opacity !== undefined && opacity); } catch (err) { err.message = `VTlayer '${layer.id}': argument sprites must not be null when using layer.layout['icon-image']`; throw err; } } } - return this; + // 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; } /** @@ -848,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); } } @@ -885,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) { @@ -926,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`; @@ -947,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. @@ -1033,24 +1072,6 @@ class Style { return this.text.anchor; } } - - /** - * Returns a string, associating `style.text.field` and properties to use to - * replace the keys in `style.text.field`. - * - * @param {Object} ctx - An object containing the feature context. - * - * @return {String|undefined} The formatted string if `style.text.field` is defined, nothing otherwise. - */ - getTextFromProperties(ctx) { - if (!this.text.field) { return; } - - if (this.text.field.expression) { - return readExpression(this.text.field, ctx); - } else { - return this.text.field.replace(/\{(.+?)\}/g, (a, b) => (ctx.properties[b] || '')).trim(); - } - } } // Add custom style sheet with iTowns specifics @@ -1069,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 d2c55ef813..bd3a7bb2b4 100644 --- a/src/Layer/LabelLayer.js +++ b/src/Layer/LabelLayer.js @@ -238,28 +238,24 @@ 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.setZoom(extent.zoom); data.features.forEach((f) => { // TODO: add support for LINE and POLYGON if (f.type !== FEATURE_TYPES.POINT) { return; } + context.setFeature(f); - const featureField = f.style.text.field; + const featureField = f.style?.text?.field; // determine if altitude style is specified by the user - const altitudeStyle = f.style.point.base_altitude; - const isDefaultElevationStyle = altitudeStyle instanceof Function && altitudeStyle.name == 'base_altitudeDefault'; + const altitudeStyle = f.style?.point?.base_altitude; + const isDefaultElevationStyle = altitudeStyle instanceof Function && altitudeStyle.name == 'baseAltitudeDefault'; // determine if the altitude needs update with ElevationLayer labels.needsAltitude = labels.needsAltitude || this.forceClampToTerrain === true || (isDefaultElevationStyle && !f.hasRawElevationData); @@ -272,31 +268,25 @@ class LabelLayer extends GeometryLayer { coord.applyMatrix4(data.matrixWorld); if (!_extent.isPointInside(coord)) { return; } - - const geometryField = g.properties.style && g.properties.style.text.field; + const geometryField = g.properties.style && g.properties.style.text && g.properties.style.text.field; 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.id)) - && !(f.style && (f.style.icon.source || f.style.icon.id)) - && !(this.style && (this.style.icon.source || this.style.icon.id))) { + 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.icon && (this.style.icon.source || this.style.icon.key))) { return; } - } else if (geometryField) { - content = g.properties.style.getTextFromProperties(context); - } else if (featureField) { - content = f.style.getTextFromProperties(context); - } else if (layerField) { - content = this.style.getTextFromProperties(context); } - const style = (g.properties.style || f.style || this.style).applyContext(context); + const label = new Label(content, coord.clone(), this.style); - const label = new Label(content, coord.clone(), 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/Parser/GeoJsonParser.js b/src/Parser/GeoJsonParser.js index 535978a18c..41905f4dc2 100644 --- a/src/Parser/GeoJsonParser.js +++ b/src/Parser/GeoJsonParser.js @@ -1,6 +1,5 @@ import Coordinates from 'Core/Geographic/Coordinates'; import { FeatureCollection, FEATURE_TYPES } from 'Core/Feature'; -import Style from 'Core/Style'; import { deprecatedParsingOptionsToNewOne } from 'Core/Deprecated/Undeprecator'; function readCRS(json) { @@ -74,7 +73,7 @@ const toFeature = { const geometry = feature.bindNewGeometry(); geometry.properties = properties; - geometry.properties.style = new Style({}, feature.style).setFromGeojsonProperties(properties, feature.type); + this.populateGeometry(crsIn, coordsIn, geometry, feature); feature.updateExtent(geometry); }, @@ -85,7 +84,6 @@ const toFeature = { } const geometry = feature.bindNewGeometry(); geometry.properties = properties; - geometry.properties.style = new Style({}, feature.style).setFromGeojsonProperties(properties, feature.type); // Then read contour and holes for (let i = 0; i < coordsIn.length; i++) { diff --git a/src/Source/FileSource.js b/src/Source/FileSource.js index 28dea00c8c..0701b21a77 100644 --- a/src/Source/FileSource.js +++ b/src/Source/FileSource.js @@ -175,10 +175,6 @@ class FileSource extends Source { this.extent.applyMatrix4(data.matrixWorld); } } - - if (data.isFeatureCollection) { - data.setParentStyle(options.out.style); - } }); } diff --git a/src/Source/VectorTilesSource.js b/src/Source/VectorTilesSource.js index b2095f50c6..9d2f69192d 100644 --- a/src/Source/VectorTilesSource.js +++ b/src/Source/VectorTilesSource.js @@ -94,9 +94,7 @@ class VectorTilesSource extends TMSSource { if (layer.type === 'background') { this.backgroundLayer = layer; } else if (ffilter(layer)) { - const style = new Style().setFromVectorTileLayer(layer, this.sprites, order, this.symbolToCircle); - style.zoom.min = layer.minzoom || 0; - style.zoom.max = layer.maxzoom || 24; + const style = Style.setFromVectorTileLayer(layer, this.sprites, order, this.symbolToCircle); this.styles[layer.id] = style; if (!this.layers[layer['source-layer']]) { @@ -136,9 +134,6 @@ class VectorTilesSource extends TMSSource { console.warn('With VectorTilesSource and FeatureGeometryLayer, the accurate option is always false'); options.out.accurate = false; } - const keys = Object.keys(this.styles); - - keys.forEach((k) => { this.styles[k].parent = options.out.style; }); } } } 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/feature2mesh.js b/test/unit/feature2mesh.js index 60867dce2b..cb25096e31 100644 --- a/test/unit/feature2mesh.js +++ b/test/unit/feature2mesh.js @@ -3,7 +3,6 @@ import proj4 from 'proj4'; import assert from 'assert'; import GeoJsonParser from 'Parser/GeoJsonParser'; import Feature2Mesh from 'Converter/Feature2Mesh'; -import Style from 'Core/Style'; const geojson = require('../data/geojson/holes.geojson.json'); const geojson2 = require('../data/geojson/simple.geojson.json'); @@ -58,63 +57,66 @@ describe('Feature2Mesh', function () { const parsed3 = GeoJsonParser.parse(geojson3, { in: { crs: 'EPSG:3946' }, out: { crs: 'EPSG:3946', buildExtent: true, mergeFeatures: false, structure: '3d' } }); it('rect mesh area should match geometry extent', function (done) { - parsed.then((collection) => { - const mesh = Feature2Mesh.convert()(collection).meshes; - const extentSize = collection.extent.planarDimensions(); + parsed + .then((collection) => { + const mesh = Feature2Mesh.convert()(collection).meshes; + const extentSize = collection.extent.planarDimensions(); - assert.equal( - extentSize.x * extentSize.y, - computeAreaOfMesh(mesh.children[0])); - done(); - }).catch(done); + assert.equal( + extentSize.x * extentSize.y, + computeAreaOfMesh(mesh.children[0])); + done(); + }).catch(done); }); it('square mesh area should match geometry extent minus holes', function (done) { - parsed.then((collection) => { - const mesh = Feature2Mesh.convert()(collection).meshes; + parsed + .then((collection) => { + const mesh = Feature2Mesh.convert()(collection).meshes; - const noHoleArea = computeAreaOfMesh(mesh.children[0]); - const holeArea = computeAreaOfMesh(mesh.children[1]); - const meshWithHoleArea = computeAreaOfMesh(mesh.children[2]); + const noHoleArea = computeAreaOfMesh(mesh.children[0]); + const holeArea = computeAreaOfMesh(mesh.children[1]); + const meshWithHoleArea = computeAreaOfMesh(mesh.children[2]); - assert.equal( - noHoleArea - holeArea, - meshWithHoleArea); - done(); - }).catch(done); + assert.equal( + noHoleArea - holeArea, meshWithHoleArea, + ); + done(); + }).catch(done); }); it('convert points, lines and mesh', function (done) { - parsed2.then((collection) => { - const mesh = Feature2Mesh.convert()(collection).meshes; - assert.equal(mesh.children[0].type, 'Points'); - assert.equal(mesh.children[1].type, 'LineSegments'); - assert.equal(mesh.children[2].type, 'Mesh'); - done(); - }).catch(done); + parsed2 + .then((collection) => { + const mesh = Feature2Mesh.convert()(collection).meshes; + assert.equal(mesh.children[0].type, 'Points'); + assert.equal(mesh.children[1].type, 'LineSegments'); + assert.equal(mesh.children[2].type, 'Mesh'); + done(); + }).catch(done); }); it('convert to instanced meshes', function (done) { - const styleModel3D = new Style({ + const styleModel3D = { point: { model: { object: makeTree() }, }, - }); - parsed3.then((collection) => { - for (const feat of collection.features) { feat.style = styleModel3D; } - const mesh = Feature2Mesh.convert()(collection).meshes; + }; + parsed3 + .then((collection) => { + const mesh = Feature2Mesh.convert({ style: styleModel3D })(collection).meshes; - let isInstancedMesh = false; - mesh.traverse((obj) => { - if (obj.isInstancedMesh) { - isInstancedMesh = true; - return null; - } - }, - ); - assert.ok(isInstancedMesh); - assert.equal(mesh.children.length, 3); - done(); - }).catch(done); + let isInstancedMesh = false; + mesh.traverse((obj) => { + if (obj.isInstancedMesh) { + isInstancedMesh = true; + return null; + } + }, + ); + assert.ok(isInstancedMesh); + assert.equal(mesh.children.length, 3); + done(); + }).catch(done); }); }); diff --git a/test/unit/label.js b/test/unit/label.js index 38e1571ec2..a0c5c7db6d 100644 --- a/test/unit/label.js +++ b/test/unit/label.js @@ -50,21 +50,22 @@ describe('Label', function () { let label; let style; const c = new Coordinates('EPSG:4326'); + const layerVT = { + type: 'symbol', + paint: {}, + layout: { + 'icon-image': 'icon', + 'icon-size': 1, + 'text-field': 'label', + }, + }; const sprites = { img: '', icon: { x: 0, y: 0, width: 10, height: 10 }, }; before('init style', function () { - style = new Style(); - style.setFromVectorTileLayer({ - type: 'symbol', - paint: {}, - layout: { - 'icon-image': 'icon', - 'icon-size': 1, - }, - }, sprites); + style = new Style(Style.setFromVectorTileLayer(layerVT, sprites)); }); it('should throw errors for bad Label construction', function () { @@ -72,9 +73,14 @@ describe('Label', function () { assert.throws(() => { label = new Label('content'); }); }); - it('should correctly create Labels', function () { - assert.doesNotThrow(() => { label = new Label('', c); }); - assert.doesNotThrow(() => { label = new Label(document.createElement('div'), c); }); + describe('should correctly create Labels', function () { + it('with label from style', function () { + assert.doesNotThrow(() => { label = new Label('', c, style); }); + assert.equal(label.content.textContent, layerVT.layout['text-field']); + }); + it('from a DomElement', function () { + assert.doesNotThrow(() => { label = new Label(document.createElement('div'), c); }); + }); }); it('should hide the DOM', function () { diff --git a/test/unit/style.js b/test/unit/style.js index 8193d17881..5a0dd312cc 100644 --- a/test/unit/style.js +++ b/test/unit/style.js @@ -1,4 +1,5 @@ import Style from 'Core/Style'; +import { FEATURE_TYPES } from 'Core/Feature'; import assert from 'assert'; import Fetcher from 'Provider/Fetcher'; import { TextureLoader } from 'three'; @@ -278,4 +279,84 @@ describe('Style', function () { }); }); }); + + describe('setFromProperties', () => { + it('FEATURE_TYPES.POINT', () => { + const properties = { + radius: 2, + 'label-color': '#eba55f', + 'icon-color': '#eba55f', + }; + const style = Style.setFromProperties(properties, { type: FEATURE_TYPES.POINT }); + assert.equal(style.point.radius, 2); + assert.equal(style.text.color, '#eba55f'); + assert.equal(style.icon.color, '#eba55f'); + }); + it('FEATURE_TYPES.POLYGON', () => { + const properties = { + fill: '#eba55f', + stroke: '#eba55f', + }; + const style = Style.setFromProperties(properties, { type: FEATURE_TYPES.POLYGON }); + assert.equal(style.stroke.color, '#eba55f'); + assert.equal(style.fill.color, '#eba55f'); + }); + }); + + describe('setFromVectorTileLayer', () => { + describe("layer.type==='fill'", () => { + const imgId = 'filler'; + const vectorTileLayer = { + type: 'fill', + paint: { 'fill-outline-color': '#eba55f' }, + }; + it('without fill-pattern (or sprites)', () => { + const style = Style.setFromVectorTileLayer(vectorTileLayer); + // fill-outline-color + assert.equal(style.stroke.color, '#eba55f'); + }); + + it('with fill-pattern (and sprites)', () => { + vectorTileLayer.paint['fill-pattern'] = imgId; + const sprites = { + filler: { x: 0, y: 0, width: 0, height: 0, pixelRatio: 1 }, + source: 'ImgUrl', + }; + const style = Style.setFromVectorTileLayer(vectorTileLayer, sprites); + // fill-pattern + assert.equal(style.fill.pattern.id, imgId); + assert.equal(style.fill.pattern.cropValues, sprites[imgId]); + }); + }); + it("layer.type==='line'", () => { + const vectorTileLayer = { + type: 'line', + paint: { + 'line-color': '#eba55f', + }, + }; + const style = Style.setFromVectorTileLayer(vectorTileLayer); + assert.equal(style.stroke.color, '#eba55f'); + }); + it("layer.type==='circle'", () => { + const vectorTileLayer = { + type: 'circle', + paint: { + 'circle-color': '#eba55f', + }, + }; + const style = Style.setFromVectorTileLayer(vectorTileLayer); + assert.equal(style.point.color, '#eba55f'); + }); + it("layer.type==='symbol'", () => { + const vectorTileLayer = { + type: 'symbol', + layout: { + 'symbol-z-order': 'auto', + }, + }; + const style = Style.setFromVectorTileLayer(vectorTileLayer); + assert.equal(style.text.zOrder, 'Y'); + }); + }); }); diff --git a/test/unit/vectortiles.js b/test/unit/vectortiles.js index 916296d80d..80d9366395 100644 --- a/test/unit/vectortiles.js +++ b/test/unit/vectortiles.js @@ -50,21 +50,21 @@ describe('Vector tiles', function () { assert.equal(square2[1], square2[4 * size + 1]); done(); - }); + }).catch(done); }); it('returns nothing', (done) => { parse(null).then((collection) => { assert.equal(collection, undefined); done(); - }); + }).catch(done); }); it('filters all features out', (done) => { parse(multipolygon, {}).then((collection) => { assert.equal(collection.features.length, 0); done(); - }); + }).catch(done); }); describe('VectorTilesSource', function () { @@ -86,7 +86,7 @@ describe('Vector tiles', function () { // eslint-disable-next-line no-template-curly-in-string assert.equal(source.url, 'http://server.geo/${z}/${x}/${y}.pbf'); done(); - }); + }).catch(done); }); it('reads the background layer', (done) => { @@ -100,7 +100,7 @@ describe('Vector tiles', function () { source.whenReady.then(() => { assert.ok(source.backgroundLayer); done(); - }); + }).catch(done); }); it('creates styles and assigns filters', (done) => { @@ -121,33 +121,7 @@ describe('Vector tiles', function () { assert.ok(source.styles.land); assert.equal(source.styles.land.fill.color, 'rgb(255,0,0)'); 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 = source.styles.land.applyContext({ globals: { zoom: 3 }, properties: () => {} }); - const styleLand_zoom_5 = source.styles.land.applyContext({ globals: { zoom: 5 }, properties: () => {} }); - 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) => { @@ -161,7 +135,7 @@ describe('Vector tiles', function () { assert.equal(source.styles.land.zoom.min, 5); assert.equal(source.styles.land.zoom.max, 13); done(); - }); + }).catch(done); }); it('sets the correct Style#zoom.min', (done) => { @@ -221,7 +195,7 @@ describe('Vector tiles', function () { assert.equal(source.styles.fourth.zoom.min, 0); assert.equal(source.styles.fifth.zoom.min, 3); done(); - }); + }).catch(done); }); it('Vector tile source mapbox url', () => {