Skip to content

Commit

Permalink
feat(ColorLayer): points can now be displayed as icons
Browse files Browse the repository at this point in the history
  • Loading branch information
mgermerie committed Jun 13, 2022
1 parent 1e525ef commit 5431108
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 70 deletions.
49 changes: 49 additions & 0 deletions examples/images/arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 16 additions & 2 deletions examples/source_file_shapefile.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,29 @@
source: velibSource,
style: new itowns.Style({
zoom: { min: 10, max: 20 },
point: { color: 'white', line: 'green' },
point: {
color: 'white',
line: 'green',
radius: 5,
width: 2,
},
icon: {
source: 'images/arrow.svg',
anchor: 'top-left',
size: 2,
},
text: {
field: '{name}\n(id: {station_id})',
size: 14,
haloColor: 'white',
haloWidth: 1,
font: ['monospace'],
}
anchor: 'top-left',
justify: 'left',
offset: [20, 18],
},
}),
displayAsIcon: true,
addLabelLayer: true,
}));
}).then((layer => {
Expand Down
11 changes: 7 additions & 4 deletions src/Converter/Feature2Texture.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ 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, style, invCtxScale, layer) {
const extentDim = extent.planarDimensions();
const scaleRadius = extentDim.x / ctx.canvas.width;
const globals = { zoom: extent.zoom };
Expand All @@ -114,6 +114,9 @@ function drawFeature(ctx, feature, extent, style, invCtxScale) {

if (contextStyle) {
if (feature.type === FEATURE_TYPES.POINT) {
// Don't display point as raster if they are already displayed as icons.
if (layer.displayAsIcon) { continue; }

// 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;
Expand Down Expand Up @@ -148,7 +151,7 @@ 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, layer, backgroundColor) {
let texture;

if (collection) {
Expand All @@ -166,7 +169,7 @@ export default {
ctx.fillStyle = backgroundColor.getStyle();
ctx.fillRect(0, 0, sizeTexture, sizeTexture);
}
ctx.globalCompositeOperation = style.globalCompositeOperation || 'source-over';
ctx.globalCompositeOperation = layer.style.globalCompositeOperation || 'source-over';
ctx.imageSmoothingEnabled = false;
ctx.lineJoin = 'round';

Expand Down Expand Up @@ -197,7 +200,7 @@ export default {

// Draw the canvas
for (const feature of collection.features) {
drawFeature(ctx, feature, featureExtent, feature.style || style, invCtxScale);
drawFeature(ctx, feature, featureExtent, feature.style || layer.style, invCtxScale, layer);
}

texture = new THREE.CanvasTexture(c);
Expand Down
2 changes: 1 addition & 1 deletion src/Converter/textureConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default {
undefined;

extentDestination.as(CRS.formatToEPSG(layer.crs), extentTexture);
texture = Feature2Texture.createTextureFromFeature(data, extentTexture, 256, layer.style, backgroundColor);
texture = Feature2Texture.createTextureFromFeature(data, extentTexture, 256, layer, backgroundColor);
texture.features = data;
texture.extent = extentDestination;
} else if (data.isTexture) {
Expand Down
6 changes: 6 additions & 0 deletions src/Core/Label.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ class Label extends THREE.Object3D {
this.content = content.cloneNode(true);
}

// Display labels with content (either text or domElement) on top of content-less labels (such as labels with
// only an icon for instance).
if (content !== '') {
this.content.style.zIndex = '1';
}

this.content.classList.add('itowns-label');
this.content.style.userSelect = 'none';
this.content.style.position = 'absolute';
Expand Down
46 changes: 32 additions & 14 deletions src/Core/Style.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ class Style {
defineStyleProperty(this, 'text', 'haloBlur', params.text.haloBlur, 0);

this.icon = {};
defineStyleProperty(this, 'icon', 'pointIcon', params.icon.pointIcon);
defineStyleProperty(this, 'icon', 'source', params.icon.source);
defineStyleProperty(this, 'icon', 'key', params.icon.key);
defineStyleProperty(this, 'icon', 'anchor', params.icon.anchor, 'center');
Expand Down Expand Up @@ -406,9 +407,8 @@ class Style {
symbolStylefromContext(context) {
const style = new Style();
mapPropertiesFromContext('text', this, style, context);
if (this.icon) {
mapPropertiesFromContext('icon', this, style, context);
}
mapPropertiesFromContext('icon', this, style, context);
mapPropertiesFromContext('point', this, style, context);
style.order = this.order;
return style;
}
Expand Down Expand Up @@ -592,23 +592,41 @@ class Style {
domElement.setAttribute('data-before', domElement.textContent);
}

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

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

let icon = cacheStyle.get(image || key, size);
if (this.icon.pointIcon) {
icon = this.icon.pointIcon;

if (!icon) {
if (key && sprites) {
icon = getImage(sprites, key);
} else {
icon = getImage(image);
icon.setAttribute('class', 'itowns-icon');

icon.width = 2 * (this.point.radius + this.point.width);
icon.height = icon.width;

icon.style.width = 2 * this.point.radius;
icon.style.height = icon.style.width;
icon.style.backgroundColor = this.point.color;
icon.style.border = `${this.point.width}px solid ${this.point.line}`;
icon.style.borderRadius = '50%';

icon.complete = true;
} else {
const image = this.icon.source;
const size = this.icon.size;
const key = this.icon.key;

icon = cacheStyle.get(image || key, size);
if (!icon) {
if (key && sprites) {
icon = getImage(sprites, key);
} else {
icon = getImage(image);
}
cacheStyle.set(icon, image || key, size);
}
cacheStyle.set(icon, image || key, size);
}

const addIcon = () => {
Expand Down
63 changes: 37 additions & 26 deletions src/Core/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,39 +83,50 @@ function _preprocessLayer(view, layer, parentLayer) {

if (layer.isLabelLayer) {
view.mainLoop.gfxEngine.label2dRenderer.registerLayer(layer);
} else if (layer.labelEnabled || layer.addLabelLayer) {
if (layer.labelEnabled) {
// eslint-disable-next-line no-console
console.info('layer.labelEnabled is deprecated use addLabelLayer, instead of');
} else if (layer.labelEnabled || layer.addLabelLayer || layer.displayAsIcon) {
if (layer.labelEnabled !== undefined) {
console.warn('layer.labelEnabled is deprecated use addLabelLayer, instead of');
layer.addLabelLayer = layer.labelEnabled;
}
// Because the features are shared between layer and labelLayer.
layer.buildExtent = true;
const labelLayer = new LabelLayer(`${layer.id}-label`, {
source,
style: layer.style,
zoom: layer.zoom,
crs: source.crs,
visible: layer.visible,
margin: 15,
});

layer.addEventListener('visible-property-changed', () => {
labelLayer.visible = layer.visible;
});
const addLabelLayer = (pointOrLabel) => {
const labelLayer = new LabelLayer(`${layer.id}-${pointOrLabel}`, {
source,
style: layer.style,
zoom: layer.zoom,
crs: source.crs,
visible: layer.visible,
margin: 15,
displayPoints: pointOrLabel === 'point',
});

const removeLabelLayer = (e) => {
if (e.layerId === layer.id) {
view.removeLayer(labelLayer.id);
}
view.removeEventListener(VIEW_EVENTS.LAYER_REMOVED, removeLabelLayer);
};
layer.addEventListener('visible-property-changed', () => {
labelLayer.visible = layer.visible;
});

const removeLabelLayer = (e) => {
if (e.layerId === layer.id) {
view.removeLayer(labelLayer.id);
}
view.removeEventListener(VIEW_EVENTS.LAYER_REMOVED, removeLabelLayer);
};

view.addEventListener(VIEW_EVENTS.LAYER_REMOVED, removeLabelLayer);
view.addEventListener(VIEW_EVENTS.LAYER_REMOVED, removeLabelLayer);

layer.whenReady = layer.whenReady.then(() => {
view.addLayer(labelLayer);
return layer;
});
layer.whenReady = layer.whenReady.then(() => {
view.addLayer(labelLayer);
return layer;
});
};

if (layer.addLabelLayer) {
addLabelLayer('label');
}
if (layer.displayAsIcon) {
addLabelLayer('point');
}
}

return layer;
Expand Down
40 changes: 23 additions & 17 deletions src/Layer/LabelLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const coord = new Coordinates('EPSG:4326', 0, 0, 0);

const _extent = new Extent('EPSG:4326', 0, 0, 0, 0);

const divDomElement = document.createElement('div');


/**
* A layer to handle a bunch of `Label`. This layer can be created on its own,
* but it is better to use the option `addLabelLayer` on another `Layer` to let
Expand Down Expand Up @@ -57,12 +60,6 @@ class LabelLayer extends Layer {
this.buildExtent = true;

this.labelDomelement = config.domElement;

// The margin property defines a space around each label that cannot be occupied by another label.
// For example, if some labelLayer has a margin value of 5, there will be at least 10 pixels
// between each labels of the layer
// TODO : this property should be moved to Style after refactoring style properties structure
this.margin = config.margin;
}

/**
Expand All @@ -85,7 +82,7 @@ class LabelLayer extends Layer {
convert(data, extent) {
const labels = [];

const layerField = this.style && this.style.text && this.style.text.field;
const layerField = this.style?.text?.field;

// Converting the extent now is faster for further operation
extent.as(data.crs, _extent);
Expand All @@ -98,7 +95,7 @@ class LabelLayer extends Layer {
return;
}

const featureField = f.style && f.style.text.field;
const featureField = f.style?.text.field;

f.geometries.forEach((g) => {
// NOTE: this only works because only POINT is supported, it
Expand All @@ -110,16 +107,23 @@ class LabelLayer extends Layer {
if (f.size == 2) { coord.z = 0; }
if (!_extent.isPointInside(coord)) { return; }

const geometryField = g.properties.style && g.properties.style.text.field;
const geometryField = g.properties.style?.text.field;
let content;
const context = { globals, properties: () => g.properties };
if (this.labelDomelement) {

const style = (g.properties.style || f.style || this.style).symbolStylefromContext(context);

if (this.displayPoints) {
style.icon.pointIcon = divDomElement;
style.icon.size = 1;
style.icon.anchor = 'center';
} else 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.source || f.style.icon.key))
&& !(this.style && (this.style.icon.source || this.style.icon.key))) {
if (!(g.properties.style?.icon.source || g.properties.style?.icon.key)
&& !(f.style?.icon.source || f.style?.icon.key)
&& !(this.style?.icon.source || this.style?.icon.key)) {
return;
}
} else if (geometryField) {
Expand All @@ -130,11 +134,10 @@ class LabelLayer extends Layer {
content = this.style.getTextFromProperties(context);
}

const style = (g.properties.style || f.style || this.style).symbolStylefromContext(context);

const label = new Label(content, coord.clone(), style, this.source.sprites);
label.layerId = this.id;
label.padding = this.margin || label.padding;
label.padding = this.margin ?? label.padding;
label.allowOverlapping = this.displayPoints;

if (f.size == 2) {
label.needsAltitude = true;
Expand Down Expand Up @@ -218,7 +221,10 @@ class LabelLayer extends Layer {
label.updateElevationFromLayer(this.parent);
}

const present = node.children.find(l => l.isLabel && l.baseContent == label.baseContent);
// TODO : this implies displaying every icon with no baseContent (default to ''). Therefore we will
// display all duplicate icons. This might be improved.
const present = label.baseContent !== ''
&& node.children.find(l => l.isLabel && l.baseContent === label.baseContent);

if (!present) {
node.add(label);
Expand Down
Loading

0 comments on commit 5431108

Please sign in to comment.