三維流體模組API開發-Voxel


[info] 小提示:

程式碼連結:https://doc-3dgdp.colife.org.tw/samplecode/#src/testweb/fluid_voxel/


初始化三維流體模組

三維流體模組需使用WebGL2,注意圖台初始化時要在參數中設定 requestWebGL2: true

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!--引入js-->
    <script src="https://sample.pilotgaea.com.tw/demo/index/src/PGWebJS/13.0/PGWeb3D.min.js"></script>
    <!--引入css-->
    <link
      href="https://sample.pilotgaea.com.tw/demo/index/src/PGWebJS/13.0/css/PGWeb3D.css"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://sample.pilotgaea.com.tw/demo/index/css/samplecode.css"
      rel="stylesheet"
      type="text/css"
    />
    <style>
      .hidden {
        display: none !important;
      }
    </style>
  </head>

  <body>
    <aside class="control" style="position: absolute; z-index: 1">
      <details open>
        <summary>資料來源</summary>
        <fieldset>
          <label for="data0">速度</label>
          <input id="data0" type="radio" name="data" value="voxelA" checked />
          <label for="data1">溫度</label>
          <input id="data1" type="radio" name="data" value="voxelB" />
          <label for="data2">氣壓</label>
          <input id="data2" type="radio" name="data" value="voxelC" />
        </fieldset>
        <section id="legend"></section>
      </details>

      <details open>
        <summary>視覺化</summary>
        <fieldset>
          <label>顯示體積</label>
          <input id="showVoxel" type="radio" name="visual" checked />
          <label>顯示裁切面</label>
          <input id="showPlane" type="radio" name="visual" />
        </fieldset>
        <fieldset id="voxelPanel">
          <hr />
          <label>透明度</label>
          <input
            id="opacity"
            type="range"
            value="1"
            min="0"
            max="1"
            step="0.01"
          />
          <label>點模式</label>
          <input id="pointMode" type="checkbox" />
          <label>顯示裁切體</label>
          <input id="slice" type="checkbox" />
          <label>顯示模型</label>
          <input id="modelset_enable" type="checkbox" checked />
        </fieldset>
      </details>

      <details id="planePanel" class="hidden" open>
        <summary>裁切面</summary>
        <section>
          <button id="addPlane">+ 增加裁切面</button>
          <div id="planeList"></div>
        </section>
      </details>

      <details id="slicePanel" class="hidden" open>
        <summary>裁切體</summary>
        <section>
          <button id="addSlice">+ 增加裁切體</button>
          <div id="sliceList"></div>
        </section>
      </details>
    </aside>

    <div
      id="MyMap"
      class="map"
      style="width: 100%; height: 100%; position: absolute; top: 0; left: 0"
    ></div>
    <script src="js/components.js" type="module"></script>
    <script src="js/main.js" type="module"></script>
  </body>
</html>

main.js

// 組件實作見 components.js
import {
  ObjectFullControl,
  ObjectAAControl,
  Legend,
  render,
} from "./components.js";

// Voxel模組需要啟用 WebGL2 才能使用
const terrainview = new ov.TerrainView("MyMap", { requestWebGL2: true });
var msl = null;

// Voxel資料列表
const infoList = {
  voxelA: {
    title: "速度資料",
    fieldName: "速度",
    fieldUnit: "m/s",
    dataSrc: "./data/LungShanRH_202301010000.csv",
    colorSet: ["#2b79ba00", "#abdda440", "#ffffbf80", "#fdae61bf", "#d7191c"],
    colorPos: [0.0, 0.25, 0.5, 0.75, 1.0],
  },
  voxelB: {
    title: "溫度資料",
    fieldName: "溫度",
    fieldUnit: "°C",
    dataSrc: "./data/LungShanTC_202301010000.csv",
    colorSet: ["#f6dfc700", "#e5a89b40", "#dd837880", "#bb4a49bf", "#b72917"],
    colorPos: [0.0, 0.25, 0.5, 0.75, 1.0],
  },
  voxelC: {
    title: "氣壓資料",
    fieldName: "氣壓",
    fieldUnit: "巴",
    dataSrc: "./data/LungShanRH_202301080010.csv",
    colorSet: ["#4d589700", "#9866ac40", "#c998c380", "#f4c5e8bf", "#fcefed"],
    colorPos: [0.0, 0.25, 0.5, 0.75, 1.0],
  },
};

/** 現在顯示的Voxel的資訊 */
let current = null;

// 在使用其他圖層前必須要先讀入地形
terrainview.openTerrain(
  {
     url: 'https://data-3dgdp.colife.org.tw/Sample_src/PGWebJS/13.0/oviewRP.ashx', // 或您自己的O'View MapServer服務
     identifier: "範例地形圖",
     callback: openCallback,
  }
);

/** 地形讀入後的回呼 */
function openCallback() {
  // 設定底圖
  terrainview.setBaseLayer({
    url: "BING_MAP",
    identifier: "IMAGE",
  });

  //設定初始位置
  let initialPos = new GeoPoint(
    121.2347878442796,
    23.427553934089445,
    465850.0013822634
  );
  let initialV = new Geo3DPoint(0, 0, -1);
  let initialUp = new Geo3DPoint(0, 1, 0);
  let initialCamera = new ov.Camera(initialPos, initialV, initialUp);
  terrainview.gotoCamera(initialCamera, false);

  terrainview.addModelSetLayer({
    url: "http://127.0.0.1:8080",
    identifier: "產權大樓",
    callback: function (success, layer) {
      if (success) {
        msl = layer;
        msl.setScale(0.5);
        msl.setOffset(14710, 156460, -1930);
        msl.setRotate(96);
      } else {
        console.log("載入不成功");
      }
    },
    urlTemplate: "https://sample.pilotgaea.com.tw/Oview.aspx?{URL}",
  });

  // 初始化場景
  initScene();
}

/** 初始化場景 */
async function initScene() {
  // 先從 TerrainView 取得 Voxel 模組
  const voxel = terrainview.getModule("voxel");
  // 用 Voxel 模組載入資料
  const keys = Object.keys(infoList);
  await Promise.allSettled(
    keys.map((key) => createVoxelEntity(voxel, infoList[key]))
  );

  changeVoxel();
  current.entity.goto(); // 跳到 Voxel 處
  setupEventListeners(voxel);
}

/** 設定使用者輸入事件 */
function setupEventListeners(voxel) {
  const dataRadios = document.querySelectorAll('input[name="data"]');
  const voxelRadio = document.getElementById("showVoxel");
  const planeRadio = document.getElementById("showPlane");
  const opacityRange = document.getElementById("opacity");
  const addPlaneButton = document.getElementById("addPlane");
  const addSliceButton = document.getElementById("addSlice");
  // 切換資料來源
  dataRadios.forEach((radio) => radio.addEventListener("click", changeVoxel));
  // 切換顯示模式
  voxelRadio.addEventListener("change", (e) =>
    toggleVisualization(e.currentTarget.checked)
  );
  planeRadio.addEventListener("change", (e) =>
    toggleVisualization(!e.currentTarget.checked)
  );
  // 透明度
  opacityRange.addEventListener(
    "input",
    () => (voxel.opacity = opacityRange.valueAsNumber)
  );
  // 增加裁切面/體
  addPlaneButton.addEventListener("click", () =>
    current.planeList.push(
      addPlane(current.entity, ObjectFullControl, {
        point: new GeoPoint(0, 0, 3),
        tilt: 90,
        orientation: 0,
      })
    )
  );
  addSliceButton.addEventListener("click", () =>
    current.sliceList.push(
      addSlice(
        current.entity,
        ObjectFullControl,
        {},
        { point: new GeoPoint(0, 0, 3), tilt: 90, orientation: 0 }
      )
    )
  );
}

async function createVoxelEntity(voxel, info) {
  const { title, fieldName, fieldUnit, dataSrc, colorSet, colorPos } = info;
  // 抓資料
  const url = new URL(dataSrc, window.location.href);
  const { data, min, max } = await fetchVoxelDataFromCSV(url);
  info.min = min;
  info.max = max;

  const entity = voxel.addVoxelEntity({
    data,
    width: 50,
    height: 20,
    depth: 3,
    colorSet: colorSet.map((c) => new ov.Color(c)),
    colorPos,
  });

  // 設定點擊時的顯示UI
  entity.setOnClickEntity((value, pos) => {
    new ov.Widget.Attributes({
      title,
      view: terrainview,
      style: { top: `${pos.y}px`, left: `${pos.x}px` },
      content: { [fieldName]: `${value.toFixed(4)} ${fieldUnit}` },
    });
  });

  // 創建可以控制裁切面/裁切體的組件
  info.planeList = [];
  info.sliceList = [];

  const planeCon = addPlane(entity, ObjectFullControl, {
    point: new GeoPoint(0, 0, 3),
    tilt: 70,
    orientation: 45,
  });
  const sliceXCon = addSlice(
    entity,
    ObjectAAControl,
    { axis: "x", min: "-80", max: "80" },
    { point: new GeoPoint(0, 0, 3), tilt: 90, orientation: 270 }
  );
  const sliceYCon = addSlice(
    entity,
    ObjectAAControl,
    { axis: "y", min: "-30", max: "30" },
    { point: new GeoPoint(0, 30, 3), tilt: 90, orientation: 0 }
  );
  const sliceZCon = addSlice(
    entity,
    ObjectAAControl,
    { axis: "z", min: "-0.1", max: "7" },
    { point: new GeoPoint(0, 0, 7), tilt: 0, orientation: 0 }
  );

  // 先不顯示
  planeCon.plane.show = false;
  sliceXCon.slice.show = false;
  sliceYCon.slice.show = false;
  sliceZCon.slice.show = false;

  info.planeList.push(planeCon);
  info.sliceList.push(sliceXCon);
  info.sliceList.push(sliceYCon);
  info.sliceList.push(sliceZCon);

  // 設定點模式和裁切體顯示的輸入事件
  const pointMode = document.getElementById("pointMode");
  const sliceBox = document.getElementById("slice");

  pointMode.addEventListener("change", (e) =>
    entity.update({ pointMode: e.target.checked })
  );
  sliceBox.addEventListener("change", (e) => toggleSlice(e.target.checked));

  //模型開關
  const mlsEnableButton = document.getElementById("modelset_enable");

  mlsEnableButton.addEventListener("change", () => {
    msl.show = mlsEnableButton.checked;
  });

  info.entity = entity;
}

function addPlane(entity, controlComponent, parameter) {
  const plane = entity.addSectionPlane(parameter);
  const control = controlComponent({ obj: plane, parameter });
  document.getElementById("planeList").append(control);
  return { control, plane };
}

function addSlice(entity, controlComponent, controlProps, parameter) {
  const slice = entity.addSlice(parameter);
  if (!slice) return; // 到達裁切上限
  const control = controlComponent({ obj: slice, parameter, ...controlProps });
  document.getElementById("sliceList").append(control);
  return { control, slice };
}

/** 更換顯示的Voxel */
function changeVoxel() {
  const checked = document.querySelector('input[name="data"]:checked').value;
  for (let key in infoList) {
    toggleVoxel(infoList[key], false);
  }
  current = infoList[checked];
  toggleVoxel(current, true);

  // 重置選項
  document.getElementById("showVoxel").checked = true;
  document.getElementById("showPlane").checked = false;
  toggleVisualization(true);
  document.getElementById("slice").checked = false;
  toggleSlice(false);

  // 更新圖例
  render(Legend(current), document.getElementById("legend"));
}

function toggleVoxel(info, show) {
  info.entity.show = show;
  info.entity.sectionPlanes.forEach((p) => (p.show = false));
  info.planeList.forEach((c) => c.control.classList.toggle("hidden", !show));
  info.sliceList.forEach((c) => c.control.classList.toggle("hidden", !show));
}

function toggleVisualization(showEntity) {
  // 體積和裁切面其實可以同時顯示,這裡為了展示擇一顯示
  current.entity.show = showEntity;
  current.entity.sectionPlanes.forEach((p) => (p.show = !showEntity));
  document.getElementById("voxelPanel").classList.toggle("hidden", !showEntity);
  document.getElementById("planePanel").classList.toggle("hidden", showEntity);

  if (!showEntity) {
    document.getElementById("slice").checked = false;
    toggleSlice(false);
  }
}

function toggleSlice(show) {
  current.entity.slices.forEach((p) => (p.show = show));
  document.getElementById("slicePanel").classList.toggle("hidden", !show);
}

async function fetchVoxelDataFromCSV(url) {
  const response = await fetch(url);
  const text = await response.text();
  let min = Infinity;
  let max = -Infinity;
  // 解析CSV
  let data = text
    .split("\r\n")
    .map((line) => line.split(",").map(parseFloat))
    .filter((row) => {
      for (let col of row) {
        if (isNaN(col)) {
          return false;
        }
      }
      // 順便撈一下範圍
      min = Math.min(min, row[3]);
      max = Math.max(max, row[3]);
      return true;
    });
  return { data, min, max };
}

components.js

// 使用到的組件,這裡是直接原生寫的
// 用框架的話可以參考註解的JSX

export function ObjectFullControl(props) {
    const { obj, parameter } = props;
    const point = parameter.point.Clone();

    const xProps = {
      obj, name: "X",
      attr: { type: "range", value: point.x.toString(), min: "-50", max: "50", step: "0.1" },
      update: (value) => { point.x = value; return { point: point.Clone() }; }
    }
    const yProps = {
      obj, name: "Y",
      attr: { type: "range", value: point.y.toString(), min: "-50", max: "50", step: "0.1" },
      update: (value) => { point.y = value; return { point: point.Clone() }; }
    }
    const zProps = {
      obj, name: "Z",
      attr: { type: "range", value: point.z.toString(), min: "0", max: "6", step: "0.1" },
      update: (value) => { point.z = value; return { point: point.Clone() }; }
    }
    const tiltProps = {
      obj, name: "傾斜",
      attr: { type: "range", value: parameter.tilt.toString(), min: "0", max: "90" },
      update: (value) => ({ tilt: value })
    }
    const orientationProps = {
      obj, name: "方位角",
      attr: { type: "range", value: parameter.orientation.toString(), min: "0", max: "360" },
      update: (value) => ({ orientation: value })
    }

    return ObjectControl(
      ControlRow(xProps),
      ControlRow(yProps),
      ControlRow(zProps),
      ControlRow(tiltProps),
      ControlRow(orientationProps));
    // <ObjectControl>
    //   <ControlRow props={xProps}/>
    //   <ControlRow props={yProps}/>
    //   <ControlRow props={zProps}/>
    //   <ControlRow props={tiltProps}/>
    //   <ControlRow props={orientationProps}/>
    // </ObjectControl>
  }

  export function ObjectAAControl(props) {
    const { obj, parameter, axis, min, max } = props;
    const point = parameter.point.Clone();
    const rowProps = {
      obj, name: axis.toUpperCase(),
      attr: { type: "range", value: point[axis].toString(), min, max, step: "0.1" },
      update: (value) => { point[axis] = value; return { point: point.Clone() }; }
    };
    console.log(rowProps);

    return ObjectControl(ControlRow(rowProps));
    // <ObjectControl>
    //   <ControlRow props={rowProps}/>
    // </ObjectControl>
  }

  function ObjectControl(...children) {
    return h("div", null,
      h("hr"),
      h("table", null, ...children));
    // <div>
    //   <hr/>
    //   <table>{children}</table>
    // </div>
  }

  function ControlRow(props) {
    const { obj, name, attr, update } = props;
    let input = h("input", attr);
    input.addEventListener("input", () => {
      const state = update(input.valueAsNumber);
      obj.update(state);
    });

    return h("tr", null,
      h("td", null, name),
      h("td", null, input));
    // <tr>
    //   <td>{ name }</td>
    //   <td><input {...attr} onInput={onInput}></input></td>
    // </tr>
  }

  export function Legend(props) {
    const { min, max, fieldUnit, colorSet, colorPos } = props;

    return h(null, null,
      LegendLabel({ min, max, fieldUnit }),
      LegendBar({ colorSet, colorPos }));

    // <>
    //   <LegendLabel props={ min, max, fieldUnit }/>
    //   <LegendBar props={ colorSet, colorPos }/>
    // </>
  }

  function LegendLabel(props) {
    const { min, max, fieldUnit } = props;
    const style = { display: "flex", justifyContent: "space-between" };

    return h("div", { style },
      h("small", null, `${min} ${fieldUnit}`),
      h("small", null, `${max} ${fieldUnit}`));
    // <div style={style}>
    //   <small>{`${min} ${fieldUnit}`}</small>
    //   <small>{`${max} ${fieldUnit}`}</small>
    // </div>
  }

  function LegendBar(props) {
    const { colorSet, colorPos } = props;
    const tokens = [];
    for (let i = 0; i < colorSet.length && i < colorPos.length; i++) {
      tokens.push(`${colorSet[i].substring(0, 7)} ${colorPos[i] * 100}%`);
    }
    const gradient = `linear-gradient(to right, ${tokens.join(", ")})`;
    const style = { width: "100%", height: "20px", backgroundImage: gradient, margin: "5px 0" };

    return h("div", { style });
    // <div style={style}>
  }

  function h(name, attr, ...content) {
    const e = (name && document.createElement(name)) || document.createDocumentFragment();
    name && attr && Object.keys(attr).forEach(key => {
      if (key in e) {
        if (key === "style") {
          Object.keys(attr.style).forEach(rule => e.style[rule] = attr.style[rule]);
        } else {
          e[key] = attr[key];
        }
      } else {
        e.setAttribute(key, attr[key]);
      }
    });
    content && content.forEach(c => {
      if (Array.isArray(c)) {
        e.append(...c);
      } else if (c !== null && c !== false) {
        e.append(c);
      }
    });
    return e;
  }

  export function render(component, domNode) {
    // 先清空內容
    while (domNode.firstChild) { 
      domNode.removeChild(domNode.firstChild); 
    }
    domNode.append(component);
  }
Copyright © NCHC 2022 Version:13.0 all right reserved,powered by Gitbook修訂時間: 2024-12-02 15:55:18