src/plugins/AnnotationsPlugin/Annotation.js
import {math} from '../../viewer/scene/math/math.js';
import {Marker} from "../../viewer/scene/marker/Marker.js";
import {utils} from "../../viewer/scene/utils.js";
const tempVec3a = math.vec3();
const tempVec3b = math.vec3();
const tempVec3c = math.vec3();
/**
* A {@link Marker} with an HTML label attached to it, managed by an {@link AnnotationsPlugin}.
*
* See {@link AnnotationsPlugin} for more info.
*/
class Annotation extends Marker {
/**
* @private
*/
constructor(owner, cfg) {
super(owner, cfg);
/**
* The {@link AnnotationsPlugin} this Annotation was created by.
* @type {AnnotationsPlugin}
*/
this.plugin = cfg.plugin;
this._container = cfg.container;
if (!this._container) {
throw "config missing: container";
}
if ((!cfg.markerElement) && (!cfg.markerHTML)) {
throw "config missing: need either markerElement or markerHTML";
}
if ((!cfg.labelElement) && (!cfg.labelHTML)) {
throw "config missing: need either labelElement or labelHTML";
}
this._htmlDirty = false;
if (cfg.markerElement) {
this._marker = cfg.markerElement;
this._marker.addEventListener("click", this._onMouseClickedExternalMarker = () => {
this.plugin.fire("markerClicked", this);
});
this._marker.addEventListener("contextmenu", this._onContextMenuExtenalMarker = () => {
this.plugin.fire("contextmenu", this);
});
this._marker.addEventListener("mouseenter", this._onMouseEnterExternalMarker = () => {
this.plugin.fire("markerMouseEnter", this);
});
this._marker.addEventListener("mouseleave", this._onMouseLeaveExternalMarker = () => {
this.plugin.fire("markerMouseLeave", this);
});
this._markerExternal = true; // Don't destroy marker when destroying Annotation
} else {
this._markerHTML = cfg.markerHTML;
this._htmlDirty = true;
this._markerExternal = false;
}
if (cfg.labelElement) {
this._label = cfg.labelElement;
this._labelExternal = true; // Don't destroy marker when destroying Annotation
} else {
this._labelHTML = cfg.labelHTML;
this._htmlDirty = true;
this._labelExternal = false;
}
this._markerShown = !!cfg.markerShown;
this._labelShown = !!cfg.labelShown;
this._values = cfg.values || {};
this._layoutDirty = true;
this._visibilityDirty = true;
this._labelPosition = 24;
this._buildHTML();
this._onTick = this.scene.on("tick", () => {
if (this._htmlDirty) {
this._buildHTML();
this._htmlDirty = false;
this._layoutDirty = true;
this._visibilityDirty = true;
}
if (this._layoutDirty || this._visibilityDirty) {
if (this._markerShown || this._labelShown) {
this._updatePosition();
this._layoutDirty = false;
}
}
if (this._visibilityDirty) {
this._marker.style.visibility = (this.visible && this._markerShown) ? "visible" : "hidden";
this._label.style.visibility = (this.visible && this._markerShown && this._labelShown) ? "visible" : "hidden";
this._visibilityDirty = false;
}
});
this.on("canvasPos", () => {
this._layoutDirty = true;
});
this.on("visible", () => {
this._visibilityDirty = true;
});
this.setMarkerShown(cfg.markerShown !== false);
this.setLabelShown(cfg.labelShown);
/**
* Optional World-space position for {@link Camera#eye}, used when this Annotation is associated with a {@link Camera} position.
*
* Undefined by default.
*
* @type {Number[]} Eye position.
*/
this.eye = cfg.eye ? cfg.eye.slice() : null;
/**
* Optional World-space position for {@link Camera#look}, used when this Annotation is associated with a {@link Camera} position.
*
* Undefined by default.
*
* @type {Number[]} The "look" vector.
*/
this.look = cfg.look ? cfg.look.slice() : null;
/**
* Optional World-space position for {@link Camera#up}, used when this Annotation is associated with a {@link Camera} position.
*
* Undefined by default.
*
* @type {Number[]} The "up" vector.
*/
this.up = cfg.up ? cfg.up.slice() : null;
/**
* Optional projection type for {@link Camera#projection}, used when this Annotation is associated with a {@link Camera} position.
*
* Undefined by default.
*
* @type {String} The projection type - "perspective" or "ortho"..
*/
this.projection = cfg.projection;
}
/**
* @private
*/
_buildHTML() {
if (!this._markerExternal) {
if (this._marker) {
this._container.removeChild(this._marker);
this._marker = null;
}
let markerHTML = this._markerHTML || "<p></p>"; // Make marker
if (utils.isArray(markerHTML)) {
markerHTML = markerHTML.join("");
}
markerHTML = this._renderTemplate(markerHTML.trim());
const markerFragment = document.createRange().createContextualFragment(markerHTML);
this._marker = markerFragment.firstChild;
this._container.appendChild(this._marker);
this._marker.style.visibility = this._markerShown ? "visible" : "hidden";
this._marker.addEventListener("click", () => {
this.plugin.fire("markerClicked", this);
});
this._marker.addEventListener("contextmenu", e => {
e.preventDefault();
this.plugin.fire("contextmenu", this);
});
this._marker.addEventListener("mouseenter", () => {
this.plugin.fire("markerMouseEnter", this);
});
this._marker.addEventListener("mouseleave", () => {
this.plugin.fire("markerMouseLeave", this);
});
this._marker.addEventListener('wheel', (event) => {
this.plugin.viewer.scene.canvas.canvas.dispatchEvent(new WheelEvent('wheel', event));
});
}
if (!this._labelExternal) {
if (this._label) {
this._container.removeChild(this._label);
this._label = null;
}
let labelHTML = this._labelHTML || "<p></p>"; // Make label
if (utils.isArray(labelHTML)) {
labelHTML = labelHTML.join("");
}
labelHTML = this._renderTemplate(labelHTML.trim());
const labelFragment = document.createRange().createContextualFragment(labelHTML);
this._label = labelFragment.firstChild;
this._container.appendChild(this._label);
this._label.style.visibility = (this._markerShown && this._labelShown) ? "visible" : "hidden";
this._label.addEventListener('wheel', (event) => {
this.plugin.viewer.scene.canvas.canvas.dispatchEvent(new WheelEvent('wheel', event));
});
}
}
/**
* @private
*/
_updatePosition() {
const px = x => x + "px";
const boundary = this.scene.canvas.boundary;
const left = boundary[0] + this.canvasPos[0];
const top = boundary[1] + this.canvasPos[1];
const markerRect = this._marker.getBoundingClientRect();
const markerWidth = markerRect.width;
const markerDir = (this._markerAlign === "right") ? -1 : ((this._markerAlign === "center") ? 0 : 1);
const markerCenter = left + markerDir * (markerWidth / 2 - 12);
this._marker.style.left = px(markerCenter - markerWidth / 2);
this._marker.style.top = px(top - 12);
this._marker.style["z-index"] = 90005 + Math.floor(this._viewPos[2]) + 1;
const labelRect = this._label.getBoundingClientRect();
const labelWidth = labelRect.width;
const labelDir = Math.sign(this._labelPosition);
this._label.style.left = px(markerCenter + labelDir * (markerWidth / 2 + Math.abs(this._labelPosition) + labelWidth / 2) - labelWidth / 2);
this._label.style.top = px(top - 17);
this._label.style["z-index"] = 90005 + Math.floor(this._viewPos[2]) + 1;
}
/**
* @private
*/
_renderTemplate(template) {
for (var key in this._values) {
if (this._values.hasOwnProperty(key)) {
const value = this._values[key];
template = template.replace(new RegExp('{{' + key + '}}', 'g'), value);
}
}
return template;
}
/**
* Sets the Marker's worldPos and entity properties based on passed {@link PickResult}
*
* @param {PickResult} pickResult A PickResult to position the Marker at.
*/
setFromPickResult(pickResult) {
if (!pickResult.worldPos || !pickResult.worldNormal) {
this.error("Param 'pickResult' does not have both worldPos and worldNormal");
} else {
const normalizedWorldNormal = math.normalizeVec3(pickResult.worldNormal, tempVec3a);
const offset = (this.plugin && this.plugin.surfaceOffset) || 0;
const offsetVec = math.mulVec3Scalar(normalizedWorldNormal, offset, tempVec3b);
const offsetWorldPos = math.addVec3(pickResult.worldPos, offsetVec, tempVec3c);
this.entity = pickResult.entity;
this.worldPos = offsetWorldPos;
}
}
/**
* Sets the horizontal alignment of the Annotation's marker HTML.
*
* @param {String} align Either "left", "center", "right" (default "left")
*/
setMarkerAlign(align) {
const valid = [ "left", "center", "right" ];
if (! valid.includes(align)) {
this.error("Param 'align' should be one of: " + JSON.stringify(valid));
} else {
this._markerAlign = align;
this._updatePosition();
}
}
/**
* Sets the relative horizontal position of the Annotation's label HTML.
*
* @param {Number} position Negative - to the left, positive - to the right, otherwise ignore (default 24)
*/
setLabelPosition(position) {
if (typeof position !== "number") {
this.error("Param 'position' is not a number");
} else if (position === 0) {
this.error("Param 'position' is zero");
} else {
this._labelPosition = position;
this._updatePosition();
}
}
/**
* Sets whether or not to show this Annotation's marker.
*
* The marker shows the Annotation's position.
*
* The marker is only visible when both this property and {@link Annotation#visible} are ````true````.
*
* See {@link AnnotationsPlugin} for more info.
*
* @param {Boolean} shown Whether to show the marker.
*/
setMarkerShown(shown) {
shown = !!shown;
if (this._markerShown === shown) {
return;
}
this._markerShown = shown;
this._visibilityDirty = true;
}
/**
* Gets whether or not to show this Annotation's marker.
*
* The marker shows the Annotation's position.
*
* The marker is only visible when both this property and {@link Annotation#visible} are ````true````.
*
* See {@link AnnotationsPlugin} for more info.
*
* @returns {Boolean} Whether to show the marker.
*/
getMarkerShown() {
return this._markerShown;
}
/**
* Sets whether or not to show this Annotation's label.
*
* The label is only visible when both this property and {@link Annotation#visible} are ````true````.
*
* See {@link AnnotationsPlugin} for more info.
*
* @param {Boolean} shown Whether to show the label.
*/
setLabelShown(shown) {
shown = !!shown;
if (this._labelShown === shown) {
return;
}
this._labelShown = shown;
this._visibilityDirty = true;
}
/**
* Gets whether or not to show this Annotation's label.
*
* The label is only visible when both this property and {@link Annotation#visible} are ````true````.
*
* See {@link AnnotationsPlugin} for more info.
*
* @returns {Boolean} Whether to show the label.
*/
getLabelShown() {
return this._labelShown;
}
/**
* Sets the value of a field within the HTML templates for either the Annotation's marker or label.
*
* See {@link AnnotationsPlugin} for more info.
*
* @param {String} key Identifies the field.
* @param {String} value The field's value.
*/
setField(key, value) {
this._values[key] = value || "";
this._htmlDirty = true;
}
/**
* Gets the value of a field within the HTML templates for either the Annotation's marker or label.
*
* See {@link AnnotationsPlugin} for more info.
*
* @param {String} key Identifies the field.
* @returns {String} The field's value.
*/
getField(key) {
return this._values[key];
}
/**
* Sets values for multiple placeholders within the Annotation's HTML templates for marker and label.
*
* See {@link AnnotationsPlugin} for more info.
*
* @param {{String:(String|Number)}} values Map of field values.
*/
setValues(values) {
for (var key in values) {
if (values.hasOwnProperty(key)) {
const value = values[key];
this.setField(key, value);
}
}
}
/**
* Gets the values that were set for the placeholders within this Annotation's HTML marker and label templates.
*
* See {@link AnnotationsPlugin} for more info.
*
* @RETURNS {{String:(String|Number)}} Map of field values.
*/
getValues() {
return this._values;
}
/**
* Destroys this Annotation.
*
* You can also call {@link AnnotationsPlugin#destroyAnnotation}.
*/
destroy() {
if (this._marker) {
if (!this._markerExternal) {
this._marker.parentNode.removeChild(this._marker);
this._marker = null;
} else {
this._marker.removeEventListener("click", this._onMouseClickedExternalMarker);
this._marker.removeEventListener("contextmenu", this._onContextMenuExtenalMarker);
this._marker.removeEventListener("mouseenter", this._onMouseEnterExternalMarker);
this._marker.removeEventListener("mouseleave", this._onMouseLeaveExternalMarker);
this._marker = null;
}
}
if (this._label) {
if (!this._labelExternal) {
this._label.parentNode.removeChild(this._label);
}
this._label = null;
}
this.scene.off(this._onTick);
super.destroy();
}
}
export {Annotation};