<html>

<head>
  <script src="https://api.catenda.com/js/v1/viewer2d"></script>
</head>

<body>

  <div id="splash">
    <div id="title">
      <div id="title-text">
        Event
      </div>
      <div id="play">
        <button id="play-button" disabled=true onclick="start()">
          <i id="play-icon" class="fa fa-play"></i>
        </button>
      </div>
    </div>
    <div id="error">
      The example is unavailable<br>
      Please try again later
    </div>
    <div id="logo">
      <image id="logo-image" src="https://archbee-image-uploads.s3.amazonaws.com/v9iuLCcKE2amnKqiFVllF/qCmgrBQdRA0cjxeSZ0Rvn_logo-api-light.png" />
    </div>
  </div>
  <div id="gui"></div>
  <div id="scene">
    <div id="viewer-container">
      <div id="viewer-2d"></div>
    </div>
    <div id="console">
      <div id="console-header">
        <div id="console-header-text">Output</div>
        <button id="console-copy" onclick="copyConsole()">
          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M11 12H1C0.447 12 0 11.553 0 11V1C0 0.448 0.447 0 1 0H11C11.553 0 12 0.448 12 1V11C12 11.553 11.553 12 11 12Z" fill="currentColor" />
            <path d="M15 16H4V14H14V4H16V15C16 15.553 15.553 16 15 16Z" fill="currentColor" />
          </svg>
          <span id="tooltip">Copy</span>
        </button>
      </div>
      <div id="console-text"></div>
    </div>
  </div>
  <script type="text/javascript">
    bimsync.setOnViewer2dLoadCallback(function() {
      document.getElementById("play-button").disabled = false
    });
    bimsync.loadViewer2d();
  </script>
</body>

</html>
html {
  height: 100%;
  overflow: hidden;
  width: 100%;
}

body {
  background-color: #ade0bc;
  background-image: url("https://archbee-image-uploads.s3.amazonaws.com/oGjtxJ5MUZmGondFfGBOZ-0l0obCRKymJG7Q9KTtAhl-20240826-204445.png");
  background-position: left;
  background-repeat: no-repeat;
  background-size: cover;
  color: #004d47;
  font-family: Inter;
  height: 100%;
  margin: 0;
  overflow: hidden;
  padding: 0;
  width: 100%;
}

#console {
  display: flex;
  flex-direction: column;
  height: 100px;
  width: 100%;
}

#console-copy {
  align-items: center;
  background-color: #f3f3f5;
  border: none;
  color: #6e7574;
  display: flex;
  justify-content: center;
  margin-right: 16px;
  padding: 5px;
}

#console-copy:hover {
  color: #212322;
  cursor: pointer;
}

#console-copy:active {
  color: #c5c7c7;
}

#console-copy #tooltip {
  border-radius: 6px;
  bottom: 102px;
  background-color: black;
  color: #fff;
  font-size: 12px;
  padding: 10px;
  position: absolute;
  right: 2px;
  visibility: hidden;
  width: 40px;
}

#console-copy:hover #tooltip {
  visibility: visible;
}

#console-header {
  align-items: center;
  background-color: #f3f3f5;
  display: flex;
  height: 32px;
  justify-content: space-between;
}

#console-header-text {
  color: #6e7574;
  font-size: 14px;
  margin-left: 16px;
  user-select: none;
}

#console-text {
  background-color: white;
  color: black;
  flex: 1;
  font-family: monospace;
  font-size: 14px;
  overflow: auto;
  padding: 10px 16px 10px 16px;
  word-break: break-all;
}

#error {
  align-self: center;
  bottom: 50px;
  display: none;
  font-size: 20px;
  font-weight: 600;
  position: absolute;
  text-align: center;
}

#gui {
  display: none;
  left: 20px;
  position: absolute;
  top: 20px;
}

#logo {
  bottom: 15px;
  position: absolute;
  right: 15px;
}

#logo-image {
  width: 200px;
}

#play {
  align-items: center;
  display: flex;
  justify-content: center;
}

#play-button {
  align-items: center;
  background-color: #004d47;
  border-color: transparent;
  border-radius: 50%;
  color: #ade0bc;
  display: flex;
  font-size: 26px;
  height: 60px;
  justify-content: center;
  margin-top: 24px;
  width: 60px;
}

#play-button:disabled {
  cursor: wait;
}

#play-button:hover:enabled {
  background-color: #001e1c;
  cursor: pointer;
}

#play-icon {
  padding-left: 5px;
}

#scene {
  display: flex;
  flex-direction: column;
  height: 100%;
  justify-content: flex-end;
  width: 100%;
}

#splash {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 100%;
}

#splash #title {
  display: flex;
  flex: 1;
  flex-direction: column;
  font-size: 36px;
  font-weight: 600;
  height: 100%;
  justify-content: center;
  text-align: center;
}

#title-text {
  align-items: center;
  display: flex;
  flex-direction: row;
  justify-content: center;
}

#viewer-container {
  flex: 1;
  overflow: hidden;
}

#viewer-2d {
  height: 100%;
  width: 100%;
}

.lil-gui {
  --background-color: #30343a;
  --focus-color: #00665e;
  --hover-color: #005952;
  --name-width: 90%;
  --number-color: #ade0bc;
  --string-color: #ade0bc;
  --text-color: #ade0bc;
  --title-background-color: #24272b;
  --title-text-color: #ade0bc;
  --widget-color: #004d47;
  --width: 200px;
}
// Global variables to ease access
let viewer2d = null;
let gui = null;
const modelId = "my-model-id";

// Enum of ids for HTML elements referenced in this file
const ElementId = {
  CONSOLE_COPY: "console-copy",
  CONSOLE_TEXT: "console-text",
  ERROR: "error",
  GUI: "gui",
  LOADING_ICON: "loading-icon",
  PLAY_BUTTON: "play-button",
  PLAY_ICON: "play-icon",
  SPLASH: "splash",
  TOOLTIP: "tooltip",
  VIEWER_2D: "viewer-2d"
};

// SVG icon for markers
const markerIcon =
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='_size_' height='_size_' viewBox='0 0 38 38'%3E%3Cdefs%3E%3Cfilter id='prefix__a' width='169.2%25' height='169.2%25' x='-34.6%25' y='-26.9%25' filterUnits='objectBoundingBox'%3E%3CfeMorphology in='SourceAlpha' operator='dilate' radius='2' result='shadowSpreadOuter1'/%3E%3CfeOffset dy='2' in='shadowSpreadOuter1' result='shadowOffsetOuter1'/%3E%3CfeGaussianBlur in='shadowOffsetOuter1' result='shadowBlurOuter1' stdDeviation='2'/%3E%3CfeComposite in='shadowBlurOuter1' in2='SourceAlpha' operator='out' result='shadowBlurOuter1'/%3E%3CfeColorMatrix in='shadowBlurOuter1' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.28 0'/%3E%3C/filter%3E%3Ccircle id='prefix__b' cx='250' cy='315' r='13'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' transform='translate(-231 -298)'%3E%3Cuse filter='url(%23prefix__a)' xlink:href='%23prefix__b'/%3E%3Cuse fill='%23000' stroke='%23FFF' stroke-width='4' xlink:href='%23prefix__b'/%3E%3C/g%3E%3C/svg%3E%0A";

// Enum of style values used in this file
const Style = {
  FLEX: "flex",
  NONE: "none"
};

// Reset tooltip text
const consoleCopy = document.getElementById(ElementId.CONSOLE_COPY);
const onCopyLeave = function () {
  document.getElementById(ElementId.TOOLTIP).innerHTML = "Copy";
};
consoleCopy.addEventListener("mouseleave", onCopyLeave);
consoleCopy.addEventListener("touchend", onCopyLeave);

/**
 *  Catenda 2D viewer Event Example.
 *  The runExample function contains the 2D viewer related calls to fulfill the example shown.
 */

// The code specific to the example
const runExample = function () {
  // gui labels
  const CLICK_EVENT = "Click Event Handler";
  const CONTEXT_MENU_EVENT = "Context Menu Event Handler";
  const DOUBLE_CLICK_EVENT = "Double Click Event Handler";
  const MARKER_CLICK_EVENT = "Marker Click Event Handler";
  const MARKER_DRAG_EVENT = "Marker Drag Event Handler";
  const MARKER_DRAG_END_EVENT = "Marker Drag End Event Handler";
  const MARKER_HOVER_EVENT = "Marker Hover Event Handler";
  const SELECT_EVENT = "Select Event Handler";
  const VIEWPOINT_EVENT = "Viewpoint Event Handler";
  const REMOVE_ALL_EVENTS = "Remove All Event Handlers";

  // Keep track of added marker to ease removal
  let markerId = null;

  /**
   * Start off by showing a storey
   */
  const storeys = viewer2d.getStoreys();
  if (storeys.length > 0) {
    viewer2d.showStorey(storeys[1].id);
  }
  viewer2d.setViewport("fit", { expandInPercentage: 15 });

  /**
   *  Store reference returned from registering an event handler that includes remove method
   */
  const eventHandlers = {};

  const addMarker = () => {
    if (!markerId) {
      const markerSize = 32;
      const marker = {
        color: "#004d47",
        clickable: true,
        draggable: true,
        icon: {
          anchor: {
            x: markerSize / 2,
            y: markerSize / 2
          },
          size: {
            height: markerSize,
            width: markerSize
          },
          url: markerIcon.replace(/_size_/g, markerSize)
        },
        x: 0,
        y: 0
      };
      viewer2d.clearMarkers();
      markerId = viewer2d.addMarker(marker);
    }
  };

  /**
   * Map on screen buttons to viewer behaviour
   */
  const onClickEventHandlerChange = function (isEnabled) {
    if (isEnabled) {
      const onClick = function (event) {
        setOutput(event);
      };
      const handler = viewer2d.addEventListener("viewer2d.click", onClick);
      eventHandlers[CLICK_EVENT] = handler;
      disableGuiProperty(SELECT_EVENT);
      enableGuiProperty(REMOVE_ALL_EVENTS);
    } else {
      eventHandlers[CLICK_EVENT].remove();
      delete eventHandlers[CLICK_EVENT];
      enableGuiProperty(SELECT_EVENT);
      setOutput("");
    }
  };
  const onContextMenuEventHandlerChange = function (isEnabled) {
    if (isEnabled) {
      const onContextMenu = function (event) {
        setOutput(event);
      };
      const handler = viewer2d.addEventListener(
        "viewer2d.contextmenu",
        onContextMenu
      );
      eventHandlers[CONTEXT_MENU_EVENT] = handler;
      enableGuiProperty(REMOVE_ALL_EVENTS);
    } else {
      eventHandlers[CONTEXT_MENU_EVENT].remove();
      delete eventHandlers[CONTEXT_MENU_EVENT];
      setOutput("");
    }
  };
  const onDoubleClickEventHandlerChange = function (isEnabled) {
    if (isEnabled) {
      const onDoubleClick = function (event) {
        setOutput(event);
      };
      const handler = viewer2d.addEventListener(
        "viewer2d.dblclick",
        onDoubleClick
      );
      eventHandlers[DOUBLE_CLICK_EVENT] = handler;
      enableGuiProperty(REMOVE_ALL_EVENTS);
    } else {
      eventHandlers[DOUBLE_CLICK_EVENT].remove();
      delete eventHandlers[DOUBLE_CLICK_EVENT];
      setOutput("");
    }
  };
  const onMarkerClickEventHandlerChange = function (isEnabled) {
    if (isEnabled) {
      const onMarkerClick = function (event) {
        setOutput(event);
      };
      const handler = viewer2d.addEventListener(
        "viewer2d.markerclick",
        onMarkerClick
      );
      eventHandlers[MARKER_CLICK_EVENT] = handler;
      enableGuiProperty(REMOVE_ALL_EVENTS);
      addMarker();
    } else {
      eventHandlers[MARKER_CLICK_EVENT].remove();
      delete eventHandlers[MARKER_CLICK_EVENT];
      if (
        !eventHandlers[MARKER_DRAG_EVENT] &&
        !eventHandlers[MARKER_DRAG_END_EVENT] &&
        !eventHandlers[MARKER_HOVER_EVENT]
      ) {
        viewer2d.removeMarker(markerId);
        markerId = null;
      }
      setOutput("");
    }
  };
  const onMarkerDragEventHandlerChange = function (isEnabled) {
    if (isEnabled) {
      const onMarkerDrag = function (event) {
        setOutput(event);
      };
      const handler = viewer2d.addEventListener(
        "viewer2d.markerdrag",
        onMarkerDrag
      );
      eventHandlers[MARKER_DRAG_EVENT] = handler;
      enableGuiProperty(REMOVE_ALL_EVENTS);
      addMarker();
    } else {
      eventHandlers[MARKER_DRAG_EVENT].remove();
      delete eventHandlers[MARKER_DRAG_EVENT];
      if (
        !eventHandlers[MARKER_CLICK_EVENT] &&
        !eventHandlers[MARKER_DRAG_END_EVENT] &&
        !eventHandlers[MARKER_HOVER_EVENT]
      ) {
        viewer2d.removeMarker(markerId);
        markerId = null;
      }
      setOutput("");
    }
  };
  const onMarkerDragEndEventHandlerChange = function (isEnabled) {
    if (isEnabled) {
      const onMarkerDragComplete = function (event) {
        setOutput(event);
      };
      const handler = viewer2d.addEventListener(
        "viewer2d.markerdragend",
        onMarkerDragComplete
      );
      eventHandlers[MARKER_DRAG_END_EVENT] = handler;
      enableGuiProperty(REMOVE_ALL_EVENTS);
      addMarker();
    } else {
      eventHandlers[MARKER_DRAG_END_EVENT].remove();
      delete eventHandlers[MARKER_DRAG_END_EVENT];
      if (
        !eventHandlers[MARKER_CLICK_EVENT] &&
        !eventHandlers[MARKER_DRAG_EVENT] &&
        !eventHandlers[MARKER_HOVER_EVENT]
      ) {
        viewer2d.removeMarker(markerId);
        markerId = null;
      }
      setOutput("");
    }
  };
  const onMarkerHoverEventHandlerChange = function (isEnabled) {
    if (isEnabled) {
      const onMarkerHover = function (event) {
        setOutput(event);
      };
      const handler = viewer2d.addEventListener(
        "viewer2d.markerhover",
        onMarkerHover
      );
      eventHandlers[MARKER_HOVER_EVENT] = handler;
      enableGuiProperty(REMOVE_ALL_EVENTS);
      addMarker();
    } else {
      eventHandlers[MARKER_HOVER_EVENT].remove();
      delete eventHandlers[MARKER_HOVER_EVENT];
      if (
        !eventHandlers[MARKER_CLICK_EVENT] &&
        !eventHandlers[MARKER_DRAG_EVENT] &&
        !eventHandlers[MARKER_DRAG_END_EVENT]
      ) {
        viewer2d.removeMarker(markerId);
        markerId = null;
      }
      setOutput("");
    }
  };
  const onSelectEventHandlerChange = function (isEnabled) {
    if (isEnabled) {
      const onSelect = function (event) {
        setOutput(event);
      };
      const handler = viewer2d.addEventListener("viewer2d.select", onSelect);
      eventHandlers[SELECT_EVENT] = handler;
      disableGuiProperty(CLICK_EVENT);
      enableGuiProperty(REMOVE_ALL_EVENTS);
    } else {
      eventHandlers[SELECT_EVENT].remove();
      delete eventHandlers[SELECT_EVENT];
      enableGuiProperty(CLICK_EVENT);
      setOutput("");
    }
  };
  const onViewpointEventHandlerChange = function (isEnabled) {
    if (isEnabled) {
      const onViewpointChange = function (event) {
        setOutput(event);
      };
      const handler = viewer2d.addEventListener(
        "viewer2d.viewpoint",
        onViewpointChange
      );
      eventHandlers[VIEWPOINT_EVENT] = handler;
      enableGuiProperty(REMOVE_ALL_EVENTS);
      const viewpoint = {
        direction: 90,
        location: {
          x: 1,
          y: 1
        }
      };
      viewer2d.setViewpoint(viewpoint);
      viewer2d.showViewpoint();
    } else {
      eventHandlers[VIEWPOINT_EVENT].remove();
      delete eventHandlers[VIEWPOINT_EVENT];
      viewer2d.hideViewpoint();
      setOutput("");
    }
  };
  const onRemoveAllEventHandlersClick = function () {
    Object.values(eventHandlers).forEach((eventHandler) =>
      eventHandler.remove()
    );
    gui.controllers.forEach((controller) => controller.enable());
    disableGuiProperty(REMOVE_ALL_EVENTS);
    Object.keys(state).forEach((key) => {
      if (key !== REMOVE_ALL_EVENTS) {
        console.log(key);
        updateGuiPropertyValue(key, false);
      }
    });
  };

  /**
   *  Map of gui items to data and callbacks
   */
  const state = {
    [CLICK_EVENT]: false,
    [CONTEXT_MENU_EVENT]: false,
    [DOUBLE_CLICK_EVENT]: false,
    [MARKER_CLICK_EVENT]: false,
    [MARKER_DRAG_EVENT]: false,
    [MARKER_DRAG_END_EVENT]: false,
    [MARKER_HOVER_EVENT]: false,
    [SELECT_EVENT]: false,
    [VIEWPOINT_EVENT]: false,
    [REMOVE_ALL_EVENTS]: onRemoveAllEventHandlersClick
  };

  /**
   * Build gui panel
   */
  gui.add(state, CLICK_EVENT).onChange(onClickEventHandlerChange);
  gui.add(state, CONTEXT_MENU_EVENT).onChange(onContextMenuEventHandlerChange);
  gui.add(state, DOUBLE_CLICK_EVENT).onChange(onDoubleClickEventHandlerChange);
  gui.add(state, MARKER_CLICK_EVENT).onChange(onMarkerClickEventHandlerChange);
  gui.add(state, MARKER_DRAG_EVENT).onChange(onMarkerDragEventHandlerChange);
  gui
    .add(state, MARKER_DRAG_END_EVENT)
    .onChange(onMarkerDragEndEventHandlerChange);
  gui.add(state, MARKER_HOVER_EVENT).onChange(onMarkerHoverEventHandlerChange);
  gui.add(state, SELECT_EVENT).onChange(onSelectEventHandlerChange);
  gui.add(state, VIEWPOINT_EVENT).onChange(onViewpointEventHandlerChange);
  gui.add(state, REMOVE_ALL_EVENTS).disable();
};

/**
 *  Code required for bootstrapping a 2D viewer instance with which to execute the example
 */

// Bootstrap a 2D viewer instance and load a model
const initialize = async function (onViewerReady) {
  // Show the loading indicator until the 2D viewer is ready
  showLoadingIndicator();
  // Hide any previous error message
  hideElement(ElementId.ERROR);

  // Create a gui instance for 2D viewer controls
  gui = new lil.GUI({
    container: document.getElementById("gui")
  });
  gui.title("Options");

  // HTML Div element where the 2D viewer is mounted
  const div2d = document.getElementById(ElementId.VIEWER_2D);

  // Options used when creating a 2D viewer instance
  const viewerOptions = {
    hoverSpaces: true,
    selectColor: "#ade0bc",
    showViewpoint: false
  };

  // Create a 2D viewer instance
  viewer2d = new bimsync.viewer2d.Viewer2D(div2d, viewerOptions);

  // Options used when loading the model
  const loadModelOptions = {
    modelId: modelId,
    modelName: "My Model Name"
  };

  // Fetch a 2D viewer token for the model to load
  const url2d = await getToken();
  // Load the model
  viewer2d
    .loadUrl(url2d, loadModelOptions)
    .then(function () {
      // 2D viewer is ready, stop loading
      showPlayButton();
      // Hide the splash screen
      hideElement(ElementId.SPLASH);
      // Show the 2D viewer controls
      showElement(ElementId.GUI);
      // 2D viewer is ready, run the callback
      onViewerReady();
    })
    .catch(function (error) {
      showError();
    });
};

// Entry point called when user clicks the play button
const start = function () {
  // Bootstrap a 2D viewer instance then run the example
  initialize(runExample);
};

/**
 * Utility functions used in this file
 */

// Set console text in clipboard
const copyConsole = async function () {
  await navigator.clipboard.writeText(
    document.getElementById(ElementId.CONSOLE_TEXT).innerHTML
  );
  document.getElementById(ElementId.TOOLTIP).innerHTML = "Copied!";
};

// Disable a property in the gui panel
const disableGuiProperty = function (propertyName) {
  gui
    .controllersRecursive()
    .find(({ property }) => property === propertyName)
    .disable();
};

// Enable a property in the gui panel
const enableGuiProperty = function (propertyName) {
  gui
    .controllersRecursive()
    .find(({ property }) => property === propertyName)
    .enable();
};

// Retrieve a 2D viewer token
const getToken = async function () {
  /**
   * Fetch a 2D viewer token from the Catenda API
   * Here we are using a CodePen specific endpoint for demo purposes
   * Replace this with your own token endpoint in a real application
   * Find out more at https://developers.catenda.com/viewer-2d/create-2d-viewer-token
   * The model this token provides access to is licensed under the Creative Commons Attribution 4.0 International License.
   * (C) original authors
   * https://github.com/buildingSMART/Sample-Test-Files
   * More info and a link to the full license text is available on http://creativecommons.org/licenses/by/4.0/
   */
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  const tryCount = 4;
  let response, result;
  // Try to fetch the token a few times before giving up
  for (let i = 1; i <= tryCount; i++) {
    response = await fetch(
      "https://documentation-edge.developers.catenda.com/token2d"
    );
    if (response.ok) {
      break;
    } else {
      // If the response is not ok, we wait a bit before trying again
      if (i < tryCount) {
        await delay(i * 1000);
      }
      continue;
    }
  }
  // If the response is ok, we can proceed
  if (response && response.ok) {
    const data = await response.json();
    result = data.url;
  } else {
    showError();
  }
  return result;
};

// Hide an HTML element passing its id
const hideElement = function (elementId) {
  document.getElementById(elementId).style.display = Style.NONE;
};

// Set the text displayed in the output panel
const setOutput = function (value) {
  const text = value instanceof Object ? JSON.stringify(value) : value;
  document.getElementById(ElementId.CONSOLE_TEXT).innerHTML = text;
};

// Show an HTML element passing its id
const showElement = function (elementId) {
  document.getElementById(elementId).style.display = Style.FLEX;
};

// Reset after an error
const showError = function () {
  viewer2d.dispose();
  gui.destroy();
  showPlayButton();
  showElement(ElementId.ERROR);
};

// Display the loading indicator
const showLoadingIndicator = function () {
  const playButton = document.getElementById(ElementId.PLAY_BUTTON);
  playButton.disabled = true;
  const playIconElement = document.getElementById(ElementId.PLAY_ICON);
  let loadingIcon = document.createElement("i");
  loadingIcon.id = ElementId.LOADING_ICON;
  loadingIcon.classList.add("fa", "fa-spinner", "fa-spin");
  playIconElement.replaceWith(loadingIcon);
};

// Display the play button
const showPlayButton = function () {
  const playButton = document.getElementById(ElementId.PLAY_BUTTON);
  playButton.disabled = false;
  const loadingIconElement = document.getElementById(ElementId.LOADING_ICON);
  let playIcon = document.createElement("i");
  playIcon.id = ElementId.PLAY_ICON;
  playIcon.classList.add("fa", "fa-play");
  loadingIconElement.replaceWith(playIcon);
};

// Update a value used in the gui panel to reflect a change
const updateGuiPropertyValue = function (propertyName, value) {
  gui
    .controllersRecursive()
    .find(({ property }) => property === propertyName)
    .setValue(value);
};

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css

External JavaScript

  1. https://cdn.jsdelivr.net/npm/lil-gui@0.19