From 4c050c145b4b75c5962b74f268899b1eac42871a Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Mon, 18 Sep 2023 17:45:16 +0200 Subject: [PATCH 01/11] chore(Feature): rename base_altitudeDefault to camelCase --- src/Core/Style.js | 8 ++++---- src/Layer/LabelLayer.js | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Core/Style.js b/src/Core/Style.js index 8856e58ede..f85986a78b 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -17,7 +17,7 @@ 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; } @@ -581,7 +581,7 @@ 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', 'base_altitude', params.fill.base_altitude, baseAltitudeDefault); defineStyleProperty(this, 'fill', 'extrusion_height', params.fill.extrusion_height); this.stroke = {}; @@ -589,7 +589,7 @@ class Style { 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,7 +597,7 @@ 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', 'base_altitude', params.point.base_altitude, baseAltitudeDefault); defineStyleProperty(this, 'point', 'model', params.point.model); this.text = {}; diff --git a/src/Layer/LabelLayer.js b/src/Layer/LabelLayer.js index d2c55ef813..ae00ceaf85 100644 --- a/src/Layer/LabelLayer.js +++ b/src/Layer/LabelLayer.js @@ -243,6 +243,7 @@ class LabelLayer extends GeometryLayer { // Converting the extent now is faster for further operation extent.as(data.crs, _extent); coord.crs = data.crs; + context.globals = { icon: true, text: true, @@ -259,7 +260,7 @@ class LabelLayer extends GeometryLayer { // 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 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); From a6c350348fd9ee1d13f501503e29073fef8ae4fc Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 8 Feb 2023 16:29:39 +0100 Subject: [PATCH 02/11] refactor(Style): change setFromGeojsonProperties() to static --- src/Core/Style.js | 54 ++++++++++++++++-------- src/Parser/GeoJsonParser.js | 5 ++- test/unit/style.js | 82 +++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 20 deletions(-) diff --git a/src/Core/Style.js b/src/Core/Style.js index f85986a78b..fafc33d3a0 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -695,17 +695,26 @@ class Style { * * @returns {StyleOptions} containing all properties for itowns.Style */ - setFromGeojsonProperties(properties, type) { + static setFromProperties(properties, 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 +722,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; } /** diff --git a/src/Parser/GeoJsonParser.js b/src/Parser/GeoJsonParser.js index 535978a18c..4662054c44 100644 --- a/src/Parser/GeoJsonParser.js +++ b/src/Parser/GeoJsonParser.js @@ -73,8 +73,9 @@ const toFeature = { } const geometry = feature.bindNewGeometry(); + properties.style = Style.setFromProperties(properties, feature.type); geometry.properties = properties; - geometry.properties.style = new Style({}, feature.style).setFromGeojsonProperties(properties, feature.type); + this.populateGeometry(crsIn, coordsIn, geometry, feature); feature.updateExtent(geometry); }, @@ -84,8 +85,8 @@ const toFeature = { return; } const geometry = feature.bindNewGeometry(); + properties.style = Style.setFromProperties(properties, feature.type); 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/test/unit/style.js b/test/unit/style.js index 8193d17881..9902970c48 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,85 @@ describe('Style', function () { }); }); }); + + describe('setFromProperties', () => { + it('FEATURE_TYPES.POINT', () => { + const properties = { + radius: 2, + 'label-color': '#eba55f', + 'icon-color': '#eba55f', + }; + const style = Style.setFromProperties(properties, 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, 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-pattern': imgId, + 'fill-outline-color': '#eba55f', + }, + }; + it('with fill-pattern (and sprites)', () => { + 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('without fill-pattern (or sprites)', () => { + const style = Style.setFromVectorTileLayer(vectorTileLayer); + // fill-outline-color + assert.equal(style.stroke.color, '#eba55f'); + }); + }); + 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'); + }); + }); }); From f1a9212c68963906f38b0c7ce778612582c5276a Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Fri, 15 Sep 2023 10:35:10 +0200 Subject: [PATCH 03/11] refactor(Style): change Style.setFromVectorTileLayer to static --- src/Core/Style.js | 108 +++++++++++++++++--------------- src/Source/VectorTilesSource.js | 8 ++- test/unit/label.js | 6 +- test/unit/style.js | 17 +++-- test/unit/vectortiles.js | 5 +- 5 files changed, 77 insertions(+), 67 deletions(-) diff --git a/src/Core/Style.js b/src/Core/Style.js index fafc33d3a0..36fd7f9a8c 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -755,19 +755,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']], @@ -780,82 +788,82 @@ 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; + return style; } /** diff --git a/src/Source/VectorTilesSource.js b/src/Source/VectorTilesSource.js index b2095f50c6..7e9c4d4a44 100644 --- a/src/Source/VectorTilesSource.js +++ b/src/Source/VectorTilesSource.js @@ -94,9 +94,11 @@ 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); + 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/label.js b/test/unit/label.js index 38e1571ec2..a97bacfe4f 100644 --- a/test/unit/label.js +++ b/test/unit/label.js @@ -56,15 +56,15 @@ describe('Label', function () { }; before('init style', function () { - style = new Style(); - style.setFromVectorTileLayer({ + const layerVT = { 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 () { diff --git a/test/unit/style.js b/test/unit/style.js index 9902970c48..df6a287c9d 100644 --- a/test/unit/style.js +++ b/test/unit/style.js @@ -308,12 +308,16 @@ describe('Style', function () { const imgId = 'filler'; const vectorTileLayer = { type: 'fill', - paint: { - 'fill-pattern': imgId, - 'fill-outline-color': '#eba55f', - }, + 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', @@ -323,11 +327,6 @@ describe('Style', function () { assert.equal(style.fill.pattern.id, imgId); assert.equal(style.fill.pattern.cropValues, sprites[imgId]); }); - it('without fill-pattern (or sprites)', () => { - const style = Style.setFromVectorTileLayer(vectorTileLayer); - // fill-outline-color - assert.equal(style.stroke.color, '#eba55f'); - }); }); it("layer.type==='line'", () => { const vectorTileLayer = { diff --git a/test/unit/vectortiles.js b/test/unit/vectortiles.js index 916296d80d..d732e89c0d 100644 --- a/test/unit/vectortiles.js +++ b/test/unit/vectortiles.js @@ -5,6 +5,7 @@ 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 @@ -140,8 +141,8 @@ describe('Vector tiles', function () { }, }); 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: () => {} }); + const styleLand_zoom_3 = new Style(source.styles.land).applyContext({ globals: { zoom: 3 }, properties: () => {} }); + const styleLand_zoom_5 = new Style(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)'); From cc3c1690e83d38ca2d6ed2360d0c6b67e499750d Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Thu, 9 Feb 2023 10:40:13 +0100 Subject: [PATCH 04/11] refactor(Style): supp collection.style and delete notion of style.parent --- src/Core/Feature.js | 10 +--------- src/Core/Style.js | 21 ++------------------- src/Source/FileSource.js | 4 ---- src/Source/VectorTilesSource.js | 3 --- 4 files changed, 3 insertions(+), 35 deletions(-) diff --git a/src/Core/Feature.js b/src/Core/Feature.js index 26bf4a69cd..c7a5561ae7 100644 --- a/src/Core/Feature.js +++ b/src/Core/Feature.js @@ -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/Style.js b/src/Core/Style.js index 36fd7f9a8c..051ed7d8c0 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -146,13 +146,7 @@ function defineStyleProperty(style, category, name, value, defaultValue) { name, { enumerable: true, - get: () => { - if (property === undefined) { - return style.parent[category][name] || defaultValue; - } else { - return property; - } - }, + get: () => property ?? defaultValue, set: (v) => { property = v; }, @@ -548,24 +542,13 @@ 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.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 || {}; 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 7e9c4d4a44..05c5b51fca 100644 --- a/src/Source/VectorTilesSource.js +++ b/src/Source/VectorTilesSource.js @@ -138,9 +138,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; }); } } } From e007b2ce00c287ca8d81c493c69ec6bb1a77fe42 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 21 Nov 2023 14:32:15 +0100 Subject: [PATCH 05/11] refactor(Feature2Mesh): add gestion feature with variable size --- src/Converter/Feature2Mesh.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Converter/Feature2Mesh.js b/src/Converter/Feature2Mesh.js index 8a66e700bd..d6d2fc0885 100644 --- a/src/Converter/Feature2Mesh.js +++ b/src/Converter/Feature2Mesh.js @@ -191,6 +191,8 @@ function featureToPoint(feature, options) { const vertices = new Float32Array(ptsIn); inverseScale.setFromMatrixScale(context.collection.matrixWorldInverse); normal.set(0, 0, 1).multiply(inverseScale); + + const pointMaterialSize = []; context.globals = { point: true }; for (const geometry of feature.geometries) { @@ -207,9 +209,13 @@ function featureToPoint(feature, options) { coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, v)); const style = feature.style.applyContext(context); - const { base_altitude, color } = style.point; + 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 +229,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,8 +251,7 @@ 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; + const lineMaterialWidth = []; context.globals = { stroke: true }; const countIndices = (count - feature.geometries.length) * 2; @@ -280,17 +289,25 @@ function featureToLine(feature, options) { coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, v)); const style = feature.style.applyContext(context); - const { base_altitude, color } = style.stroke; + 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)); @@ -634,7 +651,6 @@ export default { context.setCollection(collection); const features = collection.features; - if (!features || features.length == 0) { return; } const meshes = features.map(feature => featureToMesh(feature, options)); From e2d46f007d3f20bfd4f3cbf1ca70f36ab6cf8b68 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Fri, 7 Jul 2023 11:56:07 +0200 Subject: [PATCH 06/11] refactor(Feature): remove geometry.properties.style -> use style fct at Feature level --- examples/source_file_gpx_3d.html | 20 ++++---- src/Converter/Feature2Mesh.js | 20 +++++--- src/Core/Feature.js | 2 +- src/Core/Style.js | 5 +- src/Parser/GeoJsonParser.js | 3 -- test/unit/feature2mesh.js | 88 ++++++++++++++++---------------- test/unit/style.js | 4 +- 7 files changed, 74 insertions(+), 68 deletions(-) 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 d6d2fc0885..5b3721bf92 100644 --- a/src/Converter/Feature2Mesh.js +++ b/src/Converter/Feature2Mesh.js @@ -525,12 +525,13 @@ 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) { +function pointsToInstancedMeshes(feature, options) { const ptsIn = feature.vertices; const count = feature.geometries.length; - const modelObject = feature.style.point.model.object; + const modelObject = options.layer.style.point.model.object; if (modelObject instanceof THREE.Mesh) { return createInstancedMesh(modelObject, count, ptsIn); @@ -541,7 +542,7 @@ 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.'); } } @@ -560,9 +561,9 @@ function featureToMesh(feature, options) { let mesh; switch (feature.type) { case FEATURE_TYPES.POINT: - if (feature.style.point?.model?.object) { + if (options.layer?.style?.point?.model?.object) { try { - mesh = pointsToInstancedMeshes(feature); + mesh = pointsToInstancedMeshes(feature, options); mesh.isInstancedMesh = true; } catch (e) { mesh = featureToPoint(feature, options); @@ -575,7 +576,7 @@ function featureToMesh(feature, options) { mesh = featureToLine(feature, options); break; case FEATURE_TYPES.POLYGON: - if (feature.style.fill.extrusion_height) { + if (options.layer?.style?.fill.extrusion_height) { mesh = featureToExtrudedPolygon(feature, options); } else { mesh = featureToPolygon(feature, options); @@ -645,7 +646,12 @@ export default { options.pointMaterial = ReferLayerProperties(new THREE.PointsMaterial(), this); options.lineMaterial = ReferLayerProperties(new THREE.LineBasicMaterial(), this); options.polygonMaterial = ReferLayerProperties(new THREE.MeshBasicMaterial(), this); - options.layer = 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.setCollection(collection); diff --git a/src/Core/Feature.js b/src/Core/Feature.js index c7a5561ae7..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}. diff --git a/src/Core/Style.js b/src/Core/Style.js index 051ed7d8c0..4796895c95 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -674,11 +674,12 @@ class Style { /** * 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 */ - static setFromProperties(properties, type) { + static setFromProperties(properties, featCtx) { + const type = featCtx.type; const style = {}; if (type === FEATURE_TYPES.POINT) { const point = { diff --git a/src/Parser/GeoJsonParser.js b/src/Parser/GeoJsonParser.js index 4662054c44..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) { @@ -73,7 +72,6 @@ const toFeature = { } const geometry = feature.bindNewGeometry(); - properties.style = Style.setFromProperties(properties, feature.type); geometry.properties = properties; this.populateGeometry(crsIn, coordsIn, geometry, feature); @@ -85,7 +83,6 @@ const toFeature = { return; } const geometry = feature.bindNewGeometry(); - properties.style = Style.setFromProperties(properties, feature.type); geometry.properties = properties; // Then read contour and holes 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/style.js b/test/unit/style.js index df6a287c9d..5a0dd312cc 100644 --- a/test/unit/style.js +++ b/test/unit/style.js @@ -287,7 +287,7 @@ describe('Style', function () { 'label-color': '#eba55f', 'icon-color': '#eba55f', }; - const style = Style.setFromProperties(properties, FEATURE_TYPES.POINT); + 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'); @@ -297,7 +297,7 @@ describe('Style', function () { fill: '#eba55f', stroke: '#eba55f', }; - const style = Style.setFromProperties(properties, FEATURE_TYPES.POLYGON); + const style = Style.setFromProperties(properties, { type: FEATURE_TYPES.POLYGON }); assert.equal(style.stroke.color, '#eba55f'); assert.equal(style.fill.color, '#eba55f'); }); From 13511ee7fcf80a058ea66e4423212d540ee29683 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 19 Sep 2023 14:51:20 +0200 Subject: [PATCH 07/11] refactor(StyleContext): add setFeature to access feature.type --- src/Core/Style.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Core/Style.js b/src/Core/Style.js index 4796895c95..1301232a0d 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -163,7 +163,9 @@ function defineStyleProperty(style, category, name, value, defaultValue) { * * @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} 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 {Coordinates} coordinates The coordinates (in world space) of the last vertex (x, y, z) set with * setLocalCoordinatesFromArray(). * private properties: @@ -171,12 +173,14 @@ function defineStyleProperty(style, category, name, value, defaultValue) { * @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 @@ -185,6 +189,10 @@ export class StyleContext { this.globals = {}; } + setFeature(f) { + this.#feature = f; + } + setGeometry(g) { this.#geometry = g; } @@ -203,6 +211,10 @@ export class StyleContext { return this.#geometry.properties; } + get type() { + return this.#feature.type; + } + get coordinates() { if (!this.#worldCoordsComputed) { this.#worldCoordsComputed = true; @@ -646,7 +658,7 @@ class Style { * 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. + * @param {StyleContext} context The context of the FeatureGeometry that we want to get the Style. * * @return {Style} mapped style depending on context. */ From eb9cc78c0fa54bc95ddc585dc4c413366bf80ee8 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Thu, 9 Feb 2023 10:46:48 +0100 Subject: [PATCH 08/11] refactor(style): change Style.drawingFromContext(ctx) to Style.getFromContext(ctx) for hierarchization of style properties --- src/Converter/Feature2Mesh.js | 15 +++-- src/Converter/Feature2Texture.js | 20 +++--- src/Core/Style.js | 103 +++++++++++++++++++++---------- src/Layer/LabelLayer.js | 26 ++++---- test/unit/vectortiles.js | 35 ++++++----- 5 files changed, 123 insertions(+), 76 deletions(-) diff --git a/src/Converter/Feature2Mesh.js b/src/Converter/Feature2Mesh.js index 5b3721bf92..afbd11d385 100644 --- a/src/Converter/Feature2Mesh.js +++ b/src/Converter/Feature2Mesh.js @@ -7,7 +7,7 @@ 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(); @@ -194,6 +194,7 @@ function featureToPoint(feature, options) { const pointMaterialSize = []; context.globals = { point: true }; + context.setFeature(feature); for (const geometry of feature.geometries) { const start = geometry.indices[0].offset; @@ -208,7 +209,7 @@ function featureToPoint(feature, options) { } coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, v)); - const style = feature.style.applyContext(context); + const style = Style.applyContext(context); const { base_altitude, color, radius } = style.point; coord.z = 0; @@ -253,6 +254,7 @@ function featureToLine(feature, options) { const lineMaterialWidth = []; context.globals = { stroke: true }; + context.setFeature(feature); const countIndices = (count - feature.geometries.length) * 2; const indices = getIntArrayFromSize(countIndices, count); @@ -288,7 +290,7 @@ function featureToLine(feature, options) { } coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, v)); - const style = feature.style.applyContext(context); + const style = Style.applyContext(context); const { base_altitude, color, width } = style.stroke; coord.z = 0; @@ -322,6 +324,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); @@ -349,7 +352,7 @@ function featureToPolygon(feature, options) { } coord.copy(context.setLocalCoordinatesFromArray(feature.vertices, i)); - const style = feature.style.applyContext(context); + const style = Style.applyContext(context); const { base_altitude, color } = style.fill; coord.z = 0; @@ -410,6 +413,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); @@ -435,7 +439,7 @@ function featureToExtrudedPolygon(feature, options) { coord.copy(context.setLocalCoordinatesFromArray(ptsIn, i)); - const style = feature.style.applyContext(context); + const style = Style.applyContext(context); const { base_altitude, extrusion_height, color } = style.fill; coord.z = 0; @@ -653,6 +657,7 @@ export default { // as in examples/source_file_gpx_3d.html. options.layer = this || { style: options.style }; } + context.layerStyle = options.layer.style; context.setCollection(collection); diff --git a/src/Converter/Feature2Texture.js b/src/Converter/Feature2Texture.js index 77dcaa0d65..35ecae0435 100644 --- a/src/Converter/Feature2Texture.js +++ b/src/Converter/Feature2Texture.js @@ -25,7 +25,6 @@ function drawPolygon(ctx, vertices, indices = [{ offset: 0, count: 1 }], style = if (vertices.length === 0) { return; } - if (style.length) { for (const s of style) { _drawPolygon(ctx, vertices, indices, s, size, extent, invCtxScale, canBeFilled); @@ -73,19 +72,21 @@ 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; + context.setFeature(feature); + for (const geometry of feature.geometries) { if (Extent.intersectsExtent(geometry.extent, extent)) { context.setGeometry(geometry); - const contextStyle = (geometry.properties.style || style).applyContext(context); + + const contextStyle = Style.applyContext(context); if (contextStyle) { if ( - feature.type === FEATURE_TYPES.POINT - && contextStyle.point + feature.type === FEATURE_TYPES.POINT && contextStyle.point ) { // cross multiplication to know in the extent system the real size of // the point @@ -121,8 +122,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) { let texture; + context.layerStyle = layerStyle; if (collection) { // A texture is instancied drawn canvas @@ -139,7 +141,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'; @@ -177,7 +181,7 @@ export default { // Draw the canvas for (const feature of collection.features) { - drawFeature(ctx, feature, featureExtent, feature.style || style, invCtxScale); + drawFeature(ctx, feature, featureExtent, invCtxScale); } texture = new THREE.CanvasTexture(c); diff --git a/src/Core/Style.js b/src/Core/Style.js index 1301232a0d..b8a66ac6c0 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -166,6 +166,7 @@ function defineStyleProperty(style, category, name, value, defaultValue) { * @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 * setLocalCoordinatesFromArray(). * private properties: @@ -215,6 +216,38 @@ export class StyleContext { return this.#feature.type; } + get style() { + const layerStyle = this.layerStyle || {}; + let featureStyle = this.#feature.style; + if (featureStyle instanceof Function) { + featureStyle = readExpression(featureStyle, 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; + } + get coordinates() { if (!this.#worldCoordsComputed) { this.#worldCoordsComputed = true; @@ -645,10 +678,10 @@ 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); @@ -662,27 +695,47 @@ class Style { * * @return {Style} mapped style depending on context. */ - applyContext(context) { + static applyContext(context) { + const styleConc = new Style(context.style); const style = {}; - if (this.fill.color || this.fill.pattern || context.globals.fill) { - mapPropertiesFromContext('fill', this, style, context); + if (styleConc.fill.color || styleConc.fill.pattern || context.globals.fill) { + mapPropertiesFromContext('fill', styleConc, style, context); } - if (this.stroke.color || context.globals.stroke) { - mapPropertiesFromContext('stroke', this, style, context); + if (styleConc.stroke.color || context.globals.stroke) { + mapPropertiesFromContext('stroke', styleConc, style, context); } - if (this.point.color || this.point.model || context.globals.point) { - mapPropertiesFromContext('point', this, style, context); + if (styleConc.point.color || styleConc.point.model || context.globals.point) { + mapPropertiesFromContext('point', styleConc, style, context); } - if (this.text || context.globals.text) { - mapPropertiesFromContext('text', this, style, context); + + if (styleConc.text || context.globals.text) { + mapPropertiesFromContext('text', styleConc, style, context); } - if (this.icon || context.globals.icon) { - mapPropertiesFromContext('icon', this, style, context); + if (styleConc.icon || context.globals.icon) { + mapPropertiesFromContext('icon', styleConc, style, context); } - style.order = this.order; + style.order = styleConc.order; return new Style(style); } + /** + * Returns a string, associating `style.text.field` and properties to use to + * replace the keys in `style.text.field`. + * + * @param {FeatureContext} context The context linked to the feature + * + * @return {string|undefined} The formatted string if `style.text.field` is defined, nothing otherwise. + */ + getTextFromProperties(context) { + if (!this.text.field) { return; } + + if (this.text.field.expression) { + return readExpression(this.text.field, context); + } else { + return this.text.field.replace(/\{(.+?)\}/g, (a, b) => (context.properties()[b] || '')).trim(); + } + } + /** * set Style from (geojson-like) properties. * @param {Object} properties (geojson-like) properties. @@ -1055,24 +1108,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 diff --git a/src/Layer/LabelLayer.js b/src/Layer/LabelLayer.js index ae00ceaf85..367b851d52 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 { readExpression, StyleContext } from 'Core/Style'; +import Style, { readExpression, StyleContext } from 'Core/Style'; import { ScreenGrid } from 'Renderer/Label2DRenderer'; const context = new StyleContext(); @@ -249,17 +249,19 @@ class LabelLayer extends GeometryLayer { text: true, zoom: extent.zoom, }; + context.layerStyle = this.style; 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 altitudeStyle = f.style?.point?.base_altitude; const isDefaultElevationStyle = altitudeStyle instanceof Function && altitudeStyle.name == 'baseAltitudeDefault'; // determine if the altitude needs update with ElevationLayer @@ -273,8 +275,7 @@ 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; @@ -282,22 +283,23 @@ class LabelLayer extends GeometryLayer { 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 && this.style.icon && (this.style.icon.source || this.style.icon.key))) { return; } } else if (geometryField) { - content = g.properties.style.getTextFromProperties(context); + content = new Style(g.properties.style).getTextFromProperties(context); } else if (featureField) { - content = f.style.getTextFromProperties(context); + content = new Style(f.style).getTextFromProperties(context); } else if (layerField) { - content = this.style.getTextFromProperties(context); + content = new Style(this.style).getTextFromProperties(context); } - const style = (g.properties.style || f.style || this.style).applyContext(context); + const style = Style.applyContext(context); const label = new Label(content, coord.clone(), style); + label.layerId = this.id; label.padding = this.margin || label.padding; diff --git a/test/unit/vectortiles.js b/test/unit/vectortiles.js index d732e89c0d..ea1512e5b0 100644 --- a/test/unit/vectortiles.js +++ b/test/unit/vectortiles.js @@ -51,21 +51,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 () { @@ -87,7 +87,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) => { @@ -101,7 +101,7 @@ describe('Vector tiles', function () { source.whenReady.then(() => { assert.ok(source.backgroundLayer); done(); - }); + }).catch(done); }); it('creates styles and assigns filters', (done) => { @@ -122,7 +122,7 @@ describe('Vector tiles', function () { assert.ok(source.styles.land); assert.equal(source.styles.land.fill.color, 'rgb(255,0,0)'); done(); - }); + }).catch(done); }); it('get style from context', (done) => { @@ -140,15 +140,16 @@ describe('Vector tiles', function () { }], }, }); - source.whenReady.then(() => { - const styleLand_zoom_3 = new Style(source.styles.land).applyContext({ globals: { zoom: 3 }, properties: () => {} }); - const styleLand_zoom_5 = new Style(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(); - }); + 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) => { @@ -162,7 +163,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) => { @@ -222,7 +223,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', () => { From 30904d811f44dcca70f79955e41dd090187bb78b Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 26 Sep 2023 14:47:27 +0200 Subject: [PATCH 09/11] refactor(Style): supp getTextFromProperties() ad it's done with getContext() --- src/Core/Label.js | 2 +- src/Core/Style.js | 20 ++------------------ src/Layer/LabelLayer.js | 6 ------ test/unit/label.js | 28 +++++++++++++++++----------- 4 files changed, 20 insertions(+), 36 deletions(-) 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 b8a66ac6c0..887d94186d 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -49,6 +49,8 @@ export function readExpression(property, ctx) { // 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; } @@ -718,24 +720,6 @@ class Style { return new Style(style); } - /** - * Returns a string, associating `style.text.field` and properties to use to - * replace the keys in `style.text.field`. - * - * @param {FeatureContext} context The context linked to the feature - * - * @return {string|undefined} The formatted string if `style.text.field` is defined, nothing otherwise. - */ - getTextFromProperties(context) { - if (!this.text.field) { return; } - - if (this.text.field.expression) { - return readExpression(this.text.field, context); - } else { - return this.text.field.replace(/\{(.+?)\}/g, (a, b) => (context.properties()[b] || '')).trim(); - } - } - /** * set Style from (geojson-like) properties. * @param {Object} properties (geojson-like) properties. diff --git a/src/Layer/LabelLayer.js b/src/Layer/LabelLayer.js index 367b851d52..a0bdd79609 100644 --- a/src/Layer/LabelLayer.js +++ b/src/Layer/LabelLayer.js @@ -288,12 +288,6 @@ class LabelLayer extends GeometryLayer { && !(this.style && this.style.icon && (this.style.icon.source || this.style.icon.key))) { return; } - } else if (geometryField) { - content = new Style(g.properties.style).getTextFromProperties(context); - } else if (featureField) { - content = new Style(f.style).getTextFromProperties(context); - } else if (layerField) { - content = new Style(this.style).getTextFromProperties(context); } const style = Style.applyContext(context); diff --git a/test/unit/label.js b/test/unit/label.js index a97bacfe4f..a0c5c7db6d 100644 --- a/test/unit/label.js +++ b/test/unit/label.js @@ -50,20 +50,21 @@ 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 () { - const layerVT = { - type: 'symbol', - paint: {}, - layout: { - 'icon-image': 'icon', - 'icon-size': 1, - }, - }; style = new Style(Style.setFromVectorTileLayer(layerVT, sprites)); }); @@ -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 () { From 9a65aef59a925d88157900d9e9ba8e530f9bee9a Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Thu, 6 Jul 2023 15:53:06 +0200 Subject: [PATCH 10/11] refactor(Style): Style hierachisation in Layer.Style instanciation --- src/Converter/Feature2Mesh.js | 54 +++--- src/Converter/Feature2Texture.js | 68 +++---- src/Core/Style.js | 310 ++++++++++++++----------------- src/Layer/C3DTilesLayer.js | 3 + src/Layer/ColorLayer.js | 1 - src/Layer/LabelLayer.js | 19 +- src/Layer/Layer.js | 5 +- src/Source/VectorTilesSource.js | 4 - test/unit/3dtileslayerstyle.js | 44 +++-- test/unit/vectortiles.js | 28 --- 10 files changed, 225 insertions(+), 311 deletions(-) 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://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/vectortiles/style.json', From 74f364ee073867bb695e98cd2bab736b42ea5ebc Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 7 Jun 2023 16:25:29 +0200 Subject: [PATCH 11/11] refactor(FeatureToolTip): update with new gestion of Style --- examples/css/example.css | 2 +- examples/js/plugins/FeatureToolTip.js | 36 ++++++++++++--------------- 2 files changed, 17 insertions(+), 21 deletions(-) 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);