From b9ecdde14a9e13b10cf2203838f2fd167de0844e Mon Sep 17 00:00:00 2001 From: Lee Pepper Date: Fri, 7 Apr 2017 22:05:20 -0600 Subject: [PATCH] ZIP Uploads Add: Zip file uploads with optional GCP file --- .dockerignore | 3 + .gitignore | 6 + Dockerfile | 6 +- index.js | 884 ++++++++++++++++++++++++---------------------- package.json | 2 + public/index.html | 230 ++++++------ public/js/main.js | 310 ++++++++-------- 7 files changed, 775 insertions(+), 666 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0cc72dd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +tests +tmp diff --git a/.gitignore b/.gitignore index 3eb8312..a052743 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,9 @@ jspm_packages # Elastic Beanstalk .elasticbeanstalk + + + .vscode +.vscode/formatter.json +.vscode/settings.json +.vscode/launch.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index bf79487..acb0453 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,8 +30,12 @@ RUN cd /staging/PotreeConverter && \ RUN mkdir /var/www WORKDIR "/var/www" -RUN git clone https://github.com/OpenDroneMap/node-OpenDroneMap . +#RUN git clone https://github.com/OpenDroneMap/node-OpenDroneMap . + +COPY . /var/www + RUN npm install +RUN mkdir tmp # Fix old version of gdal2tiles.py # RUN (cd / && patch -p0) { - let dstPath = path.join("tmp", req.id); - fs.exists(dstPath, exists => { - if (!exists){ - fs.mkdir(dstPath, undefined, () => { - cb(null, dstPath); - }); - }else{ - cb(null, dstPath); - } - }); - }, - filename: (req, file, cb) => { - cb(null, file.originalname); - } - }) + storage: multer.diskStorage({ + destination: (req, file, cb) => { + let dstPath = path.join("tmp", req.id); + fs.exists(dstPath, exists => { + if (!exists) { + fs.mkdir(dstPath, undefined, () => { + cb(null, dstPath); + }); + } else { + cb(null, dstPath); + } + }); + }, + filename: (req, file, cb) => { + cb(null, file.originalname); + } + }) }); let taskManager; let server; /** @swagger -* /task/new: -* post: -* description: Creates a new task and places it at the end of the processing queue -* tags: [task] -* consumes: -* - multipart/form-data -* parameters: -* - -* name: images -* in: formData -* description: Images to process, plus an optional GPC file. If included, the GPC file should have .txt extension -* required: true -* type: file -* - -* name: name -* in: formData -* description: An optional name to be associated with the task -* required: false -* type: string -* - -* name: options -* in: formData -* description: 'Serialized JSON string of the options to use for processing, as an array of the format: [{name: option1, value: value1}, {name: option2, value: value2}, ...]. For example, [{"name":"cmvs-maxImages","value":"500"},{"name":"time","value":true}]. For a list of all options, call /options' -* required: false -* type: string -* responses: -* 200: -* description: Success -* schema: -* type: object -* required: [uuid] -* properties: -* uuid: -* type: string -* description: UUID of the newly created task -* default: -* description: Error -* schema: -* $ref: '#/definitions/Error' -*/ + * /task/new: + * post: + * description: Creates a new task and places it at the end of the processing queue + * tags: [task] + * consumes: + * - multipart/form-data + * parameters: + * - + * name: images + * in: formData + * description: Images to process, plus an optional GPC file. If included, the GPC file should have .txt extension + * required: true + * type: file + * - + * name: zipurl + * in: formData + * description: Images to process from a url zip file, plus an optional GPC file. If included, the GPC file should have .txt extension + * required: optional + * type: file + * - + * name: name + * in: formData + * description: An optional name to be associated with the task + * required: false + * type: string + * - + * name: options + * in: formData + * description: 'Serialized JSON string of the options to use for processing, as an array of the format: [{name: option1, value: value1}, {name: option2, value: value2}, ...]. For example, [{"name":"cmvs-maxImages","value":"500"},{"name":"time","value":true}]. For a list of all options, call /options' + * required: false + * type: string + * responses: + * 200: + * description: Success + * schema: + * type: object + * required: [uuid] + * properties: + * uuid: + * type: string + * description: UUID of the newly created task + * default: + * description: Error + * schema: + * $ref: '#/definitions/Error' + */ app.post('/task/new', addRequestId, upload.array('images'), (req, res) => { - if (!req.files || req.files.length === 0) res.json({error: "Need at least 1 file."}); - else{ - let srcPath = path.join("tmp", req.id); - let destPath = path.join(Directories.data, req.id); - let destImagesPath = path.join(destPath, "images"); - let destGpcPath = path.join(destPath, "gpc"); + if ((!req.files || req.files.length === 0) && !req.body.zipurl) res.json({ error: "Need at least 1 file or a zip file url." }); + else { + let srcPath = path.join("tmp", req.id); + let destPath = path.join(Directories.data, req.id); + let destImagesPath = path.join(destPath, "images"); + let destGpcPath = path.join(destPath, "gpc"); - async.series([ - cb => { - odmOptions.filterOptions(req.body.options, (err, options) => { - if (err) cb(err); - else{ - req.body.options = options; - cb(null); - } - }); - }, - // Move all uploads to data//images dir - cb => { - fs.stat(destPath, (err, stat) => { - if (err && err.code === 'ENOENT') cb(); - else cb(new Error(`Directory exists (should not have happened: ${err.code})`)); - }); - }, - cb => fs.mkdir(destPath, undefined, cb), - cb => fs.mkdir(destGpcPath, undefined, cb), - cb => fs.rename(srcPath, destImagesPath, cb), - cb => { - // Find any *.txt (GPC) file and move it to the data//gpc directory - fs.readdir(destImagesPath, (err, entries) => { - if (err) cb(err); - else{ - async.eachSeries(entries, (entry, cb) => { - if (/\.txt$/gi.test(entry)){ - fs.rename(path.join(destImagesPath, entry), path.join(destGpcPath, entry), cb); - }else cb(); - }, cb); - } - }); - }, - // Create task - cb => { - new Task(req.id, req.body.name, (err, task) => { - if (err) cb(err); - else{ - taskManager.addNew(task); - res.json({uuid: req.id}); - cb(); - } - }, req.body.options); - } - ], err => { - if (err) res.json({error: err.message}); - }); - } + + async.series([ + // moved these up becaus ei need to populat ethem if zip file first + + cb => { + odmOptions.filterOptions(req.body.options, (err, options) => { + if (err) cb(err); + else { + req.body.options = options; + cb(null); + } + }); + }, + + // Move all uploads to data//images dir + cb => { + if (req.files && req.files.length > 0) { + setTimeout(function() { + fs.stat(destPath, (err, stat) => { + if (err && err.code === 'ENOENT') cb(); + else cb(new Error(`Directory exists (should not have happened: ${err.code})`)); + }); + }, 500); + } else { + cb(); + } + }, + cb => { fs.mkdir(srcPath, undefined, cb) }, + cb => { fs.mkdir(destPath, undefined, cb) }, + cb => { fs.mkdir(destGpcPath, undefined, cb) }, + cb => fs.rename(srcPath, destImagesPath, cb), + cb => { + if (req.body.zipurl) { + var filename = path.basename(req.body.zipurl); + download(req.body.zipurl, destPath + '/' + filename, function() { + // unzip and flatten the zip file (incase there are folders in the zip) + fs.createReadStream(destPath + '/' + filename).pipe(unzip.Parse()) + .on('entry', function(entry) { + if (entry.type === 'File') { + entry.pipe(fs.createWriteStream(destImagesPath + '/' + path.basename(entry.path))); + } else { + entry.autodrain(); + } + }).on('close', function() { + cb(); + }); + }); + } else { + cb(); + } + }, + + cb => { + // Find any *.txt (GPC) file and move it to the data//gpc directory + fs.readdir(destImagesPath, (err, entries) => { + if (err) cb(err); + else { + async.eachSeries(entries, (entry, cb) => { + if (/\.txt$/gi.test(entry)) { + fs.rename(path.join(destImagesPath, entry), path.join(destGpcPath, entry), cb); + } else cb(); + }, cb); + } + }); + }, + + // Create task + cb => { + new Task(req.id, req.body.name, (err, task) => { + if (err) cb(err); + else { + taskManager.addNew(task); + res.json({ uuid: req.id }); + cb(); + } + }, req.body.options); + } + ], err => { + if (err) res.json({ error: err.message }); + }); + } + }); let getTaskFromUuid = (req, res, next) => { - let task = taskManager.find(req.params.uuid); - if (task){ - req.task = task; - next(); - }else res.json({error: `${req.params.uuid} not found`}); + let task = taskManager.find(req.params.uuid); + if (task) { + req.task = task; + next(); + } else res.json({ error: `${req.params.uuid} not found` }); }; /** @swagger -* /task/{uuid}/info: -* get: -* description: Gets information about this task, such as name, creation date, processing time, status, command line options and number of images being processed. See schema definition for a full list. -* tags: [task] -* parameters: -* - -* name: uuid -* in: path -* description: UUID of the task -* required: true -* type: string -* responses: -* 200: -* description: Task Information -* schema: -* title: TaskInfo -* type: object -* required: [uuid, name, dateCreated, processingTime, status, options, imagesCount] -* properties: -* uuid: -* type: string -* description: UUID -* name: -* type: string -* description: Name -* dateCreated: -* type: integer -* description: Timestamp -* processingTime: -* type: integer -* description: Milliseconds that have elapsed since the task started being processed. -* status: -* type: integer -* description: Status code (10 = QUEUED, 20 = RUNNING, 30 = FAILED, 40 = COMPLETED, 50 = CANCELED) -* enum: [10, 20, 30, 40, 50] -* options: -* type: array -* description: List of options used to process this task -* items: -* type: object -* required: [name, value] -* properties: -* name: -* type: string -* description: 'Option name (example: "odm_meshing-octreeDepth")' -* value: -* type: string -* description: 'Value (example: 9)' -* imagesCount: -* type: integer -* description: Number of images -* default: -* description: Error -* schema: -* $ref: '#/definitions/Error' -*/ + * /task/{uuid}/info: + * get: + * description: Gets information about this task, such as name, creation date, processing time, status, command line options and number of images being processed. See schema definition for a full list. + * tags: [task] + * parameters: + * - + * name: uuid + * in: path + * description: UUID of the task + * required: true + * type: string + * responses: + * 200: + * description: Task Information + * schema: + * title: TaskInfo + * type: object + * required: [uuid, name, dateCreated, processingTime, status, options, imagesCount] + * properties: + * uuid: + * type: string + * description: UUID + * name: + * type: string + * description: Name + * dateCreated: + * type: integer + * description: Timestamp + * processingTime: + * type: integer + * description: Milliseconds that have elapsed since the task started being processed. + * status: + * type: integer + * description: Status code (10 = QUEUED, 20 = RUNNING, 30 = FAILED, 40 = COMPLETED, 50 = CANCELED) + * enum: [10, 20, 30, 40, 50] + * options: + * type: array + * description: List of options used to process this task + * items: + * type: object + * required: [name, value] + * properties: + * name: + * type: string + * description: 'Option name (example: "odm_meshing-octreeDepth")' + * value: + * type: string + * description: 'Value (example: 9)' + * imagesCount: + * type: integer + * description: Number of images + * default: + * description: Error + * schema: + * $ref: '#/definitions/Error' + */ app.get('/task/:uuid/info', getTaskFromUuid, (req, res) => { - res.json(req.task.getInfo()); + res.json(req.task.getInfo()); }); /** @swagger -* /task/{uuid}/output: -* get: -* description: Retrieves the console output of the OpenDroneMap's process. Useful for monitoring execution and to provide updates to the user. -* tags: [task] -* parameters: -* - -* name: uuid -* in: path -* description: UUID of the task -* required: true -* type: string -* - -* name: line -* in: query -* description: Optional line number that the console output should be truncated from. For example, passing a value of 100 will retrieve the console output starting from line 100. Defaults to 0 (retrieve all console output). -* default: 0 -* required: false -* type: integer -* responses: -* 200: -* description: Console Output -* schema: -* type: string -* default: -* description: Error -* schema: -* $ref: '#/definitions/Error' -*/ + * /task/{uuid}/output: + * get: + * description: Retrieves the console output of the OpenDroneMap's process. Useful for monitoring execution and to provide updates to the user. + * tags: [task] + * parameters: + * - + * name: uuid + * in: path + * description: UUID of the task + * required: true + * type: string + * - + * name: line + * in: query + * description: Optional line number that the console output should be truncated from. For example, passing a value of 100 will retrieve the console output starting from line 100. Defaults to 0 (retrieve all console output). + * default: 0 + * required: false + * type: integer + * responses: + * 200: + * description: Console Output + * schema: + * type: string + * default: + * description: Error + * schema: + * $ref: '#/definitions/Error' + */ app.get('/task/:uuid/output', getTaskFromUuid, (req, res) => { - res.json(req.task.getOutput(req.query.line)); + res.json(req.task.getOutput(req.query.line)); }); /** @swagger -* /task/{uuid}/download/{asset}: -* get: -* description: Retrieves an asset (the output of OpenDroneMap's processing) associated with a task -* tags: [task] -* produces: [application/zip] -* parameters: -* - name: uuid -* in: path -* type: string -* description: UUID of the task -* required: true -* - name: asset -* in: path -* type: string -* description: Type of asset to download. Use "all.zip" for zip file containing all assets. -* required: true -* enum: -* - all.zip -* - orthophoto.tif -* responses: -* 200: -* description: Asset File -* schema: -* type: file -* default: -* description: Error message -* schema: -* $ref: '#/definitions/Error' -*/ + * /task/{uuid}/download/{asset}: + * get: + * description: Retrieves an asset (the output of OpenDroneMap's processing) associated with a task + * tags: [task] + * produces: [application/zip] + * parameters: + * - name: uuid + * in: path + * type: string + * description: UUID of the task + * required: true + * - name: asset + * in: path + * type: string + * description: Type of asset to download. Use "all.zip" for zip file containing all assets. + * required: true + * enum: + * - all.zip + * - orthophoto.tif + * responses: + * 200: + * description: Asset File + * schema: + * type: file + * default: + * description: Error message + * schema: + * $ref: '#/definitions/Error' + */ app.get('/task/:uuid/download/:asset', getTaskFromUuid, (req, res) => { - let asset = req.params.asset !== undefined ? req.params.asset : "all.zip"; - let filePath = req.task.getAssetsArchivePath(asset); - if (filePath){ - if (fs.existsSync(filePath)){ - res.setHeader('Content-Disposition', `attachment; filename=${asset}`); - res.setHeader('Content-Type', mime.lookup(asset)); - res.setHeader('Content-Length', fs.statSync(filePath)["size"]); + let asset = req.params.asset !== undefined ? req.params.asset : "all.zip"; + let filePath = req.task.getAssetsArchivePath(asset); + if (filePath) { + if (fs.existsSync(filePath)) { + res.setHeader('Content-Disposition', `attachment; filename=${asset}`); + res.setHeader('Content-Type', mime.lookup(asset)); + res.setHeader('Content-Length', fs.statSync(filePath)["size"]); - const filestream = fs.createReadStream(filePath); - filestream.pipe(res); - }else{ - res.json({error: "Asset not ready"}); - } - }else{ - res.json({error: "Invalid asset"}); - } + const filestream = fs.createReadStream(filePath); + filestream.pipe(res); + } else { + res.json({ error: "Asset not ready" }); + } + } else { + res.json({ error: "Invalid asset" }); + } }); /** @swagger -* definition: -* Error: -* type: object -* required: -* - error -* properties: -* error: -* type: string -* description: Description of the error -* Response: -* type: object -* required: -* - success -* properties: -* success: -* type: boolean -* description: true if the command succeeded, false otherwise -* error: -* type: string -* description: Error message if an error occured -*/ + * definition: + * Error: + * type: object + * required: + * - error + * properties: + * error: + * type: string + * description: Description of the error + * Response: + * type: object + * required: + * - success + * properties: + * success: + * type: boolean + * description: true if the command succeeded, false otherwise + * error: + * type: string + * description: Error message if an error occured + */ let uuidCheck = (req, res, next) => { - if (!req.body.uuid) res.json({error: "uuid param missing."}); + if (!req.body.uuid) res.json({ error: "uuid param missing." }); else next(); }; let successHandler = res => { - return err => { - if (!err) res.json({success: true}); - else res.json({success: false, error: err.message}); - }; + return err => { + if (!err) res.json({ success: true }); + else res.json({ success: false, error: err.message }); + }; }; /** @swagger -* /task/cancel: -* post: -* description: Cancels a task (stops its execution, or prevents it from being executed) -* parameters: -* - -* name: uuid -* in: body -* description: UUID of the task -* required: true -* schema: -* type: string -* responses: -* 200: -* description: Command Received -* schema: -* $ref: "#/definitions/Response" -*/ + * /task/cancel: + * post: + * description: Cancels a task (stops its execution, or prevents it from being executed) + * parameters: + * - + * name: uuid + * in: body + * description: UUID of the task + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Command Received + * schema: + * $ref: "#/definitions/Response" + */ app.post('/task/cancel', uuidCheck, (req, res) => { - taskManager.cancel(req.body.uuid, successHandler(res)); + taskManager.cancel(req.body.uuid, successHandler(res)); }); /** @swagger -* /task/remove: -* post: -* description: Removes a task and deletes all of its assets -* parameters: -* - -* name: uuid -* in: body -* description: UUID of the task -* required: true -* schema: -* type: string -* responses: -* 200: -* description: Command Received -* schema: -* $ref: "#/definitions/Response" -*/ + * /task/remove: + * post: + * description: Removes a task and deletes all of its assets + * parameters: + * - + * name: uuid + * in: body + * description: UUID of the task + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Command Received + * schema: + * $ref: "#/definitions/Response" + */ app.post('/task/remove', uuidCheck, (req, res) => { - taskManager.remove(req.body.uuid, successHandler(res)); + taskManager.remove(req.body.uuid, successHandler(res)); }); /** @swagger -* /task/restart: -* post: -* description: Restarts a task that was previously canceled or that had failed to process -* parameters: -* - -* name: uuid -* in: body -* description: UUID of the task -* required: true -* schema: -* type: string -* responses: -* 200: -* description: Command Received -* schema: -* $ref: "#/definitions/Response" -*/ + * /task/restart: + * post: + * description: Restarts a task that was previously canceled or that had failed to process + * parameters: + * - + * name: uuid + * in: body + * description: UUID of the task + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Command Received + * schema: + * $ref: "#/definitions/Response" + */ app.post('/task/restart', uuidCheck, (req, res) => { - taskManager.restart(req.body.uuid, successHandler(res)); + taskManager.restart(req.body.uuid, successHandler(res)); }); /** @swagger -* /options: -* get: -* description: Retrieves the command line options that can be passed to process a task -* tags: [server] -* responses: -* 200: -* description: Options -* schema: -* type: array -* items: -* title: Option -* type: object -* required: [name, type, value, domain, help] -* properties: -* name: -* type: string -* description: Command line option (exactly as it is passed to the OpenDroneMap process, minus the leading '--') -* type: -* type: string -* description: Datatype of the value of this option -* enum: -* - int -* - float -* - string -* - bool -* value: -* type: string -* description: Default value of this option -* domain: -* type: string -* description: Valid range of values (for example, "positive integer" or "float > 0.0") -* help: -* type: string -* description: Description of what this option does -*/ + * /options: + * get: + * description: Retrieves the command line options that can be passed to process a task + * tags: [server] + * responses: + * 200: + * description: Options + * schema: + * type: array + * items: + * title: Option + * type: object + * required: [name, type, value, domain, help] + * properties: + * name: + * type: string + * description: Command line option (exactly as it is passed to the OpenDroneMap process, minus the leading '--') + * type: + * type: string + * description: Datatype of the value of this option + * enum: + * - int + * - float + * - string + * - bool + * value: + * type: string + * description: Default value of this option + * domain: + * type: string + * description: Valid range of values (for example, "positive integer" or "float > 0.0") + * help: + * type: string + * description: Description of what this option does + */ app.get('/options', (req, res) => { - odmOptions.getOptions((err, options) => { - if (err) res.json({error: err.message}); - else res.json(options); - }); + odmOptions.getOptions((err, options) => { + if (err) res.json({ error: err.message }); + else res.json(options); + }); }); /** @swagger -* /info: -* get: -* description: Retrieves information about this node -* tags: [server] -* responses: -* 200: -* description: Info -* schema: -* type: object -* required: [version, taskQueueCount] -* properties: -* version: -* type: string -* description: Current version -* taskQueueCount: -* type: integer -* description: Number of tasks currently being processed or waiting to be processed -*/ + * /info: + * get: + * description: Retrieves information about this node + * tags: [server] + * responses: + * 200: + * description: Info + * schema: + * type: object + * required: [version, taskQueueCount] + * properties: + * version: + * type: string + * description: Current version + * taskQueueCount: + * type: integer + * description: Number of tasks currently being processed or waiting to be processed + */ app.get('/info', (req, res) => { res.json({ version: packageJson.version, @@ -495,46 +548,47 @@ app.get('/info', (req, res) => { }); let gracefulShutdown = done => { - async.series([ - cb => taskManager.dumpTaskList(cb), - cb => { - logger.info("Closing server"); - server.close(); - logger.info("Exiting..."); - process.exit(0); - } - ], done); + async.series([ + cb => taskManager.dumpTaskList(cb), + cb => { + logger.info("Closing server"); + server.close(); + logger.info("Exiting..."); + process.exit(0); + } + ], done); }; // listen for TERM signal .e.g. kill -process.on ('SIGTERM', gracefulShutdown); +process.on('SIGTERM', gracefulShutdown); // listen for INT signal e.g. Ctrl-C -process.on ('SIGINT', gracefulShutdown); +process.on('SIGINT', gracefulShutdown); // Startup if (config.test) logger.info("Running in test mode"); let commands = [ - cb => odmOptions.initialize(cb), - cb => { taskManager = new TaskManager(cb); }, - cb => { server = app.listen(config.port, err => { - if (!err) logger.info('Server has started on port ' + String(config.port)); - cb(err); - }); - } + cb => odmOptions.initialize(cb), + cb => { taskManager = new TaskManager(cb); }, + cb => { + server = app.listen(config.port, err => { + if (!err) logger.info('Server has started on port ' + String(config.port)); + cb(err); + }); + } ]; -if (config.powercycle){ - commands.push(cb => { - logger.info("Power cycling is set, application will shut down..."); - process.exit(0); - }); +if (config.powercycle) { + commands.push(cb => { + logger.info("Power cycling is set, application will shut down..."); + process.exit(0); + }); } async.series(commands, err => { - if (err){ - logger.error("Error during startup: " + err.message); - process.exit(1); - } -}); + if (err) { + logger.error("Error during startup: " + err.message); + process.exit(1); + } +}); \ No newline at end of file diff --git a/package.json b/package.json index 1b245db..1d3da45 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,10 @@ "multer": "^1.1.0", "node-schedule": "^1.1.1", "node-uuid": "^1.4.7", + "request": "^2.81.0", "rimraf": "^2.5.3", "swagger-jsdoc": "^1.3.1", + "unzip": "^0.1.11", "winston": "^2.2.0" }, "devDependencies": { diff --git a/public/index.html b/public/index.html index b9e7632..71f33f2 100644 --- a/public/index.html +++ b/public/index.html @@ -1,146 +1,164 @@ - - - - Node-OpenDroneMap - - - - - - + + + + Node-OpenDroneMap + + - - - -
- -
-
-

New Task

-
-
-
-
+ + + + + + - - - + + + + \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js index 3f76a0d..85b5d09 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -15,30 +15,30 @@ 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 . */ -$(function(){ - function hoursMinutesSecs(t){ +$(function() { + function hoursMinutesSecs(t) { var ch = 60 * 60 * 1000, cm = 60 * 1000, h = Math.floor(t / ch), - m = Math.floor( (t - h * ch) / cm), - s = Math.round( (t - h * ch - m * cm) / 1000), - pad = function(n){ return n < 10 ? '0' + n : n; }; - if( s === 60 ){ - m++; - s = 0; - } - if( m === 60 ){ - h++; - m = 0; - } - return [pad(h), pad(m), pad(s)].join(':'); + m = Math.floor((t - h * ch) / cm), + s = Math.round((t - h * ch - m * cm) / 1000), + pad = function(n) { return n < 10 ? '0' + n : n; }; + if (s === 60) { + m++; + s = 0; + } + if (m === 60) { + h++; + m = 0; + } + return [pad(h), pad(m), pad(s)].join(':'); } - function TaskList(){ + function TaskList() { var uuids = JSON.parse(localStorage.getItem("odmTaskList") || "[]"); if (Object.prototype.toString.call(uuids) !== "[object Array]") uuids = []; - this.tasks = ko.observableArray($.map(uuids, function(uuid){ + this.tasks = ko.observableArray($.map(uuids, function(uuid) { return new Task(uuid); })); } @@ -46,14 +46,13 @@ $(function(){ this.tasks.push(task); this.saveTaskListToLocalStorage(); }; - TaskList.prototype.saveTaskListToLocalStorage = function(){ - localStorage.setItem("odmTaskList", JSON.stringify($.map(this.tasks(), function(task){ - return task.uuid; - }) - )); + TaskList.prototype.saveTaskListToLocalStorage = function() { + localStorage.setItem("odmTaskList", JSON.stringify($.map(this.tasks(), function(task) { + return task.uuid; + }))); }; - TaskList.prototype.remove = function(task){ - this.tasks.remove(function(t){ + TaskList.prototype.remove = function(task) { + this.tasks.remove(function(t) { return t === task; }); this.saveTaskListToLocalStorage(); @@ -67,7 +66,7 @@ $(function(){ CANCELED: 50 }; - function Task(uuid){ + function Task(uuid) { var self = this; this.uuid = uuid; @@ -101,90 +100,90 @@ $(function(){ } }; - this.statusDescr = ko.pureComputed(function(){ - if (this.info().status && this.info().status.code){ - if(statusCodes[this.info().status.code]){ + this.statusDescr = ko.pureComputed(function() { + if (this.info().status && this.info().status.code) { + if (statusCodes[this.info().status.code]) { return statusCodes[this.info().status.code].descr; - }else return "Unknown (Status Code: " + this.info().status.code + ")"; - }else return "-"; + } else return "Unknown (Status Code: " + this.info().status.code + ")"; + } else return "-"; }, this); - this.icon = ko.pureComputed(function(){ - if (this.info().status && this.info().status.code){ - if(statusCodes[this.info().status.code]){ + this.icon = ko.pureComputed(function() { + if (this.info().status && this.info().status.code) { + if (statusCodes[this.info().status.code]) { return statusCodes[this.info().status.code].icon; - }else return "glyphicon-question-sign"; - }else return ""; + } else return "glyphicon-question-sign"; + } else return ""; }, this); - this.showCancel = ko.pureComputed(function(){ - return this.info().status && - (this.info().status.code === codes.QUEUED || this.info().status.code === codes.RUNNING); + this.showCancel = ko.pureComputed(function() { + return this.info().status && + (this.info().status.code === codes.QUEUED || this.info().status.code === codes.RUNNING); }, this); - this.showRestart = ko.pureComputed(function(){ - return this.info().status && - (this.info().status.code === codes.CANCELED); + this.showRestart = ko.pureComputed(function() { + return this.info().status && + (this.info().status.code === codes.CANCELED); }, this); - this.showRemove = ko.pureComputed(function(){ - return this.info().status && - (this.info().status.code === codes.FAILED || this.info().status.code === codes.COMPLETED || this.info().status.code === codes.CANCELED); + this.showRemove = ko.pureComputed(function() { + return this.info().status && + (this.info().status.code === codes.FAILED || this.info().status.code === codes.COMPLETED || this.info().status.code === codes.CANCELED); }, this); - this.showDownload = ko.pureComputed(function(){ - return this.info().status && - (this.info().status.code === codes.COMPLETED); + this.showDownload = ko.pureComputed(function() { + return this.info().status && + (this.info().status.code === codes.COMPLETED); }, this); this.startRefreshingInfo(); } - Task.prototype.refreshInfo = function(){ + Task.prototype.refreshInfo = function() { var self = this; var url = "/task/" + this.uuid + "/info"; $.get(url) - .done(function(json){ - // Track time + .done(function(json) { + // Track time - if (json.processingTime && json.processingTime !== -1){ - self.timeElapsed(hoursMinutesSecs(json.processingTime)); - } - self.info(json); - }) - .fail(function(){ - self.info({error: url + " is unreachable."}); - }) - .always(function(){ self.loading(false); }); + if (json.processingTime && json.processingTime !== -1) { + self.timeElapsed(hoursMinutesSecs(json.processingTime)); + } + self.info(json); + }) + .fail(function() { + self.info({ error: url + " is unreachable." }); + }) + .always(function() { self.loading(false); }); }; - Task.prototype.consoleMouseOver = function(){ this.autoScrollOutput = false; }; - Task.prototype.consoleMouseOut = function(){ this.autoScrollOutput = true; }; - Task.prototype.resetOutput = function(){ + Task.prototype.consoleMouseOver = function() { this.autoScrollOutput = false; }; + Task.prototype.consoleMouseOut = function() { this.autoScrollOutput = true; }; + Task.prototype.resetOutput = function() { this.viewOutputLine = 0; this.autoScrollOutput = true; this.output.removeAll(); }; - Task.prototype.viewOutput = function(){ + Task.prototype.viewOutput = function() { var self = this; - function fetchOutput(){ + function fetchOutput() { var url = "/task/" + self.uuid + "/output"; - $.get(url, {line: self.viewOutputLine}) - .done(function(output){ - for (var i in output){ - self.output.push(output[i]); - } - if (output.length){ - self.viewOutputLine += output.length; - if (self.autoScrollOutput){ - var $console = $("#console_" + self.uuid); - $console.scrollTop($console[0].scrollHeight - $console.height()); + $.get(url, { line: self.viewOutputLine }) + .done(function(output) { + for (var i in output) { + self.output.push(output[i]); } - } - }) - .fail(function(){ - self.info({error: url + " is unreachable."}); - }); + if (output.length) { + self.viewOutputLine += output.length; + if (self.autoScrollOutput) { + var $console = $("#console_" + self.uuid); + $console.scrollTop($console[0].scrollHeight - $console.height()); + } + } + }) + .fail(function() { + self.info({ error: url + " is unreachable." }); + }); } this.fetchOutputInterval = setInterval(fetchOutput, 2000); fetchOutput(); this.viewingOutput(true); }; - Task.prototype.hideOutput = function(){ + Task.prototype.hideOutput = function() { if (this.fetchOutputInterval) clearInterval(this.fetchOutputInterval); this.viewingOutput(false); }; @@ -192,12 +191,12 @@ $(function(){ var self = this; this.stopRefreshingInfo(); this.refreshInfo(); - this.refreshInterval = setInterval(function(){ + this.refreshInterval = setInterval(function() { self.refreshInfo(); }, 500); // TODO: change to larger value }; Task.prototype.stopRefreshingInfo = function() { - if (this.refreshInterval){ + if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; } @@ -206,62 +205,62 @@ $(function(){ var self = this; var url = "/task/remove"; - function doRemove(){ + function doRemove() { $.post(url, { - uuid: self.uuid - }) - .done(function(json){ - if (json.success || self.info().error){ - taskList.remove(self); - }else{ - self.info({error: json.error}); - } + uuid: self.uuid + }) + .done(function(json) { + if (json.success || self.info().error) { + taskList.remove(self); + } else { + self.info({ error: json.error }); + } - self.stopRefreshingInfo(); - }) - .fail(function(){ - self.info({error: url + " is unreachable."}); - self.stopRefreshingInfo(); - }); + self.stopRefreshingInfo(); + }) + .fail(function() { + self.info({ error: url + " is unreachable." }); + self.stopRefreshingInfo(); + }); } - if (this.info().status && this.info().status.code === codes.COMPLETED){ + if (this.info().status && this.info().status.code === codes.COMPLETED) { if (confirm("Are you sure?")) doRemove(); - }else{ + } else { doRemove(); } }; - function genApiCall(url, onSuccess){ - return function(){ + function genApiCall(url, onSuccess) { + return function() { var self = this; $.post(url, { - uuid: this.uuid - }) - .done(function(json){ - if (json.success){ - if (onSuccess !== undefined) onSuccess(self, json); - self.startRefreshingInfo(); - }else{ + uuid: this.uuid + }) + .done(function(json) { + if (json.success) { + if (onSuccess !== undefined) onSuccess(self, json); + self.startRefreshingInfo(); + } else { + self.stopRefreshingInfo(); + self.info({ error: json.error }); + } + }) + .fail(function() { + self.info({ error: url + " is unreachable." }); self.stopRefreshingInfo(); - self.info({error: json.error}); - } - }) - .fail(function(){ - self.info({error: url + " is unreachable."}); - self.stopRefreshingInfo(); - }); + }); }; } Task.prototype.cancel = genApiCall("/task/cancel"); - Task.prototype.restart = genApiCall("/task/restart", function(task){ + Task.prototype.restart = genApiCall("/task/restart", function(task) { task.resetOutput(); }); - Task.prototype.download = function(){ + Task.prototype.download = function() { location.href = "/task/" + this.uuid + "/download/all.zip"; }; - Task.prototype.downloadOrthophoto = function(){ + Task.prototype.downloadOrthophoto = function() { location.href = "/task/" + this.uuid + "/download/orthophoto.tif"; }; @@ -276,52 +275,75 @@ $(function(){ elErrorContainer: '#errorBlock', showUpload: false, uploadAsync: false, - uploadExtraData: function(){ + uploadExtraData: function() { return { name: $("#taskName").val(), + zipurl: $("#zipurl").val(), options: JSON.stringify(optionsModel.getUserOptions()) }; } }); - $("#btnUpload").click(function(){ + $("#btnUpload").click(function() { $("#btnUpload").attr('disabled', true) - .val("Uploading..."); + .val("Uploading..."); // Start upload $("#images").fileinput('upload'); - }); + }); + +// zip file control +$('#btnShowImport').on('click', function(e){ + e.preventDefault(); + $('#zipFileInput').removeClass('hidden'); + $('#btnShowUpload').removeClass('hidden'); + + $('#imagesInput').addClass('hidden'); + $('#btnShowImport').addClass('hidden'); + +}); + +$('#btnShowUpload').on('click', function(e){ + e.preventDefault(); + $('#imagesInput').removeClass('hidden'); + $('#btnShowImport').removeClass('hidden'); + + $('#zipFileInput').addClass('hidden'); + $('#btnShowUpload').addClass('hidden'); + +}) + var btnUploadLabel = $("#btnUpload").val(); $("#images") - .on('filebatchuploadsuccess', function(e, params){ + .on('filebatchuploadsuccess', function(e, params) { $("#images").fileinput('reset'); - if (params.response && params.response.uuid){ + if (params.response && params.response.uuid) { taskList.add(new Task(params.response.uuid)); } }) - .on('filebatchuploadcomplete', function(){ + .on('filebatchuploadcomplete', function() { $("#btnUpload").removeAttr('disabled') - .val(btnUploadLabel); + .val(btnUploadLabel); }) .on('filebatchuploaderror', console.warn); // Load options - function Option(properties){ + function Option(properties) { this.properties = properties; this.value = ko.observable(); } - Option.prototype.resetToDefault = function(){ + Option.prototype.resetToDefault = function() { this.value(undefined); }; - function OptionsModel(){ + function OptionsModel() { var self = this; this.options = ko.observableArray(); - this.options.subscribe(function(){ - setTimeout(function(){ + this.options.subscribe(function() { + setTimeout(function() { $('#options [data-toggle="tooltip"]').tooltip(); }, 100); }); @@ -329,23 +351,23 @@ $(function(){ this.error = ko.observable(); $.get("/options") - .done(function(json){ - if (json.error) self.error(json.error); - else{ - for (var i in json){ - self.options.push(new Option(json[i])); + .done(function(json) { + if (json.error) self.error(json.error); + else { + for (var i in json) { + self.options.push(new Option(json[i])); + } } - } - }) - .fail(function(){ - self.error("options are not available."); - }); + }) + .fail(function() { + self.error("options are not available."); + }); } - OptionsModel.prototype.getUserOptions = function(){ + OptionsModel.prototype.getUserOptions = function() { var result = []; - for (var i = 0; i < this.options().length; i++){ + for (var i = 0; i < this.options().length; i++) { var opt = this.options()[i]; - if (opt.value() !== undefined){ + if (opt.value() !== undefined) { result.push({ name: opt.properties.name, value: opt.value() @@ -357,4 +379,4 @@ $(function(){ var optionsModel = new OptionsModel(); ko.applyBindings(optionsModel, document.getElementById("options")); -}); +}); \ No newline at end of file