JSON: Markup


A Markup is a JSON object stored inside a Bookmark record. It describes a set of 2D screen overlays and 3D world annotations that are rendered on top of the Cesium viewer when the bookmark is activated.

Markups are composed of Divs (HTML text boxes and shapes), Lines (SVG connector / callout lines), and Cesium objects (polygons, polylines, circles drawn in 3D space). The most commonly used elements are Divs and Callouts (a Div + Line pair).

Markup files use the file extension ".bmarkup" but are actually JSON text files. They can be exported/imported from the Markup interface in Navigator.

Top-level structure

The root object carries arrays for each element category and records the original screen dimensions the markup was authored against. The renderer scales elements proportionally when the viewer is a different size.

interface IMarkup {
    // Array of HTML div overlays (text boxes, shapes, callout boxes).
    divs?: IDiv[];
    // Array of SVG vector elements (lines and free-paint strokes).
    vectors?: (ILine | IPaint)[];
    // Array of 3D Cesium entities (polygons, polylines, circles, etc).
    cesiumObjects?: (IPolygon | IPolyline | ICircle | IWalkscore)[];
    // Original screen dimensions the markup was authored at.
    // The renderer uses these to scale elements to the current viewer size.
    width?: number;
    height?: number;
    // Default = true. Set to false to hide the entire markup.
    visible?: boolean;
}

Base element

Every markup element (divs, lines, cesium objects) extends a common base that carries an id and optional metadata. The id must be unique within the markup and is used for cross-referencing (e.g. a line snapping to a div).

interface IElement {
    // Unique id of the element within the markup.
    id: string;
    // Optional human-readable label shown in editing screens.
    name?: string;
    // Default = true.
    visible?: boolean;
    // Screen dimensions when this element was originally drawn.
    // Used with the markup-level width/height to calculate scaling.
    screenWidth?: number;
    screenHeight?: number;
}

Div

A Div is the primary building block. It renders as an absolutely-positioned HTML box on the viewer overlay. Divs are used for text boxes, info panels, image tiles, and callout labels.

Sizing

The div itself has a fixed pixel width and height. Inside the div, content is laid out as rows. Each row has a height expressed as a percentage of the div height (all row heights should add up to 100). Within a row, each cell has a width expressed as a percentage of the row width (all cell widths in a row should add up to 100). For example, two equal columns in a single row would each have width: 50.

Anchoring & positioning

The position field controls how the div is anchored to the screen. It determines which corner of the viewport the div's coordinates are measured from:

PositionCoordinatesDescription
TopLeftleft, topMeasured from the top-left corner. This is the default.
TopRightright, topMeasured from the top-right corner.
BottomLeftleft, bottomMeasured from the bottom-left corner.
BottomRightright, bottomMost commonly used anchor. The bottom-right area typically has the most available space. A typical offset is right: 120, bottom: 12.
"fixed"The div is not shifted by UI panel offsets (sidebars, toolbars).

When the viewport resizes, the renderer recalculates the div position relative to its anchor corner so it stays in the intended screen region. Divs anchored to BottomRight will stay pinned to the bottom-right regardless of window size.

Colours

All colour fields (backgroundColor, fontColor, borderColor, etc.) accept standard CSS colour strings, hex (#1d70e7), rgba(29,112,231,0.75), or named colours.

// Anchor side enum.
enum EAnchorDivSide {
    TopLeft = "TopLeft",
    BottomLeft = "BottomLeft",
    BottomRight = "BottomRight",
    TopRight = "TopRight"
}

interface IDiv extends IElement {
    // "default" for a regular div, "callout" for one anchored in 3D via a line.
    type?: "default" | "callout";
    // Content laid out as rows of cells (see IRow / IRowItem below).
    contentRows: IRow[];
    // CSS box shadow.
    boxShadow?: IBoxShadow;
    // Border radius in pixels (or percent if borderUsingPercent is true).
    borderRadius?: number;
    borderUsingPercent?: boolean;
    // CSS border colour string.
    borderColor?: string;
    borderWidth?: number;
    // Position offsets in pixels, interpreted relative to the anchor side.
    // Use the pair that matches your anchor: left/top, right/top, left/bottom, or right/bottom.
    left?: number;
    top?: number;
    right?: number;
    bottom?: number;
    // Fixed pixel dimensions of the div box.
    width: number;
    height: number;
    // Screen anchor. Determines which corner coordinates are relative to.
    // Default is "TopLeft". Most common for info panels is "BottomRight".
    position?: EAnchorDivSide | "fixed" | "absolute";
    // CSS colour strings for default text styling.
    backgroundColor?: string;
    fontColor?: string;
    fontSize?: number;
    // Maximum camera distance (meters) at which this div is visible.
    // Useful for callouts that should disappear when zoomed out.
    visibleMaxDistance?: number;
}

Row & cell

Div content is a list of rows. Each row contains cells. Row heights and cell widths are percentages that should each total 100 within their parent.

interface IRow {
    // Cells within this row.
    items: IRowItem[];
    // Height of the row as a percentage of the div height. All rows should sum to 100.
    height: number;
}

// Cell content type determines rendering behaviour.
enum EItemType {
    Default = "DEFAULT",   // Plain text content.
    Embed = "EMBED",       // Raw HTML content via embedRaw.
    Icon = "ICON",         // Font-awesome icon via icon field.
    Image = "IMAGE",       // Background image.
    None = "NONE"          // No content, shape only.
}

interface IRowItem {
    // Plain text to display (auto-cleaned).
    text?: string;
    // Raw HTML when contentType is "EMBED".
    embedRaw?: string;
    // Font-awesome icon class when contentType is "ICON".
    icon?: string;
    // Action to perform when this cell is clicked.
    action?: IItemAction;
    // Background image for this cell.
    bgImage?: { FileID?: string; };
    // CSS colour strings.
    backgroundColor?: string;
    fontColor?: string;
    fontSize?: number;
    // CSS text alignment.
    textAlign?: string;
    verticalAlign?: string;
    // Padding. Prefer innerPadding (CSS shorthand) over the deprecated numeric padding.
    innerPadding?: string;
    padding?: number;
    textShadow?: ITextShadow;
    // Content type. Defaults to "DEFAULT".
    contentType?: EItemType;
    // Width of this cell as a percentage of the row width.
    // All cells in a row should sum to 100.
    width?: number;
}

Callout

A Callout is not a separate element type, it is a Div with type: "callout" paired with a Line whose end-snap references the div's id. The line's start-snap holds a 3D world position (the anchor point in the scene), and its end-snap attaches to a side of the div. This is what makes the callout "float" in 3D space, the line projects from a real-world coordinate to the screen-space div.

Common defaults: the line is typically white with brushSize: 3 and angled: true. The div usually has a semi-transparent background like rgba(29,112,231,0.75) with white text.

// Example: a callout attached to a 3D position.
// 1. Create the div with type "callout".
const calloutDiv: IDiv = {
    id: "callout-1",
    type: "callout",
    width: 160,
    height: 40,
    contentRows: [{
        height: 100,
        items: [{
            text: "Pump Station A",
            width: 100,
            backgroundColor: "rgba(29,112,231,0.75)",
            fontColor: "white",
            fontSize: 13,
            textAlign: "center",
            verticalAlign: "center"
        }]
    }],
    borderRadius: 8,
    borderColor: "white",
    borderWidth: 1
};

// 2. Create a line whose end-snap references the div.
const calloutLine: ILine = {
    id: "callout-line-1",
    pathType: "LINE",
    path: null,
    transform: null,
    brushColor: "rgba(255,255,255,0.75)",
    brushSize: 3,
    angled: true,
    snaps: [
        {
            // Start: anchored to a 3D world position.
            // lat/lon are in degrees, altitude is absolute height in meters.
            side: 0,   // EAnchorLineSide.Start
            position: { latitude: 40.7128, longitude: -74.0060, altitude: 0 }
        },
        {
            // End: attached to the left edge of the callout div.
            side: 1,   // EAnchorLineSide.End
            id: "callout-1",
            snapPosition: 5   // EAnchorDivSide.CenterLeft
        }
    ]
};

Line

A Line is an SVG-based vector rendered on the 2D overlay. Lines can connect two screen positions, two 3D world positions, or snap to divs. When a snap references a div id, the line end follows that div as it moves.

enum ELineHead {
    None = "NONE",
    Circle = "CIRCLE",
    Arrow = "ARROW"
}

interface ILine extends IElement {
    pathType: "LINE";
    // SVG path string (auto-generated when using snaps).
    path: string;
    // SVG transform string.
    transform: string;
    // Snap points for each end of the line.
    snaps: IAnchor[];
    // CSS colour of the line.
    brushColor?: string;
    // Stroke width in pixels.
    brushSize?: number;
    // If true, the line angles towards its destination instead of going straight.
    angled?: boolean;
    // Line-head style (arrow, circle, or none).
    brushHead?: ELineHead;
    // CSS colour of the outline.
    outlineColor?: string;
}

// Determines which side of a div an anchor snaps to.
enum EAnchorDivSide {
    TopLeft = 0,
    TopRight = 1,
    BottomLeft = 2,
    BottomRight = 3,
    Center = 4,
    CenterLeft = 5,
    CenterRight = 6,
    CenterTop = 7,
    CenterBottom = 8
}

enum EAnchorLineSide {
    Start = 0,
    End = 1
}

interface IAnchor {
    // Id of a markup Div this anchor is attached to (makes the line follow the div).
    id?: string;
    // Which side of the div to snap to.
    snapPosition?: EAnchorDivSide;
    // Which end of the line this anchor represents.
    side?: EAnchorLineSide;
    // Visual head style at this end.
    brushHead?: ELineHead;
    // CSS fill colour.
    fill?: string;
    // Size of the head (arrow/circle).
    size?: number;
    // CSS stroke colour.
    stroke?: string;
    // 3D world position (Cartesian3 or Cartographic in degrees).
    position?: { x: number; y: number; z: number };
    // 2D screen position.
    screenPos?: { x: number; y: number };
    // If true, position is relative to the markup's parent entity.
    positionFromEntity?: boolean;
}

Free-paint

A free-form SVG stroke drawn on the overlay. Unlike lines, free-paints do not have snap anchors.

interface IPaint extends IElement {
    pathType: "PLAIN";
    // SVG path string.
    path: string;
    transform: string;
    brushColor?: string;
    brushSize?: number;
    brushHead?: "NONE" | "CIRCLE" | "ARROW";
    outlineColor?: string;
}

Polygon

A filled polygon rendered in 3D world space via Cesium. Positions are Cartesian3 or Cartographic (degrees) coordinates.

interface IPolygon extends IElement {
    type: "POLYGON";
    // Array of 3D positions defining the polygon boundary.
    positions: { x: number; y: number; z: number }[];
    // CSS colour of the fill.
    color?: string;
    // CSS colour of the outline.
    outlineColor?: string;
    // Outline width in pixels.
    outlineWidth?: number;
    // Extrusion height in meters (0 for flat).
    extrudedHeight?: number;
    // "BOTH" overlaps terrain and 3D models. "TERRAIN" overlaps terrain only.
    classificationType?: "BOTH" | "TERRAIN";
    // Number of Chaikin smoothing passes. 0 = no smoothing.
    smoothen?: number;
}

Polyline

A line rendered in 3D world space via Cesium. Similar to polygon but renders as a stroke.

interface IPolyline extends IElement {
    type: "POLYLINE";
    positions: { x: number; y: number; z: number }[];
    color?: string;
    // Width in pixels.
    outlineWidth?: number;
    smoothen?: number;
}

Circle

A filled circle rendered in 3D world space via Cesium.

interface ICircle extends IElement {
    type: "CIRCLE";
    // Center position in 3D world space.
    position: { x: number; y: number; z: number };
    // Radius in meters.
    radius: number;
    color?: string;
    // Outline width in meters.
    outlineWidth?: number;
    // Extrusion height in meters.
    extrudedHeight?: number;
    outlineExtrusion?: number;
    outlineColor?: string;
}

Cell actions

Each cell in a div row can have an optional action that fires when clicked.

enum EActionType {
    None = "NONE",
    // Open a URL. Can open in new tab.
    OpenLink = "LINK",
    // Allow this cell to be used as a drag handle for the div.
    Draggable = "DRAGABBLE",
    // Close the markup.
    Close = "CLOSE",
    // Activate another Bookmark in the same Project View.
    OpenBookmark = "BOOKMARK"
}

interface IItemAction {
    type: EActionType;
    params: IActionLinkParams | IActionBookmarkParams | {};
}

interface IActionLinkParams {
    URL?: string;
    openInNewTab?: boolean;
    closeMarkupOnOpen?: boolean;
}

interface IActionBookmarkParams {
    bookmarkID?: string;
    // Styling applied to the cell when the target Bookmark is active.
    activeBackgroundColor?: string;
    activeFontColor?: string;
}

Example: Colour legend

A common use case is a draggable colour legend anchored to the bottom-right of the viewer. The example below shows a single div with a title row and six category rows, each pairing a label cell with a colour swatch cell.

{
  "width": 1920,
  "height": 1008,
  "visible": true,
  "cesiumObjects": [],
  "vectors": [],
  "divs": [
    {
      "id": "legend-001",
      "type": "default",
      "position": "BottomRight",
      "right": 120,
      "bottom": 15,
      "width": 200,
      "height": 280,
      "borderRadius": 8,
      "borderWidth": 0,
      "borderColor": "black",
      "borderUsingPercent": false,
      "screenWidth": 1920,
      "screenHeight": 1008,
      "boxShadow": {
        "horizontal": 2,
        "vertical": 2,
        "blur": 5,
        "spread": 3,
        "color": "rgba(0, 0, 0, 0.25)"
      },
      "contentRows": [
        {
          "height": 16,
          "items": [
            {
              "width": 100,
              "contentType": "DEFAULT",
              "text": "Colour Legend",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "white",
              "backgroundColor": "rgba(1, 39, 73, 1)",
              "innerPadding": "5px 5px 5px 5px",
              "action": { "type": "DRAGABBLE", "params": {} }
            }
          ]
        },
        {
          "height": 14,
          "items": [
            {
              "width": 60,
              "contentType": "DEFAULT",
              "text": "Category A",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "white",
              "innerPadding": "5px 5px 5px 5px"
            },
            {
              "width": 40,
              "contentType": "DEFAULT",
              "text": "",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "rgba(0, 184, 217, 1)",
              "innerPadding": "5px 5px 5px 5px"
            }
          ]
        },
        {
          "height": 14,
          "items": [
            {
              "width": 60,
              "contentType": "DEFAULT",
              "text": "Category B",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "white",
              "innerPadding": "5px 5px 5px 5px"
            },
            {
              "width": 40,
              "contentType": "DEFAULT",
              "text": "",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "rgba(0, 200, 83, 1)",
              "innerPadding": "5px 5px 5px 5px"
            }
          ]
        },
        {
          "height": 14,
          "items": [
            {
              "width": 60,
              "contentType": "DEFAULT",
              "text": "Category C",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "white",
              "innerPadding": "5px 5px 5px 5px"
            },
            {
              "width": 40,
              "contentType": "DEFAULT",
              "text": "",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "rgba(170, 0, 255, 1)",
              "innerPadding": "5px 5px 5px 5px"
            }
          ]
        },
        {
          "height": 14,
          "items": [
            {
              "width": 60,
              "contentType": "DEFAULT",
              "text": "Category D",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "white",
              "innerPadding": "5px 5px 5px 5px"
            },
            {
              "width": 40,
              "contentType": "DEFAULT",
              "text": "",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "rgba(255, 171, 0, 1)",
              "innerPadding": "5px 5px 5px 5px"
            }
          ]
        },
        {
          "height": 14,
          "items": [
            {
              "width": 60,
              "contentType": "DEFAULT",
              "text": "Category E",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "white",
              "innerPadding": "5px 5px 5px 5px"
            },
            {
              "width": 40,
              "contentType": "DEFAULT",
              "text": "",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "rgba(41, 121, 255, 1)",
              "innerPadding": "5px 5px 5px 5px"
            }
          ]
        },
        {
          "height": 14,
          "items": [
            {
              "width": 60,
              "contentType": "DEFAULT",
              "text": "Category F",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "white",
              "innerPadding": "5px 5px 5px 5px"
            },
            {
              "width": 40,
              "contentType": "DEFAULT",
              "text": "",
              "textAlign": "center",
              "verticalAlign": "center",
              "fontSize": 13,
              "fontColor": "black",
              "backgroundColor": "rgba(100, 221, 23, 1)",
              "innerPadding": "5px 5px 5px 5px"
            }
          ]
        }
      ]
    }
  ]
}