2016-07-28 22:59:08 +00:00
|
|
|
/*
|
|
|
|
Node-OpenDroneMap Node.js App and REST API to access OpenDroneMap.
|
2016-07-18 22:56:27 +00:00
|
|
|
Copyright (C) 2016 Node-OpenDroneMap Contributors
|
|
|
|
|
|
|
|
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 3 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 the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
2016-07-06 18:44:20 +00:00
|
|
|
"use strict";
|
2016-07-30 18:30:56 +00:00
|
|
|
|
|
|
|
let async = require('async');
|
2016-07-06 18:44:20 +00:00
|
|
|
let assert = require('assert');
|
2016-07-30 18:30:56 +00:00
|
|
|
let logger = require('./logger');
|
2016-07-07 22:07:17 +00:00
|
|
|
let fs = require('fs');
|
2016-07-30 18:30:56 +00:00
|
|
|
let path = require('path');
|
2016-07-14 21:42:12 +00:00
|
|
|
let rmdir = require('rimraf');
|
2016-07-08 20:44:48 +00:00
|
|
|
let odmRunner = require('./odmRunner');
|
2016-07-18 22:56:27 +00:00
|
|
|
let archiver = require('archiver');
|
2016-07-29 22:16:22 +00:00
|
|
|
let os = require('os');
|
2016-07-06 18:44:20 +00:00
|
|
|
|
2016-07-08 20:44:48 +00:00
|
|
|
let statusCodes = require('./statusCodes');
|
2016-07-06 18:44:20 +00:00
|
|
|
|
|
|
|
module.exports = class Task{
|
2016-07-26 19:20:37 +00:00
|
|
|
constructor(uuid, name, done, options = []){
|
2016-07-06 18:44:20 +00:00
|
|
|
assert(uuid !== undefined, "uuid must be set");
|
2016-07-15 21:19:50 +00:00
|
|
|
assert(done !== undefined, "ready must be set");
|
2016-07-06 18:44:20 +00:00
|
|
|
|
|
|
|
this.uuid = uuid;
|
|
|
|
this.name = name != "" ? name : "Task of " + (new Date()).toISOString();
|
|
|
|
this.dateCreated = new Date().getTime();
|
2016-07-14 21:42:12 +00:00
|
|
|
this.processingTime = -1;
|
|
|
|
this.setStatus(statusCodes.QUEUED);
|
2016-07-26 19:20:37 +00:00
|
|
|
this.options = options;
|
2016-07-30 18:30:56 +00:00
|
|
|
this.gpcFiles = [];
|
2016-07-08 20:44:48 +00:00
|
|
|
this.output = [];
|
|
|
|
this.runnerProcess = null;
|
2016-07-07 22:07:17 +00:00
|
|
|
|
2016-07-30 18:30:56 +00:00
|
|
|
async.series([
|
|
|
|
// Read images info
|
|
|
|
cb => {
|
|
|
|
fs.readdir(this.getImagesFolderPath(), (err, files) => {
|
|
|
|
if (err) cb(err);
|
|
|
|
else{
|
|
|
|
this.images = files;
|
|
|
|
logger.debug(`Found ${this.images.length} images for ${this.uuid}`)
|
|
|
|
cb(null);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
// Find GCP (if any)
|
|
|
|
cb => {
|
|
|
|
fs.readdir(this.getGpcFolderPath(), (err, files) => {
|
|
|
|
if (err) cb(err);
|
|
|
|
else{
|
|
|
|
files.forEach(file => {
|
|
|
|
if (/\.txt$/gi.test(file)){
|
|
|
|
this.gpcFiles.push(file);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
logger.debug(`Found ${this.gpcFiles.length} GPC files (${this.gpcFiles.join(" ")}) for ${this.uuid}`);
|
|
|
|
cb(null);
|
|
|
|
}
|
|
|
|
});
|
2016-07-07 22:07:17 +00:00
|
|
|
}
|
2016-07-30 18:30:56 +00:00
|
|
|
], err => {
|
|
|
|
done(err, this);
|
2016-07-07 22:07:17 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-07-15 21:19:50 +00:00
|
|
|
static CreateFromSerialized(taskJson, done){
|
|
|
|
new Task(taskJson.uuid, taskJson.name, (err, task) => {
|
|
|
|
if (err) done(err);
|
|
|
|
else{
|
|
|
|
// Override default values with those
|
|
|
|
// provided in the taskJson
|
|
|
|
for (let k in taskJson){
|
|
|
|
task[k] = taskJson[k];
|
|
|
|
}
|
2016-07-28 22:59:08 +00:00
|
|
|
|
2016-07-15 21:19:50 +00:00
|
|
|
// Tasks that were running should be put back to QUEUED state
|
|
|
|
if (task.status.code === statusCodes.RUNNING){
|
|
|
|
task.status.code = statusCodes.QUEUED;
|
|
|
|
}
|
|
|
|
done(null, task);
|
|
|
|
}
|
2016-07-26 19:20:37 +00:00
|
|
|
}, taskJson.options);
|
2016-07-15 21:19:50 +00:00
|
|
|
}
|
|
|
|
|
2016-07-08 20:44:48 +00:00
|
|
|
// Get path where images are stored for this task
|
|
|
|
// (relative to nodejs process CWD)
|
|
|
|
getImagesFolderPath(){
|
2016-07-09 16:58:14 +00:00
|
|
|
return `${this.getProjectFolderPath()}/images`;
|
|
|
|
}
|
|
|
|
|
2016-07-30 18:30:56 +00:00
|
|
|
// Get path where GPC file(s) are stored
|
|
|
|
// (relative to nodejs process CWD)
|
|
|
|
getGpcFolderPath(){
|
|
|
|
return `${this.getProjectFolderPath()}/gpc`;
|
|
|
|
}
|
|
|
|
|
2016-07-09 16:58:14 +00:00
|
|
|
// Get path of project (where all images and assets folder are contained)
|
|
|
|
// (relative to nodejs process CWD)
|
|
|
|
getProjectFolderPath(){
|
|
|
|
return `data/${this.uuid}`;
|
2016-07-08 20:44:48 +00:00
|
|
|
}
|
|
|
|
|
2016-07-18 21:00:01 +00:00
|
|
|
// Get the path of the archive where all assets
|
|
|
|
// outputted by this task are stored.
|
|
|
|
getAssetsArchivePath(){
|
|
|
|
return `${this.getProjectFolderPath()}/all.zip`;
|
|
|
|
}
|
|
|
|
|
2016-07-14 21:42:12 +00:00
|
|
|
// Deletes files and folders related to this task
|
|
|
|
cleanup(cb){
|
|
|
|
rmdir(this.getProjectFolderPath(), cb);
|
|
|
|
}
|
|
|
|
|
2016-07-08 20:44:48 +00:00
|
|
|
setStatus(code, extra){
|
|
|
|
this.status = {
|
|
|
|
code: code
|
|
|
|
};
|
2016-07-28 22:59:08 +00:00
|
|
|
for (let k in extra){
|
2016-07-08 20:44:48 +00:00
|
|
|
this.status[k] = extra[k];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-14 21:42:12 +00:00
|
|
|
updateProcessingTime(resetTime){
|
2016-07-28 22:59:08 +00:00
|
|
|
this.processingTime = resetTime ?
|
2016-07-14 21:42:12 +00:00
|
|
|
-1 :
|
|
|
|
new Date().getTime() - this.dateCreated;
|
|
|
|
}
|
|
|
|
|
|
|
|
startTrackingProcessingTime(){
|
|
|
|
this.updateProcessingTime();
|
|
|
|
if (!this._updateProcessingTimeInterval){
|
|
|
|
this._updateProcessingTimeInterval = setInterval(() => {
|
|
|
|
this.updateProcessingTime();
|
|
|
|
}, 1000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
stopTrackingProcessingTime(resetTime){
|
|
|
|
this.updateProcessingTime(resetTime);
|
|
|
|
if (this._updateProcessingTimeInterval){
|
|
|
|
clearInterval(this._updateProcessingTimeInterval);
|
|
|
|
this._updateProcessingTimeInterval = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-08 20:44:48 +00:00
|
|
|
getStatus(){
|
|
|
|
return this.status.code;
|
|
|
|
}
|
|
|
|
|
2016-07-14 21:42:12 +00:00
|
|
|
isCanceled(){
|
|
|
|
return this.status.code === statusCodes.CANCELED;
|
|
|
|
}
|
|
|
|
|
2016-07-08 20:44:48 +00:00
|
|
|
// Cancels the current task (unless it's already canceled)
|
2016-07-07 22:07:17 +00:00
|
|
|
cancel(cb){
|
|
|
|
if (this.status.code !== statusCodes.CANCELED){
|
2016-07-14 21:42:12 +00:00
|
|
|
let wasRunning = this.status.code === statusCodes.RUNNING;
|
2016-07-08 20:44:48 +00:00
|
|
|
this.setStatus(statusCodes.CANCELED);
|
2016-07-28 22:59:08 +00:00
|
|
|
|
2016-07-14 21:42:12 +00:00
|
|
|
if (wasRunning && this.runnerProcess){
|
2016-07-25 20:10:34 +00:00
|
|
|
// TODO: this does NOT guarantee that
|
2016-07-14 21:42:12 +00:00
|
|
|
// the process will immediately terminate.
|
|
|
|
// In fact, often times ODM will continue running for a while
|
2016-07-28 22:59:08 +00:00
|
|
|
// This might need to be fixed on ODM's end.
|
2016-07-14 21:42:12 +00:00
|
|
|
this.runnerProcess.kill('SIGINT');
|
|
|
|
this.runnerProcess = null;
|
|
|
|
}
|
2016-07-07 22:07:17 +00:00
|
|
|
|
2016-07-14 21:42:12 +00:00
|
|
|
this.stopTrackingProcessingTime(true);
|
2016-07-07 22:07:17 +00:00
|
|
|
cb(null);
|
|
|
|
}else{
|
|
|
|
cb(new Error("Task already cancelled"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-08 20:44:48 +00:00
|
|
|
// Starts processing the task with OpenDroneMap
|
|
|
|
// This will spawn a new process.
|
|
|
|
start(done){
|
2016-07-18 22:56:27 +00:00
|
|
|
const postProcess = () => {
|
|
|
|
let output = fs.createWriteStream(this.getAssetsArchivePath());
|
|
|
|
let archive = archiver.create('zip', {});
|
|
|
|
|
|
|
|
archive.on('finish', () => {
|
|
|
|
// TODO: is this being fired twice?
|
|
|
|
this.setStatus(statusCodes.COMPLETED);
|
|
|
|
finished();
|
|
|
|
});
|
|
|
|
|
|
|
|
archive.on('error', err => {
|
|
|
|
this.setStatus(statusCodes.FAILED);
|
|
|
|
finished(err);
|
|
|
|
});
|
|
|
|
|
|
|
|
archive.pipe(output);
|
|
|
|
archive
|
|
|
|
.directory(`${this.getProjectFolderPath()}/odm_orthophoto`, 'odm_orthophoto')
|
|
|
|
.directory(`${this.getProjectFolderPath()}/odm_georeferencing`, 'odm_georeferencing')
|
|
|
|
.directory(`${this.getProjectFolderPath()}/odm_texturing`, 'odm_texturing')
|
|
|
|
.directory(`${this.getProjectFolderPath()}/odm_meshing`, 'odm_meshing')
|
|
|
|
.finalize();
|
|
|
|
};
|
2016-07-17 23:01:38 +00:00
|
|
|
|
2016-07-18 22:56:27 +00:00
|
|
|
const finished = err => {
|
2016-07-17 23:01:38 +00:00
|
|
|
this.stopTrackingProcessingTime();
|
2016-07-18 22:56:27 +00:00
|
|
|
done(err);
|
|
|
|
};
|
2016-07-17 23:01:38 +00:00
|
|
|
|
2016-07-08 20:44:48 +00:00
|
|
|
if (this.status.code === statusCodes.QUEUED){
|
2016-07-14 21:42:12 +00:00
|
|
|
this.startTrackingProcessingTime();
|
2016-07-08 20:44:48 +00:00
|
|
|
this.setStatus(statusCodes.RUNNING);
|
2016-07-29 22:16:22 +00:00
|
|
|
|
|
|
|
let runnerOptions = this.options.reduce((result, opt) => {
|
|
|
|
result[opt.name] = opt.value;
|
|
|
|
return result;
|
|
|
|
}, {});
|
|
|
|
|
2016-07-30 18:30:56 +00:00
|
|
|
runnerOptions["project-path"] = fs.realpathSync(this.getProjectFolderPath());
|
2016-07-29 22:16:22 +00:00
|
|
|
runnerOptions["pmvs-num-cores"] = os.cpus().length;
|
|
|
|
|
2016-07-30 18:30:56 +00:00
|
|
|
if (this.gpcFiles.length > 0){
|
|
|
|
runnerOptions["odm_georeferencing-useGcp"] = true;
|
|
|
|
runnerOptions["odm_georeferencing-gcpFile"] = fs.realpathSync(path.join(this.getGpcFolderPath(), this.gpcFiles[0]));
|
|
|
|
}
|
|
|
|
|
2016-07-29 22:16:22 +00:00
|
|
|
this.runnerProcess = odmRunner.run(runnerOptions, (err, code, signal) => {
|
2016-07-08 20:44:48 +00:00
|
|
|
if (err){
|
|
|
|
this.setStatus(statusCodes.FAILED, {errorMessage: `Could not start process (${err.message})`});
|
2016-07-17 23:01:38 +00:00
|
|
|
finished();
|
2016-07-08 20:44:48 +00:00
|
|
|
}else{
|
2016-07-14 21:42:12 +00:00
|
|
|
// Don't evaluate if we caused the process to exit via SIGINT?
|
|
|
|
if (this.status.code !== statusCodes.CANCELED){
|
|
|
|
if (code === 0){
|
2016-07-18 22:56:27 +00:00
|
|
|
postProcess();
|
2016-07-14 21:42:12 +00:00
|
|
|
}else{
|
|
|
|
this.setStatus(statusCodes.FAILED, {errorMessage: `Process exited with code ${code}`});
|
2016-07-17 23:01:38 +00:00
|
|
|
finished();
|
2016-07-14 21:42:12 +00:00
|
|
|
}
|
2016-07-17 23:01:38 +00:00
|
|
|
}else{
|
|
|
|
finished();
|
2016-07-08 20:44:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}, output => {
|
2016-07-09 16:58:14 +00:00
|
|
|
// Replace console colors
|
|
|
|
output = output.replace(/\x1b\[[0-9;]*m/g, "");
|
2016-07-08 20:44:48 +00:00
|
|
|
this.output.push(output);
|
|
|
|
});
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}else{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Re-executes the task (by setting it's state back to QUEUED)
|
|
|
|
// Only tasks that have been canceled or have failed can be restarted.
|
2016-07-07 22:07:17 +00:00
|
|
|
restart(cb){
|
2016-07-08 20:44:48 +00:00
|
|
|
if (this.status.code === statusCodes.CANCELED || this.status.code === statusCodes.FAILED){
|
|
|
|
this.setStatus(statusCodes.QUEUED);
|
2016-07-14 21:42:12 +00:00
|
|
|
this.dateCreated = new Date().getTime();
|
|
|
|
this.output = [];
|
|
|
|
this.stopTrackingProcessingTime(true);
|
2016-07-07 22:07:17 +00:00
|
|
|
cb(null);
|
|
|
|
}else{
|
|
|
|
cb(new Error("Task cannot be restarted"));
|
|
|
|
}
|
2016-07-06 18:44:20 +00:00
|
|
|
}
|
|
|
|
|
2016-07-08 20:44:48 +00:00
|
|
|
// Returns the description of the task.
|
2016-07-06 18:44:20 +00:00
|
|
|
getInfo(){
|
|
|
|
return {
|
|
|
|
uuid: this.uuid,
|
|
|
|
name: this.name,
|
|
|
|
dateCreated: this.dateCreated,
|
2016-07-14 21:42:12 +00:00
|
|
|
processingTime: this.processingTime,
|
2016-07-06 18:44:20 +00:00
|
|
|
status: this.status,
|
2016-07-07 22:07:17 +00:00
|
|
|
options: this.options,
|
|
|
|
imagesCount: this.images.length
|
2016-07-06 18:44:20 +00:00
|
|
|
}
|
|
|
|
}
|
2016-07-08 20:44:48 +00:00
|
|
|
|
|
|
|
// Returns the output of the OpenDroneMap process
|
|
|
|
// Optionally starting from a certain line number
|
|
|
|
getOutput(startFromLine = 0){
|
2016-07-09 16:58:14 +00:00
|
|
|
return this.output.slice(startFromLine, this.output.length);
|
2016-07-08 20:44:48 +00:00
|
|
|
}
|
2016-07-15 21:19:50 +00:00
|
|
|
|
|
|
|
// Returns the data necessary to serialize this
|
|
|
|
// task to restore it later.
|
|
|
|
serialize(){
|
|
|
|
return {
|
|
|
|
uuid: this.uuid,
|
|
|
|
name: this.name,
|
|
|
|
dateCreated: this.dateCreated,
|
|
|
|
status: this.status,
|
|
|
|
options: this.options
|
|
|
|
}
|
|
|
|
}
|
2016-07-28 22:59:08 +00:00
|
|
|
};
|