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}`;

     })
 }
Copyright © NCHC 2022 Version:13.0 all right reserved,powered by Gitbook修訂時間: 2024-11-20 13:52:09