模型切割API開發


[info] 小提示:

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


功能說明

本範例文件說明如何在傾斜攝影圖層中進行以下功能

  1. 呼叫 inputSurfacePolygon API 來選擇範圍
  2. 使用 shapefile 來選擇範圍
  3. 呼叫 addClipPolygon API 來裁切選定範圍
  4. 呼叫 addClipPolygon API 來保留選定範圍
  5. 呼叫 addClipPolygon API 來對選定範圍進行三維裁切

[info] 特別提醒:

讀取 shapefile 依賴開源套件, 可從 SandBox 中取得, 或者直接到 github 下載
https://github.com/calvinmetcalf/shapefile-js

如果需要處理 Big5 編碼, 可使用這個基於原版擴充編碼選擇功能的套件
https://github.com/Rainbowrain-TW/shapefile-js

index.html

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">

<head>
  <meta charset="utf-8" />
  <title>Model Slice DEMO</title>
  <!-- 引用 js -->
  <script src="PGWeb3D.min.js"></script>
  <script src="shp.js"></script>
  <!-- 引用 css -->
  <link href="PGWeb3D.css" rel="stylesheet" type="text/css" />
  <link href="main.css" rel="stylesheet" type="text/css" />
</head>

<body>
  <!-- 功能控制面板 -->
  <div id="MyControl">
    <label><input type="radio" name="polygonType" value="remove" checked />裁切</label>
    <label><input type="radio" name="polygonType" value="keep" />保留</label>
    <label><input type="radio" name="polygonType" value="3Dremove" />三維裁切</label>
    <table>
      <tr>
        <td>起始高</td>
        <td><input type="number" id="startHeight" disabled="true" /></td>
      </tr>
      <tr>
        <td>結束高</td>
        <td><input type="number" id="endHeight" disabled="true" /></td>
      </tr>
    </table>
    <button id="addClip">選擇範圍</button>
    <button id="removeClip">刪除</button>
    <label id="mouseHeight"></label>
    <hr />
    <div class="ShpSelectorContainer">
      <div>讀取SHP檔案作為選取範圍</div>
      <div>
        <label for="EPSG_List">EPSG:</label>
        <input list="EPSG_List" id="EPSG_Input" name="EPSG_Input" placeholder="輸入EPSG代號或從下拉選單中選擇"
          onfocus="this.value=null;" onchange="this.blur();" />
        <datalist id="EPSG_List">
          <option value="4326">WGS84 / GPS Coordinates</option>
          <option value="3857">WGS84 / Web Mercator</option>
          <option value="3826">TWD97 / TM2 zone 121</option>
          <option value="3825">TWD97 / TM2 zone 119</option>
        </datalist>
      </div>
      <div>
        <!-- file input,限制檔案類型為 zip 及 shp -->
        <input type="file" id="shpfileInput" onchange="setPolygonPreview()" accept=".zip,.shp" />
        <span class="shpfileInputHint"></span><br />
      </div>
      <div>
        <button id="btnTogglePreview" onclick="togglePreview()">
          查看SHP範圍
        </button>
        <button id="btnSumbitSHP" onclick="setClipPolygonFromSHP()">
          使用SHP範圍做裁切
        </button>
      </div>
      <div></div>
    </div>
  </div>
  <!-- 圖台初始化對象 -->
  <div id="MyMap" style="width: 100%; height: 100%; position: absolute; top: 0; left: 0;"></div>
  <script src="main.js"></script>
</body>

</html>

main.js

// 建立 EPSGEngine 用來轉換座標
const epsgEng = GetEPSGEngine();
window.epsgEng = epsgEng;

// Local Server
const localServerIp = "127.0.0.1";
const localServerPort = "8080";

// 初始化圖台
var terrainview = new ov.TerrainView("MyMap");

// 傾斜攝影建模圖層
var photogrammetryLayer = null;

// 載入地形
terrainview.openTerrain({
  url: `http://${localServerIp}:${localServerPort}`,
  identifier: "terrain",
  urlTemplate: "https://sample.pilotgaea.com.tw/Oview.aspx?{URL}",
  callback: openCallback
});

// 地形載入完成後的callback
function openCallback(result) {
  //設定底圖
  terrainview.setBaseLayer({
    url: "BING_MAP",
    identifier: "VECTOR"
  });

  // 設定滑鼠移動事件-顯示高度
  document.getElementById("MyMap").addEventListener("mousemove", function () {
    let pos = terrainview.mousePos;
    if (pos !== null) {
      document.getElementById("mouseHeight").innerHTML =
        "高度: " + pos.z.toFixed(2).toString() + "(m)";
    }
  });

  // 設定裁切模式切換事件-設定為裁切模式
  document
    .getElementsByName("polygonType")[0]
    .addEventListener("change", function () {
      if (this.checked) {
        document.getElementById("startHeight").disabled = true;
        document.getElementById("endHeight").disabled = true;
        document.getElementById("btnSumbitSHP").innerText = "使用SHP範圍做裁切";
      }
    });

  // 設定裁切模式切換事件-設定為保留模式
  document
    .getElementsByName("polygonType")[1]
    .addEventListener("change", function () {
      if (this.checked) {
        document.getElementById("startHeight").disabled = true;
        document.getElementById("endHeight").disabled = true;
        document.getElementById("btnSumbitSHP").innerText = "使用SHP範圍做保留";
      }
    });

  // 設定裁切模式切換事件-設定為三維裁切模式
  document
    .getElementsByName("polygonType")[2]
    .addEventListener("change", function () {
      if (this.checked) {
        document.getElementById("startHeight").disabled = false;
        document.getElementById("endHeight").disabled = false;
        document.getElementById("btnSumbitSHP").innerText =
          "使用SHP範圍做三維裁切";
      }
    });

  //設定初始位置
  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.addPhotogrammetryModelLayer({
    url: "http://127.0.0.1:8080",
    identifier: "範例傾斜攝影模型圖",
    urlTemplate: "https://sample.pilotgaea.com.tw/Oview.aspx?{URL}",
    callback: function (success, layer) {
      if (success) {
        photogrammetryLayer = layer;
        photogrammetryLayer.setMaxVisibleDistance(9000);

        // 加入村里界圖層
        photogrammetryLayer.goto(() => {
          AddWMTSOverlayFromNLSC("Village");
        });
      } else {
        console.log("傾斜攝影不成功");
      }
    }
  });
}

// 設定按鈕事件-使用滑鼠點擊選取範圍並按選擇的裁切模式處理選取區域
document.getElementById("addClip").addEventListener("click", function () {
  // 移除選取區域預覽圖層
  removePreviewLayer();

  // 取得裁切模式
  let type = getClipType();

  // 呼叫 inputSurfacePolygon API 來選取範圍
  terrainview.inputSurfacePolygon({
    color: new ov.Color("#FF0000"),
    onCompleted: function (ret) {
      // 按裁切模式加入裁切區域 addClipPolygon, ret.geo 為選取區域的座標資料
      addClipPolygon(ret.geo);
    }
  });
});

// 設定按鈕事件-刪除先前建立的裁切區域
document.getElementById("removeClip").addEventListener("click", function () {
  // 刪除先前在Acute3DLayer上的裁切區域
  // ov.Acute3DLayer.removeClipPolygon(index)
  // @param {number} index 索引,不填則全刪
  // @return {bool} 是否刪除成功
  photogrammetryLayer.removeClipPolygon();
});

// 取得裁切模式
function getClipType() {
  let type = "";
  type = document.getElementsByName("polygonType")[0].checked ? "remove" : type;
  type = document.getElementsByName("polygonType")[1].checked ? "keep" : type;
  type = document.getElementsByName("polygonType")[2].checked ? "3Dremove" : type;
  return type;
}

// 按裁切模式加入裁切區域addClipPolygon
function addClipPolygon(geo) {
  // 依傳入的座標資料建立多邊形區域
  let polygon = new GeoPolygon(geo);

  // 取得裁切模式
  let type = getClipType();

  // 在PhotogrammetryLayer上加入要裁切的區域
  // ov.PhotogrammetryLayer.addClipPolygon(polygon)
  // @param {GeoPolygon} polygon 多邊形區域
  // @param {ov.CLIP_MODE} mode 模式
  // @return {bool} 是否加入成功
  // photogrammetryLayer.addClipPolygon(polygon, ov.CLIP_MODE.REMOVE);

  // 裁切模式
  if (type == "remove") {
    photogrammetryLayer.addClipPolygon(polygon, ov.CLIP_MODE.REMOVE);
  }
  // 保留模式
  else if (type == "keep") {
    photogrammetryLayer.addClipPolygon(polygon, ov.CLIP_MODE.KEEP);
  }
  // 三維裁切模式
  else if (type == "3Dremove") {
    // 取得用於三維裁切的起始高與結束高
    let startHeight = parseFloat(document.getElementById("startHeight").value);
    let endHeight = parseFloat(document.getElementById("endHeight").value);

    // 在PhotogrammetryLayer上加入要三維裁切的區域
    // ov.PhotogrammetryLayer.addClip3DPolygon(polygon)
    // @param {GeoPolygon} polygon 多邊形區域
    // @param {Number} startHeight 起始高
    // @param {Number} endHeight 結束高
    // @return {bool} 是否加入成功
    // photogrammetryLayer.addClip3DPolygon(polygon, startHeight, endHeight);
    photogrammetryLayer.addClip3DPolygon(polygon, startHeight, endHeight);
  }
}

// 設定SHP範圍預覽
async function setPolygonPreview() {
  // 如果 epsg 是空字串或是 EPSGEngine 不支援則不做任何事
  if (verifyEPSG(document.getElementById("EPSG_Input").value) === false) return;

  // 取得 EPSG
  const epsg = Number(document.getElementById("EPSG_Input").value);

  try {
    // 從檔案取得 geometry
    const geometry = await getGeometryFromFile();
    // 建立SHP範圍預覽圖層
    createPolygonAreaPreview(geometry, epsg);
  } catch (error) {
    console.log("error :>> ", error);
    alert("讀取檔案失敗");
  }
}

// 按SHP範圍預覽加入裁切區域
async function setClipPolygonFromSHP() {
  // 如果 epsg 是空字串或是 EPSGEngine 不支援則不做任何事
  if (verifyEPSG(document.getElementById("EPSG_Input").value) === false) return;

  // 取得 EPSG
  const epsg = Number(document.getElementById("EPSG_Input").value);

  try {
    // 從檔案取得 geometry
    const geometry = await getGeometryFromFile();
    // 將 geometry 的座標轉換成指定的 EPSG
    const transferedGeo = transferGeometryEPSG(geometry, epsg, 4326);
    // 依轉換後的座標資料建立多邊形區域
    const geo = new window.GeoPolygon(transferedGeo);
    // 按裁切模式加入裁切區域
    addClipPolygon(geo);
    // 移除選取區域預覽圖層
    removePreviewLayer();
  } catch (error) {
    console.log("error :>> ", error);
    alert("讀取檔案失敗");
  }
}

// 從檔案取得geometry
function getGeometryFromFile() {
  // 檔案讀取需要使用 Promise 來處理非同步作業
  return new Promise((resolve, reject) => {
    // 取得及檢查 input file
    const inputElement = document.getElementById("shpfileInput");
    const files = inputElement.files;

    if (files.length === 0) {
      removePreviewLayer();
      resolve();
    }

    // 取得檔案
    const file = files[0];
    const fileExtension = getFileExtension(file.name);

    // 檢查檔案副檔名
    if (fileExtension !== "zip" && fileExtension !== "shp") {
      alert("請選擇 Shapefile 或包含 Shapefile 的 .zip 壓縮檔");
      resolve();
    }

    // 建立 FileReader
    const reader = new FileReader();

    // 設定讀取完成後的 callback
    reader.onload = async function (e) {
      // 取得 arrayBuffer
      const arrayBuffer = e.target.result;

      let geometry;

      // 使用 shpjs 解析 Shapefile
      try {
        // 依檔案副檔名使用不同的解析方式        
        if (fileExtension === "zip") {
          // 解析 zip 檔取得 geometry
          const geojsonData = await shp(arrayBuffer);          
          geometry = geojsonData.features[0].geometry;
        } else if (fileExtension === "shp") {
          // 解析 shp 檔取得 geometry
          geometry = shp.parseShp(arrayBuffer)[0];
        }
      } catch (error) {
        shpErrorHandler(error);
        resolve();
      }

      // 回傳 geometry
      if (geometry) {
        resolve(geometry);
      } else {
        reject("Read Geometry from file failed");
      }
    };

    // 設定讀取失敗後的 callback
    reader.onerror = function () {
      alert("讀取文件時出錯");
    };

    // 開始讀取檔案
    reader.readAsArrayBuffer(file);
  });
}

// 將 geometry 的座標轉換成指定的 EPSG
function transferGeometryEPSG(geometry, fromEPSG, toEPSG) {
  const newGeometry = geometry.coordinates[0].map((coord) => {
    const [x, y] = coord;
    const coordinate = { x, y };
    epsgEng.Transfer(fromEPSG, toEPSG, coordinate);
    return new window.GeoPoint(coordinate);
  });

  return newGeometry;
}

// 驗證EPSG是否為數字
function verifyEPSG(epsg) {
  // 如果 epsg 是空字串或是 EPSGEngine 不支援
  if (epsgEng.GetName(Number(epsg)) === "") {
    alert("請輸入正確的 EPSG");
    return false;
  }
  return true;
}

// 加入wmts圖層
function AddWMTSOverlayFromNLSC(identifier) {
  terrainview.addTerrainWMTSOverlay(
    {
      url: "https://wmts.nlsc.gov.tw/wmts",
      identifier: identifier,
      layername: identifier
    },
    function (info) {
      console.log("[WMTS] addTerrainWMTSOverlay response info :>> ", info);
      wmts_v = info;
    }
  );
}

// 處理 shpjs 的錯誤
function shpErrorHandler(error) {
  switch (error.message) {
    case "no layers founds":
      alert(
        "請選擇包含 Shapefile 的 .zip 壓縮檔或檢查您選擇的 Shapefile 是否正確"
      );
      break;
    case "forgot to pass buffer":
    default:
      alert("解析 Shapefile 時出錯");
      break;
  }
}

// 取得檔案副檔名
function getFileExtension(filename) {
  const lastDot = filename.lastIndexOf(".");
  return lastDot === -1 ? "" : filename.substring(lastDot + 1);
}

// 建立SHP範圍預覽圖層
function createPolygonAreaPreview(geometry, epsg) {
  // 移除選取區域預覽圖層
  removePreviewLayer();

  // 建立自畫圖層
  let customLayer = terrainview.addCustomLayer({ layername: "previewPolygon" });

  // 建立自畫物件參數
  var param = {};
  param.geo = new window.GeoPolygon();
  param.geo.FromGeoJSON(JSON.stringify(geometry));
  param.color = new window.ov.Color("#FF0000");
  param.opacity = 0.5;
  param.epsg = epsg;
  param.coverModel = true;

  // 加入自畫物件來預覽選取區域
  var shapeEntity = customLayer.addSurfacePolygonSetEntity(param);
  shapeEntity.goto();
}

// 切換SHP範圍預覽圖層
function togglePreview() {
  // 取得及檢查 input file
  const inputElement = document.getElementById("shpfileInput");
  const files = inputElement.files;
  if (files.length === 0) {
    alert("請先選擇檔案");
    return;
  }

  // 取得預覽圖層
  const previewLayer = terrainview.findLayer("previewPolygon");

  // 切換預覽圖層的顯示狀態, 如果沒有預覽圖層則建立預覽圖層
  if (previewLayer) {
    previewLayer.show = !previewLayer.show;
  } else {
    setPolygonPreview();
  }
}

// 移除SHP範圍預覽圖層
function removePreviewLayer() {
  terrainview.findLayer("previewPolygon") &&
    terrainview.removeLayer(terrainview.findLayer("previewPolygon"));
}

// EPSG輸入框點擊事件-清空內容
function onEpsgInputClick(el) {
  el.value = "";
  el.focus();
}

main.css

#MyControl {
  padding: 5px;
  font-size: 16px;
  font-family: "Microsoft JhengHei", Arial, Helvetica, sans-serif;
  position: absolute;
  z-index: 1;
  color: white;
  background-color: black;
}

table {
  color: white;
}

.ShpSelectorContainer {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: start;
  gap: 1px;
}

.shpfileInputHint {
  background-color: gray;
  border-radius: 50%;
  margin-right: 5px;
  font-size: 8px;
  padding: 2px;
  position: relative;
}

#EPSG_Input {
  width: 80px;
}

.shpfileInputHint:hover::after {
  content: "選擇你的shp檔(或包含 .shp 的 zip 檔)";
  position: absolute;
  width: 200px;
  background-color: #222;
  border: 1px solid white;
  color: silver;
  font-size: 12px;
  top: 25px;
  transform: translateX(-50%);
}
Copyright © NCHC 2022 Version:13.0 all right reserved,powered by Gitbook修訂時間: 2024-11-20 13:52:09