diff --git a/Dockerfile b/Dockerfile index 99bc877..96c961a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ EXPOSE 3000 USER root RUN curl --silent --location https://deb.nodesource.com/setup_6.x | sudo bash - -RUN apt-get install -y nodejs +RUN apt-get install -y nodejs python-gdal RUN npm install -g nodemon RUN mkdir /var/www diff --git a/index.js b/index.js index b80faab..f71ee68 100644 --- a/index.js +++ b/index.js @@ -300,6 +300,7 @@ app.get('/task/:uuid/output', getTaskFromUuid, (req, res) => { * - orthophoto.png * - orthophoto.tif * - textured_model.zip +* - orthophoto_tiles.zip * responses: * 200: * description: Asset File diff --git a/libs/Task.js b/libs/Task.js index 691b8e3..a3943ca 100644 --- a/libs/Task.js +++ b/libs/Task.js @@ -26,6 +26,7 @@ let glob = require("glob"); let path = require('path'); let rmdir = require('rimraf'); let odmRunner = require('./odmRunner'); +let gdalRunner = require('./gdalRunner'); let archiver = require('archiver'); let os = require('os'); let Directories = require('./Directories'); @@ -46,6 +47,7 @@ module.exports = class Task{ this.gpcFiles = []; this.output = []; this.runnerProcess = null; + this.tilingProcess = null; async.series([ // Read images info @@ -126,6 +128,7 @@ module.exports = class Task{ case "georeferenced_model.las.zip": case "georeferenced_model.csv.zip": case "textured_model.zip": + case "orthophoto_tiles.zip": // OK break; case "orthophoto.png": @@ -194,13 +197,20 @@ module.exports = class Task{ let wasRunning = this.status.code === statusCodes.RUNNING; this.setStatus(statusCodes.CANCELED); - if (wasRunning && this.runnerProcess){ - // TODO: this does NOT guarantee that - // the process will immediately terminate. - // In fact, often times ODM will continue running for a while - // This might need to be fixed on ODM's end. - this.runnerProcess.kill('SIGINT'); - this.runnerProcess = null; + if (wasRunning){ + if (this.runnerProcess){ + // TODO: this does NOT guarantee that + // the process will immediately terminate. + // In fact, often times ODM will continue running for a while + // This might need to be fixed on ODM's end. + this.runnerProcess.kill('SIGINT'); + this.runnerProcess = null; + } + + if (this.tilingProcess){ + this.tilingProcess.kill('SIGINT'); + this.tilingProcess = null; + } } this.stopTrackingProcessingTime(true); @@ -283,16 +293,38 @@ module.exports = class Task{ }; }; + const generateTiles = (inputFile, outputDir) => { + return (done) => { + this.tilingProcess = gdalRunner.runTiler({ + zoomLevels: "16-22", + inputFile: path.join(this.getProjectFolderPath(), inputFile), + outputDir: path.join(this.getProjectFolderPath(), outputDir) + }, (err, code, signal) => { + if (err) done(err); + }else{ + // Don't evaluate if we caused the process to exit via SIGINT? + if (code === 0) done(); + else done(new Error(`Process exited with code ${code}`)); + } + }, output => { + this.output.push(output); + }); + }; + }; + + // All paths are relative to the project directory (./data//) async.series([ - createZipArchive("all.zip", ['odm_orthophoto', 'odm_georeferencing', 'odm_texturing', 'odm_meshing']), - createZipArchive("georeferenced_model.ply.zip", [path.join('odm_georeferencing', 'odm_georeferenced_model.ply')]), - createZipArchive("georeferenced_model.las.zip", [path.join('odm_georeferencing', 'odm_georeferenced_model.ply.las')]), - createZipArchive("georeferenced_model.csv.zip", [path.join('odm_georeferencing', 'odm_georeferenced_model.csv')]), - createZipArchive("textured_model.zip", [ - path.join("odm_texturing", "*.jpg"), - path.join("odm_texturing", "odm_textured_model_geo.obj"), - path.join("odm_texturing", "odm_textured_model_geo.mtl") - ]) + generateTiles(path.join('odm_orthophoto', 'odm_orthophoto.tif'), 'orthophoto_tiles'), + createZipArchive('all.zip', ['odm_orthophoto', 'odm_georeferencing', 'odm_texturing', 'odm_meshing']), + createZipArchive('georeferenced_model.ply.zip', [path.join('odm_georeferencing', 'odm_georeferenced_model.ply')]), + createZipArchive('georeferenced_model.las.zip', [path.join('odm_georeferencing', 'odm_georeferenced_model.ply.las')]), + createZipArchive('georeferenced_model.csv.zip', [path.join('odm_georeferencing', 'odm_georeferenced_model.csv')]), + createZipArchive('textured_model.zip', [ + path.join('odm_texturing', '*.jpg'), + path.join('odm_texturing', 'odm_textured_model_geo.obj'), + path.join('odm_texturing', 'odm_textured_model_geo.mtl') + ]), + createZipArchive('orthophoto_tiles.zip', ['orthophoto_tiles']) ], (err) => { if (!err){ this.setStatus(statusCodes.COMPLETED); @@ -324,7 +356,7 @@ module.exports = class Task{ this.runnerProcess = odmRunner.run(runnerOptions, (err, code, signal) => { if (err){ this.setStatus(statusCodes.FAILED, {errorMessage: `Could not start process (${err.message})`}); - finished(); + finished(err); }else{ // Don't evaluate if we caused the process to exit via SIGINT? if (this.status.code !== statusCodes.CANCELED){ diff --git a/libs/gdalRunner.js b/libs/gdalRunner.js new file mode 100644 index 0000000..fadf596 --- /dev/null +++ b/libs/gdalRunner.js @@ -0,0 +1,71 @@ +/* +Node-OpenDroneMap Node.js App and REST API to access OpenDroneMap. +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 . +*/ +"use strict"; +let fs = require('fs'); +let path = require('path'); +let assert = require('assert'); +let spawn = require('child_process').spawn; +let config = require('../config.js'); +let logger = require('./logger'); + + +module.exports = { + runTiler: function(options, done, outputReceived){ + assert(options.zoomLevels !== undefined, "zoomLevels must be defined"); + assert(options.inputFile !== undefined, "inputFile must be defined"); + assert(options.outputDir !== undefined, "outputDir must be defined"); + + let command = ["-z", options.zoomLevels, + "-n", + fs.realpathSync(options.inputFile), + fs.realpathSync(option.outputDir) + ]; + logger.info(`About to run: gdal2tiles.py ${command.join(" ")}`); + + if (config.test){ + logger.info("Test mode is on, command will not execute"); + + let outputTestFile = path.join("..", "tests", "gdal2tiles_output.txt"); + fs.readFile(path.resolve(__dirname, outputTestFile), 'utf8', (err, text) => { + if (!err){ + let lines = text.split("\n"); + lines.forEach(line => outputReceived(line)); + + done(null, 0, null); + }else{ + logger.warn(`Error: ${err.message}`); + done(err); + } + }); + + return;// Skip rest + } + + // Launch + let childProcess = spawn("gdal2tiles.py", command, {cwd: config.odm_path}); + + childProcess + .on('exit', (code, signal) => done(null, code, signal)) + .on('error', done); + + childProcess.stdout.on('data', chunk => outputReceived(chunk.toString())); + childProcess.stderr.on('data', chunk => outputReceived(chunk.toString())); + + return childProcess; + } +}; diff --git a/tests/gdal2tiles_output.txt b/tests/gdal2tiles_output.txt new file mode 100644 index 0000000..3212601 --- /dev/null +++ b/tests/gdal2tiles_output.txt @@ -0,0 +1,4 @@ +Generating Base Tiles: +0...10...20...30...40...50...60...70...80...90...100 - done. +Generating Overview Tiles: +0...10...20...30...40...50...60...70...80...90...100 - done. diff --git a/tests/processing_results/orthophoto_tiles/.gitignore b/tests/processing_results/orthophoto_tiles/.gitignore new file mode 100644 index 0000000..e69de29