Sharing to OAM working

pull/492/head
Piero Toffanin 2018-07-27 14:18:03 -04:00
rodzic a847edcd06
commit 8f0791aacc
12 zmienionych plików z 364 dodań i 50 usunięć

Wyświetl plik

@ -11,6 +11,7 @@ from string import Template
from django.http import HttpResponse
from app.models import Setting
from webodm import settings
logger = logging.getLogger('app.logger')
@ -174,6 +175,10 @@ def get_dynamic_script_handler(script_path, callback=None, **kwargs):
return handleRequest
def get_site_settings():
return Setting.objects.first()
def versionToInt(version):
"""
Converts a WebODM version string (major.minor.build) to a integer value

Wyświetl plik

@ -61,7 +61,10 @@ module.exports = {
resolve: {
modules: ['node_modules', 'bower_components'],
extensions: ['.js', '.jsx']
extensions: ['.js', '.jsx'],
alias: {
webodm: path.resolve(__dirname, '../../../app/static/app/js')
}
},
externals: {

Wyświetl plik

@ -553,21 +553,21 @@ class TaskListItem extends React.Component {
return (
<div className="task-list-item">
<div className="row">
<div className="col-md-5 name">
<div className="col-sm-5 name">
<i onClick={this.toggleExpanded} className={"clickable fa " + (this.state.expanded ? "fa-minus-square-o" : " fa-plus-square-o")}></i> <a href="javascript:void(0);" onClick={this.toggleExpanded}>{name}</a>
</div>
<div className="col-md-1 details">
<div className="col-sm-1 details">
<i className="fa fa-image"></i> {task.images_count}
</div>
<div className="col-md-2 details">
<div className="col-sm-2 details">
<i className="fa fa-clock-o"></i> {this.hoursMinutesSecs(this.state.time)}
</div>
<div className="col-md-3">
<div className="col-sm-3">
{showEditLink ?
<a href="javascript:void(0);" onClick={this.startEditing}>{statusLabel}</a>
: statusLabel}
</div>
<div className="col-md-1 text-right">
<div className="col-sm-1 text-right">
<div className="status-icon">
<i className={statusIcon}></i>
</div>

Wyświetl plik

@ -16,7 +16,7 @@ class TaskPluginActionButtons extends React.Component {
};
constructor(props){
super();
super(props);
this.state = {
buttons: []

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "WebODM",
"version": "0.5.3",
"version": "0.6.0",
"description": "Open Source Drone Image Processing",
"main": "index.js",
"scripts": {

Wyświetl plik

@ -1,6 +1,6 @@
PluginsAPI.Map.willAddControls([
'measure/build/app.js',
'measure/build/app.css'
], function(options, App){
new App(options.map);
], function(args, App){
new App(args.map);
});

Wyświetl plik

@ -1,12 +1,26 @@
import json
from datetime import datetime
import os
from urllib.parse import urlencode
import piexif
from PIL import Image
from rest_framework import serializers
from rest_framework import status
from rest_framework.response import Response
from app.plugins import GlobalDataStore
from app.models import ImageUpload
from app.plugins import GlobalDataStore, get_site_settings
from app.plugins.views import TaskView
from app.plugins.worker import task
from webodm import settings
import requests
import logging
logger = logging.getLogger('app.logger')
ds = GlobalDataStore('openaerialmap')
@ -28,27 +42,77 @@ def set_task_info(task_id, json):
# TODO: task info cleanup when task is deleted via signal
class ShareInfo(TaskView):
class Info(TaskView):
def get(self, request, pk=None):
task = self.get_and_check_task(request, pk)
return Response(get_task_info(task.id), status=status.HTTP_200_OK)
task_info = get_task_info(task.id)
# Populate fields from first image in task
img = ImageUpload.objects.filter(task=task).exclude(image__iendswith='.txt').first()
img_path = os.path.join(settings.MEDIA_ROOT, img.path())
im = Image.open(img_path)
# TODO: for better data we could look over all images
# and find actual end and start time
# Here we're picking an image at random and assuming a one hour flight
task_info['endDate'] = datetime.utcnow().timestamp() * 1000
task_info['sensor'] = ''
task_info['title'] = task.name
task_info['provider'] = get_site_settings().organization_name
if 'exif' in im.info:
exif_dict = piexif.load(im.info['exif'])
if 'Exif' in exif_dict:
if piexif.ExifIFD.DateTimeOriginal in exif_dict['Exif']:
try:
parsed_date = datetime.strptime(exif_dict['Exif'][piexif.ExifIFD.DateTimeOriginal].decode('ascii'),
'%Y:%m:%d %H:%M:%S')
task_info['endDate'] = parsed_date.timestamp() * 1000
except ValueError:
# Ignore date field if we can't parse it
pass
if '0th' in exif_dict:
if piexif.ImageIFD.Make in exif_dict['0th']:
task_info['sensor'] = exif_dict['0th'][piexif.ImageIFD.Make].decode('ascii').strip(' \t\r\n\0')
if piexif.ImageIFD.Model in exif_dict['0th']:
task_info['sensor'] = (task_info['sensor'] + " " + exif_dict['0th'][piexif.ImageIFD.Model].decode('ascii')).strip(' \t\r\n\0')
task_info['startDate'] = task_info['endDate'] - 60 * 60 * 1000
set_task_info(task.id, task_info)
return Response(task_info, status=status.HTTP_200_OK)
class JSONSerializer(serializers.Serializer):
oamParams = serializers.JSONField(help_text="OpenAerialMap share parameters (sensor, title, provider, etc.)")
class Share(TaskView):
def post(self, request, pk=None):
task = self.get_and_check_task(request, pk)
upload_orthophoto_to_oam.delay(task.id, task.get_asset_download_path('orthophoto.tif'))
serializer = JSONSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
oam_params = serializer['oamParams'].value
upload_orthophoto_to_oam.delay(task.id,
task.get_asset_download_path('orthophoto.tif'),
oam_params)
task_info = get_task_info(task.id)
task_info['sharing'] = True
task_info['oam_upload_id'] = ''
task_info['error'] = ''
set_task_info(task.id, task_info)
return Response(task_info, status=status.HTTP_200_OK)
@task
def upload_orthophoto_to_oam(task_id, orthophoto_path):
def upload_orthophoto_to_oam(task_id, orthophoto_path, oam_params):
# Upload to temporary central location since
# OAM requires a public URL and not all WebODM
# instances are public
@ -58,15 +122,28 @@ def upload_orthophoto_to_oam(task_id, orthophoto_path):
('file', ('orthophoto.tif', open(orthophoto_path, 'rb'), 'image/tiff')),
]).json()
task_info = get_task_info(task_id)
if 'url' in res:
orthophoto_public_url = res['url']
logger.info("Orthophoto uploaded to intermediary public URL " + orthophoto_public_url)
import logging
logger = logging.getLogger('app.logger')
# That's OK... we :heart: dronedeploy
res = requests.post('https://api.openaerialmap.org/dronedeploy?{}'.format(urlencode(oam_params)),
json={
'download_path': orthophoto_public_url
}).json()
logger.info("UPLOADED TO " + orthophoto_public_url)
if 'results' in res and 'upload' in res['results']:
task_info['oam_upload_id'] = res['results']['upload']
task_info['shared'] = True
else:
task_info['error'] = 'Could not upload orthophoto to OAM. The server replied: {}'.format(json.dumps(res))
# Attempt to cleanup intermediate results
requests.get('https://www.webodm.org/oam/cleanup/{}'.format(os.path.basename(orthophoto_public_url)))
else:
task_info = get_task_info(task_id)
task_info['sharing'] = False
task_info['error'] = 'Could not upload orthophoto to intermediate location.'
set_task_info(task_id, task_info)
task_info['error'] = 'Could not upload orthophoto to intermediate location: {}.'.format(json.dumps(res))
task_info['sharing'] = False
set_task_info(task_id, task_info)

Wyświetl plik

@ -1,12 +1,10 @@
PluginsAPI.Dashboard.addTaskActionButton([
'openaerialmap/build/ShareButton.js',
'openaerialmap/build/ShareButton.css'
],function(options, ShareButton){
var task = options.task;
],function(args, ShareButton){
var task = args.task;
if (task.available_assets.indexOf("orthophoto.tif") !== -1){
console.log("INSTANTIATED");
return {
button: React.createElement(ShareButton, {task: task, token: "${token}"}),
task: task

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "OpenAerialMap",
"webodmMinVersion": "0.5.3",
"webodmMinVersion": "0.6.0",
"description": "A plugin to upload orthophotos to OpenAerialMap",
"version": "0.1.0",
"author": "Piero Toffanin",

Wyświetl plik

@ -5,7 +5,7 @@ from app.plugins import PluginBase, Menu, MountPoint
from django.contrib.auth.decorators import login_required
from django import forms
from plugins.openaerialmap.api import ShareInfo, Share
from plugins.openaerialmap.api import Info, Share
class TokenForm(forms.Form):
@ -45,7 +45,7 @@ class Plugin(PluginBase):
def api_mount_points(self):
return [
MountPoint('task/(?P<pk>[^/.]+)/shareinfo', ShareInfo.as_view()),
MountPoint('task/(?P<pk>[^/.]+)/info', Info.as_view()),
MountPoint('task/(?P<pk>[^/.]+)/share', Share.as_view())
]

Wyświetl plik

@ -1,6 +1,9 @@
import './ShareButton.scss';
import React from 'react';
import PropTypes from 'prop-types';
import ShareDialog from './ShareDialog';
import Storage from 'webodm/classes/Storage';
import ErrorMessage from 'webodm/components/ErrorMessage';
import $ from 'jquery';
module.exports = class ShareButton extends React.Component{
@ -19,41 +22,125 @@ module.exports = class ShareButton extends React.Component{
this.state = {
loading: true,
shared: false,
taskInfo: {},
error: ''
};
console.log("AH!");
}
componentDidMount(){
const { task } = this.props;
this.updateTaskInfo(false);
}
$.ajax({
type: 'GET',
url: `/api/plugins/openaerialmap/task/${task.id}/shareinfo`,
contentType: "application/json"
}).done(result => {
this.setState({shared: result.shared, loading: false})
}).fail(error => {
this.setState({error, loading: false});
});
updateTaskInfo = (showErrors) => {
const { task } = this.props;
return $.ajax({
type: 'GET',
url: `/api/plugins/openaerialmap/task/${task.id}/info`,
contentType: 'application/json'
}).done(taskInfo => {
// Allow a user to specify a better name for the sensor
// and remember it.
let sensor = Storage.getItem("oam_sensor_pref_" + taskInfo.sensor);
if (sensor) taskInfo.sensor = sensor;
// Allow a user to change the default provider name
let provider = Storage.getItem("oam_provider_pref");
if (provider) taskInfo.provider = provider;
this.setState({taskInfo, loading: false});
if (taskInfo.error && showErrors) this.setState({error: taskInfo.error});
}).fail(error => {
this.setState({error, loading: false});
});
}
componentWillUnmount(){
if (this.monitorTimeout) clearTimeout(this.monitorTimeout);
}
handleClick = () => {
console.log("HEY!", this.props.token);
const { taskInfo } = this.state;
if (!taskInfo.shared){
this.shareDialog.show();
}else if (taskInfo.oam_upload_id){
window.open(`https://map.openaerialmap.org/#/upload/status/${encodeURIComponent(taskInfo.oam_upload_id)}`, '_blank');
}
}
shareToOAM = (formData) => {
const { task } = this.props;
const oamParams = {
token: this.props.token,
sensor: formData.sensor,
acquisition_start: formData.startDate,
acquisition_end: formData.endDate,
title: formData.title,
provider: formData.provider,
tags: formData.tags
};
return $.ajax({
url: `/api/plugins/openaerialmap/task/${task.id}/share`,
contentType: 'application/json',
data: JSON.stringify({
oamParams: oamParams
}),
dataType: 'json',
type: 'POST'
}).done(taskInfo => {
// Allow a user to associate the sensor name coming from the EXIF tags
// to one that perhaps is more human readable.
Storage.setItem("oam_sensor_pref_" + taskInfo.sensor, formData.sensor);
Storage.setItem("oam_provider_pref", formData.provider);
this.setState({taskInfo});
this.monitorProgress();
});
}
monitorProgress = () => {
if (this.state.taskInfo.sharing){
// Monitor progress
this.monitorTimeout = setTimeout(() => {
this.updateTaskInfo(true).always(this.monitorProgress);
}, 10000);
}
}
render(){
const { loading, shared } = this.state;
const { loading, taskInfo } = this.state;
return (<button
const getButtonIcon = () => {
if (loading || taskInfo.sharing) return "fa fa-circle-o-notch fa-spin fa-fw";
else return "oam-icon fa";
};
const getButtonLabel = () => {
if (loading) return "";
else if (taskInfo.sharing) return " Sharing...";
else if (taskInfo.shared) return " View In OAM";
else return " Share To OAM";
}
const result = [
<ErrorMessage bind={[this, "error"]} />,
<button
onClick={this.handleClick}
disabled={loading || shared}
disabled={loading || taskInfo.sharing}
className="btn btn-sm btn-primary">
{loading ?
<i className="fa fa-circle-o-notch fa-spin fa-fw"></i> :
[<i className="oam-icon fa"></i>, (shared ? "Shared To OAM" : " Share To OAM")]}
</button>);
{[<i className={getButtonIcon()}></i>, getButtonLabel()]}
</button>];
if (taskInfo.sensor !== undefined){
result.unshift(<ShareDialog
ref={(domNode) => { this.shareDialog = domNode; }}
task={this.props.task}
taskInfo={taskInfo}
saveAction={this.shareToOAM}
/>);
}
return result;
}
}

Wyświetl plik

@ -0,0 +1,144 @@
import React from 'react';
import ErrorMessage from 'webodm/components/ErrorMessage';
import FormDialog from 'webodm/components/FormDialog';
import PropTypes from 'prop-types';
import $ from 'jquery';
class ShareDialog extends React.Component {
static defaultProps = {
task: null,
taskInfo: null,
title: "Share To OpenAerialMap",
saveLabel: "Share",
savingLabel: "Sharing...",
saveIcon: "fa fa-share",
show: false
};
static propTypes = {
task: PropTypes.object.isRequired,
taskInfo: PropTypes.object.isRequired,
saveAction: PropTypes.func.isRequired,
title: PropTypes.string,
saveLabel: PropTypes.string,
savingLabel: PropTypes.string,
saveIcon: PropTypes.string,
show: PropTypes.bool
};
constructor(props){
super(props);
this.state = this.getInitialState(props);
this.reset = this.reset.bind(this);
this.getFormData = this.getFormData.bind(this);
this.onShow = this.onShow.bind(this);
this.handleChange = this.handleChange.bind(this);
}
getInitialState = (props) => {
return {
sensor: props.taskInfo.sensor,
startDate: this.toDatetimeLocal(new Date(props.taskInfo.startDate)),
endDate: this.toDatetimeLocal(new Date(props.taskInfo.endDate)),
title: props.taskInfo.title,
provider: props.taskInfo.provider,
tags: ""
};
}
// Credits to https://gist.github.com/WebReflection/6076a40777b65c397b2b9b97247520f0
toDatetimeLocal = (date) => {
const ten = function (i) {
return (i < 10 ? '0' : '') + i;
};
const YYYY = date.getFullYear(),
MM = ten(date.getMonth() + 1),
DD = ten(date.getDate()),
HH = ten(date.getHours()),
II = ten(date.getMinutes()),
SS = ten(date.getSeconds())
return YYYY + '-' + MM + '-' + DD + 'T' +
HH + ':' + II + ':' + SS;
};
reset(){
this.setState(this.getInitialState(this.props));
}
getFormData(){
return this.state;
}
onShow(){
this.titleInput.focus();
}
show(){
this.dialog.show();
}
hide(){
this.dialog.hide();
}
handleChange(field){
return (e) => {
let state = {};
state[field] = e.target.value;
this.setState(state);
}
}
render(){
// startDate, endDate, tags
return (
<FormDialog {...this.props}
getFormData={this.getFormData}
reset={this.reset}
ref={(domNode) => { this.dialog = domNode; }}>
<div className="form-group">
<label className="col-sm-3 control-label">Title</label>
<div className="col-sm-9">
<input type="text" className="form-control" ref={(domNode) => { this.titleInput = domNode; }} value={this.state.title} onChange={this.handleChange('title')} />
</div>
</div>
<div className="form-group">
<label className="col-sm-3 control-label">Sensor</label>
<div className="col-sm-9">
<input type="text" className="form-control" ref={(domNode) => { this.sensorInput = domNode; }} value={this.state.sensor} onChange={this.handleChange('sensor')} />
</div>
</div>
<div className="form-group">
<label className="col-sm-3 control-label">Provider</label>
<div className="col-sm-9">
<input type="text" className="form-control" ref={(domNode) => { this.providerInput = domNode; }} value={this.state.provider} onChange={this.handleChange('provider')} />
</div>
</div>
<div className="form-group">
<label className="col-sm-3 control-label">Flight Start Date</label>
<div className="col-sm-9">
<input type="datetime-local" className="form-control" ref={(domNode) => { this.startDateInput = domNode; }} value={this.state.startDate} onChange={this.handleChange('startDate')} />
</div>
</div>
<div className="form-group">
<label className="col-sm-3 control-label">Flight End Date</label>
<div className="col-sm-9">
<input type="datetime-local" className="form-control" ref={(domNode) => { this.endDateInput = domNode; }} value={this.state.endDate} onChange={this.handleChange('endDate')} />
</div>
</div>
<div className="form-group">
<label className="col-sm-3 control-label">Tags (comma separated)</label>
<div className="col-sm-9">
<input type="text" className="form-control" ref={(domNode) => { this.tagsInput = domNode; }} value={this.state.tags} onChange={this.handleChange('tags')} />
</div>
</div>
</FormDialog>
);
}
}
export default ShareDialog;