Skip to content

Commit

Permalink
feature(Style): add support for custom icon in labels
Browse files Browse the repository at this point in the history
  • Loading branch information
mgermerie authored and gchoqueux committed Jul 29, 2021
1 parent fe2a2d9 commit 7f355c4
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 45 deletions.
3 changes: 3 additions & 0 deletions examples/source_file_kml_raster.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
haloWidth: 1,
transform: 'uppercase',
},
icon: {
anchor: 'bottom',
},
});

var kmlLayer = new itowns.ColorLayer('Kml', {
Expand Down
34 changes: 34 additions & 0 deletions src/Core/Label.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ class Label extends THREE.Object3D {
this.styleOffset = [0, 0];
}

this.icon = this.content.getElementsByClassName('itowns-icon')[0];
this.iconOffset = { left: 0, right: 0, top: 0, bottom: 0 };

this.zoom = {
min: style.zoom && style.zoom.min != undefined ? style.zoom.min : 2,
max: style.zoom && style.zoom.max != undefined ? style.zoom.max : 24,
Expand Down Expand Up @@ -130,15 +133,36 @@ class Label extends THREE.Object3D {
this.boundaries.right = x + this.offset.right + this.padding;
this.boundaries.top = y + this.offset.top - this.padding;
this.boundaries.bottom = y + this.offset.bottom + this.padding;

// The boundaries of the label are the union of the boundaries of the text
// and the boundaries of the icon, if it exists.
// Checking if this.icon is not only zeros is mandatory, to prevent case
// when a boundary is set to x or y coordinate
if (
this.iconOffset.left !== 0 && this.iconOffset.right !== 0
&& this.iconOffset.top !== 0 && this.iconOffset.bottom !== 0
) {
this.boundaries.left = Math.min(this.boundaries.left, x + this.iconOffset.left);
this.boundaries.right = Math.max(this.boundaries.right, x + this.iconOffset.right);
this.boundaries.top = Math.min(this.boundaries.top, y + this.iconOffset.top);
this.boundaries.bottom = Math.max(this.boundaries.bottom, y + this.iconOffset.bottom);
}
}
}

updateCSSPosition() {
// translate all content according to its given anchor
this.content.style[STYLE_TRANSFORM] = `translate(${
this.projectedPosition.x + this.offset.left
}px, ${
this.projectedPosition.y + this.offset.top
}px)`;

// translate possible icon inside content to cancel anchoring on it, so that it can later be positioned
// according to its own anchor
if (this.icon) {
this.icon.style[STYLE_TRANSFORM] = `translate(${-this.offset.left}px, ${-this.offset.top}px)`;
}
}

/**
Expand All @@ -157,6 +181,16 @@ class Label extends THREE.Object3D {
};
this.offset.right = this.offset.left + width;
this.offset.bottom = this.offset.top + height;

if (this.icon) {
rect = this.icon.getBoundingClientRect();
this.iconOffset = {
left: Math.floor(rect.x),
top: Math.floor(rect.y),
right: Math.ceil(rect.x + rect.width),
bottom: Math.ceil(rect.y + rect.height),
};
}
}
}

Expand Down
68 changes: 45 additions & 23 deletions src/Core/Style.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,15 @@ function defineStyleProperty(style, category, name, value, defaultValue) {
* @property {number|function} text.haloBlur - The blur value of the halo, in pixels.
* Default is `0`.
*
* @property {Object} icon - Defines the appearance of icons attached to label.
* @property {String} icon.source - The url of the icons' image file.
* @property {String} icon.key - The key of the icons' image in a vector tile data set.
* @property {string} [icon.anchor='center'] - The anchor of the icon compared to the label position.
* Can be `left`, `bottom`, `right`, `center`, `top-left`, `top-right`, `bottom-left`
* or `bottom-right`.
* @property {number} icon.size - If the icon's image is passed with `icon.source` or
* `icon.key`, it size when displayed on screen is multiplied by `icon.size`. Default is `1`.
*
* @example
* const style = new itowns.Style({
* stroke: { color: 'red' },
Expand Down Expand Up @@ -295,13 +304,15 @@ class Style {
stroke: {},
point: {},
text: {},
icon: {},
};

params.zoom = params.zoom || {};
params.fill = params.fill || {};
params.stroke = params.stroke || {};
params.point = params.point || {};
params.text = params.text || {};
params.icon = params.icon || {};

this.zoom = {};
defineStyleProperty(this, 'zoom', 'min', params.zoom.min);
Expand Down Expand Up @@ -354,6 +365,12 @@ class Style {
defineStyleProperty(this, 'text', 'haloColor', params.text.haloColor, '#000000');
defineStyleProperty(this, 'text', 'haloWidth', params.text.haloWidth, 0);
defineStyleProperty(this, 'text', 'haloBlur', params.text.haloBlur, 0);

this.icon = {};
defineStyleProperty(this, 'icon', 'source', params.icon.source);
defineStyleProperty(this, 'icon', 'key', params.icon.key);
defineStyleProperty(this, 'icon', 'anchor', params.icon.anchor, 'center');
defineStyleProperty(this, 'icon', 'size', params.icon.size, 1);
}

/**
Expand Down Expand Up @@ -407,6 +424,7 @@ class Style {
Object.assign(this.stroke, style.stroke);
Object.assign(this.point, style.point);
Object.assign(this.text, style.text);
Object.assign(this.icon, style.icon);
return this;
}

Expand Down Expand Up @@ -438,7 +456,7 @@ class Style {
this.text.size = properties['label-size'];

if (properties.icon) {
this.icon = { image: properties.icon, size: 1 };
this.icon.source = properties.icon;
}
} else {
this.stroke.color = properties.stroke;
Expand Down Expand Up @@ -537,7 +555,7 @@ class Style {
// additional icon
const key = readVectorProperty(layer.layout['icon-image']);
if (key) {
this.icon = { key };
this.icon.key = key;
this.icon.size = readVectorProperty(layer.layout['icon-size']) || 1;
}
}
Expand Down Expand Up @@ -573,14 +591,12 @@ class Style {
domElement.setAttribute('data-before', domElement.textContent);
}

if (!this.icon) {
if (!this.icon.source && !this.icon.key) {
return;
}

const image = this.icon.image;

const image = this.icon.source;
const size = this.icon.size;

const key = this.icon.key;

let icon = cacheStyle.get(image || key, size);
Expand All @@ -591,45 +607,51 @@ class Style {
} else {
icon = getImage(image);
}
icon.style.position = 'absolute';
cacheStyle.set(icon, image || key, size);
}

const addIcon = () => {
const cIcon = icon.cloneNode();
cIcon.width *= size;
cIcon.height *= size;
switch (this.text.anchor) {

cIcon.setAttribute('class', 'itowns-icon');

cIcon.width = icon.width * this.icon.size;
cIcon.height = icon.height * this.icon.size;
cIcon.style.position = 'absolute';
cIcon.style.top = '0';
cIcon.style.left = '0';

switch (this.icon.anchor) { // center by default
case 'left':
cIcon.style.right = `calc(100% - ${cIcon.width * 0.5}px)`;
cIcon.style.top = `calc(50% - ${cIcon.height * 0.5}px)`;
cIcon.style.top = `${-0.5 * cIcon.height}px`;
break;
case 'right':
cIcon.style.top = `calc(50% - ${cIcon.height * 0.5}px)`;
cIcon.style.top = `${-0.5 * cIcon.height}px`;
cIcon.style.left = `${-cIcon.width}px`;
break;
case 'top':
cIcon.style.right = `calc(50% - ${cIcon.width * 0.5}px)`;
cIcon.style.left = `${-0.5 * cIcon.width}px`;
break;
case 'bottom':
cIcon.style.top = `calc(100% - ${cIcon.height * 0.5}px)`;
cIcon.style.right = `calc(50% - ${cIcon.width * 0.5}px)`;
cIcon.style.top = `${-cIcon.height}px`;
cIcon.style.left = `${-0.5 * cIcon.width}px`;
break;
case 'bottom-left':
cIcon.style.top = `calc(100% - ${cIcon.height * 0.5}px)`;
cIcon.style.right = `calc(100% - ${cIcon.width * 0.5}px)`;
cIcon.style.top = `${-cIcon.height}px`;
break;
case 'bottom-right':
cIcon.style.top = `calc(100% - ${cIcon.height * 0.5}px)`;
cIcon.style.top = `${-cIcon.height}px`;
cIcon.style.left = `${-cIcon.width}px`;
break;
case 'top-left':
cIcon.style.right = `calc(100% - ${cIcon.width * 0.5}px)`;
break;
case 'top-right':
cIcon.style.left = `${-cIcon.width}px`;
break;
case 'center':
default:
cIcon.style.top = `calc(50% - ${cIcon.height * 0.5}px)`;
cIcon.style.right = `calc(50% - ${cIcon.width * 0.5}px)`;
cIcon.style.top = `${-0.5 * cIcon.height}px`;
cIcon.style.left = `${-0.5 * cIcon.width}px`;
break;
}

Expand Down Expand Up @@ -700,6 +722,6 @@ 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 = ['image', 'size', 'key'];
style_properties.icon = Object.keys(style.icon);

export default Style;
6 changes: 3 additions & 3 deletions src/Layer/LabelLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ class LabelLayer extends Layer {
const context = { globals, properties: () => g.properties };
if (!geometryField && !featureField && !layerField) {
// Check if there is an icon, with no text
if (!(g.properties.style && g.properties.style.icon)
&& !(f.style && f.style.icon)
&& !(this.style && this.style.icon)) {
if (!(g.properties.style && (g.properties.style.icon.source || g.properties.style.icon.key))
&& !(f.style && (f.style.icon.source || f.style.icon.key))
&& !(this.style && (this.style.icon.source || this.style.icon.key))) {
return;
}
} else if (geometryField) {
Expand Down
3 changes: 3 additions & 0 deletions test/unit/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ class DOMElement {
}
}
createSVGMatrix() {}
getElementsByClassName(className) {
return [this.children.find(element => element.class === className)];
}
}

// Mock document object for Mocha.
Expand Down
43 changes: 24 additions & 19 deletions test/unit/label.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,40 +78,44 @@ describe('Label', function () {
const img = cacheStyle.get('icon', 1);
img.complete = true;
img.emitEvent('load');
assert.equal(label.content.children[0].style.right, 'calc(50% - 5px)');
assert.equal(label.content.children[0].style.top, 'calc(50% - 5px)');
assert.equal(label.content.children[0].style.left, `${-0.5 * img.width}px`);
assert.equal(label.content.children[0].style.top, `${-0.5 * img.height}px`);


style.text.anchor = 'left';
style.icon.anchor = 'left';
label = new Label('', c, style);
assert.equal(label.content.children[0].style.right, 'calc(100% - 5px)');
assert.equal(label.content.children[0].style.top, 'calc(50% - 5px)');
assert.equal(label.content.children[0].style.left, '0');
assert.equal(label.content.children[0].style.top, `${-0.5 * img.height}px`);

style.text.anchor = 'right';
style.icon.anchor = 'right';
label = new Label('', c, style);
assert.equal(label.content.children[0].style.top, 'calc(50% - 5px)');
assert.equal(label.content.children[0].style.left, `${-img.width}px`);
assert.equal(label.content.children[0].style.top, `${-0.5 * img.height}px`);

style.text.anchor = 'top';
style.icon.anchor = 'top';
label = new Label('', c, style);
assert.equal(label.content.children[0].style.right, 'calc(50% - 5px)');
assert.equal(label.content.children[0].style.left, `${-0.5 * img.width}px`);
assert.equal(label.content.children[0].style.top, '0');

style.text.anchor = 'bottom';
style.icon.anchor = 'bottom';
label = new Label('', c, style);
assert.equal(label.content.children[0].style.top, 'calc(100% - 5px)');
assert.equal(label.content.children[0].style.right, 'calc(50% - 5px)');
assert.equal(label.content.children[0].style.left, `${-0.5 * img.width}px`);
assert.equal(label.content.children[0].style.top, `${-img.height}px`);

style.text.anchor = 'bottom-left';
style.icon.anchor = 'bottom-left';
label = new Label('', c, style);
assert.equal(label.content.children[0].style.right, 'calc(100% - 5px)');
assert.equal(label.content.children[0].style.top, 'calc(100% - 5px)');
assert.equal(label.content.children[0].style.left, '0');
assert.equal(label.content.children[0].style.top, `${-img.height}px`);

style.text.anchor = 'bottom-right';
style.icon.anchor = 'bottom-right';
label = new Label('', c, style);
assert.equal(label.content.children[0].style.top, 'calc(100% - 5px)');
assert.equal(label.content.children[0].style.left, `${-img.width}px`);
assert.equal(label.content.children[0].style.top, `${-img.height}px`);

style.text.anchor = 'top-left';
style.icon.anchor = 'top-left';
label = new Label('', c, style);
assert.equal(label.content.children[0].style.right, 'calc(100% - 5px)');
assert.equal(label.content.children[0].style.left, '0');
assert.equal(label.content.children[0].style.top, '0');
});

it('should hide the DOM', function () {
Expand All @@ -125,6 +129,7 @@ describe('Label', function () {
});

it('initializes the dimensions', function () {
style.text.anchor = 'top-left';
label = new Label('', c, style);
assert.equal(label.offset, undefined);

Expand Down

0 comments on commit 7f355c4

Please sign in to comment.