umn-drone / project / apps / transit_service / web / js / main.js
main.js
Raw
// Important global data for the campus/city environment.
//var socket = new WebSocket("ws://" + location.hostname+(location.port ? ':'+location.port: ''), "web_server");
let api = new WSApi();
var connected = false;
var entities = {};
var entityList = [];
var sceneFile = "scenes/umn.json";
var sceneModel = "assets/model/umn.obj";
var sceneTexture = "assets/texture/umn.png";
var sceneScale = [0.05,0.05,0.05];
var scenePosition = [0,-13,0];
var camera, scene, renderer, controls;
var models = [];
var routes = [];
var paths = {};
var currentView = -1;
var showRoutes = false;
var showPaths = true;

// More important related to models and animation.
var geometry, material, mesh;
var container = document.querySelector( '#scene-container' );
const mixers = [];
const clock = new THREE.Clock();
const loader = new THREE.GLTFLoader();
const objloader = new THREE.OBJLoader();
var simSpeed = 1.0;
// Function definitions start here...

// This is the function that is called once the document is started.
$( document ).ready(function() {
  var simSpeedSlider = document.getElementById("simSpeed");
  
  // Update the current slider value (each time you drag the slider handle)
  simSpeedSlider.oninput = function() {
    simSpeed = this.value / 10.0;
  }

  // Init() starts up the scene and its update loop.
  init();

  // Start checking for when the user resizes their application window.
  window.addEventListener( 'resize', onWindowResize );

  // Listen for when the system wants to create new scene objects.
  try {
    api.onmessage = function(msg, data)  {
      if ("event" in data) {
        if (data.event == "AddEntity") {
          console.log(data.details);
          addEntity(data.details);
        }
        if (data.event == "UpdateEntity") {
          //console.log(data.details);
          var e = data.details;

          if (e.id in entities) {
            var model = entities[e.id];
            model.position.x = e.pos[0];
            model.position.y = e.pos[1];
            model.position.z = e.pos[2];
            
            model.position.x = model.position.x/14.2;
            model.position.y = model.position.y/20.0 - 13.0;
            model.position.z = model.position.z/14.2;

            model.position.x += model.offset.x;
            model.position.y += model.offset.y;
            model.position.z += model.offset.z;

            var dir = new THREE.Vector3(e.dir[0], e.dir[1], e.dir[2]);
            var pos = new THREE.Vector3();
            pos.addVectors(dir, model.position);

            var vector = new THREE.Vector3( 0, 1, 0 );
            vector = model.worldToLocal(vector);

            var adjustedDirVector = model.localToWorld(new THREE.Vector3(0,0,0)).add(dir);
            model.lookAt(adjustedDirVector);
          }

          if (currentView >= 0) {
            controls.target.copy(entities[currentView].position);
            controls.update();
          }
        }
        if (data.event == "RemoveEntity") {
          //console.log(data);
          removeEntity(data.details.id);
        }
      }
    }
  }
  catch(exception) {
    alert('<p>Error' + exception);
  }

  loadScene(sceneFile);
});

/*// This function is triggered once the web socket is opened.
socket.onopen = function() {
  socket.send(JSON.stringify({command: "test"}));
  loadScene(sceneFile);
  connected = true;
}*/

// This function defines the properties of the scene as well as starts the
// update loop.
function init() {
  const fov = 35; // fov = Field Of View
  const aspect = container.clientWidth / container.clientHeight;
  const near = 0.1;
  const far = 1000;

  camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
  camera.position.set( -10, 10, 10 );
  controls = new THREE.OrbitControls( camera, container );

  scene = new THREE.Scene();
  scene.background = new THREE.Color( 'skyblue' );

  const textureLoader = new THREE.TextureLoader();

  const texture = textureLoader.load( sceneTexture );
  // set the "color space" of the texture
  texture.encoding = THREE.sRGBEncoding;
  // reduce blurring at glancing angles
  texture.anisotropy = 16;
  // create a Standard material using the texture we just loaded as a color map
  material = new THREE.MeshStandardMaterial( { map: texture } );

  geometry = new THREE.BoxBufferGeometry( 2, 2, 2 );
  mesh = new THREE.Mesh( geometry, material );

  geometry = new THREE.PlaneBufferGeometry( 100, 100, 100 );
  mesh = new THREE.Mesh( geometry, material );
  mesh.rotation.x = -3.14159/2.0;

  const ambientLight = new THREE.AmbientLight( 0xffffff, 1 );
  scene.add( ambientLight );
  const light = new THREE.DirectionalLight( 0xffffff, 1 );
  light.position.set( 10, 10, 10 );
  scene.add( ambientLight, light );
  const light2 = new THREE.DirectionalLight( 0xffffff, 1 );
  light2.position.set( 0, 10, -10 );
  scene.add( ambientLight, light2 );

  renderer = new THREE.WebGLRenderer( { antialias: true } );
  renderer.setSize( window.innerWidth, window.innerHeight );
  document.body.appendChild( renderer.domElement );
  // start the animation/render loop
  renderer.setAnimationLoop( () => {
    update();
    render();
  });
}

function changeView() {
  currentView = $("#entitySelect").val();
  if (currentView >= 0) {
    controls.target.copy(entities[currentView].position);
    controls.update();
  }
}

// This function builds the initial campus/city scene.
function loadScene(file, initialScene = true) {
  sceneFile = file;
  $.getJSON(sceneFile, function(json) {
    console.log(json);
    for (var i = 0; i < json.length; i++) {
      var command = json[i];
      console.log(command);
      if (command.command == "SetScene") {
        console.log(command);
        sceneModel = command.params.mesh;
        sceneTexture = command.params.texture;
        sceneScale = command.params.scale;
        scenePosition = command.params.position;
      }
      if (command.command == "AddMesh") {
        addMesh(command.params);
      }
      if (command.command == "CreateEntity") {
        api.sendCommand(command.command, command.params);
      }
    }
    loadModels();
    /*if (initialScene) {
      socket.send(JSON.stringify({command: "runScript", "init":true, "script": json}));
    }
    else {
      socket.send(JSON.stringify({command: "runScript", "script": json}));
    }*/
  });
}

function displayJSON(data) {
  //data should be a standard JSON-style object
  if (data["command"] == "notification") {
    if (data["value"] == 'updateDetails') {
      if (data["details"].color != undefined) {
        entities[data["id"]].children[1].material.color.setHex(parseInt(data["details"].color, 16));
        entities[data["id"]].children[1].material.opacity = 0.5;
      }
      else {
        entities[data["id"]].children[1].material.opacity = 0.0;
      }
      return;
    }

    type_lookup = {
      'scheduled': ' scheduled.',
      'delivered': ' delivered to customer.',
      'en route': ' picked up.',
      'moving': ' now moving.',
      'idle': ' stopped moving.'
    }
    string_ending = type_lookup[data["value"]];
    additional_string = "Entity #" + data["id"] + string_ending + "\r\n";
    notifbar = document.getElementById("notification-bar");
    notifbar.textContent += additional_string;

    const entityId = data["id"];

    if (data["value"] == 'idle' || data["value"] == 'moving') {
      if (entityId in paths) {
        scene.remove(paths[entityId]);
        delete paths[entityId];
      }
    }
    if (data["value"] == 'idle') {
      for ( var mixer of mixers ) {
        if (entityId == mixer.id) {
          mixer.duration = 0;
        }
      }
    }
    else if (data["value"] == 'moving') {
      if ("path" in data) {
        //create a blue LineBasicMaterial
        var material = new THREE.LineBasicMaterial( { color: 0xf0fc03 } );
        const points = [];
        for (var point of  data["path"]) {
          points.push( new THREE.Vector3( point[0], point[1], point[2] ) );
        }
        /*points.push( new THREE.Vector3( - 10, 0, 0 ) );
        points.push( new THREE.Vector3( 0, 10, 0 ) );
        points.push( new THREE.Vector3( 10, 0, 0 ) );*/
        const geometry = new THREE.BufferGeometry().setFromPoints( points );

        const line = new THREE.Line( geometry, material );
        //console.log(routes);
        if (routes.length > 0) {
          line.position.copy( routes[0].position );
          line.scale.copy( routes[0].scale );
          line.material.color.setHex(routes[0].material.color.getHex());
        }

        //"position": [-0.0,-12.5,-0.0],
        //      "scale": [0.0705,0.05,0.0705],
        paths[entityId] = line;
        scene.add( line );
      }

      for ( var mixer of mixers ) {
        if (entityId == mixer.id) {

          mixer.duration = 2;
        }
      }
    }
  }
}

// This function is a helper for loadScene().
function loadModels() {
  // instantiate a loader
  var loader2 = new THREE.OBJLoader();

  // load a resource
  loader2.load(
    // resource URL
    sceneModel,
    // called when resource is loaded
    function ( object ) {
      object.position.copy( new THREE.Vector3( scenePosition[0], scenePosition[1], scenePosition[2]) )
      object.scale.copy( new THREE.Vector3( sceneScale[0], sceneScale[1], sceneScale[2]));



      const textureLoader = new THREE.TextureLoader();
      const texture = textureLoader.load( sceneTexture + "-ground.png" );
      texture.encoding = THREE.sRGBEncoding;
      texture.anisotropy = 16;
      var objmaterial = new THREE.MeshStandardMaterial( { map: texture } );

      const texture2 = textureLoader.load(sceneTexture );
      texture.encoding = THREE.sRGBEncoding;
      texture.anisotropy = 16;

      object.traverse( function ( node ) {
          if ( node.name == "EXPORT_GOOGLE_SAT_WM" ) {
            node.material = objmaterial;
          }
          else if ( node.name == "Areas:building" ) {
          var material = new THREE.MeshStandardMaterial( {color: 0x85868f, map: texture2});
          node.material = material;
          }
          else if (!node.isGroup) {
            node.visible = false;
          }
          //node.geometry.computeBoundingBox();
          // max: Object { x: 2249.523193359375, y: 286.9197998046875, z: 1261.8768310546875 }
          // min: Object { x: -2030.950927734375, y: 220.99996948242188, z: -1184.1085205078125 }
          console.log(node);

        } );

        models.push(object);
      scene.add( object );
    },
    // called when loading is in progresses
    function ( xhr ) {
      console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
    },
    // called when loading has errors
    function ( error ) {
      console.log( 'An error happened', error);
    }
  );
}

function toggleRoutes() {
  showRoutes = !showRoutes;
  for (var routeNum = 0; routeNum < routes.length; routeNum++) {
    routes[routeNum].visible = showRoutes;
  }
}

function togglePaths() {
  showPaths = !showPaths;
  for (const key in paths) {
    paths[key].visible = showPaths;
  }
}

// This function is a helper for loadScene().
function addMesh(obj) {
  // instantiate a loader
  var loader2 = new THREE.OBJLoader();

  // load a resource
  loader2.load(
    // resource URL
    obj.mesh,
    // called when resource is loaded
    function ( object ) {
      //object.position.copy( new THREE.Vector3( scenePosition[0]+1.5, scenePosition[1]+0.5, scenePosition[2]+1.5) )
      object.position.copy( new THREE.Vector3( obj.position[0], obj.position[1], obj.position[2]) )
      object.scale.copy( new THREE.Vector3( obj.scale[0], obj.scale[1], obj.scale[2]) ) //*1.41

      object.traverse( function ( node ) {
        //var material = new THREE.MeshStandardMaterial( {color: 0x8589a1});
        var material = new THREE.LineBasicMaterial( {
          color: parseInt(obj.color, 16), //0xff00e1,
          linewidth: 1,
          linecap: 'round', //ignored by WebGLRenderer
          linejoin:  'round' //ignored by WebGLRenderer
        } );
        node.material = material;
        //node.position.copy( new THREE.Vector3( 30, 10, 35.0) );
        //node.scale.copy( new THREE.Vector3( 1.41, 1.0, 1.41) );
        console.log(node);
        
      } );

      if (obj.type == "route") {
        object.visible = showRoutes;
        routes.push(object);
      }
        models.push(object);
      scene.add( object );
    },
    // called when loading is in progresses
    function ( xhr ) {
      console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
    },
    // called when loading has errors
    function ( error ) {
      console.log( 'An error happened', error);
    }
  );
}

// A reusable function to set up the models. We're passing in a position
// parameter so that they can be individually placed around the scene.
// Currently, this only works for GLTF/glb formats.
const onLoad = ( gltf, position, scale, start, duration, details, id ) => {
  const model = gltf.scene.children[ 0 ];
  model.scale.copy( scale );

  const animation = gltf.animations[ 0 ];
  console.log(animation);
  
  if (!(typeof animation === "undefined")) {
	const mixer = new THREE.AnimationMixer( model );
    mixers.push( {id: id, mixer: mixer, start: start, duration: duration} );
  var action = mixer.clipAction(animation);
  	action.play();
  }

  var sphereGeom =  new THREE.SphereGeometry( 40, 32, 16 );
  var opacity = details.color == undefined ? 0.0 : 0.5;
  var sphereMaterial = new THREE.MeshBasicMaterial( { color: parseInt(details.color, 16), transparent: true, opacity: opacity } );
  var sphere = new THREE.Mesh( sphereGeom, sphereMaterial );
  sphere.scale.copy(new THREE.Vector3(0.03, 0.03, 0.03));

  const group = new THREE.Group();
  group.add( model );
  group.add(sphere);
  group.position.copy(position);
  
  if (details.offset != undefined) {
    group.offset = new THREE.Vector3(details.offset[0], details.offset[1], details.offset[2]);
  }
  else {
    group.offset = new THREE.Vector3(0.0, 0.0, 0.0);
  }

  models.push(group);
  scene.add( group );
  entities[id] = group;
  entityList.push(id);
};

// This function is called whenever a new object needs to be added to the scene.
function addEntity(data) {
  $("#entitySelect").append($('<option value="' + data.id + '">' + data.details.name + '</option>'));
  // the loader will report the loading progress to this function
  const onProgress = () => {};
  // the loader will send any error messages to this function, and we'll log
  // them to to console
  const onError = ( errorMessage ) => { console.log( errorMessage ); };
  // Choose a locally-stored gltf/glb model and attempt to load it.
  var position = new THREE.Vector3(data.details.position[0], data.details.position[1], data.details.position[2]);
  
  position.x = position.x/14.2;
  position.y = position.y/20.0 - 13.0;
  position.z = position.z/14.2;

  console.log(data.details);

  var scale = new THREE.Vector3(data.details.scale[0], data.details.scale[1], data.details.scale[2]);
  var id = data.id;
  loader.load( data.details.mesh, gltf => onLoad( gltf, position, scale, data.details.start, data.details.duration, data.details, id ), onProgress, onError );
}

function removeEntity(id) {
  console.log(models);
  var model = entities[id];
  models = models.filter(function(value, index, arr){ return value != model;});
  scene.remove( model );
  $("#entitySelect option[value='" + id + "']").remove();
  delete entities[id];
  if (currentView == id) {
    currentView = -1;
  }
  console.log(models);
}

/*// This function kills the webpage's socket connection.
function kill() {
  if (connected) {
    socket.send(JSON.stringify({command: "kill"}));
  }
}*/

var time = 0.0;

// This function updates the scene's animation cycle.
function update() {
  // Get the time since the last animation frame.
  const delta = clock.getDelta();
  time += delta;

  // Iterate through and update the animation mixers for each object in the
  // scene.
  for ( const mixer of mixers ) {
    if (mixer.start == undefined || mixer.duration == undefined) {
      mixer.mixer.update(delta);
    }
    else {
      var newTime = time - mixer.start;
      var iterations = Math.floor(newTime/mixer.duration);
      newTime = newTime - iterations*mixer.duration + mixer.start;
      mixer.mixer.setTime(newTime);
    }
  }

  // Send the update command to the socket.
  //if (connected) {
    //socket.send(JSON.stringify({command: "update", delta: delta}));
    //socket.send(JSON.stringify({command: "update", delta: delta, simSpeed: simSpeed}));
    api.sendCommand("Update", { simSpeed: simSpeed });
  //}
}

// This function simply renders the scene based on the camera position.
function render() {
  renderer.render( scene, camera );
}

// This function updates the projection matrix and renderer whenever the
// user's application window is resized.
function onWindowResize() {
  // set the aspect ratio to match the new browser window aspect ratio
  camera.aspect = container.clientWidth / container.clientHeight;

  // update the camera's frustum
  camera.updateProjectionMatrix();

  // update the size of the renderer AND the canvas
  renderer.setSize( container.clientWidth, container.clientHeight );
}