Source: core/File.js

/**
 * MLJLib
 * MeshLabJS Library
 * 
 * Copyright(C) 2015
 * Paolo Cignoni 
 * Visual Computing Lab
 * ISTI - CNR
 * 
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify it under 
 * the terms of the GNU General Public License as published by the Free Software 
 * Foundation; either version 2 of the License, or (at your option) any later 
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT 
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
 * FOR A PARTICULAR PURPOSE. See theGNU General Public License 
 * (http://www.gnu.org/licenses/gpl.txt) for more details.
 * 
 */

/**
 * @file Defines the functions to manage files
 * @author Stefano Gabriele
 */

/**
 * MLJ.core.File namespace
 * @namespace MLJ.core.File
 * @memberOf MLJ.core
 * @author Stefano Gabriele
 */
MLJ.core.File = {
    ErrorCodes: {
        EXTENSION: 1
    },
    SupportedExtensions: {
        OFF: ".off",
        OBJ: ".obj",
        PLY: ".ply",
        STL: ".stl"
    }, 
    SupportedSketchfabExtensions: {
        OBJ: ".obj",
        PLY: ".ply",
        STL: ".stl"        
    },
    SupportedWebsites: {
        SKF: "Sketchfab"   
    }
};

(function () {
    var _openedList = new MLJ.util.AssociativeArray();

    function isExtensionValid(extension) {

        switch (extension) {
            case ".off":
            case ".obj":
            case ".ply":
            case ".stl":
            case ".zip":
                return true;
        }

        return false;
    }

    /**
     * Loads 'file' in the virtual file system as an Int8Array and reads it into the layer 'mf'
     */
    function loadMeshDataFromFile(file, mf, onLoaded) {
        var fileReader = new FileReader();
        fileReader.readAsArrayBuffer(file);
        fileReader.onloadend = function (fileLoadedEvent) {
            console.log("Read file " + file.name + " size " + fileLoadedEvent.target.result.byteLength + " bytes");
            console.timeEnd("Read mesh file");
            //  Emscripten need a Arrayview so from the returned arraybuffer we must create a view of it as 8bit chars
            var int8buf = new Int8Array(fileLoadedEvent.target.result);
            FS.createDataFile("/", file.name, int8buf, true, true);
            
            console.time("Parsing Mesh Time");  
//            console.log("File extension: " +file.name.split('.').pop());
            var resOpen = -1;
            if(file.name.split('.').pop() === "zip")
                resOpen = mf.cppMesh.openMeshZip(file.name);
            else
                resOpen = mf.cppMesh.openMesh(file.name);
        
            if (resOpen !== 0) {
                console.log("Ops! Error in Opening File. Try again.");
                FS.unlink(file.name);
                onLoaded(false);
            }
            console.timeEnd("Parsing Mesh Time");        
            FS.unlink(file.name);
            onLoaded(true, mf);
        };
    }

    

    /**
     * Opens a mesh file or a list of mesh files     
     * @param {(File | FileList)} toOpen A single mesh file or a list of mesh files
     * @memberOf MLJ.core.File
     * @fires MLJ.core.File#MeshFileOpened
     * @author Stefano Gabriele
     */
    this.openMeshFile = function (toOpen) {
        $(toOpen).each(function (key, file) {
            console.time("Read mesh file");

            if (!(file instanceof File)) {
                console.error("MLJ.file.openMeshFile(file): the parameter 'file' must be a File instace.");
                return;
            }

            //Add file to opened list
            _openedList.set(file.name, file);

            //Extract file extension
            var pointPos = file.name.lastIndexOf('.');
            var extension = file.name.substr(pointPos);

            //Validate file extension
            if (!isExtensionValid(extension)) {
                console.error("MeshLabJs allows file format '.off', '.ply', '.obj' and '.stl'. \nTry again.");
                return;
            }

            var mf = MLJ.core.Scene.createLayer(file.name);
            mf.fileName = file.name; 

            loadMeshDataFromFile(file, mf, function (loaded, meshFile) {
                if (loaded) {
                    /**
                     *  Triggered when a mash file is opened
                     *  @event MLJ.core.File#MeshFileOpened
                     *  @type {Object}
                     *  @property {MLJ.core.Layer} meshFile The opened mesh file
                     *  @example
                     *  <caption>Event Interception:</caption>
                     *  $(document).on("MeshFileOpened",
                     *      function (event, meshFile) {
                     *          //do something
                     *      }
                     *  );
                     */
                    $(document).trigger("MeshFileOpened", [meshFile]);
                }
            });
        });
    };

    /**
     * Reloads an existing layer, that is recovers the file linked to the layer
     * and reinitializes the cppMesh of the layer with it
     * @param {MLJ.core.Layer} mf the MeshFile to be reloaded
     * @memberOf MLJ.core.File
     * @fires MLJ.core.File#MeshFileReloaded
     */
    this.reloadMeshFile = function (mf) {
        var file = _openedList.getByKey(mf.fileName);

        if (file === undefined) {
            console.warn("MLJ.file.reloadMeshFile(name): the scene not contains file '" + name + "'.");
            return;
        }

        loadMeshDataFromFile(file, mf, function (loaded, meshFile) {
            if (loaded) {
                /**
                 *  Triggered when a mesh file is reloaded
                 *  @event MLJ.core.File#MeshFileReloaded
                 *  @type {Object}
                 *  @property {MLJ.core.Layer} meshFile The reloaded mesh file
                 *  @example
                 *  <caption>Event Interception:</caption>
                 *  $(document).on("MeshFileReloaded",
                 *      function (event, meshFile) {
                 *          //do something
                 *      }
                 *  );
                 */
                $(document).trigger("MeshFileReloaded", [meshFile]);
            }
        });
    };

    this.saveMeshFile = function (mesh, fileName) {        
        //Create data file in FS
        var int8buf = new Int8Array(mesh.ptrMesh());
        FS.createDataFile("/", fileName, int8buf, true, true);
        
        //call a saveMesh method from above class
        var resSave = mesh.cppMesh.saveMesh(fileName);
        
        //handle errors ...        
        
        //readFile is a Emscripten function which read a file into memory by path
        var file = FS.readFile(fileName);
        //create a Blob by filestream
        var blob = new Blob([file], {type: "application/octet-stream"});
        //call a saveAs function of FileSaver Library
        saveAs(blob, fileName);
        
        FS.unlink(fileName);
    };
    
    
    this.saveMeshFileZip = function (mesh, fileName, archiveName) {
        mesh.cppMesh.saveMesh(fileName);             
        mesh.cppMesh.saveMeshZip(fileName, archiveName);
        
        var file = FS.readFile(archiveName);
        var blob = new Blob([file], {type: "application/octet-stream"});
        saveAs(blob, archiveName);
        
        FS.unlink(archiveName);
        FS.unlink(fileName);
    };  
    
    
    this.uploadToSketchfab = function (meshFile, meshExt, zipBool, dialog) {
        var meshInfo = meshFile.name.split(".");
        var meshFileName = meshInfo[0]+meshExt;
        var fileName = meshFileName;
         
        var resSave = meshFile.cppMesh.saveMesh(meshFileName);        
        
        if(zipBool){
            fileName = meshInfo[0] +".zip";   
            meshFile.cppMesh.saveMeshZip(meshFileName, fileName);
        }
        
        console.log("Uploading " +meshFileName +" compressed: " +zipBool +" filename: " +fileName); 
        var file = FS.readFile(fileName);        
        var blob = new Blob([file], {type: "application/octet-stream"});
        
        var sketchfabApiUrl = 'https://api.sketchfab.com/v2/models';
        var sketchfabModelUrl = 'https://sketchfab.com/models/';
        // var data = document.getElementById( 'the-form' );
        var data = $( '#the-form' )[0];
        var modelName = data[1].value;   
        uploadModel(data);
        $( '#status' ).html( 'Uploading File...' );
        $('#status').removeClass('error');
        
        FS.unlink(fileName);

        function uploadModel( data ) {
//          console.log( 'Begin upload of ' + String( data.modelFile.value ) );  
          var formData = new FormData();

          formData.append("modelFile", blob, fileName);
          formData.append("token", data[0].value);   
          formData.append("name", data[1].value);    
          formData.append("description", data[2].value + "    \n\Made with **MeshLabJS**, the mesh processing tool on the web. Freely available at [www.meshlabjs.net](http://www.meshlabjs.net)   \n\![MeshLabJS](http://www.meshlabjs.net/img/favicon48.png \"MeshLabJS\")");  
          formData.append("tags", data[3].value + " meshlab meshlabjs");   
          formData.append("private", data[4].value);  
          formData.append("password", data[5].value);
          
//          var entries = formData.entries();
//          console.log(entries.next().value);
//          console.log(entries.next().value);
//          console.log(entries.next().value);
//          console.log(entries.next().value);
//          console.log(entries.next().value);
//          console.log(entries.next().value);
//          console.log(entries.next().value);
//          console.log(entries.next().value);
          var abort = false;
          var upError = false;
          var upSuccess = false;
          
          var progressBar = $('#progressBar');
          var pBarLabel = $('#pBarLabel');
          
          var xhr = $.ajax({
            xhr: function()
            {
              var xhrProgr = new window.XMLHttpRequest();
              xhrProgr.upload.addEventListener("progress", function(evt){
                if (evt.lengthComputable) {  
                  var percentComplete = Math.round((evt.loaded / evt.total) * 100);
                  var percentProgressBar = Math.round((evt.loaded / evt.total) * 50);
                  $('#status').html('Uploading File ' +percentComplete +'%');
                  $('#status').removeClass('error');
                  progressBar.width(percentProgressBar+'%');
                  pBarLabel.html(percentProgressBar+'%');
                }
              }, false); 
              return xhrProgr;
            },
            url: sketchfabApiUrl,
            data: formData,
            cache: false,
            source: 'meshlabjs',
            contentType: false,
            processData: false,
            type: 'POST',
            success: function( response ) {
              upSuccess = true;
              var uid = response.uid;
              console.log( 'Begin polling processing status. If successful, the model will be available at ' + sketchfabModelUrl + uid );
              $('#status').removeClass('error');
              $( '#status' ).html( 'Upload successful, polling...' );
              $('#exitUpdateButton').prop('disabled',true);
              progressBar.width("50%");
              pBarLabel.html("50%");
              pollProcessingStatus( uid );
            },
            error: function( response ) {
                if(!abort){
                    upError = true;
                    console.log( 'Upload Error!' );
                    console.log( JSON.stringify( response ) );
                    $( '#status' ).html( 'Upload error!' );
                    $('#status').addClass('error');
                }
                else{
                     console.log("Upload Aborted");
                    $( '#status' ).html( 'Upload Aborted!' );
                    $('#status').addClass('error');
                }
                
                $('#exitUpdateButton').prop('disabled',false);
                $('#exitUpdateButton').prop('disabled',false);
                $('#exitUpdateButton').text('Exit');
            }
          } );
          
          $('#exitUpdateButton').click(function(){
                if(!abort && !upError && !upSuccess){
                      abort = true;                   
                      xhr.abort();
                } 
                else if(upError || upSuccess){
                      dialog.destroy();
                }
                else {
                      dialog.destroy();
                }
            });          
        }
        
        function pollProcessingStatus( urlid ) {
          var url = sketchfabApiUrl + '/' + urlid + '/status?token=' + data.token.value;
          var progressBar = $('#progressBar');
          var pBarLabel = $('#pBarLabel');
          var errors = 0;
          var maxErrors = 10;
          var retry = 0;
          var maxRetries = 50;
          var retryTimeout = 5000;  // milliseconds
          var retryTimeoutSec = retryTimeout / 1000; // seconds
          var complete = false;
          function getStatus() {
            $.ajax( {
              url: url,
              type: 'GET',
              dataType: 'json',
              success: function( response ) {
                retry++;
                console.log( 'Got status...' )
                var status = response.processing;

                switch( status ) {
                  case 'PENDING':
                    console.log( 'Model is in the processing queue. Waiting ' + ( retryTimeoutSec ) + ' seconds to try again...' );
                    $( '#status' ).html( 'Model in queue...' );
                    $('#status').removeClass('error');
                    progressBar.width('60%'); 
                    pBarLabel.html("60%");    
                    break;
                  case 'PROCESSING':
                    console.log( 'Model is being processed. Waiting ' + ( retryTimeoutSec ) + ' seconds to try again...' );
                    $( '#status' ).html( 'Model processing...' );
                    $('#status').removeClass('error');
                    progressBar.width('75%'); 
                    pBarLabel.html("75%");
                    break;
                  case 'FAILED':
                    console.log( 'Model processing failed:' );
                    console.log( response.error );
                    $( '#status' ).html( 'Model processing failed!' ); 
                    $('#status').addClass('error');
                    $('#exitUpdateButton').prop('disabled',false);
                    $('#exitUpdateButton').removeClass("ui-button-disabled ui-state-disabled");
                    $('#exitUpdateButton').text('Exit');
                    complete = true;
                    break;
                  case 'SUCCEEDED':
                    console.log( 'It worked!' );
                    console.log( sketchfabModelUrl + urlid );
                    complete = true;
                    $('#status').removeClass('error');
                    $( '#status' ).html( 'It worked! See it here: <a href="' + sketchfabModelUrl + urlid + '" target="_blank"> Sketchfab: ' +modelName +'</a>' );  
                    $('#exitUpdateButton').button().text('Ok');       
                    $('#exitUpdateButton').prop('disabled',false);
                    $('#exitUpdateButton').removeClass("ui-button-disabled ui-state-disabled");
                    progressBar.width('100%');
                    pBarLabel.html("100%");
                    break;
                  default:
                    console.log( 'This message should never appear...something changed in the processing response. See: ' + url );
                }

                if ( retry < maxRetries && !complete ) {
                  setTimeout( getStatus, retryTimeout );
                } else if ( complete ) {
                  return;
                } else if ( retry >= maxRetries ) {
                  console.log( 'Polled ' + maxRetries + ' times and it\'s still not finished...quitting' );
                } else {
                  console.log( 'Something weird happened...quitting' );
                }
              },
              error: function( response ) {
                errors++;
                retry++;
                if ( errors < maxErrors && retry < maxRetries && !complete ) {
                  console.log( 'Error: ' + JSON.stringify( response ) );
                  console.log( 'Error getting status ( ' + errors + ' ). Trying again...' );
                  setTimeout( getStatus, retryTimeout );
                } else {
                  console.log( 'Too many errors...quitting' );
                }
              }
            } );            
          }
          getStatus();
        }         
    };    

}).call(MLJ.core.File);