Skip to content

Commit b560005

Browse files
mgermeriegchoqueux
authored andcommitted
feature(Label): add support to pass custom domElements to labels
1 parent 29b6435 commit b560005

File tree

5 files changed

+181
-12
lines changed

5 files changed

+181
-12
lines changed

examples/config.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
"misc_compare_25d_3d": "Compare 2.5D and 3D maps",
6060
"misc_georeferenced_images": "Georeferenced image",
6161
"misc_orthographic_camera": "Orthographic camera",
62-
"misc_custom_controls": "Define custom controls"
62+
"misc_custom_controls": "Define custom controls",
63+
"misc_custom_label": "Custom label popup"
6364
},
6465

6566
"Plugins": {

examples/misc_custom_label.html

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>Custom popup</title>
5+
<meta charset="UTF-8">
6+
<link rel="stylesheet" type="text/css" href="css/example.css">
7+
<link rel="stylesheet" type="text/css" href="css/LoadingScreen.css">
8+
9+
<style>
10+
.bubble {
11+
background-color: #fff;
12+
border-radius: 10px;
13+
text-align: center;
14+
padding: 10px 15px;
15+
box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, .3), 0 0.0625rem 0.125rem rgba(0, 0, 0, .2);
16+
}
17+
.pointer {
18+
height: 12px;
19+
width: 12px;
20+
background-color: #fff;
21+
transform: translate(-50%, -50%) rotate(45deg);
22+
position: relative;
23+
left: 80%;
24+
}
25+
</style>
26+
27+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
28+
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"></script>
29+
</head>
30+
<body>
31+
<div id="description">
32+
Clicking a position while holding `p` key down will pop a custom popup.
33+
</div>
34+
<div id="viewerDiv" class="viewer"></div>
35+
36+
<script src="js/GUI/GuiTools.js"></script>
37+
<script src="../dist/itowns.js"></script>
38+
<script src="../dist/debug.js"></script>
39+
<script src="js/GUI/LoadingScreen.js"></script>
40+
<script src="js/plugins/FeatureToolTip.js"></script>
41+
<script type="text/javascript">
42+
/* global itowns */
43+
44+
// Setup view and layers
45+
const placement = {
46+
coord: new itowns.Coordinates('EPSG:4326', 3.5, 44),
47+
range: 1000000,
48+
};
49+
const viewerDiv = document.getElementById('viewerDiv');
50+
const view = new itowns.GlobeView(viewerDiv, placement);
51+
var menuGlobe = new GuiTools('menuDiv', view);
52+
itowns.Fetcher.json('./layers/JSONLayers/Ortho.json').then(function _(config) {
53+
config.source = new itowns.WMTSSource(config.source);
54+
view.addLayer(new itowns.ColorLayer('Ortho', config));
55+
});
56+
function addElevationLayerFromConfig(config) {
57+
config.source = new itowns.WMTSSource(config.source);
58+
view.addLayer(new itowns.ElevationLayer(config.id, config));
59+
}
60+
itowns.Fetcher.json('./layers/JSONLayers/WORLD_DTM.json').then(addElevationLayerFromConfig);
61+
itowns.Fetcher.json('./layers/JSONLayers/IGN_MNT_HIGHRES.json').then(addElevationLayerFromConfig);
62+
debug.createTileDebugUI(menuGlobe.gui, view);
63+
64+
// Create a custom div which will be displayed as a label
65+
const customDiv = document.createElement('div');
66+
const bubble = document.createElement('div');
67+
bubble.classList.add('bubble');
68+
customDiv.appendChild(bubble);
69+
const pointer = document.createElement('div');
70+
pointer.classList.add('pointer');
71+
customDiv.appendChild(pointer);
72+
73+
// Define a method to set the content of custom label
74+
function setLabelContent(properties) {
75+
const coord = properties.position.as('EPSG:4326');
76+
bubble.textContent = `
77+
lon : ${Math.round(coord.x * 100) / 100}° |
78+
lat : ${Math.round(coord.y * 100) / 100}° |
79+
alt : ${Math.round(coord.z)}m `;
80+
return customDiv;
81+
}
82+
83+
// Add a Label on `p` + click
84+
let pickMode = false;
85+
viewerDiv.addEventListener('keydown', function () {
86+
if (event.key === 'p') { pickMode = true; }
87+
}, false);
88+
viewerDiv.addEventListener('keyup', function () {
89+
if (event.key === 'p') { pickMode = false; }
90+
}, false);
91+
viewerDiv.addEventListener('mousedown', addFeatureFromMouseEvent, false);
92+
93+
// this const shall receive the mouse coordinates when user clicks while holding `p` pressed
94+
const featureCoord = new itowns.Coordinates(view.referenceCrs);
95+
96+
function addFeatureFromMouseEvent(event) {
97+
if (!pickMode) {
98+
return;
99+
}
100+
101+
featureCoord.setFromVector3(view.getPickingPositionFromDepth(view.eventToViewCoords(event)));
102+
103+
const features = createFeatureAt(featureCoord);
104+
105+
// the source of the feature layer
106+
const source = new itowns.FileSource({ features });
107+
108+
// create labelLayer
109+
const layer = new itowns.LabelLayer('idLabelLayer', {
110+
source: source,
111+
domElement: setLabelContent,
112+
style: new itowns.Style({
113+
text: { anchor: [-0.8, -1] },
114+
}),
115+
});
116+
117+
view.addLayer(layer);
118+
119+
// remove mouse event listener to prevent adding other layer sharing `idLabelLayer` id
120+
viewerDiv.removeEventListener('mousedown', addFeatureFromMouseEvent, false);
121+
}
122+
123+
function createFeatureAt(coordinate) {
124+
// create new featureCollection
125+
const collection = new itowns.FeatureCollection({
126+
crs: view.tileLayer.extent.crs,
127+
buildExtent: true,
128+
structure: '2d',
129+
});
130+
131+
// create new feature
132+
const feature = collection.requestFeatureByType(itowns.FEATURE_TYPES.POINT);
133+
134+
// add geometries to feature
135+
const geometry = feature.bindNewGeometry();
136+
geometry.startSubGeometry(1, feature);
137+
geometry.pushCoordinates(coordinate, feature);
138+
geometry.properties.position = coordinate;
139+
140+
geometry.updateExtent();
141+
feature.updateExtent(geometry);
142+
collection.updateExtent(feature.extent);
143+
144+
return collection;
145+
}
146+
</script>
147+
</body>
148+
</html>

src/Core/Label.js

+13-9
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,28 @@ class Label extends THREE.Object3D {
7979
this.projectedPosition = { x: 0, y: 0 };
8080
this.boundaries = { left: 0, right: 0, top: 0, bottom: 0 };
8181

82-
this.content = document.createElement('div');
83-
this.content.classList.add('itowns-label');
84-
this.content.style.userSelect = 'none';
85-
this.content.style.position = 'absolute';
86-
if (typeof content == 'string') {
82+
if (typeof content === 'string') {
83+
this.content = document.createElement('div');
8784
this.content.textContent = content;
8885
} else {
89-
this.content.appendChild(content);
86+
this.content = content.cloneNode(true);
9087
}
88+
89+
this.content.classList.add('itowns-label');
90+
this.content.style.userSelect = 'none';
91+
this.content.style.position = 'absolute';
92+
9193
this.baseContent = content;
9294

9395
if (style.isStyle) {
9496
this.anchor = style.getTextAnchorPosition();
9597
this.styleOffset = style.text.offset;
96-
if (style.text.haloWidth > 0) {
97-
this.content.classList.add('itowns-stroke-single');
98+
if (typeof content === 'string') {
99+
if (style.text.haloWidth > 0) {
100+
this.content.classList.add('itowns-stroke-single');
101+
}
102+
style.applyToHTML(this.content, sprites);
98103
}
99-
style.applyToHTML(this.content, sprites);
100104
} else {
101105
this.anchor = [0, 0];
102106
this.styleOffset = [0, 0];

src/Core/Style.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -692,9 +692,11 @@ class Style {
692692
*
693693
* @param {Object} ctx - An object containing the feature context.
694694
*
695-
* @return {string} The formatted string.
695+
* @return {string|undefined} The formatted string if `style.text.field` is defined, nothing otherwise.
696696
*/
697697
getTextFromProperties(ctx) {
698+
if (!this.text.field) { return; }
699+
698700
if (this.text.field.expression) {
699701
return readExpression(this.text.field, ctx);
700702
} else {

src/Layer/LabelLayer.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Coordinates from 'Core/Geographic/Coordinates';
66
import Extent from 'Core/Geographic/Extent';
77
import Label from 'Core/Label';
88
import { FEATURE_TYPES } from 'Core/Feature';
9+
import { readExpression } from 'Core/Style';
910

1011
const coord = new Coordinates('EPSG:4326', 0, 0, 0);
1112

@@ -33,6 +34,15 @@ class LabelLayer extends Layer {
3334
* contains three elements `name, protocol, extent`, these elements will be
3435
* available using `layer.name` or something else depending on the property
3536
* name.
37+
* @param {domElement|function} config.domElement - An HTML domElement.
38+
* If set, all `Label` displayed within the current instance `LabelLayer`
39+
* will be this domElement.
40+
*
41+
* It can be set to a method. The single parameter of this method gives the
42+
* properties of each feature on which a `Label` is created.
43+
*
44+
* If set, all the parameters set in the `LabelLayer` `Style.text` will be overridden,
45+
* except for the `Style.text.anchor` parameter which can help place the label.
3646
*/
3747
constructor(id, config = {}) {
3848
super(id, config);
@@ -45,6 +55,8 @@ class LabelLayer extends Layer {
4555
});
4656

4757
this.buildExtent = true;
58+
59+
this.labelDomelement = config.domElement;
4860
}
4961

5062
/**
@@ -95,7 +107,9 @@ class LabelLayer extends Layer {
95107
const geometryField = g.properties.style && g.properties.style.text.field;
96108
let content;
97109
const context = { globals, properties: () => g.properties };
98-
if (!geometryField && !featureField && !layerField) {
110+
if (this.labelDomelement) {
111+
content = readExpression(this.labelDomelement, context);
112+
} else if (!geometryField && !featureField && !layerField) {
99113
// Check if there is an icon, with no text
100114
if (!(g.properties.style && (g.properties.style.icon.source || g.properties.style.icon.key))
101115
&& !(f.style && (f.style.icon.source || f.style.icon.key))

0 commit comments

Comments
 (0)