kopia lustrzana https://github.com/OpenDroneMap/WebODM
Sharing to OAM working
rodzic
a847edcd06
commit
8f0791aacc
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -16,7 +16,7 @@ class TaskPluginActionButtons extends React.Component {
|
|||
};
|
||||
|
||||
constructor(props){
|
||||
super();
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
buttons: []
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "WebODM",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.0",
|
||||
"description": "Open Source Drone Image Processing",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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())
|
||||
]
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
Ładowanie…
Reference in New Issue