模型切割API開發
[info] 小提示:
程式碼連結:https://doc-3dgdp.colife.org.tw/samplecode/#src/testweb/photogrammetryModel_clip_from_shp/
功能說明
本範例文件說明如何在傾斜攝影圖層中進行以下功能
- 呼叫 inputSurfacePolygon API 來選擇範圍
- 使用 shapefile 來選擇範圍
- 呼叫 addClipPolygon API 來裁切選定範圍
- 呼叫 addClipPolygon API 來保留選定範圍
- 呼叫 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%);
}