Real-Time Deviated Schematics

This tutorial demonstrates how to use the Deviated Schematics Widget for real-time data visualization. The Deviated Schematics Widget is derived from geotoolkit/widgets/AnnotatedWidget and uses RealTimeDeviatedSchematics/data/DynamicWellBoreData that extends the supplied geotoolkit/schematics/data/WellBoreData as the data model. Familiarization with Schematics - Basics and Schematics - DeviatedSchematicsWidget tutorials will be beneficial.

# Widget Initialization

Widget initialization is similar to a deviated schematic widget. First, create a new instance of geotoolkit/schematics/widgets/DeviatedSchematicWidget. The only required parameter is the data object. data includes the elements property that contains the DynamicWellBoreData and the trajectory property that provides data on how the well deviates.

After creating the DeviatedSchematicWidget object, instantiate a new geotoolkit/plot/Plot instance and add the widget as a root.

import { Axis } from "@int/geotoolkit/axis/Axis.ts";
import { NumberFormat } from "@int/geotoolkit/util/NumberFormat.ts";
import { from } from "@int/geotoolkit/selection/from.ts";
import { Plot } from "@int/geotoolkit/plot/Plot.ts";
import { CompositeSchematicsWidget, DisplayMode } from "@int/geotoolkit/schematics/widgets/CompositeSchematicsWidget.ts";
import { LocationType } from "@int/geotoolkit/schematics/labeling/LocationType.ts";
import { Trajectory2d } from "@int/geotoolkit/deviation/Trajectory2d.ts";
import { ViewMode } from "@int/geotoolkit/schematics/scene/WellBoreNode.ts";
import { Rect } from "@int/geotoolkit/util/Rect.ts";
import { SVGComponentsLoader } from "@int/geotoolkit/schematics/factory/SVGComponentsLoader.ts";
import { ComponentNodeFactoryRegistry } from "@int/geotoolkit/schematics/factory/ComponentNodeFactoryRegistry.ts";
import trajectoryData from "/src/code/Schematics/RealTimeDeviatedSchematics/data/payload.json?import";
import { data as wbData } from "/src/code/Schematics/RealTimeDeviatedSchematics/data/wellBoreData.ts";
import { deepMergeObject } from "@int/geotoolkit/base.js";
import { MathUtil } from "@int/geotoolkit/util/MathUtil.ts";
import { DynamicWellBoreData } from "/src/code/Schematics/RealTimeDeviatedSchematics/data/DynamicWellBoreData.ts";
import { TableView } from "@int/geotoolkit/widgets/TableView.ts";
import { DataTable } from "@int/geotoolkit/data/DataTable.ts";
import { DataTableAdapter } from "@int/geotoolkit/widgets/data/DataTableAdapter.ts";
import { Dimension } from "@int/geotoolkit/util/Dimension.ts";
import { Group } from "@int/geotoolkit/scene/Group.ts";
import { FlexboxLayout, FlexDirection } from "@int/geotoolkit/layout/FlexboxLayout.ts";
const TIME_REFRESH = 300;
const SCALE_FACTOR = 1.2;
const AUTO_SCROLL_THRESHOLD = 100;
const ROUND_ACCURACY_BASE = 100;
function round(value) {
  return MathUtil.round(value, ROUND_ACCURACY_BASE);
}
function updateSchematicsWidget(widget, nextX, nextY, nextMD) {
  const deviation = widget.getDeviation();
  if (deviation == null || deviation["trajectory"] == null) {
    return;
  }
  const trajectory = deviation["trajectory"];
  const wbData2 = widget.getData();
  if (wbData2 instanceof DynamicWellBoreData) {
    const depthRange = {
      "from": trajectory.minDepth(),
      "to": nextMD
    };
    const trajectoryUnits = trajectory.getProperties()["options"]["units"];
    if (trajectoryUnits != null && trajectoryUnits["distance"] != null) {
      depthRange["units"] = trajectoryUnits["distance"];
    }
    wbData2.update(depthRange);
  }
  trajectory.add(nextX, nextY, nextMD);
}
function resetSchematicsWidget(widget, data) {
  const wbData2 = widget.getData();
  if (wbData2 != null) {
    wbData2.removeAll().addComponents(data["elements"].getProperties()["elements"]);
  }
  const deviation = widget.getDeviation();
  const trajectory = deviation != null ? deviation["trajectory"] : null;
  if (trajectory != null) {
    trajectory.clear().add(data["trajectory"].getArrayX(), data["trajectory"].getArrayY(), data["trajectory"].getDepths());
  }
  if (wbData2 == null || trajectory == null) {
    widget.setData(data);
  }
}
function updateTrajectoryTable(widget, nextX, nextY, nextMD) {
  const provider = widget.getTableViewShape().getDataProvider();
  if (!(provider instanceof DataTableAdapter)) {
    return;
  }
  const dataTable = provider.getDataTable();
  if (dataTable == null) {
    return;
  }
  dataTable.addRow([
    round(nextX),
    round(nextY),
    round(nextMD)
  ]);
  widget.setData({
    "rows": dataTable.getNumberOfRows(),
    "cols": dataTable.getNumberOfColumns()
  });
}
function resetTrajectoryTable(widget, trajectory) {
  const provider = widget.getTableViewShape().getDataProvider();
  if (!(provider instanceof DataTableAdapter)) {
    return;
  }
  const dataTable = provider.getDataTable();
  if (dataTable == null) {
    return;
  }
  dataTable.clear().setColumnValues(0, trajectory.getArrayX().map((value) => round(value))).setColumnValues(1, trajectory.getArrayY().map((value) => round(value))).setColumnValues(2, trajectory.getDepths().map((value) => round(value)));
  widget.setData({
    "rows": dataTable.getNumberOfRows(),
    "cols": dataTable.getNumberOfColumns()
  });
}
function createData() {
  const staticData = [];
  const dynamicData = [];
  wbData.forEach((element) => {
    if (element["name"] === "casing" || element["name"] === "cement") {
      staticData.push(element);
    } else {
      const dynamicElement = createDynamicElement(element);
      dynamicData.push(dynamicElement);
    }
  });
  const wellBoreData = new DynamicWellBoreData().addComponents([...staticData, ...dynamicData]);
  const trajectory = createInitialTrajectory(wellBoreData);
  const initDepthRange = {
    "from": trajectory.minDepth(),
    "to": trajectory.maxDepth()
  };
  const trajectoryUnits = trajectory.getProperties()["options"]["units"];
  if (trajectoryUnits != null && trajectoryUnits["distance"] != null) {
    initDepthRange["units"] = trajectoryUnits["distance"];
  }
  wellBoreData.update(initDepthRange);
  return {
    "elements": wellBoreData,
    "trajectory": trajectory
  };
}
function createDynamicElement(staticElement) {
  let updateMethod;
  if (staticElement["name"].toLowerCase().includes("pipe")) {
    updateMethod = (el, depthRange) => {
      const geometry2 = el["geometry"];
      if (geometry2["depth"] == null || depthRange["to"] == null) {
        return;
      }
      geometry2["depth"]["to"] = depthRange["to"];
      el["extent"] = geometry2["depth"]["to"] - geometry2["depth"]["from"];
    };
  } else {
    updateMethod = (el, depthRange) => {
      const geometry2 = el["geometry"];
      if (el["extent"] == null || geometry2["depth"] == null || depthRange["to"] == null) {
        return;
      }
      geometry2["depth"]["to"] = depthRange["to"];
      geometry2["depth"]["from"] = geometry2["depth"]["to"] - el["extent"];
    };
  }
  const dynamicElement = deepMergeObject(staticElement, {
    "data": {
      "geometry": {},
      "updatemethod": updateMethod
    }
  });
  const geometry = Array.isArray(dynamicElement["data"]["geometry"]) ? dynamicElement["data"]["geometry"][0] : dynamicElement["data"]["geometry"];
  if (geometry["depth"] != null) {
    dynamicElement["data"]["extent"] = geometry["depth"]["to"] - geometry["depth"]["from"];
  }
  return dynamicElement;
}
function createInitialTrajectory(wellBoreData) {
  const geometryBounds = wellBoreData.getStaticGeometryBounds() || wellBoreData.getDynamicGeometryBounds();
  if (geometryBounds == null) {
    return new Trajectory2d();
  }
  const maxDepth = geometryBounds.getBottom();
  const trajIdx = trajectoryData.items.findIndex((item) => item.WAS_MD >= maxDepth);
  if (trajIdx < 0) {
    throw new Error();
  }
  let lastStaticMD;
  let lastStaticTVD;
  let lastStaticVS;
  const currItem = trajectoryData.items[trajIdx];
  if (trajIdx > 0 && trajIdx < trajectoryData.items.length - 1 && currItem.WAS_MD !== maxDepth) {
    const prevItem = trajectoryData.items[trajIdx - 1];
    const mdRatio = (maxDepth - prevItem.WAS_MD) / (currItem.WAS_MD - prevItem.WAS_MD);
    lastStaticMD = maxDepth;
    lastStaticTVD = prevItem.WAS_TVD + (currItem.WAS_TVD - prevItem.WAS_TVD) * mdRatio;
    lastStaticVS = prevItem.WAS_VS + (currItem.WAS_VS - prevItem.WAS_VS) * mdRatio;
  } else {
    lastStaticMD = currItem.WAS_MD;
    lastStaticTVD = currItem.WAS_TVD;
    lastStaticVS = currItem.WAS_VS;
  }
  const trajData = readTrajectory(trajIdx);
  trajData.d.push(lastStaticMD);
  trajData.x.push(lastStaticVS);
  trajData.y.push(lastStaticTVD);
  return new Trajectory2d({
    "data": {
      "x": trajData.x,
      "y": trajData.y,
      "d": trajData.d
    }
  });
}
function readTrajectory(stationCount) {
  const d = [];
  const x = [];
  const y = [];
  for (let i = 0; i < stationCount; i++) {
    const currItem = trajectoryData.items[i];
    d.push(currItem.WAS_MD);
    x.push(currItem.WAS_VS);
    y.push(currItem.WAS_TVD);
  }
  return { x, y, d };
}
function setDataLimits(widget) {
  const fullTrajectory = new Trajectory2d({
    "data": readTrajectory(trajectoryData.items.length)
  });
  const entireSceneArea = new Rect(
    fullTrajectory.getMinX(),
    fullTrajectory.getMinY(),
    fullTrajectory.getMaxX(),
    fullTrajectory.getMaxY()
  );
  widget.setDeviation({
    "datamodellimits": entireSceneArea
  });
}
function createTrajectoryTable(trajectory) {
  const rows = [];
  for (let i = 0; i < trajectory.count(); i++) {
    rows.push([
      round(trajectory.getX(i)),
      round(trajectory.getY(i)),
      round(trajectory.getDepth(i))
    ]);
  }
  const dataTable = new DataTable({
    cols: [
      { name: "X", type: "number" },
      { name: "Y", type: "number" },
      { name: "MD", type: "number" }
    ],
    rowsdata: rows
  });
  const bridge = new DataTableAdapter({
    "datatable": dataTable
  });
  const tableViewWidget = new TableView({
    "verticalscroll": "floating",
    "horizontalscroll": "floating"
  }).setData({
    "defaultcellsize": new Dimension(233, 25),
    "indextitle": "Index",
    "dataprovider": bridge,
    "rows": dataTable.getNumberOfRows(),
    "cols": dataTable.getNumberOfColumns()
  });
  const splitterTool = tableViewWidget.getToolByName("TableViewSplitter");
  if (splitterTool != null) {
    splitterTool.setEnabled(false);
  }
  return tableViewWidget.setVisibleTableLimits(tableViewWidget.getTableSize().getHeight() - 1);
}
function createScene(canvas) {
  const registrySVG = new ComponentNodeFactoryRegistry(true);
  const loader = new SVGComponentsLoader({
    "registry": registrySVG,
    "path": RESOURCES + "svg/components.json"
  });
  loader.load().then(() => {
    const registryJS = new ComponentNodeFactoryRegistry(true);
    registrySVG.setFactory("casing", registryJS.getFactory("casing"));
    registrySVG.setFactory("cement", registryJS.getFactory("cement"));
  });
  const data = createData();
  const options = {
    "wellborenode": {
      "registry": registrySVG
    },
    "north": {
      "title": {
        "text": "Deviated Schematics Widget"
      }
    },
    "gap": {
      "left": {
        "visible": true,
        "size": "180px",
        "resizable": false
      },
      "right": {
        "visible": true,
        "size": "100px",
        "resizable": false
      },
      "top": {
        "visible": true,
        "size": "50px",
        "resizable": false
      },
      "bottom": {
        "visible": true,
        "size": "50px",
        "resizable": false
      }
    },
    "deviation": {
      "trackwidth": 30
    },
    "trajectory": {
      "stations": {
        "visible": false
      },
      "lines": {
        "visible": true,
        "linestyle": { "color": "blue" }
      }
    },
    "labeling": {
      "defaultlocation": LocationType.Auto
    },
    "data": createData()
  };
  const widget = new CompositeSchematicsWidget(options);
  setDataLimits(widget);
  widget.setViewMode(ViewMode.Regular);
  widget.setDisplayMode(DisplayMode.Deviated);
  from(widget).where((child) => child instanceof Axis).selectToArray().forEach((axis) => {
    axis.getTickGenerator().setLabelFormat("major", new NumberFormat({ "maximumfractiondigits": 0 }));
  });
  const trajectoryTable = createTrajectoryTable(data.trajectory);
  widget.setLayoutStyle({
    "width": 800,
    "height": 600
  });
  trajectoryTable.setLayoutStyle({
    "width": 800,
    "height": 200
  });
  const rootGroup = new Group({
    "children": [widget, trajectoryTable],
    "layout": new FlexboxLayout({
      "flexdirection": FlexDirection.Column
    })
  });
  return new Plot({
    "canvaselement": canvas,
    "root": rootGroup
  });
}
class RealTimeDeviatedSchematicsDemo {
  constructor(canvas) {
    this._plot = createScene(canvas);
    this._widget = from(this._plot.getRoot()).where((item) => item instanceof CompositeSchematicsWidget).selectFirst();
    this._trajectoryTable = from(this._plot.getRoot()).where((item) => item instanceof TableView).selectFirst();
    this._timer = null;
    this._isStarted = false;
    this._isSuspended = false;
    this._isAutoScrollEnabled = true;
  }
  getPlot() {
    return this._plot;
  }
  zoomIn() {
    this._widget.scaleModel(SCALE_FACTOR, SCALE_FACTOR, 0, 0);
    if (this.isAutoScrollEnabled()) {
      this.adjustAutoScrollPosition();
    }
  }
  zoomOut() {
    this._widget.scaleModel(1 / SCALE_FACTOR, 1 / SCALE_FACTOR, 0, 0);
    if (this.isAutoScrollEnabled()) {
      this.adjustAutoScrollPosition();
    }
  }
  zoomToFit() {
    this._widget.fitToBounds();
  }
  start() {
    if (this.isStarted() && !this.isSuspended()) {
      return;
    }
    this._timer = setInterval(this.updateData.bind(this), TIME_REFRESH);
    this._isStarted = true;
    this._isSuspended = false;
  }
  isStarted() {
    return this._isStarted;
  }
  toggleSuspend() {
    if (this.isSuspended()) {
      this._timer = setInterval(this.updateData.bind(this), TIME_REFRESH);
    } else if (this._timer != null) {
      clearInterval(this._timer);
    }
    this._isSuspended = !this._isSuspended;
  }
  isSuspended() {
    return this._isSuspended;
  }
  stop() {
    this._isStarted = false;
    this._isSuspended = false;
    if (this._timer != null) {
      clearInterval(this._timer);
    }
    this._timer = null;
    this.resetData();
    this.adjustAutoScrollPosition();
  }
  toggleAutoScroll() {
    this._isAutoScrollEnabled = !this._isAutoScrollEnabled;
  }
  isAutoScrollEnabled() {
    return this._isAutoScrollEnabled;
  }
  dispose() {
    this.stop();
    this._plot.dispose(true);
  }
  updateData() {
    const deviation = this._widget.getDeviation();
    if (deviation == null || deviation["trajectory"] == null) {
      return;
    }
    const trajectory = deviation["trajectory"];
    const currDepth = trajectory.maxDepth();
    const nextSampleIdx = trajectoryData.items.findIndex((item) => item.WAS_MD > currDepth);
    if (nextSampleIdx > 0) {
      const nextSample = trajectoryData.items[nextSampleIdx];
      const nextX = nextSample.WAS_VS;
      const nextY = nextSample.WAS_TVD;
      const nextMD = nextSample.WAS_MD;
      updateSchematicsWidget(this._widget, nextX, nextY, nextMD);
      updateTrajectoryTable(this._trajectoryTable, nextX, nextY, nextMD);
    } else {
      this.resetData();
    }
    if (this.isAutoScrollEnabled()) {
      this.adjustAutoScrollPosition();
    }
  }
  resetData() {
    const initData = createData();
    resetSchematicsWidget(this._widget, initData);
    resetTrajectoryTable(this._trajectoryTable, initData["trajectory"]);
  }
  adjustAutoScrollPosition() {
    const trajectory = this._widget.getDeviation() != null ? this._widget.getDeviation()["trajectory"] : null;
    if (trajectory == null) {
      return;
    }
    const bounds = this._widget.getModel().getVisibleDeviceLimits();
    if (bounds == null) {
      return;
    }
    const cnt = trajectory.count();
    let posX = trajectory.getX(cnt - 1);
    let posY = trajectory.getY(cnt - 1);
    const sceneTr = this._widget.getWellBoreWithLabels().getSceneTransform();
    if (sceneTr != null && !sceneTr.isIdentity()) {
      const devicePos = sceneTr.transformXY(posX, posY);
      posX = devicePos.getX();
      posY = devicePos.getY();
    }
    const left = bounds.getLeft() + AUTO_SCROLL_THRESHOLD - posX;
    const right = posX - (bounds.getRight() - AUTO_SCROLL_THRESHOLD);
    const top = bounds.getTop() + AUTO_SCROLL_THRESHOLD - posY;
    const bottom = posY - (bounds.getBottom() - AUTO_SCROLL_THRESHOLD);
    const dx = left > right ? Math.max(left, 0) : -Math.max(right, 0);
    const dy = top > bottom ? Math.max(top, 0) : -Math.max(bottom, 0);
    if (!MathUtil.equals(dx, 0) || !MathUtil.equals(dy, 0)) {
      this._widget.translateModel(dx, dy);
    }
    this._trajectoryTable.setVisibleTableLimits(this._trajectoryTable.getTableSize().getHeight() - 1);
  }
}
export { RealTimeDeviatedSchematicsDemo };

createScene();

# Data Initialization

Two objects are required to instantiate an instance of DeviatedSchematicsWidget:

  1. An elements object, which is an object containing geotoolkit/schematics/data/WellBoreData to draw the well schematic. Here we will use data from a separate file, TESTWELLBOREDATA.ts. First, we need to sort elements into static and dynamic ones, then specify update method for dynamic elements and finally, create a new DynamicWellBoreData object.
  2. A trajectory object created by the geotoolkit/deviation/Trajectory2d constructor.

Note that we need to set data model limits for entire deviated scene in order to make layout properly (see setDataLimits method for details).

# Data Updating

To update DeviatedSchematicsWidget with incoming data we need to update both well bore data and trajectory.