kopia lustrzana https://github.com/OpenDroneMap/WebODM
commit
3ffae5a64e
|
@ -34,7 +34,7 @@ class ProcessingNodeFilter(FilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProcessingNode
|
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):
|
class ProcessingNodeViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -97,11 +97,9 @@ def boot():
|
||||||
def add_default_presets():
|
def add_default_presets():
|
||||||
try:
|
try:
|
||||||
Preset.objects.update_or_create(name='Volume Analysis', system=True,
|
Preset.objects.update_or_create(name='Volume Analysis', system=True,
|
||||||
defaults={'options': [{'name': 'use-opensfm-dense', 'value': True},
|
defaults={'options': [{'name': 'dsm', 'value': True},
|
||||||
{'name': 'dsm', 'value': True},
|
|
||||||
{'name': 'dem-resolution', 'value': '2'},
|
{'name': 'dem-resolution', 'value': '2'},
|
||||||
{'name': 'depthmap-resolution', 'value': '1000'},
|
{'name': 'depthmap-resolution', 'value': '1000'}]})
|
||||||
{'name': 'opensfm-depthmap-min-patch-sd', 'value': '0'}]})
|
|
||||||
Preset.objects.update_or_create(name='3D Model', system=True,
|
Preset.objects.update_or_create(name='3D Model', system=True,
|
||||||
defaults={'options': [{'name': 'mesh-octree-depth', 'value': "11"},
|
defaults={'options': [{'name': 'mesh-octree-depth', 'value': "11"},
|
||||||
{'name': 'use-3dmesh', 'value': True},
|
{'name': 'use-3dmesh', 'value': True},
|
||||||
|
@ -126,7 +124,8 @@ def add_default_presets():
|
||||||
Preset.objects.update_or_create(name='Fast Orthophoto', system=True,
|
Preset.objects.update_or_create(name='Fast Orthophoto', system=True,
|
||||||
defaults={'options': [{'name': 'fast-orthophoto', 'value': True}]})
|
defaults={'options': [{'name': 'fast-orthophoto', 'value': True}]})
|
||||||
Preset.objects.update_or_create(name='High Resolution', system=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': 'depthmap-resolution', 'value': '1000'},
|
||||||
{'name': 'dem-resolution', 'value': "2.0"},
|
{'name': 'dem-resolution', 'value': "2.0"},
|
||||||
{'name': 'orthophoto-resolution', 'value': "2.0"},
|
{'name': 'orthophoto-resolution', 'value': "2.0"},
|
||||||
|
|
|
@ -22,6 +22,7 @@ from django.contrib.postgres import fields
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db import connection
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from urllib3.exceptions import ReadTimeoutError
|
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)
|
os.rename(resized_image_path, image_path)
|
||||||
|
|
||||||
logger.info("Resized {} to {}x{}".format(image_path, resized_width, resized_height))
|
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)))
|
logger.warning("Cannot resize {}: {}.".format(image_path, str(e)))
|
||||||
if done is not None:
|
if done is not None:
|
||||||
done()
|
done()
|
||||||
|
@ -174,22 +175,7 @@ class Task(models.Model):
|
||||||
(pending_actions.IMPORT, 'IMPORT'),
|
(pending_actions.IMPORT, 'IMPORT'),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Not an exact science
|
TASK_PROGRESS_LAST_VALUE = 0.85
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid_module.uuid4, unique=True, serialize=False, editable=False)
|
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)
|
raise NodeServerError(e)
|
||||||
|
|
||||||
self.refresh_from_db()
|
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")
|
images_json = self.assets_path("images.json")
|
||||||
if os.path.exists(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?)
|
# Need to update status (first time, queued or running?)
|
||||||
if self.uuid and self.status in [None, status_codes.QUEUED, status_codes.RUNNING]:
|
if self.uuid and self.status in [None, status_codes.QUEUED, status_codes.RUNNING]:
|
||||||
# Update task info from processing node
|
# 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.processing_time = info.processing_time
|
||||||
self.status = info.status.value
|
self.status = info.status.value
|
||||||
|
|
||||||
current_lines_count = len(self.console_output.split("\n"))
|
if len(info.output) > 0:
|
||||||
console_output = self.processing_node.get_task_console_output(self.uuid, current_lines_count)
|
self.console_output += "\n".join(info.output) + '\n'
|
||||||
if len(console_output) > 0:
|
|
||||||
self.console_output += "\n".join(console_output) + '\n'
|
|
||||||
|
|
||||||
# Update running progress
|
# Update running progress
|
||||||
for line in console_output:
|
self.running_progress = (info.progress / 100.0) * self.TASK_PROGRESS_LAST_VALUE
|
||||||
for line_match, value in self.TASK_OUTPUT_MILESTONES.items():
|
|
||||||
if line_match in line:
|
|
||||||
self.running_progress = value
|
|
||||||
break
|
|
||||||
|
|
||||||
if info.last_error != "":
|
if info.last_error != "":
|
||||||
self.last_error = info.last_error
|
self.last_error = info.last_error
|
||||||
|
@ -607,9 +593,10 @@ class Task(models.Model):
|
||||||
|
|
||||||
os.makedirs(assets_dir)
|
os.makedirs(assets_dir)
|
||||||
|
|
||||||
logger.info("Downloading all.zip for {}".format(self))
|
# Download and try to extract results up to 4 times
|
||||||
|
# (~95% of the times, on large downloads, the archive could be corrupted)
|
||||||
# Download all assets
|
retry_num = 0
|
||||||
|
extracted = False
|
||||||
last_update = 0
|
last_update = 0
|
||||||
|
|
||||||
def callback(progress):
|
def callback(progress):
|
||||||
|
@ -619,17 +606,32 @@ class Task(models.Model):
|
||||||
|
|
||||||
if time_has_elapsed or int(progress) == 100:
|
if time_has_elapsed or int(progress) == 100:
|
||||||
Task.objects.filter(pk=self.id).update(running_progress=(
|
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()
|
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
|
# Download all assets
|
||||||
os.rename(zip_path, os.path.join(os.path.dirname(zip_path), 'all.zip'))
|
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:
|
else:
|
||||||
# FAILED, CANCELED
|
# FAILED, CANCELED
|
||||||
self.save()
|
self.save()
|
||||||
|
@ -648,17 +650,15 @@ class Task(models.Model):
|
||||||
def extract_assets_and_complete(self):
|
def extract_assets_and_complete(self):
|
||||||
"""
|
"""
|
||||||
Extracts assets/all.zip and populates task fields where required.
|
Extracts assets/all.zip and populates task fields where required.
|
||||||
|
It will raise a zipfile.BadZipFile exception is the archive is corrupted.
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
assets_dir = self.assets_path("")
|
assets_dir = self.assets_path("")
|
||||||
zip_path = self.assets_path("all.zip")
|
zip_path = self.assets_path("all.zip")
|
||||||
|
|
||||||
# Extract from zip
|
# Extract from zip
|
||||||
try:
|
with zipfile.ZipFile(zip_path, "r") as zip_h:
|
||||||
with zipfile.ZipFile(zip_path, "r") as zip_h:
|
zip_h.extractall(assets_dir)
|
||||||
zip_h.extractall(assets_dir)
|
|
||||||
except zipfile.BadZipFile:
|
|
||||||
raise NodeServerError("Invalid zip file")
|
|
||||||
|
|
||||||
logger.info("Extracted all.zip for {}".format(self))
|
logger.info("Extracted all.zip for {}".format(self))
|
||||||
|
|
||||||
|
@ -678,6 +678,12 @@ class Task(models.Model):
|
||||||
raster = GDALRaster(raster_path)
|
raster = GDALRaster(raster_path)
|
||||||
extent = OGRGeometry.from_bbox(raster.extent)
|
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
|
# It will be implicitly transformed into the SRID of the model’s field
|
||||||
# self.field = GEOSGeometry(...)
|
# self.field = GEOSGeometry(...)
|
||||||
setattr(self, field, GEOSGeometry(extent.wkt, srid=raster.srid))
|
setattr(self, field, GEOSGeometry(extent.wkt, srid=raster.srid))
|
||||||
|
|
|
@ -20,12 +20,15 @@ class Console extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.autoscroll = props.autoscroll === true;
|
this.autoscroll = props.autoscroll === true;
|
||||||
|
this.showFullscreenButton = props.showFullscreenButton === true;
|
||||||
|
|
||||||
this.setRef = this.setRef.bind(this);
|
this.setRef = this.setRef.bind(this);
|
||||||
this.handleMouseOver = this.handleMouseOver.bind(this);
|
this.handleMouseOver = this.handleMouseOver.bind(this);
|
||||||
this.handleMouseOut = this.handleMouseOut.bind(this);
|
this.handleMouseOut = this.handleMouseOut.bind(this);
|
||||||
this.downloadTxt = this.downloadTxt.bind(this);
|
this.downloadTxt = this.downloadTxt.bind(this);
|
||||||
this.copyTxt = this.copyTxt.bind(this);
|
this.copyTxt = this.copyTxt.bind(this);
|
||||||
|
this.enterFullscreen = this.enterFullscreen.bind(this);
|
||||||
|
this.exitFullscreen = this.exitFullscreen.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
|
@ -79,6 +82,19 @@ class Console extends React.Component {
|
||||||
console.log("Output copied to clipboard");
|
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(){
|
tearDownDynamicSource(){
|
||||||
if (this.sourceTimeout) clearTimeout(this.sourceTimeout);
|
if (this.sourceTimeout) clearTimeout(this.sourceTimeout);
|
||||||
if (this.sourceRequest) this.sourceRequest.abort();
|
if (this.sourceRequest) this.sourceRequest.abort();
|
||||||
|
@ -138,11 +154,17 @@ class Console extends React.Component {
|
||||||
onMouseOver={this.handleMouseOver}
|
onMouseOver={this.handleMouseOver}
|
||||||
onMouseOut={this.handleMouseOut}
|
onMouseOut={this.handleMouseOut}
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
>{lines.map(line => {
|
><a href="javascript:void(0);" onClick={this.exitFullscreen} className="exit-fullscreen btn btn-sm btn-primary" title="Toggle Fullscreen">
|
||||||
|
<i className="fa fa-expand"></i> Exit Fullscreen
|
||||||
|
</a>
|
||||||
|
{lines.map(line => {
|
||||||
if (this.props.lang) return (<div key={i++} dangerouslySetInnerHTML={prettyLine(line)}></div>);
|
if (this.props.lang) return (<div key={i++} dangerouslySetInnerHTML={prettyLine(line)}></div>);
|
||||||
else return line + "\n";
|
else return line + "\n";
|
||||||
})}
|
})}
|
||||||
{"\n"}
|
{"\n"}
|
||||||
|
<a href="javascript:void(0);" onClick={this.exitFullscreen} className="exit-fullscreen btn btn-sm btn-primary" title="Toggle Fullscreen">
|
||||||
|
<i className="fa fa-expand"></i> Exit Fullscreen
|
||||||
|
</a>
|
||||||
</pre>];
|
</pre>];
|
||||||
|
|
||||||
if (this.props.showConsoleButtons){
|
if (this.props.showConsoleButtons){
|
||||||
|
@ -153,6 +175,9 @@ class Console extends React.Component {
|
||||||
<a href="javascript:void(0);" onClick={this.copyTxt} className="btn btn-sm btn-primary" title="Copy To Clipboard">
|
<a href="javascript:void(0);" onClick={this.copyTxt} className="btn btn-sm btn-primary" title="Copy To Clipboard">
|
||||||
<i className="fa fa-clipboard"></i>
|
<i className="fa fa-clipboard"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="javascript:void(0);" onClick={this.enterFullscreen} className="btn btn-sm btn-primary" title="Toggle Fullscreen">
|
||||||
|
<i className="fa fa-expand"></i>
|
||||||
|
</a>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 (<span>...</span>);
|
|
||||||
}else if (state === 'completed'){
|
|
||||||
return (<i className="fa fa-check"></i>);
|
|
||||||
}else if (state === 'errored'){
|
|
||||||
return (<i className="fa fa-times"></i>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { rf, loaded } = this.state;
|
|
||||||
const imageUploadState = this.props.taskStatus === null
|
|
||||||
? 'running'
|
|
||||||
: 'completed';
|
|
||||||
|
|
||||||
return (<div className={"basic-task-view " + (loaded ? 'loaded' : '')}>
|
|
||||||
<div className={imageUploadState + " processing-step"}>
|
|
||||||
<i className={this.imageUpload.icon + " fa-fw"}></i> {this.imageUpload.label} {this.suffixFor(imageUploadState)}
|
|
||||||
</div>
|
|
||||||
{rf.map(p => {
|
|
||||||
const state = loaded ? p.state : 'queued';
|
|
||||||
|
|
||||||
return (<div key={p.action} className={state + " processing-step"}>
|
|
||||||
<i className={p.icon + " fa-fw"}></i> {p.label} {this.suffixFor(state)}
|
|
||||||
</div>);
|
|
||||||
})}
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BasicTaskView;
|
|
|
@ -382,12 +382,15 @@ class ProjectListItem extends React.Component {
|
||||||
className="btn btn-primary btn-sm"
|
className="btn btn-primary btn-sm"
|
||||||
onClick={this.handleUpload}
|
onClick={this.handleUpload}
|
||||||
ref={this.setRef("uploadButton")}>
|
ref={this.setRef("uploadButton")}>
|
||||||
<i className="glyphicon glyphicon-upload"></i>
|
<i className="glyphicon glyphicon-upload"></i>
|
||||||
Select Images and GCP
|
Select Images and GCP
|
||||||
</button><button type="button" className="btn btn-sm dropdown-toggle btn-primary" data-toggle="dropdown"><span className="caret"></span></button>
|
</button>
|
||||||
<ul className="dropdown-menu">
|
<button type="button"
|
||||||
<li><a href="javascript:void(0);" onClick={this.handleImportTask}><i className="glyphicon glyphicon-import"></i> Import Existing Assets</a></li>
|
className="btn btn-default btn-sm"
|
||||||
</ul></div>
|
onClick={this.handleImportTask}>
|
||||||
|
<i className="glyphicon glyphicon-import"></i> Import
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
: ""}
|
: ""}
|
||||||
|
|
||||||
<button disabled={this.state.upload.error !== ""}
|
<button disabled={this.state.upload.error !== ""}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import HistoryNav from '../classes/HistoryNav';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TaskPluginActionButtons from './TaskPluginActionButtons';
|
import TaskPluginActionButtons from './TaskPluginActionButtons';
|
||||||
import PipelineSteps from '../classes/PipelineSteps';
|
import PipelineSteps from '../classes/PipelineSteps';
|
||||||
import BasicTaskView from './BasicTaskView';
|
|
||||||
import Css from '../classes/Css';
|
import Css from '../classes/Css';
|
||||||
|
|
||||||
class TaskListItem extends React.Component {
|
class TaskListItem extends React.Component {
|
||||||
|
@ -468,33 +467,33 @@ class TaskListItem extends React.Component {
|
||||||
expanded = (
|
expanded = (
|
||||||
<div className="expanded-panel">
|
<div className="expanded-panel">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-3 no-padding">
|
<div className="col-md-12 no-padding">
|
||||||
<div className="labels">
|
<div className="console-switch text-right pull-right">
|
||||||
<strong>Created on: </strong> {(new Date(task.created_at)).toLocaleString()}<br/>
|
<div className="console-output-label">Task Output: </div><ul className="list-inline">
|
||||||
|
<li>
|
||||||
|
<div className="btn-group btn-toggle">
|
||||||
|
<button onClick={this.setView("console")} className={"btn btn-xs " + (this.state.view === "basic" ? "btn-default" : "btn-primary")}>On</button>
|
||||||
|
<button onClick={this.setView("basic")} className={"btn btn-xs " + (this.state.view === "console" ? "btn-default" : "btn-primary")}>Off</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="labels">
|
|
||||||
<strong>Processing Node: </strong> {task.processing_node_name || "-"} ({task.auto_processing_node ? "auto" : "manual"})<br/>
|
<div className="mb">
|
||||||
</div>
|
<div className="labels">
|
||||||
{Array.isArray(task.options) ?
|
<strong>Created on: </strong> {(new Date(task.created_at)).toLocaleString()}<br/>
|
||||||
<div className="labels">
|
|
||||||
<strong>Options: </strong> {this.optionsToList(task.options)}<br/>
|
|
||||||
</div>
|
</div>
|
||||||
: ""}
|
<div className="labels">
|
||||||
{/* TODO: List of images? */}
|
<strong>Processing Node: </strong> {task.processing_node_name || "-"} ({task.auto_processing_node ? "auto" : "manual"})<br/>
|
||||||
|
</div>
|
||||||
{showOrthophotoMissingWarning ?
|
{Array.isArray(task.options) ?
|
||||||
<div className="task-warning"><i className="fa fa-warning"></i> <span>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.</span></div> : ""}
|
<div className="labels">
|
||||||
|
<strong>Options: </strong> {this.optionsToList(task.options)}<br/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-9">
|
: ""}
|
||||||
<div className="switch-view text-right pull-right">
|
{/* TODO: List of images? */}
|
||||||
<i className="fa fa-list-ul"></i> <a href="javascript:void(0);" onClick={this.setView("basic")}
|
</div>
|
||||||
className={this.state.view === 'basic' ? "selected" : ""}>Simple</a>
|
|
||||||
|
|
|
||||||
<i className="fa fa-desktop"></i> <a href="javascript:void(0);" onClick={this.setView("console")}
|
|
||||||
className={this.state.view === 'console' ? "selected" : ""}>Console</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.view === 'console' ?
|
{this.state.view === 'console' ?
|
||||||
<Console
|
<Console
|
||||||
className="floatfix"
|
className="floatfix"
|
||||||
|
@ -508,14 +507,8 @@ class TaskListItem extends React.Component {
|
||||||
maximumLines={500}
|
maximumLines={500}
|
||||||
/> : ""}
|
/> : ""}
|
||||||
|
|
||||||
{this.state.view === 'basic' ?
|
{showOrthophotoMissingWarning ?
|
||||||
<BasicTaskView
|
<div className="task-warning"><i className="fa fa-warning"></i> <span>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.</span></div> : ""}
|
||||||
source={this.consoleOutputUrl}
|
|
||||||
ref={domNode => this.basicView = domNode}
|
|
||||||
refreshInterval={this.shouldRefresh() ? 3000 : undefined}
|
|
||||||
onAddLines={this.checkForCommonErrors}
|
|
||||||
taskStatus={task.status}
|
|
||||||
/> : ""}
|
|
||||||
|
|
||||||
{showMemoryErrorWarning ?
|
{showMemoryErrorWarning ?
|
||||||
<div className="task-warning"><i className="fa fa-support"></i> <span>It looks like your processing node ran out of memory. If you are using docker, make sure that your docker environment has <a href={memoryErrorLink} target="_blank">enough RAM allocated</a>. 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 <a href="javascript:void(0);" onClick={this.startEditing}>options</a>. You can also try to use a <a href="https://www.opendronemap.org/webodm/lightning/" target="_blank">cloud processing node</a>.</span></div> : ""}
|
<div className="task-warning"><i className="fa fa-support"></i> <span>It looks like your processing node ran out of memory. If you are using docker, make sure that your docker environment has <a href={memoryErrorLink} target="_blank">enough RAM allocated</a>. 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 <a href="javascript:void(0);" onClick={this.startEditing}>options</a>. You can also try to use a <a href="https://www.opendronemap.org/webodm/lightning/" target="_blank">cloud processing node</a>.</span></div> : ""}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import BasicTaskView from '../BasicTaskView';
|
|
||||||
|
|
||||||
describe('<BasicTaskView />', () => {
|
|
||||||
it('renders without exploding', () => {
|
|
||||||
const wrapper = mount(<BasicTaskView source="http://localhost/output" taskStatus={40} />);
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
})
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,4 +6,17 @@
|
||||||
a{
|
a{
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.console{
|
||||||
|
.exit-fullscreen{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.console:fullscreen{
|
||||||
|
.exit-fullscreen{
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -57,6 +57,8 @@
|
||||||
|
|
||||||
.task-warning{
|
.task-warning{
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
i.fa.fa-warning{
|
i.fa.fa-warning{
|
||||||
color: #ff8000;
|
color: #ff8000;
|
||||||
}
|
}
|
||||||
|
@ -100,25 +102,20 @@
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-view{
|
.console-switch{
|
||||||
|
margin-right: 10px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 90%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
|
.console-output-label{
|
||||||
a{
|
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
&.selected{
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
&:last-child{
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.console-output-label, ul{
|
||||||
i{
|
display: inline-block;
|
||||||
margin-left: 8px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mb{
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -20,8 +20,12 @@
|
||||||
<td>{{ processing_node.api_version }}</td>
|
<td>{{ processing_node.api_version }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans "ODM Version" %}</td>
|
<td>{% trans "Engine" %}</td>
|
||||||
<td>{{ processing_node.odm_version }}</td>
|
<td>{{ processing_node.engine }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans "Engine Version" %}</td>
|
||||||
|
<td>{{ processing_node.engine_version }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans "Queue Count" %}</td>
|
<td>{% trans "Queue Count" %}</td>
|
||||||
|
|
|
@ -355,8 +355,11 @@ class TestApi(BootTestCase):
|
||||||
# Verify max images field
|
# Verify max images field
|
||||||
self.assertTrue("max_images" in res.data)
|
self.assertTrue("max_images" in res.data)
|
||||||
|
|
||||||
# Verify odm version
|
# Verify engine version
|
||||||
self.assertTrue("odm_version" in res.data)
|
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)
|
# label should be hostname:port (since no label is set)
|
||||||
self.assertEqual(res.data['label'], pnode.hostname + ":" + str(pnode.port))
|
self.assertEqual(res.data['label'], pnode.hostname + ":" + str(pnode.port))
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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")
|
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.")
|
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)
|
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.")
|
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):
|
def __str__(self):
|
||||||
if self.label != "":
|
if self.label != "":
|
||||||
|
@ -61,7 +62,8 @@ class ProcessingNode(models.Model):
|
||||||
self.api_version = info.version
|
self.api_version = info.version
|
||||||
self.queue_count = info.task_queue_count
|
self.queue_count = info.task_queue_count
|
||||||
self.max_images = info.max_images
|
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()))
|
options = list(map(lambda o: o.__dict__, api_client.options()))
|
||||||
self.available_options = options
|
self.available_options = options
|
||||||
|
@ -116,7 +118,7 @@ class ProcessingNode(models.Model):
|
||||||
task = api_client.create_task(images, opts, name, progress_callback)
|
task = api_client.create_task(images, opts, name, progress_callback)
|
||||||
return task.uuid
|
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,
|
Gets information about this task, such as name, creation date,
|
||||||
processing time, status, command line options and number of
|
processing time, status, command line options and number of
|
||||||
|
@ -124,7 +126,13 @@ class ProcessingNode(models.Model):
|
||||||
"""
|
"""
|
||||||
api_client = self.api_client()
|
api_client = self.api_client()
|
||||||
task = api_client.get_task(uuid)
|
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):
|
def get_task_console_output(self, uuid, line):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "WebODM",
|
"name": "WebODM",
|
||||||
"version": "0.9.0",
|
"version": "0.9.1",
|
||||||
"description": "Open Source Drone Image Processing",
|
"description": "Open Source Drone Image Processing",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -37,7 +37,7 @@ pip-autoremove==0.9.0
|
||||||
psycopg2==2.7.4
|
psycopg2==2.7.4
|
||||||
psycopg2-binary==2.7.4
|
psycopg2-binary==2.7.4
|
||||||
PyJWT==1.5.3
|
PyJWT==1.5.3
|
||||||
pyodm==1.4.0
|
pyodm==1.5.1
|
||||||
pyparsing==2.1.10
|
pyparsing==2.1.10
|
||||||
pytz==2018.3
|
pytz==2018.3
|
||||||
rcssmin==1.0.6
|
rcssmin==1.0.6
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
"hostname": "nodeodm.masseranolabs.com",
|
"hostname": "nodeodm.masseranolabs.com",
|
||||||
"port": 80,
|
"port": 80,
|
||||||
"api_version": "1.0.1",
|
"api_version": "1.0.1",
|
||||||
|
"engine_version": "0.6.0",
|
||||||
|
"engine": "odm",
|
||||||
"last_refreshed": "2017-03-01T21:14:49.918276Z",
|
"last_refreshed": "2017-03-01T21:14:49.918276Z",
|
||||||
"queue_count": 0,
|
"queue_count": 0,
|
||||||
"max_images": null,
|
"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
|
hostname | string | Hostname/IP address
|
||||||
port | int | Port
|
port | int | Port
|
||||||
api_version | string | Version of NodeODM currently running
|
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
|
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.
|
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.
|
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
|
api_version | | "" | Filter by API version
|
||||||
queue_count | | "" | Filter by queue count
|
queue_count | | "" | Filter by queue count
|
||||||
max_images | | "" | Filter by max images
|
max_images | | "" | Filter by max images
|
||||||
|
engine_version | | "" | Filter by engine version
|
||||||
|
engine | | "" | Filter by engine identifier
|
||||||
ordering | | "" | Ordering field to sort results by
|
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`.
|
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`.
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue