AR模組開發教學
[info] 小提示:
程式碼連結:https://doc-3dgdp.colife.org.tw/samplecode/#src/testweb/ar-sample/
初始化AR
AR初始化須依賴核心生成的div
,所以需要寫在地形開啟後的CallBack
中。
這裡先撰寫一個最基本的圖台,並帶有AR控制按鈕。
[info] 特別提醒:
若是系統沒有偵測到AR設備,在初始化AR時會將按鈕狀態設為disabled無法點擊進入。
index.html
:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<meta charset="utf-8" />
<script src="./PGWeb3D.min.js"></script>
<script src="https://sample.pilotgaea.com.tw/demo/index/src/PGWebJS/13.0/PGWeb3DMilitary.min.js"></script>
<link rel="stylesheet" type="text/css"
href="https://sample.pilotgaea.com.tw/demo/index/src/PGWebJS/13.0/css/PGWeb3D.css" />
<link rel="stylesheet" type="text/css" href="https://sample.pilotgaea.com.tw/demo/index/css/index.css" />
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<style>
.baseBtn{
font-size: 20px;width:100px;height: 80px;-webkit-user-select: none;-moz-user-select: none;-o-user-select: none;user-select: none;
}
.btnRow{
display: flex;
justify-content: space-between;
pointer-events: auto;
}
#MyControl{
position: absolute;z-index: 1;color: white;margin-top: 10px;
pointer-events: none;
}
.ov-widget-timeline-block-tooltip{
display: none;
cursor: none;
}
.ov-widget-timeline,
.ov-widget-timelinePlayer
{
pointer-events: auto;
}
</style>
</head>
<body>
<div id="MyControl">
<div class="btnRow">
<button id="ar" class="baseBtn" disabled>開啟AR</button>
<button id="adjust" class="baseBtn">校正地形</button>
<button id="north" class="baseBtn">北方校正</button>
<button id="gps" class="baseBtn">GPS資訊</button>
</div>
<div class="btnRow">
<button id="surface" class="baseBtn">開關地形</button>
<button id="model" class="baseBtn">新增模型</button>
<button id="flightaware" class="baseBtn" style="width: 204px" class="baseBtn">載入飛行軌跡</button>
</div>
<div>
<div id="coordInfo" style="opacity: 0.95;"></div>
<div id="orientationInfo" style="opacity: 0.95;"></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
:
var terrainview = new window.ov.TerrainView("MyMap");
var buttonAR = document.getElementById("ar");
var customLayer = null;
var lastTimeUpdate = new Date().getTime();
var surface = false;
var text = "";
var isARMode = false;
var isApple = detectMobileAppleDevice();
var orientationWatcher;
var positionWatcher;
var isMobile = detectMobileDevice();
const DEGTORAD = .017453292519943295;
const RADTODEG = 57.29577951308232;
// FlightAware variables
var FlightAwareEntity = null;
var widgetTimeline = null;
var widgetTimelinePlayer = null;
var earliestTime = 0;
var latestTime = 0;
var playSpeed = 40; // 400
// var originPosition = new window.GeoPoint( 120.6835274639447, 24.138971359241935, 80);
// var originPosition = new window.GeoPoint(121.18684433838007, 24.87455509941111, 296.5079825706828);
var originPosition = new window.GeoPoint(121.18684433838007, 24.87455509941111, 296.5079825706828);
var con = document.getElementById("MyControl");
console.log(`outerHeight: ${window.outerHeight}, innerHeight: ${window.innerHeight}`)
console.log(`outerWidth: ${window.outerWidth}, innerWidth: ${window.innerWidth}`)
var surfacebutton = document.getElementById("surface");
surfacebutton.addEventListener("click", switchTerrainSurface);
var dialog = document.getElementById("dialog");
var modelbutton = document.getElementById("model");
modelbutton.addEventListener("click", addModel);
var adjustbutton = document.getElementById("adjust");
adjustbutton.addEventListener("click", adjustSurface);
var gpsbutton = document.getElementById("gps");
gpsbutton.addEventListener("click", getgps);
var northButton = document.getElementById("north");
northButton.addEventListener("click", northSet);
var flightwareButton = document.getElementById("flightaware");
flightwareButton.addEventListener("click", flightaware);
var coordInfo = document.getElementById("coordInfo");
var orientationInfo = document.getElementById("orientationInfo");
terrainview.openTerrain({
url: "http://127.0.0.1:8080",
identifier: "範例地形圖",
callback: openCallback,
urlTemplate: "https://sample.pilotgaea.com.tw/Oview.aspx?{URL}"
});
// 在使用其他圖層前必須要先讀入地形 其他圖層的新增都會在callback中
function openCallback(result) {
// 設定底圖
terrainview.setBaseLayer({
url: "BING_MAP",
identifier: "VECTOR_IMAGE",
urlTemplate: "https://sample.pilotgaea.com.tw/Oview.aspx?{URL}"
});
// 設定初始位置
let initialPos = originPosition;
// let initialV = new window.Geo3DPoint(0, 1, 0);
// let initialUp = new window.Geo3DPoint(0, 0, 1);
let initialV = new window.Geo3DPoint(0.43526317436773043, 0.8997484208071095, -0.0316029792019615);
let initialUp = new window.Geo3DPoint(0.013762487394193657, 0.028448940844331472, 0.9995005011032063);
let initialCamera = new window.ov.Camera(initialPos, initialV, initialUp);
terrainview.gotoCamera(initialCamera, false);
customLayer = terrainview.addCustomLayer({ layername: "ar" });
createPositionPoint();
terrainview.maxVisualDistance = 300000;
// 載入軌跡模組
track = terrainview.getModule("track");
track.depthTest = false;
// 建立時間軸 widgetTimeline.updateParameter({style: {top: `${window.innerHeight - 40}px`, width: `${(window.innerWidth * 0.9) - 70 }px`}})
widgetTimeline = new ov.Widget.Timeline({ view: terrainview, style: {top: `${window.innerHeight - 40}px`, left:"70px", width: `${(window.innerWidth * 0.9) - 70 }px`} });
widgetTimelinePlayer = new ov.Widget.TimelinePlayer({ view: terrainview, timeline: widgetTimeline, playSpeed: playSpeed, style: { height:"10px",width:"20px", top:`${window.innerHeight-40}px`} });
moveTimelineWidget();
initAR();
console.log("c");
}
//初始化VR
function initAR() {
terrainview.initAR( buttonAR, function () {
console.log("initAR")
if (isARMode) {
console.log("mark-1")
isARMode = false;
terrainview.closeVR();
buttonAR.innerText = "開啟AR";
}else{
buttonAR.innerText = "關閉AR";
terrainview.backgroundColor = new ov.Color("#00000000");
terrainview.enableOuterSpaceBox = false;
terrainview.enableAtmosphere = false;
console.log(`outerHeight: ${window.outerHeight}, innerHeight: ${window.innerHeight}`)
console.log(`outerWidth: ${window.outerWidth}, innerWidth: ${window.innerWidth}`)
widgetTimeline.updateParameter({style: {top: `${window.innerHeight - 40}px`, left:"70px", width: `${(window.innerWidth * 0.9) - 70 }px`}})
widgetTimelinePlayer.updateParameter({style: {top: `${window.innerHeight - 40}px`}})
terrainview.drawTerrainSetting = {
referenceSurface: false
};
isARMode = true;
updateDialog();
}
},
{
optionalFeatures: ["dom-overlay"],
domOverlay: { root: document.getElementById("MyControl") }
}
);
}
function switchTerrainSurface() {
terrainview.drawTerrainSetting = { surface: surface };
surface = !surface;
}
function addModel() {
terrainview.setARModel();
}
function adjustSurface() {
terrainview.correctARHitTestPlane();
}
function getgps() {
// console.log("gps")
// text = "取得中......";
// dialog.innerText = text;
//todo
if (window.navigator.geolocation) {
positionWatcher = watchPosition();
} else {
alert("Get Current Position Error");
}
if (isApple) {
DeviceOrientationEvent.requestPermission().then(response => {
if (response == 'granted') {
// alert("request permission success")
orientationWatcher = watchDeviceOrientation();
}else{
alert("request permission failed")
}
}).catch(console.error)
}else{
orientationWatcher = watchDeviceOrientation();
}
}
function getCurrentPositionCallBack(position) {
if (position.coords.heading) {
let tV = rotateByAngle(position.coords.heading);
terrainview.setXRView(testSetView(tV));
text = "Heading:";
text += position.coords.heading;
}
text += `latitude:${position.coords.latitude}, longitude:${position.coords.longitude} `;
dialog.innerText = text;
}
function getCurrentPositionErrorCallBack(messageObj) {
text = messageObj.message;
dialog.innerText = text;
}
// set current device look direct as north
function northSet() {
let tV = new window.GeoPoint(0, 1, 0);
terrainview.setXRView(new Geo3DPoint(0, 1, 0));
// terrainview.setXRView(testSetView(tV));
}
function testSetView(origin) {
let a = window.GeoUtility.PolarAngle(origin.x, origin.y);
let b = window.GeoUtility.PolarAngle(
terrainview.camera.v.x,
terrainview.camera.v.y
);
let delta = b - a;
var up = new window.Geo3DPoint(0, 0, 1);
let newV = new window.Geo3DPoint(terrainview._TerrainEngine.XRHelper.OriginV);
newV.RotateBy(up, DEGTORAD * -delta, new window.Geo3DPoint(0, 0, 0));
return newV;
}
// 旋轉角度向量計算
function rotateByAngle(angle) {
function deg2rad(deg) {
return deg * 0.0174532925;
}
var tV = new window.Geo3DPoint(0, 1, 0);
let originPoint = new window.Geo3DPoint(0, 0, 0);
var up = new window.Geo3DPoint(0, 0, 1);
tV.RotateBy(up, deg2rad(-angle), originPoint);
return tV;
}
function updateDialog() {
if (!isARMode) return;
requestAnimationFrame(updateDialog);
let now = new Date().getTime();
if (now - lastTimeUpdate < 500) {
return;
}
lastTimeUpdate = now;
let obj = {};
let information = terrainview.getXRViewInformation().views;
if (information[0]) {
obj = information[0];
obj.model = null;
}
let model = terrainview.getARModelEntity();
if (model !== null) {
let param = model.getParameter();
let filterParam = {
position: param.position,
rotate: param.rotate,
scale: param.scale
};
obj.model = filterParam;
} else {
obj.model = null;
}
// console.log(obj);
}
function createPositionPoint(){
customLayer.addPointEntity({
geo: new window.GeoPoint(originPosition.x + 0.00015, originPosition.y, 296),
size: 60,
absHeight:true,
color: "#FF0000",
label: { text: "東", size: 30 }
});
customLayer.addPointEntity({
geo: new window.GeoPoint(originPosition.x - 0.00015, originPosition.y, 296),
size: 60,
absHeight:true,
color: "#0000FF",
label: {text: "西",size: 30}
});
customLayer.addPointEntity({
geo: new window.GeoPoint(originPosition.x, originPosition.y + 0.00015, 296),
size: 60,
absHeight:true,
color: "#FFFF00",
label: {text: "北",size: 30}
});
customLayer.addPointEntity({
geo: new window.GeoPoint(originPosition.x, originPosition.y - 0.00015, 296),
size: 60,
absHeight:true,
color: "#00FF00",
label: {text: "南",size: 30}
});
}
//
function flightaware() {
if (!FlightAwareEntity){
showFlightAware();
}else{
widgetTimeline.toStart();
widgetTimelinePlayer.play();
}
}
function showFlightAware(){
if (FlightAwareEntity) {
FlightAwareEntity.forEach(function (ent) { ent.show = this.checked; }.bind(this));
widgetTimeline.updateTime(FlightAwareEntity[0].playingInfo.TimeSpan[0]);
}
else {
const color_green = new ov.Color("#00FF00");
const color_green2 = new ov.Color("#00FE00");
const color_blue = new ov.Color("#0000FF");
const color_red = new ov.Color("#FF0000");
FlightAwareEntity = [];
var sources = [];
//發布glb需額外設定IIS的MIME type
// for (let i = 0; i < 26; i++) {
const numOfTrack = 1;
for (let i = 0; i < numOfTrack; i++) {
track?.addFlightAwareEntity({
source: "./data/fa2_" + (i+1) + ".txt",
target: {
src: "./data/737BLUE.glb",
// scale: 1000,
scale: 50,
tooltip: "flight" + i,
rotate: { y: 90 },
minRange: 3
},
path: {
color: color_blue,
opacity: 0.1,
leftPath: {
color: color_green,
opacity: 1,
}
},
callback: function (ent) {
if (i==0) { console.log(ent) }
// console.log(i);
widgetTimeline.addLink(ent);
FlightAwareEntity.push(ent);
ent.show = this.checked;
checkTime(ent.playingInfo.TimeSpan);
if (i==numOfTrack-1) {
widgetTimeline.updateTime(earliestTime);
widgetTimeline._StartTime = earliestTime;
widgetTimeline._StopTime = latestTime;
}
}.bind(this)
});
}
flightwareButton.innerText = "播放飛行軌跡";
}
}
// check time for earliest and lastest time.
function checkTime(array) {
var last = array[array.length - 1];
var early = array[0];
earliestTime = earliestTime == 0 ? early : Math.min(earliestTime, early);
latestTime = Math.max(latestTime, last);
}
function moveTimelineWidget() {
document.querySelectorAll('div.ov-ui div.ov-widget-timeline').forEach(element => {
// 逐個檢查是否父元素是 'div.ov-ui'
if (element.closest('div.ov-ui')) {
console.log(element.closest('div.ov-ui')); // 這將會輸出每一個滿足條件的父元素
con.appendChild(element)
}
});
document.querySelectorAll('div.ov-ui div.ov-widget-timelinePlayer').forEach(element => {
// 逐個檢查是否父元素是 'div.ov-ui'
if (element.closest('div.ov-ui')) {
console.log(element.closest('div.ov-ui')); // 這將會輸出每一個滿足條件的父元素
con.appendChild(element)
}
});
}
function detectMobileDevice() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
console.log('userAgent :>> ', userAgent);
// 這裡的正則表達式涵蓋了大多數移動裝置
return /android|avantgo|blackberry|bb|opera mini|iemobile|ipad|iphone|ipod|iemobile|windows phone|kindle|silk|webos|fennec|nokia|minimo|opera mobi|opera mini|symbian|windows.ce|palm/i.test(userAgent.toLowerCase());
}
function detectMobileAppleDevice() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
console.log('userAgent :>> ', userAgent);
// 這裡的正則表達式涵蓋了大多數移動裝置
return /ipad|iphone|ipod/i.test(userAgent.toLowerCase());
}
function watchPosition(){
return navigator.geolocation.watchPosition(function(position) {
let { coords } = position;
let { latitude, longitude } = coords;
const lat = latitude?.toFixed(6);
const lon = longitude?.toFixed(6);
coordInfo.innerText = `Lat: ${lat}, Lon: ${lon}`;
});
}
function watchDeviceOrientation() {
window.addEventListener(isApple ? 'deviceorientation' : 'deviceorientationabsolute', (event) => {
let { alpha } = event;
let heading = isApple ? event.webkitCompassHeading?.toFixed(2) : (360-alpha).toFixed(2);
let direction;
if (heading >= 22.5 && heading < 67.5) { direction = '東北'; } else
if (heading >= 67.5 && heading < 112.5) { direction = '東'; } else
if (heading >= 112.5 && heading < 157.5) { direction = '東南'; } else
if (heading >= 157.5 && heading < 202.5) { direction = '南'; } else
if (heading >= 202.5 && heading < 247.5) { direction = '西南'; } else
if (heading >= 247.5 && heading < 292.5) { direction = '西'; } else
if (heading >= 292.5 && heading < 337.5) { direction = '西北'; } else
{ direction = '北'; }
orientationInfo.innerText = `Clockwise from north: ${heading}°, Face to: ${direction}`;
})
}