Markers

A Marker is a visual indicator whose position in a view follows a fixed location in world space. The name and the concept derive from the same type in the Google Maps api. Markers may also provide actions performed when the mouse moves over them, and when the are clicked.
Markers can display 3 optional parts:
- World decorations that are drawn into the scene with the z-buffer enabled.
- Canvas decorations that are drawn on top scene using CanvasRenderingContext2D calls
- HTML decorations that are HTMLElements on top of the view
Markers are often used to show locations in physical space where records from an external data source are located. They provide a way for applications to show additional information from the external source as the cursor hovers over them, and actions to be performed when they are clicked.
Sometimes Markers are used to show the location of elements within an iModel that are of interest. In that case the location of the Marker can be established from the origin, center, or perhaps other points derived from the element's properties.
MarkerSets
Often there will be many Markers relevant to show a group of points of interest. When the set of Markers becomes large, or when the user zooms far away from Marker locations, they tend to overlap one another and create clutter. For this purpose, the class MarkerSet provides a way to group sets of related Markers together such that overlapping groups of them form a Cluster. MarkerSet provides techniques for you to supply the graphics to visually indicate the set of Markers it represents.
Note: Only Markers from the same MarkerSet will be clustered. Independent Markers or Markers from different MarkerSets will not cluster.
The following example illustrates creating a marker set using random locations within the ProjectExtents:
class IncidentMarker extends Marker {
private static _size = Point2d.create(30, 30);
private static _imageSize = Point2d.create(40, 40);
private static _imageOffset = Point2d.create(0, 30);
private static _amber = ColorDef.create(ColorByName.amber);
private static _sweep360 = AngleSweep.create360();
private _color: ColorDef;
public static makeColor(severity: number): ColorDef {
return (severity <= 16 ? ColorDef.green.lerp(this._amber, (severity - 1) / 15.) :
this._amber.lerp(ColorDef.red, (severity - 16) / 14.));
}
public override onMouseButton(ev: BeButtonEvent): boolean {
if (ev.button === BeButton.Data) {
if (ev.isDown) {
IModelApp.notifications.openMessageBox(MessageBoxType.LargeOk, `severity = ${this.severity}`, MessageBoxIconType.Information);
}
}
return true;
}
constructor(location: XYAndZ, public severity: number, public id: number, icon: HTMLImageElement) {
super(location, IncidentMarker._size);
this._color = IncidentMarker.makeColor(severity);
this.setImage(icon);
this.imageOffset = IncidentMarker._imageOffset;
this.imageSize = IncidentMarker._imageSize;
this.title = `Severity: ${severity}<br>Id: ${id}`;
this.setScaleFactor({ low: .2, high: 1.4 });
this.htmlElement = document.createElement("div");
this.htmlElement.innerHTML = id.toString();
}
public override addMarker(context: DecorateContext) {
super.addMarker(context);
const builder = context.createGraphicBuilder(GraphicType.WorldDecoration);
const ellipse = Arc3d.createScaledXYColumns(this.worldLocation, context.viewport.rotation.transpose(), .2, .2, IncidentMarker._sweep360);
builder.setSymbology(ColorDef.white, this._color, 1);
builder.addArc(ellipse, false, false);
builder.setBlankingFill(this._color);
builder.addArc(ellipse, true, true);
context.addDecorationFromBuilder(builder);
}
}
class IncidentClusterMarker extends Marker {
private _clusterColor: string;
public override drawFunc(ctx: CanvasRenderingContext2D) {
ctx.beginPath();
ctx.strokeStyle = this._clusterColor;
ctx.fillStyle = "white";
ctx.lineWidth = 5;
ctx.arc(0, 0, 13, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
constructor(location: XYAndZ, size: XAndY, cluster: Cluster<IncidentMarker>, image: Promise<MarkerImage> | MarkerImage | undefined) {
super(location, size);
const sorted: IncidentMarker[] = [];
const maxLen = 10;
cluster.markers.forEach((marker) => {
if (maxLen > sorted.length || marker.severity > sorted[sorted.length - 1].severity) {
const index = sorted.findIndex((val) => val.severity < marker.severity);
if (index === -1)
sorted.push(marker);
else
sorted.splice(index, 0, marker);
if (sorted.length > maxLen)
sorted.length = maxLen;
}
});
this.imageOffset = new Point3d(0, 28);
this.imageSize = new Point2d(30, 30);
this.label = cluster.markers.length.toLocaleString();
this.labelColor = "black";
this.labelFont = "bold 14px sans-serif";
let title = "";
sorted.forEach((marker) => {
if (title !== "")
title += "<br>";
title += `Severity: ${marker.severity} Id: ${marker.id}`;
});
if (cluster.markers.length > maxLen)
title += "<br>...";
this.title = title;
this._clusterColor = IncidentMarker.makeColor(sorted[0].severity).toHexString();
if (image)
this.setImage(image);
}
}
class IncidentMarkerSet extends MarkerSet<IncidentMarker> {
protected getClusterMarker(cluster: Cluster<IncidentMarker>): Marker {
return new IncidentClusterMarker(cluster.getClusterLocation(), cluster.markers[0].size, cluster, IncidentMarkerDemo.decorator!.warningSign);
}
}
export class IncidentMarkerDemo {
private _awaiting = false;
private _loading?: Promise<any>;
private _images: Array<HTMLImageElement | undefined> = [];
private _incidents = new IncidentMarkerSet();
private static _numMarkers = 500;
public static decorator?: IncidentMarkerDemo;
public get warningSign() { return this._images[0]; }
private async loadOne(src: string) {
try {
return await imageElementFromUrl(src);
} catch (err) {
const msg = `Could not load image ${src}`;
Logger.logError("IncidentDemo", msg);
console.log(msg);
}
return undefined;
}
private async loadAll(extents: AxisAlignedBox3d) {
const loads = [
this.loadOne("Warning_sign.svg"),
this.loadOne("Hazard_biological.svg"),
this.loadOne("Hazard_electric.svg"),
this.loadOne("Hazard_flammable.svg"),
this.loadOne("Hazard_toxic.svg"),
this.loadOne("Hazard_tripping.svg"),
];
await (this._loading = Promise.all(loads));
for (const img of loads)
this._images.push(await img);
const len = this._images.length;
const pos = new Point3d();
for (let i = 0; i < IncidentMarkerDemo._numMarkers; ++i) {
pos.x = extents.low.x + (Math.random() * extents.xLength());
pos.y = extents.low.y + (Math.random() * extents.yLength());
pos.z = extents.low.z + (Math.random() * extents.zLength());
const img = this._images[(i % len) + 1];
if (undefined !== img)
this._incidents.markers.add(new IncidentMarker(pos, 1 + Math.round(Math.random() * 29), i, img));
}
this._loading = undefined;
}
public constructor(extents: AxisAlignedBox3d) {
this.loadAll(extents);
}
public decorate(context: DecorateContext) {
if (!context.viewport.view.isSpatialView())
return;
if (undefined === this._loading) {
this._incidents.addDecoration(context);
return;
}
if (!this._awaiting) {
this._awaiting = true;
this._loading.then(() => {
context.viewport.invalidateDecorations();
this._awaiting = false;
}).catch(() => undefined);
}
}
public static toggle(extents: AxisAlignedBox3d) {
if (undefined === IncidentMarkerDemo.decorator) {
IncidentMarkerDemo.decorator = new IncidentMarkerDemo(extents);
IModelApp.viewManager.addDecorator(IncidentMarkerDemo.decorator);
} else {
IModelApp.viewManager.dropDecorator(IncidentMarkerDemo.decorator);
IncidentMarkerDemo.decorator = undefined;
}
}
}
Last Updated:
02 February, 2022