Play with Labels
In this tutorial we will create the following app:
In order to keep it simple, we choose to use the basic browser integration
Display the Map with floor interactions
Follow the following steps of Learn the Basics:
Label Edition
First of all let's create the label selection / unselect functions:
let currentLabelObject = null;
function edit(labelObject) {
if (currentLabelObject === labelObject) {
return;
}
if (currentLabelObject !== null) {
currentLabelObject.unselect();
currentLabelObject = null;
}
if (labelObject !== null) {
labelObject.select();
}
currentLabelObject = labelObject;
}
Then let's create a function that will retrieve the label given a mouse click event:
function getClickedLabel(intersects) {
if (intersects.length === 0) {
return null;
}
// Let's grab the first intersect
let { object } = intersects[0];
if (object.isLabel) {
return object;
}
// If building or space, then let's select the first label if any
if (object.isBuilding || object.isSpace) {
// labels is a Set, let's convert to Array
const labels = Array.from(object.labels.values());
return labels.length === 0 ? null : labels[0];
}
return null;
}
Finally let's just register the event listener, inside the onStart
function:
function onStart() {
// ...
// Let's add the event listener
adsumWebMap.mouseManager.addEventListener(
AdsumWebMap.MOUSE_EVENTS.click,
(event) => {
edit(getClickedLabel(event.intersects));
}
);
}
We need to add the ui for updating properties. In order to simplify, let's expose the gui
variable
in the upper scope.
let gui = null;
function onStart() {
loadingDiv.classList.add('hidden');
gui = new dat.GUI();
const floorControls = {
// The current floor label
currentFloor: "none",
// A map containing the display name to floor id
floorNameToId: {
"none": null,
},
// Handle User floor change
changeFloor: (floorName) => {
const floorID = floorControls.floorNameToId[floorName];
const floorObject = floorID === null ? null : adsumWebMap.objectManager.floors.get(floorID);
// First change the floor
return adsumWebMap.sceneManager.setCurrentFloor(floorObject)
.then(() => {
// Don't forget to center the camera on floor !
return adsumWebMap.cameraManager.centerOnFloor(floorObject);
});
}
};
// Get all the floors to aliment the floor select list
adsumWebMap.objectManager.floors.forEach((floorObject) => {
floorControls.floorNameToId[String(floorObject.id)] = floorObject.id;
});
// Update the floor controls as depending of the device, the default floor is not always none.
const currentFloorObject = adsumWebMap.sceneManager.currentFloor;
floorControls.currentFloor = currentFloorObject === null ? "none" : String(currentFloorObject.id);
gui.add(
floorControls, // The object containing the data
'currentFloor', // The property to update
Object.keys(floorControls.floorNameToId), // List of choices
)
.listen() // listen currentFloor changes
.onChange(floorControls.changeFloor);
// Let's add the event listener
adsumWebMap.mouseManager.addEventListener(
AdsumWebMap.MOUSE_EVENTS.click,
(event) => {
edit(getClickedLabel(event.intersects));
}
);
}
Now we can create the UI each time a label is clicked.
let currentLabelObject = null;
let propertiesUi = null;
function edit(labelObject) {
if (currentLabelObject === labelObject) {
return;
}
if (currentLabelObject !== null) {
currentLabelObject.unselect();
currentLabelObject = null;
// Remove the UI on unselect
gui.removeFolder(propertiesUi);
propertiesUi = null;
}
if (labelObject !== null) {
labelObject.select();
// Create the label edition UI
propertiesUi = gui.addFolder('Properties');
if (labelObject.isLabelText) {
createLabelTextUi(propertiesUi, labelObject);
} else {
createLabelImageUi(propertiesUi, labelObject);
}
propertiesUi.open();
}
currentLabelObject = labelObject;
}
Then let's create the UI for LabelText & LabelImage:
function createLabelObjectBaseUi(ui, labelObject) {
ui
.add({autoScale: labelObject.autoScale}, 'autoScale')
.onChange((autoScale) => {
labelObject.setAutoScale(autoScale);
});
ui
.add({isPermanentDisplay: labelObject.isPermanentDisplay}, 'isPermanentDisplay')
.onChange((isPermanentDisplay) => {
labelObject.setPermanentDisplay(isPermanentDisplay);
});
ui
.add(
{orientationMode: labelObject.orientationMode},
'orientationMode',
[
AdsumWebMap.LABEL_ORIENTATION_MODES.BILLBOARD,
AdsumWebMap.LABEL_ORIENTATION_MODES.STATIC,
],
)
.onChange((orientationMode) => {
labelObject.setOrientationMode(orientationMode);
});
ui
.add({offsetX: labelObject.offset.x}, 'offsetX')
.onChange((offsetX) => {
labelObject.moveTo(
offsetX,
labelObject.offset.y,
labelObject.offset.z,
);
});
ui
.add({offsetY: labelObject.offset.y}, 'offsetY')
.onChange((offsetY) => {
labelObject.moveTo(
labelObject.offset.x,
offsetY,
labelObject.offset.z,
);
});
ui
.add({offsetZ: labelObject.offset.z}, 'offsetZ')
.onChange((offsetZ) => {
labelObject.moveTo(
labelObject.offset.x,
labelObject.offset.y,
offsetZ,
);
});
ui
.add(
{opacity: labelObject.opacity},
'opacity',
0,
1,
0.1,
)
.onChange((opacity) => {
labelObject.setOpacity(opacity);
});
ui
.add(
{rotation: labelObject.rotation},
'rotation',
0,
360,
1,
)
.onChange((rotation) => {
labelObject.setRotation(rotation);
});
ui
.add({scaleX: labelObject.scale.x}, 'scaleX')
.onChange((scaleX) => {
labelObject.setScale(
scaleX,
labelObject.scale.y,
labelObject.scale.z,
);
});
ui
.add({scaleY: labelObject.scale.y}, 'scaleY')
.onChange((scaleY) => {
labelObject.setScale(
labelObject.scale.x,
scaleY,
labelObject.scale.z,
);
});
ui
.add({scaleZ: labelObject.scale.z}, 'scaleZ')
.onChange((scaleZ) => {
labelObject.setScale(
labelObject.scale.x,
labelObject.scale.y,
scaleZ,
);
});
}
function createLabelImageUi(ui, labelObject) {
createLabelObjectBaseUi(ui, labelObject);
ui
.add({image: labelObject.image}, 'image')
.onChange((image) => {
labelObject.setImage(image);
});
ui
.add({height: labelObject.height}, 'height')
.onChange((height) => {
labelObject.setHeight(height);
});
ui
.add({width: labelObject.width}, 'width')
.onChange((width) => {
labelObject.setWidth(width);
});
}
function createLabelTextUi(ui, labelObject) {
createLabelObjectBaseUi(ui, labelObject);
ui
.add({text: labelObject.text.replace('\n', '\\n')}, 'text')
.onChange((text) => {
labelObject.setText(text.replace('\\n', '\n'));
});
ui
.add({font: labelObject.style.font}, 'font')
.onChange((font) => {
labelObject.setStyle({font});
});
ui
.add({size: labelObject.style.size}, 'size')
.onChange((size) => {
labelObject.setStyle({size});
});
ui
.addColor({color: labelObject.style.color}, 'color')
.onChange((color) => {
labelObject.setStyle({color});
});
ui
.add({lineHeight: labelObject.style.lineHeight}, 'lineHeight')
.onChange((lineHeight) => {
labelObject.setStyle({lineHeight});
});
ui
.addColor({backgroundColor: labelObject.style.backgroundColor}, 'backgroundColor')
.onChange((backgroundColor) => {
labelObject.setStyle({backgroundColor});
});
ui
.add(
{backgroundOpacity: labelObject.style.backgroundOpacity},
'backgroundOpacity',
0,
1,
)
.onChange((backgroundOpacity) => {
labelObject.setStyle({backgroundOpacity});
});
ui
.add({backgroundPadding: labelObject.style.backgroundPadding}, 'backgroundPadding')
.onChange((backgroundPadding) => {
labelObject.setStyle({backgroundPadding});
});
ui
.add({backgroundRadius: labelObject.style.backgroundRadius}, 'backgroundRadius')
.onChange((backgroundRadius) => {
labelObject.setStyle({backgroundRadius});
});
ui
.add({quality: labelObject.style.quality}, 'quality')
.onChange((quality) => {
labelObject.setStyle({quality});
});
return ui;
}
Create a label text
We want to be able to create a label text on click, so let's modify the current code in order to add a selection mode.
const behaviorControls = { onClick: 'edit' };
let gui = null;
function onStart() {
loadingDiv.classList.add('hidden');
gui = new dat.GUI();
const floorControls = {
// The current floor label
currentFloor: "none",
// A map containing the display name to floor id
floorNameToId: {
"none": null,
},
// Handle User floor change
changeFloor: (floorName) => {
const floorID = floorControls.floorNameToId[floorName];
const floorObject = floorID === null ? null : adsumWebMap.objectManager.floors.get(floorID);
// First change the floor
return adsumWebMap.sceneManager.setCurrentFloor(floorObject)
.then(() => {
// Don't forget to center the camera on floor !
return adsumWebMap.cameraManager.centerOnFloor(floorObject);
});
}
};
// Get all the floors to aliment the floor select list
adsumWebMap.objectManager.floors.forEach((floorObject) => {
floorControls.floorNameToId[String(floorObject.id)] = floorObject.id;
});
// Update the floor controls as depending of the device, the default floor is not always none.
const currentFloorObject = adsumWebMap.sceneManager.currentFloor;
floorControls.currentFloor = currentFloorObject === null ? "none" : String(currentFloorObject.id);
gui.add(
floorControls, // The object containing the data
'currentFloor', // The property to update
Object.keys(floorControls.floorNameToId), // List of choices
)
.listen() // listen currentFloor changes
.onChange(floorControls.changeFloor);
// Add the onClick behavior selection
gui.add(behaviorControls, 'onClick', ['edit', 'createText', 'createImage'])
.onFinishChange(() => {
// Make sure to reset the edition
edit(null);
});
// Let's add the event listener
adsumWebMap.mouseManager.addEventListener(
AdsumWebMap.MOUSE_EVENTS.click,
(event) => {
switch(behaviorControls.onClick) {
case 'edit':
edit(getClickedLabel(event.intersects));
break;
case 'createText':
createLabelText(event);
break;
case 'createImage':
createLabelImage(event);
break;
}
}
);
}
And let's add createLabelText
& createLabelImage
:
function createLabelText(mouseEvent) {
if (mouseEvent.intersects.length === 0) {
return;
}
let {object, position} = mouseEvent.intersects[0];
if (object.isSite || object.isBuilding || object.isFloor || object.isSpace) {
const label = new AdsumWebMap.LabelTextObject({
text: 'Hello world !\nThis is a multi-line one.',
offset: position
});
adsumWebMap.objectManager.addLabel(label, object);
}
}
function createLabelImage(mouseEvent) {
if (mouseEvent.intersects.length === 0) {
return;
}
let {object, position} = mouseEvent.intersects[0];
if (object.isSite || object.isBuilding || object.isFloor || object.isSpace) {
const label = new AdsumWebMap.LabelImageObject({
image: '/files/homer-simpson.png', width: 60, height: 60, offset: position,
});
adsumWebMap.objectManager.addLabel(label, object);
}
}
Delete a Label
Let's add a new case on the behavior selector:
// Add the onClick behavior selection
gui.add(behaviorControls, 'onClick', ['edit', 'createText', 'createImage', 'delete'])
.onFinishChange(() => {
// Make sure to reset the edition
edit(null);
});
// Let's add the event listener
adsumWebMap.mouseManager.addEventListener(
AdsumWebMap.MOUSE_EVENTS.click,
(event) => {
switch (behaviorControls.onClick) {
case 'edit':
edit(getClickedLabel(event.intersects));
break;
case 'createText':
createLabelText(event);
break;
case 'createImage':
createLabelImage(event);
break;
case 'delete':
deleteLabel(event);
break;
}
}
);
And create the deleteLabel
:
function deleteLabel(mouseEvent) {
if (mouseEvent.intersects.length === 0) {
return;
}
let {object} = mouseEvent.intersects[0];
if (object.isLabel) {
adsumWebMap.objectManager.removeLabel(object);
} else if (object.isBuilding || object.isSpace) {
object.labels.forEach((label) => {
adsumWebMap.objectManager.removeLabel(label);
});
}
}
Level Of Details
Description
Level of details is the feature that change the visibility of the labels depending on the distance to the camera.
A LevelOfDetails is composed by several LevelStateInterface which are applied to the object at a certain distance.
For example, if we want our label to be visible at low distance (0 to 100 meters), then invisible at intermediate distance (100 to 200 meters) and visible at high distances (over 200 meters) we will do the following levels:
- 0: DisplayLevelState with DISPLAY_MODES.VISIBLE
- 100: DisplayLevelState with DISPLAY_MODES.NONE
- 200: DisplayLevelState with DISPLAY_MODES.VISIBLE
Note: if a label is selected, then the visibility will overwrite the one defined by the level of details.
See LabelVisibility for more details.
Let's improve our example by adding a way to edit LabelObject#levelOfDetails
Implementation
Let's update the index.html
file to add our table lod-ui
inside the wrapper container to edit the level of details:
<!DOCTYPE html>
<html>
<head>
<link href="style.css" rel="stylesheet" />
</head>
<body>
<div id="wrapper">
<img id="loading" src="../../files/loading.gif"/>
<img id="error" class="hidden" src="../../files/error.png"/>
<div id="adsum-web-map-container"></div>
<div id="lod-ui" class="hidden">
<table class="table-fill">
<thead>
<tr>
<th>Distance</th>
<th>Display Mode</th>
<th>Action</th>
</tr>
</thead>
<tbody class="table-hover" id="lod-list">
<tr>
<td>0</td>
<td>visible</td>
<td><button>REMOVE</button></td>
</tr>
<tr>
<td>10</td>
<td>none</td>
<td><button>REMOVE</button></td>
</tr>
</tbody>
<tfoot>
<tr>
<td><input id="startAt" type="number" min="0" /></td>
<td>
<select id="displayMode">
<option value="none" selected>none</option>
<option value="visible">visible</option>
</select>
</td>
<td><button id="lod-add-action">ADD</button></td>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- Require the dependencies -->
<script src="../../node_modules/@adactive/adsum-client-api/build/adsum-client-api.browser.js"></script>
<script src="../../node_modules/@adactive/adsum-web-map/build/adsum-web-map.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.0/dat.gui.js"></script>
<!-- Let's add the script -->
<script src="start.js"></script>
</body>
</html>
Add a little Css customization into style.css
:
#lod-ui {
position: absolute;
top: 20px;
left: 20px;
z-index: 5;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
animation: float 5s infinite;
}
.table-fill {
background: white;
border-radius:3px;
border-collapse: collapse;
padding:5px;
}
th {
color:#D5DDE5;;
background:#1b1e24;
border-bottom:4px solid #9ea7af;
border-right: 1px solid #343a45;
font-size:13px;
font-weight: 100;
padding:14px;
text-align:left;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
vertical-align:middle;
}
th:first-child {
border-top-left-radius:3px;
}
th:last-child {
border-top-right-radius:3px;
border-right:none;
}
tr {
border-top: 1px solid #C1C3D1;
border-bottom-: 1px solid #C1C3D1;
color:#666B85;
font-size:11px;
font-weight:normal;
text-shadow: 0 1px 1px rgba(256, 256, 256, 0.1);
}
tr:hover td {
background:#4E5066;
color:#FFFFFF;
border-top: 1px solid #22262e;
}
tr:first-child {
border-top:none;
}
tr:last-child {
border-bottom:none;
}
tr:nth-child(odd) td {
background:#EBEBEB;
}
tr:nth-child(odd):hover td {
background:#4E5066;
}
tr:last-child td:first-child {
border-bottom-left-radius:3px;
}
tr:last-child td:last-child {
border-bottom-right-radius:3px;
}
td {
background:#FFFFFF;
padding:10px;
text-align:left;
vertical-align:middle;
font-weight:300;
font-size:11px;
text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.1);
border-right: 1px solid #C1C3D1;
}
td:last-child {
border-right: 0px;
}
Now we need to add the level of details UI when editing:
function updateLevelOfDetailsUi() {
const lodUi = document.getElementById('lod-ui');
if (currentLabelObject === null) {
lodUi.classList.add('hidden');
} else {
lodUi.classList.remove('hidden');
const levelStates = currentLabelObject.levelOfDetails.getLevelStates();
const levelsUi = document.getElementById('lod-list');
levelsUi.innerHTML = '';
levelStates.forEach(({ startAt, levelState }) => {
const row = document.createElement('tr');
const key = document.createElement('td');
key.innerText = String(startAt);
row.appendChild(key);
const value = document.createElement('td');
value.innerText = levelState.displayMode;
row.appendChild(value);
const removeCell = document.createElement('td');
const removeBtn = document.createElement('button');
removeBtn.innerText = "Remove";
removeBtn.onclick = () => {
currentLabelObject.levelOfDetails.removeLevelState(startAt);
updateLevelOfDetailsUi();
};
removeCell.appendChild(removeBtn);
row.appendChild(removeCell);
levelsUi.appendChild(row);
});
}
}
function addLevelState() {
if (currentLabelObject !== null) {
const startAt = parseFloat(document.getElementById('startAt').value);
const displayMode = document.getElementById('displayMode').value;
currentLabelObject.levelOfDetails.addLevelState(startAt, new AdsumWebMap.DisplayLevelState(displayMode));
updateLevelOfDetailsUi();
}
}
document.getElementById('lod-add-action').onclick = addLevelState;
Finally we need to update edit function in order to display the level of details ui.
function edit(labelObject) {
if (currentLabelObject === labelObject) {
return;
}
if (currentLabelObject !== null) {
currentLabelObject.unselect();
currentLabelObject = null;
// Remove the UI on unselect
gui.removeFolder(propertiesUi);
propertiesUi = null;
}
if (labelObject !== null) {
labelObject.select();
// Create the label edition UI
propertiesUi = gui.addFolder('Properties');
if (labelObject.isLabelText) {
createLabelTextUi(propertiesUi, labelObject);
} else {
createLabelImageUi(propertiesUi, labelObject);
}
propertiesUi.open();
}
currentLabelObject = labelObject;
updateLevelOfDetailsUi();
}
Note: label selection take priority over level of details. So we editing the level states, you will need to unselect in order to show the changes over level of details.
You can find complete code here