我們使用Visual Studio 2022開發,把下載好後的PotreeDesktop原始碼新增到Visual Studio中。
開啟Visual Studio 2022,新建Asp.Net Core空專案,如下圖所示。
點選下一步按鈕,設定專案的名稱、儲存路徑以及解決方案名稱等。如下圖所示。
點選下一步按鈕,最後點選建立按鈕,完成建立工作。建立後的根目錄如下圖所示。
開啟WOBM.Potree.Web目錄,裡面的內容如下圖所示。
建立的工程在Visual Studio中的工程樹如下圖所示。
接下來,我們把PotreeDesktop下的原始碼,拷貝到WOBM.Potree.Web目錄下,拷貝後,資料夾組織如下圖所示。
從Visual Studio的工程樹上,通過新增、刪除和專案中排除等方法,最終工程樹的組織如下圖所示。
此時,雙擊PotreeDesktop.bat,如果系統能夠成功啟動,那麼開發環境就算搭建成功了,接下來就可以在Visual Studio寫我們自定義的html和js程式碼了。
系統是從PotreeDesktop.bat開始啟動的,裡面的內容如下所示。
start ./node_modules/electron/dist/electron.exe ./main
系統從electron.exe啟動,後面跟了一個引數,引數啟動了main.js檔案。在main.js中有下面一句程式碼。
mainWindow.loadFile(path.join(__dirname, 'index.html'));
該程式碼的意思,是在mainWindow中載入index.html檔案,所以index.html頁面是我們展示的主頁面。開啟index.html頁面,我們可以看到,該頁面就是我們平常開發的Web頁面,把這個頁面單獨放到Web伺服器,然後通過瀏覽器存取也是可以的。
在index.html頁面中,我們看到了熟悉的程式碼,例如對其他js庫的參照,html元素的定義和一些js程式碼。js程式碼中,定義了Potree.Viewer,也就是顯示點雲的主UI。
整個執行流程如下,通過PotreeDesktop.bat啟動electron.exe,electron.exe載入解析main.js檔案,在main.js檔案中設定載入index.html頁面,至此完成了我們開發的主頁面的載入。
const electron = require('electron') const app = electron.app const BrowserWindow = electron.BrowserWindow const Menu = electron.Menu; const MenuItem = electron.MenuItem; const remote = electron.remote; const path = require('path') const url = require('url') let mainWindow
通過這段程式碼,獲取了對electron的參照,app為系統的主App,BrowserWindow是主對話方塊,Menu是主對話方塊中的選單。接下來對app進行操作。
app.on('ready', createWindow) app.on('window-all-closed', function () { if(process.platform !== 'darwin') { app.quit() } }) app.on('activate', function () { if(mainWindow === null) { createWindow() } })
當app準備好之後,系統會呼叫createWindow函數。當所有的視窗關閉後,呼叫app的quit函數。當app被啟用的時候,如果當前的mainWindow為null的話,呼叫createWindow函數。
下面我們順藤摸瓜,看下定義在main.js中的createWindow函數。程式碼如下。
function createWindow () { mainWindow= new BrowserWindow({ width:1600, height:1200, webPreferences:{ nodeIntegration:true, backgroundThrottling:false, } }) mainWindow.loadFile(path.join(__dirname,'index.html')); lettemplate = [ { label:"Window", submenu:[ {label:"Reload", click() {mainWindow.webContents.reloadIgnoringCache() }}, {label:"Toggle Developer Tools", click() {mainWindow.webContents.toggleDevTools() }}, ] } ]; letmenu = Menu.buildFromTemplate(template); mainWindow.setMenu(menu); { const{ ipcMain } = require('electron'); ipcMain.on('asynchronous-message',(event, arg) => { console.log(arg)// prints "ping" event.reply('asynchronous-reply','pong') }) ipcMain.on('synchronous-message',(event, arg) => { console.log(arg)// prints "ping" event.returnValue= 'pong' }) } mainWindow.on('closed',function () { mainWindow= null }) }
在該程式碼中,系統初始化了一個BrowserWindow物件,也就是初始化一個主對話方塊,設定了寬度為1600,寬度為1200。呼叫mainWindow.loadFile函數,載入Index.html頁面。下面的程式碼,就是組織主選單上的選單,我們開發的時候,一般不用electron上定義的選單,而直接在Index.html中的選單。所以實際開發的時候,會把這段程式碼去掉。
最後是監聽mainWindow的closed事件,當表單關閉後,把主變數mainWindow設定為null。
在該檔案的Head部分,系統參照了一些css,程式碼如下所示。
<link rel="stylesheet" type="text/css" href="./libs/potree/potree.css"> <link rel="stylesheet" type="text/css" href="./libs/jquery-ui/jquery-ui.min.css"> <link rel="stylesheet" type="text/css" href="./libs/openlayers3/ol.css"> <link rel="stylesheet" type="text/css" href="./libs/spectrum/spectrum.css"> <link rel="stylesheet" type="text/css" href="./libs/jstree/themes/mixed/style.css"> <link rel="stylesheet" type="text/css" href="./src/desktop.css">
<script> if (typeof module === 'object') { window.module = module; module = undefined; } </script> <script src="./libs/jquery/jquery-3.1.1.min.js"></script> <script src="./libs/spectrum/spectrum.js"></script> <script src="./libs/jquery-ui/jquery-ui.min.js"></script> <script src="./libs/other/BinaryHeap.js"></script> <script src="./libs/tween/tween.min.js"></script> <script src="./libs/d3/d3.js"></script> <script src="./libs/proj4/proj4.js"></script> <script src="./libs/openlayers3/ol.js"></script> <script src="./libs/i18next/i18next.js"></script> <script src="./libs/jstree/jstree.js"></script> <script src="./libs/potree/potree.js"></script> <script src="./libs/plasio/js/laslaz.js"></script>
<div class="potree_container" style="position: absolute; width: 100%; height: 100%; left: 0px; top: 0px; "> <div id="potree_render_area"></div> <div id="potree_sidebar_container"></div> </div>
其中potree_render_area用來放主渲染UI物件Viewer,potree_sidebar_container用來放左側的功能面板。
let elRenderArea = document.getElementById("potree_render_area"); let viewerArgs = { noDragAndDrop: true }; window.viewer = new Potree.Viewer(elRenderArea, viewerArgs); viewer.setEDLEnabled(true); viewer.setFOV(60); viewer.setPointBudget(3 * 1000 * 1000); viewer.setMinNodeSize(0); viewer.loadSettingsFromURL(); viewer.setDescription(""); viewer.loadGUI(() => { viewer.setLanguage('en'); $("#menu_appearance").next().show(); $("#menu_tools").next().show(); $("#menu_scene").next().show(); $("#menu_filters").next().show(); viewer.toggleSidebar(); });
系統啟動後,整個系統的主介面就出來了,如下圖所示。
左側為功能面板區,是potree_sidebar_container對應的區域,右側為點雲主顯示區,是potree_render_area對應的區域。系統啟動後,我們會發現,可以通過把las檔案拖到介面上的方式,處理和載入點雲資料。這個功能是在desktop.js中實現的。
在index.html中定義了和拖動las檔案相關的程式碼,如下所示。
import { loadDroppedPointcloud, createPlaceholder, convert_17, convert_20, doConversion, dragEnter, dragOver, dragLeave, dropHandler, } from "./src/desktop.js"; const shell = require('electron').shell; $(document).on('click', 'a[href^="http"]', function (event) { event.preventDefault(); shell.openExternal(this.href); }); let elBody = document.body; elBody.addEventListener("dragenter", dragEnter, false); elBody.addEventListener("dragover", dragOver, false); elBody.addEventListener("dragleave", dragLeave, false); elBody.addEventListener("drop", dropHandler, false);
從desktop.js檔案參照 dragEnter,dragOver, dragLeave, dropHandler等模組。然後在document.body上註冊事件,在dragenter的時候,呼叫desktop.js定義的dragEnter模組,dragover的時候,呼叫dragOver函數,dragleave的時候,呼叫dragleave函數,觸發drop事件的時候,呼叫dropHandler函數。
下面我們再看下desktop.js檔案中,關於這幾個函數和模組的定義。
export function dragEnter(e) { e.dataTransfer.dropEffect = 'copy'; e.preventDefault(); e.stopPropagation(); console.log("enter"); showDropzones(); return false; } export function dragOver(e){ e.preventDefault(); e.stopPropagation(); showDropzones(); return false; } export function dragLeave(e){ e.preventDefault(); e.stopPropagation(); hideDropzones(); return false; }
這三個函數定義的比較簡單,當拖到主區域的時候,顯示拖拽區域,當移動到主區域的時候,也顯示拖拽區域,當離開的時候,則隱藏該區域。顯示拖拽區域的效果如下圖所示。
當滑鼠放下之後,系統會呼叫dropHandler函數,該函數的定義如下。
export async function dropHandler(event) { event.preventDefault(); event.stopPropagation(); hideDropzones(); let u = event.clientX / document.body.clientWidth; console.log(u); const cloudJsFiles = []; const lasLazFiles = []; let suggestedDirectory = null; let suggestedName = null; for (let i = 0; i < event.dataTransfer.items.length; i++) { let item = event.dataTransfer.items[i]; if (item.kind !== "file") { continue; } let file = item.getAsFile(); let path = file.path; const fs = require("fs"); const fsp = fs.promises; const np = require('path'); const whitelist = [".las", ".laz"]; let isFile = fs.lstatSync(path).isFile(); if (isFile && path.indexOf("cloud.js") >= 0) { cloudJsFiles.push(file.path); } else if (isFile && path.indexOf("metadata.json") >= 0) { cloudJsFiles.push(file.path); } else if (isFile) { const extension = np.extname(path).toLowerCase(); if (whitelist.includes(extension)) { lasLazFiles.push(file.path); if (suggestedDirectory == null) { suggestedDirectory = np.normalize(`${path}/..`); suggestedName = np.basename(path, np.extname(path)) + "_converted"; } } } else if (fs.lstatSync(path).isDirectory()) { console.log("start readdir!"); const files = await fsp.readdir(path); console.log("readdir done!"); for (const file of files) { const extension = np.extname(file).toLowerCase(); if (whitelist.includes(extension)) { lasLazFiles.push(`${path}/${file}`); if (suggestedDirectory == null) { suggestedDirectory = np.normalize(`${path}/..`); suggestedName = np.basename(path, np.extname(path)) + "_converted"; } } else if (file.toLowerCase().endsWith("cloud.js")) { cloudJsFiles.push(`${path}/${file}`); } else if (file.toLowerCase().endsWith("metadata.json")) { cloudJsFiles.push(`${path}/${file}`); } }; } } if (lasLazFiles.length > 0) { doConversion(lasLazFiles, suggestedDirectory, suggestedName); } for (const cloudjs of cloudJsFiles) { loadDroppedPointcloud(cloudjs); } return false; };
該程式碼中,判斷拖入系統的內容,可能是檔案或者目錄,系統需要取出拖入內容包含的cloud.js檔案、metadata.json檔案或者.las和.laz檔案。如果是cloud.js和metadata.json檔案,則不需要進行轉換,直接呼叫loadDroppedPointcloud函數載入即可。如果檔案是.las和.laz檔案,則呼叫doConversion函數。
export function loadDroppedPointcloud(cloudjsPath) { const folderName = cloudjsPath.replace(/\\/g, "/").split("/").reverse()[1]; Potree.loadPointCloud(cloudjsPath).then(e => { let pointcloud = e.pointcloud; let material = pointcloud.material; pointcloud.name = folderName; viewer.scene.addPointCloud(pointcloud); let hasRGBA = pointcloud.getAttributes().attributes.find(a => a.name === "rgba") !== undefined if (hasRGBA) { pointcloud.material.activeAttributeName = "rgba"; } else { pointcloud.material.activeAttributeName = "color"; } material.size = 1; material.pointSizeType = Potree.PointSizeType.ADAPTIVE; viewer.zoomTo(e.pointcloud); }); };
獲取點雲檔案路徑後,呼叫Potree.loadPointCloud函數,載入點雲資料。該函數執行後,獲取pointcloud,系統呼叫viewer.scene.addPointCloud函數,把開啟的點雲資料載入到場景中。後面的程式碼是判斷該點雲是否包含RGBA屬性,如果包含,則使用rgba方式顯示點雲,如果不包含,則使用單顏色模式顯示。
程式碼最後,設定點顯示大小,大小模式並把場景縮放至該點雲資料。
當檔案是.las和.laz時候,呼叫doConversion函數,該函數會判斷當前使用者選擇的是使用1.7版本的轉換程式,還是使用2.0版本的轉換程式,選擇不同,呼叫desktop.js中定義的不同轉換函數,以2.0為例,其定義程式碼如下所示。
export function convert_20(inputPaths, chosenPath, pointcloudName) { viewer.postMessage(message, { duration: 15000 }); const { spawn, fork, execFile } = require('child_process'); let exe = './libs/PotreeConverter2/PotreeConverter.exe'; let parameters = [ ...inputPaths, "-o", chosenPath ]; const converter = spawn(exe, parameters); let placeholder = null; let outputBuffer = ""; converter.stdout.on('data', (data) => { const string = new TextDecoder("utf-8").decode(data); console.log("stdout", string); }); converter.stderr.on('data', (data) => { console.log("=="); console.error(`stderr: ${data}`); }); converter.on('exit', (code) => { console.log(`child process exited with code ${code}`); const cloudJS = `${chosenPath}/metadata.json`; console.log("now loading point cloud: " + cloudJS); let message = `conversion finished, now loading ${cloudJS}`; viewer.postMessage(message, { duration: 15000 }); Potree.loadPointCloud(cloudJS).then(e => { let pointcloud = e.pointcloud; let material = pointcloud.material; pointcloud.name = pointcloudName; let hasRGBA = pointcloud.getAttributes().attributes.find(a => a.name === "rgba") !== undefined if (hasRGBA) { pointcloud.material.activeAttributeName = "rgba"; } else { pointcloud.material.activeAttributeName = "color"; } material.size = 1; material.pointSizeType = Potree.PointSizeType.ADAPTIVE; viewer.scene.addPointCloud(pointcloud); viewer.zoomTo(e.pointcloud); }); }); }
系統通過spawn呼叫PotreeConverter.exe,並傳入組織好的引數,引數包括輸入檔案和輸出檔案路徑等。在spawn執行的過程中,可以通過converter.stdout.on('data', (data)捕捉進度資訊,通過converter.stderr.on('data', (data)捕捉錯誤資訊,通過converter.on('exit', (code)捕捉執行結束事件。當執行結束後,轉換後的結果會包含metadata.json檔案,然後呼叫Potree.loadPointCloud函數載入該檔案即可,載入的過程和上面提到的直接載入點雲資料一致。