diff --git a/app/api/processingnodes.py b/app/api/processingnodes.py index 0b1ea188..ed37c384 100644 --- a/app/api/processingnodes.py +++ b/app/api/processingnodes.py @@ -34,7 +34,7 @@ class ProcessingNodeFilter(FilterSet): class Meta: model = ProcessingNode - fields = ['has_available_options', 'id', 'hostname', 'port', 'api_version', 'queue_count', 'max_images', 'label', ] + fields = ['has_available_options', 'id', 'hostname', 'port', 'api_version', 'queue_count', 'max_images', 'label', 'engine', 'engine_version', ] class ProcessingNodeViewSet(viewsets.ModelViewSet): """ diff --git a/app/boot.py b/app/boot.py index e2d80352..e8a5cb6f 100644 --- a/app/boot.py +++ b/app/boot.py @@ -97,11 +97,9 @@ def boot(): def add_default_presets(): try: Preset.objects.update_or_create(name='Volume Analysis', system=True, - defaults={'options': [{'name': 'use-opensfm-dense', 'value': True}, - {'name': 'dsm', 'value': True}, + defaults={'options': [{'name': 'dsm', 'value': True}, {'name': 'dem-resolution', 'value': '2'}, - {'name': 'depthmap-resolution', 'value': '1000'}, - {'name': 'opensfm-depthmap-min-patch-sd', 'value': '0'}]}) + {'name': 'depthmap-resolution', 'value': '1000'}]}) Preset.objects.update_or_create(name='3D Model', system=True, defaults={'options': [{'name': 'mesh-octree-depth', 'value': "11"}, {'name': 'use-3dmesh', 'value': True}, @@ -126,7 +124,8 @@ def add_default_presets(): Preset.objects.update_or_create(name='Fast Orthophoto', system=True, defaults={'options': [{'name': 'fast-orthophoto', 'value': True}]}) Preset.objects.update_or_create(name='High Resolution', system=True, - defaults={'options': [{'name': 'dsm', 'value': True}, + defaults={'options': [{'name': 'ignore-gsd', 'value': True}, + {'name': 'dsm', 'value': True}, {'name': 'depthmap-resolution', 'value': '1000'}, {'name': 'dem-resolution', 'value': "2.0"}, {'name': 'orthophoto-resolution', 'value': "2.0"}, diff --git a/app/models/task.py b/app/models/task.py index cf31db68..877c6576 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -22,6 +22,7 @@ from django.contrib.postgres import fields from django.core.exceptions import ValidationError from django.db import models from django.db import transaction +from django.db import connection from django.utils import timezone from urllib3.exceptions import ReadTimeoutError @@ -115,7 +116,7 @@ def resize_image(image_path, resize_to, done=None): os.rename(resized_image_path, image_path) logger.info("Resized {} to {}x{}".format(image_path, resized_width, resized_height)) - except IOError as e: + except (IOError, ValueError) as e: logger.warning("Cannot resize {}: {}.".format(image_path, str(e))) if done is not None: done() @@ -174,22 +175,7 @@ class Task(models.Model): (pending_actions.IMPORT, 'IMPORT'), ) - # Not an exact science - TASK_OUTPUT_MILESTONES_LAST_VALUE = 0.85 - TASK_OUTPUT_MILESTONES = { - 'Running ODM Load Dataset Cell': 0.01, - 'Running ODM Load Dataset Cell - Finished': 0.05, - 'opensfm/bin/opensfm match_features': 0.10, - 'opensfm/bin/opensfm reconstruct': 0.20, - 'opensfm/bin/opensfm export_visualsfm': 0.30, - 'Running ODM Meshing Cell': 0.50, - 'Running MVS Texturing Cell': 0.55, - 'Running ODM Georeferencing Cell': 0.60, - 'Running ODM DEM Cell': 0.70, - 'Running ODM Orthophoto Cell': 0.75, - 'Running ODM OrthoPhoto Cell - Finished': 0.80, - 'Compressing all.zip:': TASK_OUTPUT_MILESTONES_LAST_VALUE - } + TASK_PROGRESS_LAST_VALUE = 0.85 id = models.UUIDField(primary_key=True, default=uuid_module.uuid4, unique=True, serialize=False, editable=False) @@ -373,7 +359,11 @@ class Task(models.Model): raise NodeServerError(e) self.refresh_from_db() - self.extract_assets_and_complete() + + try: + self.extract_assets_and_complete() + except zipfile.BadZipFile: + raise NodeServerError("Invalid zip file") images_json = self.assets_path("images.json") if os.path.exists(images_json): @@ -573,22 +563,18 @@ class Task(models.Model): # Need to update status (first time, queued or running?) if self.uuid and self.status in [None, status_codes.QUEUED, status_codes.RUNNING]: # Update task info from processing node - info = self.processing_node.get_task_info(self.uuid) + current_lines_count = len(self.console_output.split("\n")) + + info = self.processing_node.get_task_info(self.uuid, current_lines_count) self.processing_time = info.processing_time self.status = info.status.value - current_lines_count = len(self.console_output.split("\n")) - console_output = self.processing_node.get_task_console_output(self.uuid, current_lines_count) - if len(console_output) > 0: - self.console_output += "\n".join(console_output) + '\n' + if len(info.output) > 0: + self.console_output += "\n".join(info.output) + '\n' # Update running progress - for line in console_output: - for line_match, value in self.TASK_OUTPUT_MILESTONES.items(): - if line_match in line: - self.running_progress = value - break + self.running_progress = (info.progress / 100.0) * self.TASK_PROGRESS_LAST_VALUE if info.last_error != "": self.last_error = info.last_error @@ -607,9 +593,10 @@ class Task(models.Model): os.makedirs(assets_dir) - logger.info("Downloading all.zip for {}".format(self)) - - # Download all assets + # Download and try to extract results up to 4 times + # (~95% of the times, on large downloads, the archive could be corrupted) + retry_num = 0 + extracted = False last_update = 0 def callback(progress): @@ -619,17 +606,32 @@ class Task(models.Model): if time_has_elapsed or int(progress) == 100: Task.objects.filter(pk=self.id).update(running_progress=( - self.TASK_OUTPUT_MILESTONES_LAST_VALUE + (float(progress) / 100.0) * 0.1)) + self.TASK_PROGRESS_LAST_VALUE + (float(progress) / 100.0) * 0.1)) last_update = time.time() - zip_path = self.processing_node.download_task_assets(self.uuid, assets_dir, progress_callback=callback) + while not extracted: + last_update = 0 + logger.info("Downloading all.zip for {}".format(self)) - # Rename to all.zip - os.rename(zip_path, os.path.join(os.path.dirname(zip_path), 'all.zip')) + # Download all assets + zip_path = self.processing_node.download_task_assets(self.uuid, assets_dir, progress_callback=callback) - logger.info("Extracting all.zip for {}".format(self)) + # Rename to all.zip + all_zip_path = self.assets_path("all.zip") + os.rename(zip_path, all_zip_path) - self.extract_assets_and_complete() + logger.info("Extracting all.zip for {}".format(self)) + + try: + self.extract_assets_and_complete() + extracted = True + except zipfile.BadZipFile: + if retry_num < 4: + logger.warning("{} seems corrupted. Retrying...".format(all_zip_path)) + retry_num += 1 + os.remove(all_zip_path) + else: + raise NodeServerError("Invalid zip file") else: # FAILED, CANCELED self.save() @@ -648,17 +650,15 @@ class Task(models.Model): def extract_assets_and_complete(self): """ Extracts assets/all.zip and populates task fields where required. + It will raise a zipfile.BadZipFile exception is the archive is corrupted. :return: """ assets_dir = self.assets_path("") zip_path = self.assets_path("all.zip") # Extract from zip - try: - with zipfile.ZipFile(zip_path, "r") as zip_h: - zip_h.extractall(assets_dir) - except zipfile.BadZipFile: - raise NodeServerError("Invalid zip file") + with zipfile.ZipFile(zip_path, "r") as zip_h: + zip_h.extractall(assets_dir) logger.info("Extracted all.zip for {}".format(self)) @@ -678,6 +678,12 @@ class Task(models.Model): raster = GDALRaster(raster_path) extent = OGRGeometry.from_bbox(raster.extent) + # Make sure PostGIS supports it + with connection.cursor() as cursor: + cursor.execute("SELECT SRID FROM spatial_ref_sys WHERE SRID = %s", [raster.srid]) + if cursor.rowcount == 0: + raise NodeServerError("Unsupported SRS {}. Please make sure you picked a supported SRS.".format(raster.srid)) + # It will be implicitly transformed into the SRID of the model’s field # self.field = GEOSGeometry(...) setattr(self, field, GEOSGeometry(extent.wkt, srid=raster.srid)) diff --git a/app/static/app/js/Console.jsx b/app/static/app/js/Console.jsx index 99aadde3..c3911c93 100644 --- a/app/static/app/js/Console.jsx +++ b/app/static/app/js/Console.jsx @@ -20,12 +20,15 @@ class Console extends React.Component { } this.autoscroll = props.autoscroll === true; + this.showFullscreenButton = props.showFullscreenButton === true; this.setRef = this.setRef.bind(this); this.handleMouseOver = this.handleMouseOver.bind(this); this.handleMouseOut = this.handleMouseOut.bind(this); this.downloadTxt = this.downloadTxt.bind(this); this.copyTxt = this.copyTxt.bind(this); + this.enterFullscreen = this.enterFullscreen.bind(this); + this.exitFullscreen = this.exitFullscreen.bind(this); } componentDidMount(){ @@ -79,6 +82,19 @@ class Console extends React.Component { console.log("Output copied to clipboard"); } + enterFullscreen(){ + const consoleElem = this.$console.get(0); + if (consoleElem.requestFullscreen) { + consoleElem.requestFullscreen(); + } + } + + exitFullscreen(){ + if (document.exitFullscreen){ + document.exitFullscreen(); + } + } + tearDownDynamicSource(){ if (this.sourceTimeout) clearTimeout(this.sourceTimeout); if (this.sourceRequest) this.sourceRequest.abort(); @@ -138,11 +154,17 @@ class Console extends React.Component { onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut} ref={this.setRef} - >{lines.map(line => { + > + Exit Fullscreen + + {lines.map(line => { if (this.props.lang) return (
); else return line + "\n"; })} {"\n"} + + Exit Fullscreen + ]; if (this.props.showConsoleButtons){ @@ -153,6 +175,9 @@ class Console extends React.Component { + + + ); } diff --git a/app/static/app/js/components/BasicTaskView.jsx b/app/static/app/js/components/BasicTaskView.jsx deleted file mode 100644 index a5ce0d4f..00000000 --- a/app/static/app/js/components/BasicTaskView.jsx +++ /dev/null @@ -1,235 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import '../css/BasicTaskView.scss'; -import update from 'immutability-helper'; -import PipelineSteps from '../classes/PipelineSteps'; -import StatusCodes from '../classes/StatusCodes'; -import $ from 'jquery'; - -class BasicTaskView extends React.Component { - static defaultProps = {}; - - static propTypes = { - source: PropTypes.oneOfType([ - PropTypes.string.isRequired, - PropTypes.func.isRequired - ]), - taskStatus: PropTypes.number, - onAddLines: PropTypes.func - }; - - constructor(props){ - super(); - - this.state = { - lines: [], - currentRf: 0, - rf: PipelineSteps.get(), - loaded: false - }; - - this.imageUpload = { - action: "imageupload", - label: "Image Resize / Upload", - icon: "fa fa-image" - }; - - // Add a post processing step - this.state.rf.push({ - action: "postprocessing", - label: "Post Processing", - icon: "fa fa-file-archive-o", - beginsWith: "Running ODM OrthoPhoto Cell - Finished", - endsWith: null - }); - - this.addLines = this.addLines.bind(this); - this.setupDynamicSource = this.setupDynamicSource.bind(this); - this.reset = this.reset.bind(this); - this.tearDownDynamicSource = this.tearDownDynamicSource.bind(this); - this.getRfEndStatus = this.getRfEndStatus.bind(this); - this.getRfRunningStatus = this.getRfRunningStatus.bind(this); - this.suffixFor = this.suffixFor.bind(this); - this.updateRfState = this.updateRfState.bind(this); - this.getInitialStatus = this.getInitialStatus.bind(this); - } - - componentDidMount(){ - this.reset(); - } - - setupDynamicSource(){ - const updateFromSource = () => { - let sourceUrl = typeof this.props.source === 'function' ? - this.props.source(this.state.lines.length) : - this.props.source; - - // Fetch - this.sourceRequest = $.get(sourceUrl, text => { - if (text !== ""){ - let lines = text.split("\n"); - this.addLines(lines); - } - }) - .always((_, textStatus) => { - if (textStatus !== "abort" && this.props.refreshInterval !== undefined){ - this.sourceTimeout = setTimeout(updateFromSource, this.props.refreshInterval); - } - if (!this.state.loaded) this.setState({loaded: true}); - }); - }; - - updateFromSource(); - } - - getRfEndStatus(){ - return this.props.taskStatus === StatusCodes.COMPLETED ? - 'completed' : - 'errored'; - } - - getRfRunningStatus(){ - return [StatusCodes.RUNNING, StatusCodes.QUEUED].indexOf(this.props.taskStatus) !== -1 ? - 'running' : - 'errored'; - } - - getInitialStatus(){ - if ([null, StatusCodes.QUEUED, StatusCodes.RUNNING].indexOf(this.props.taskStatus) !== -1){ - return 'queued'; - }else{ - return this.getRfEndStatus(); - } - } - - reset(){ - this.state.rf.forEach(p => { - p.state = this.getInitialStatus(); - }); - - if ([StatusCodes.RUNNING].indexOf(this.props.taskStatus) !== -1){ - this.state.rf[0].state = 'running'; - } - - this.tearDownDynamicSource(); - this.setState({lines: [], currentRf: 0, loaded: false, rf: this.state.rf}); - this.setupDynamicSource(); - } - - tearDownDynamicSource(){ - if (this.sourceTimeout) clearTimeout(this.sourceTimeout); - if (this.sourceRequest) this.sourceRequest.abort(); - } - - componentDidUpdate(prevProps){ - - let taskFailed; - let taskCompleted; - let taskRestarted; - - taskFailed = [StatusCodes.FAILED, StatusCodes.CANCELED].indexOf(this.props.taskStatus) !== -1; - taskCompleted = this.props.taskStatus === StatusCodes.COMPLETED; - - if (prevProps.taskStatus !== this.props.taskStatus){ - taskRestarted = this.props.taskStatus === null; - } - - this.updateRfState(taskFailed, taskCompleted, taskRestarted); - } - - componentWillUnmount(){ - this.tearDownDynamicSource(); - } - - addLines(lines){ - if (!Array.isArray(lines)) lines = [lines]; - - let currentRf = this.state.currentRf; - - const updateRf = (rfIndex, line) => { - const current = this.state.rf[rfIndex]; - if (!current) return; - - if (current.beginsWith && line.endsWith(current.beginsWith)){ - current.state = this.getRfRunningStatus(); - - // Set previous as done - if (this.state.rf[rfIndex - 1]){ - this.state.rf[rfIndex - 1].state = 'completed'; - } - }else if (current.endsWith && line.endsWith(current.endsWith)){ - current.state = this.getRfEndStatus(); - - // Check next - updateRf(rfIndex + 1, line); - - currentRf = rfIndex + 1; - } - }; - - lines.forEach(line => { - updateRf(currentRf, line); - }); - - this.setState(update(this.state, { - lines: {$push: lines} - })); - this.setState({ - rf: this.state.rf, - currentRf - }); - this.updateRfState(); - - if (this.props.onAddLines) this.props.onAddLines(lines); - } - - updateRfState(taskFailed, taskCompleted, taskRestarted){ - // If the task has just failed, update all items that were either running or in queued state - if (taskFailed){ - this.state.rf.forEach(p => { - if (p.state === 'queued' || p.state === 'running') p.state = this.getInitialStatus(); - }); - } - - // If completed, all steps must have completed - if (taskCompleted){ - this.state.rf.forEach(p => p.state = 'completed'); - } - - if (taskRestarted){ - this.state.rf.forEach(p => p.state = 'queued'); - } - } - - suffixFor(state){ - if (state === 'running'){ - return (...); - }else if (state === 'completed'){ - return (); - }else if (state === 'errored'){ - return (); - } - } - - render() { - const { rf, loaded } = this.state; - const imageUploadState = this.props.taskStatus === null - ? 'running' - : 'completed'; - - return (
-
- {this.imageUpload.label} {this.suffixFor(imageUploadState)} -
- {rf.map(p => { - const state = loaded ? p.state : 'queued'; - - return (
- {p.label} {this.suffixFor(state)} -
); - })} -
); - } -} - -export default BasicTaskView; diff --git a/app/static/app/js/components/ProjectListItem.jsx b/app/static/app/js/components/ProjectListItem.jsx index cabce614..009b8ec4 100644 --- a/app/static/app/js/components/ProjectListItem.jsx +++ b/app/static/app/js/components/ProjectListItem.jsx @@ -382,12 +382,15 @@ class ProjectListItem extends React.Component { className="btn btn-primary btn-sm" onClick={this.handleUpload} ref={this.setRef("uploadButton")}> - - Select Images and GCP - - + + Select Images and GCP + + + : ""} + + + + -
- Processing Node: {task.processing_node_name || "-"} ({task.auto_processing_node ? "auto" : "manual"})
-
- {Array.isArray(task.options) ? -
- Options: {this.optionsToList(task.options)}
+ +
+
+ Created on: {(new Date(task.created_at)).toLocaleString()}
- : ""} - {/* TODO: List of images? */} - - {showOrthophotoMissingWarning ? -
An orthophoto could not be generated. To generate one, make sure GPS information is embedded in the EXIF tags of your images, or use a Ground Control Points (GCP) file.
: ""} - -
-
-
- Simple - | - Console -
- +
+ Processing Node: {task.processing_node_name || "-"} ({task.auto_processing_node ? "auto" : "manual"})
+
+ {Array.isArray(task.options) ? +
+ Options: {this.optionsToList(task.options)}
+
+ : ""} + {/* TODO: List of images? */} +
+ {this.state.view === 'console' ? : ""} - {this.state.view === 'basic' ? - this.basicView = domNode} - refreshInterval={this.shouldRefresh() ? 3000 : undefined} - onAddLines={this.checkForCommonErrors} - taskStatus={task.status} - /> : ""} + {showOrthophotoMissingWarning ? +
An orthophoto could not be generated. To generate one, make sure GPS information is embedded in the EXIF tags of your images, or use a Ground Control Points (GCP) file.
: ""} {showMemoryErrorWarning ?
It looks like your processing node ran out of memory. If you are using docker, make sure that your docker environment has enough RAM allocated. Alternatively, make sure you have enough physical RAM, reduce the number of images, make your images smaller, or reduce the max-concurrency parameter from the task's options. You can also try to use a cloud processing node.
: ""} diff --git a/app/static/app/js/components/tests/BasicTaskView.test.jsx b/app/static/app/js/components/tests/BasicTaskView.test.jsx deleted file mode 100644 index ad459c28..00000000 --- a/app/static/app/js/components/tests/BasicTaskView.test.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import BasicTaskView from '../BasicTaskView'; - -describe('', () => { - it('renders without exploding', () => { - const wrapper = mount(); - expect(wrapper.exists()).toBe(true); - }) -}); \ No newline at end of file diff --git a/app/static/app/js/css/BasicTaskView.scss b/app/static/app/js/css/BasicTaskView.scss deleted file mode 100644 index 8b1eb9a5..00000000 --- a/app/static/app/js/css/BasicTaskView.scss +++ /dev/null @@ -1,38 +0,0 @@ -.basic-task-view{ - margin-bottom: 8px; - opacity: 0.7; - &.loaded{ - opacity: 1; - } - - .processing-step{ - opacity: 0.7; - - &.completed{ - opacity: 1; - /* font-weight: bold; */ - } - &.running{ - opacity: 1; - animation: pulse ease-in-out 1.4833s infinite; - } - &.queued{ - - } - &.errored{ - opacity: 1; - } - } - - @keyframes pulse { - 0% { - opacity: 0.7; - } - 50% { - opacity: 1; - } - 100% { - opacity: 0.7; - } - } -} \ No newline at end of file diff --git a/app/static/app/js/css/Console.scss b/app/static/app/js/css/Console.scss index 27906952..1b262a5b 100644 --- a/app/static/app/js/css/Console.scss +++ b/app/static/app/js/css/Console.scss @@ -6,4 +6,17 @@ a{ margin-left: 4px; } +} + +.console{ + .exit-fullscreen{ + display: none; + } +} + +.console:fullscreen{ + .exit-fullscreen{ + display: block; + margin-bottom: 12px; + } } \ No newline at end of file diff --git a/app/static/app/js/css/TaskListItem.scss b/app/static/app/js/css/TaskListItem.scss index a3064759..e9a85d68 100644 --- a/app/static/app/js/css/TaskListItem.scss +++ b/app/static/app/js/css/TaskListItem.scss @@ -57,6 +57,8 @@ .task-warning{ margin-top: 16px; + margin-bottom: 16px; + i.fa.fa-warning{ color: #ff8000; } @@ -100,25 +102,20 @@ display: inline; } - .switch-view{ + .console-switch{ + margin-right: 10px; margin-bottom: 8px; - font-size: 90%; position: relative; z-index: 99; - - a{ + .console-output-label{ margin-right: 8px; - &.selected{ - font-weight: bold; - } - &:last-child{ - margin-right: 0; - } } - - i{ - margin-left: 8px; + .console-output-label, ul{ + display: inline-block; } } - + + .mb{ + margin-bottom: 12px; + } } \ No newline at end of file diff --git a/app/templates/app/processing_node.html b/app/templates/app/processing_node.html index e8328c76..c74012d8 100644 --- a/app/templates/app/processing_node.html +++ b/app/templates/app/processing_node.html @@ -20,8 +20,12 @@ {{ processing_node.api_version }} - {% trans "ODM Version" %} - {{ processing_node.odm_version }} + {% trans "Engine" %} + {{ processing_node.engine }} + + + {% trans "Engine Version" %} + {{ processing_node.engine_version }} {% trans "Queue Count" %} diff --git a/app/tests/test_api.py b/app/tests/test_api.py index af5cfd4d..fe98c60f 100644 --- a/app/tests/test_api.py +++ b/app/tests/test_api.py @@ -355,8 +355,11 @@ class TestApi(BootTestCase): # Verify max images field self.assertTrue("max_images" in res.data) - # Verify odm version - self.assertTrue("odm_version" in res.data) + # Verify engine version + self.assertTrue("engine_version" in res.data) + + # Verify engine + self.assertTrue("engine" in res.data) # label should be hostname:port (since no label is set) self.assertEqual(res.data['label'], pnode.hostname + ":" + str(pnode.port)) diff --git a/nodeodm/migrations/0007_auto_20190520_1258.py b/nodeodm/migrations/0007_auto_20190520_1258.py new file mode 100644 index 00000000..1b36b748 --- /dev/null +++ b/nodeodm/migrations/0007_auto_20190520_1258.py @@ -0,0 +1,27 @@ +# Generated by Django 2.1.7 on 2019-05-20 12:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nodeodm', '0006_auto_20190220_1842'), + ] + + operations = [ + migrations.RemoveField( + model_name='processingnode', + name='odm_version', + ), + migrations.AddField( + model_name='processingnode', + name='engine', + field=models.CharField(help_text='Engine used by the node.', max_length=255, null=True), + ), + migrations.AddField( + model_name='processingnode', + name='engine_version', + field=models.CharField(help_text='Engine version used by the node.', max_length=32, null=True), + ), + ] diff --git a/nodeodm/models.py b/nodeodm/models.py index 3d200027..82c65367 100644 --- a/nodeodm/models.py +++ b/nodeodm/models.py @@ -25,8 +25,9 @@ class ProcessingNode(models.Model): available_options = fields.JSONField(default=dict, help_text="Description of the options that can be used for processing") token = models.CharField(max_length=1024, blank=True, default="", help_text="Token to use for authentication. If the node doesn't have authentication, you can leave this field blank.") max_images = models.PositiveIntegerField(help_text="Maximum number of images accepted by this node.", blank=True, null=True) - odm_version = models.CharField(max_length=32, null=True, help_text="ODM version used by the node.") + engine_version = models.CharField(max_length=32, null=True, help_text="Engine version used by the node.") label = models.CharField(max_length=255, default="", blank=True, help_text="Optional label for this node. When set, this label will be shown instead of the hostname:port name.") + engine = models.CharField(max_length=255, null=True, help_text="Engine used by the node.") def __str__(self): if self.label != "": @@ -61,7 +62,8 @@ class ProcessingNode(models.Model): self.api_version = info.version self.queue_count = info.task_queue_count self.max_images = info.max_images - self.odm_version = info.odm_version + self.engine_version = info.engine_version + self.engine = info.engine options = list(map(lambda o: o.__dict__, api_client.options())) self.available_options = options @@ -116,7 +118,7 @@ class ProcessingNode(models.Model): task = api_client.create_task(images, opts, name, progress_callback) return task.uuid - def get_task_info(self, uuid): + def get_task_info(self, uuid, with_output=None): """ Gets information about this task, such as name, creation date, processing time, status, command line options and number of @@ -124,7 +126,13 @@ class ProcessingNode(models.Model): """ api_client = self.api_client() task = api_client.get_task(uuid) - return task.info() + task_info = task.info(with_output) + + # Output support for older clients + if not api_client.version_greater_or_equal_than("1.5.1") and with_output: + task_info.output = self.get_task_console_output(uuid, with_output) + + return task_info def get_task_console_output(self, uuid, line): """ diff --git a/package.json b/package.json index e1f655fd..97f9bd81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "0.9.0", + "version": "0.9.1", "description": "Open Source Drone Image Processing", "main": "index.js", "scripts": { diff --git a/requirements.txt b/requirements.txt index 04aeed29..4d74523d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,7 +37,7 @@ pip-autoremove==0.9.0 psycopg2==2.7.4 psycopg2-binary==2.7.4 PyJWT==1.5.3 -pyodm==1.4.0 +pyodm==1.5.1 pyparsing==2.1.10 pytz==2018.3 rcssmin==1.0.6 diff --git a/slate/source/includes/reference/_processingnode.md b/slate/source/includes/reference/_processingnode.md index 1a0d53a6..94512b14 100644 --- a/slate/source/includes/reference/_processingnode.md +++ b/slate/source/includes/reference/_processingnode.md @@ -9,6 +9,8 @@ "hostname": "nodeodm.masseranolabs.com", "port": 80, "api_version": "1.0.1", + "engine_version": "0.6.0", + "engine": "odm", "last_refreshed": "2017-03-01T21:14:49.918276Z", "queue_count": 0, "max_images": null, @@ -34,7 +36,8 @@ online | bool | Whether the processing node could be reached in the last 5 minut hostname | string | Hostname/IP address port | int | Port api_version | string | Version of NodeODM currently running -odm_version | string | Version of ODM currently being used +engine_version | string | Version of processing engine currently being used +engine | string | Lowercase identifier of processing engine last_refreshed | string | Date and time this node was last seen online. This value is typically refreshed every 15-30 seconds and is used to decide whether a node is offline or not queue_count | int | Number of [Task](#task) items currently being processed/queued on this node. max_images | int | Optional maximum number of images this processing node can accept. null indicates no limit. @@ -89,6 +92,8 @@ port | | "" | Filter by port api_version | | "" | Filter by API version queue_count | | "" | Filter by queue count max_images | | "" | Filter by max images +engine_version | | "" | Filter by engine version +engine | | "" | Filter by engine identifier ordering | | "" | Ordering field to sort results by has_available_options | | "" | Return only processing nodes that have a valid set of processing options (check that the `available_options` field is populated). Either `true` or `false`.