Piero Toffanin 2017-05-01 10:27:35 -04:00
commit b05da61ac3
13 zmienionych plików z 267 dodań i 59 usunięć

Wyświetl plik

@ -4,6 +4,17 @@
A free, user-friendly, extendable application and [API](https://opendronemap.github.io/WebODM/) for drone image processing. Generate georeferenced maps, point clouds and textured 3D models from aerial images. It uses [OpenDroneMap](https://github.com/OpenDroneMap/OpenDroneMap) for processing.
* [Getting Started](#getting-started)
* [Common Troubleshooting](#common-troubleshooting)
* [Add More Processing Nodes](#add-more-processing-nodes)
* [Security](#security)
* [API Docs](#api-docs)
* [Run the docker version as a Linux Service](#run-the-docker-version-as-a-linux-service)
* [Run it natively](#run-it-natively)
* [OpenDroneMap, node-OpenDroneMap, WebODM... what?](#opendronemap-node-opendronemap-webodm-what)
* [Roadmap](#roadmap)
* [Terminology](#terminology)
![Alt text](/screenshots/ui-mockup.png?raw=true "WebODM")
![Alt text](/screenshots/pointcloud.png?raw=true "3D Display")
@ -58,7 +69,7 @@ We recommend that you read the [Docker Documentation](https://docs.docker.com/)
Sympthoms | Possible Solutions
--------- | ------------------
While starting WebODM you get: `from six.moves import _thread as thread ImportError: cannot import name _thread` | Try running: `sudo pip install --ignore-installed six`
Task output or console shows one of the following:<ul><li>`MemoryError`</li><li>`Killed`</li></ul> | Make sure that your Docker environment has enough RAM allocated. http://stackoverflow.com/a/39720010
Task output or console shows one of the following:<ul><li>`MemoryError`</li><li>`Killed`</li></ul> | Make sure that your Docker environment has enough RAM allocated: [MacOS Instructions](http://stackoverflow.com/a/39720010), [Windows Instructions](https://docs.docker.com/docker-for-windows/#advanced)
After an update, you get: `django.contrib.auth.models.DoesNotExist: Permission matching query does not exist.` | Try to remove your WebODM folder and start from a fresh git clone
Task fails with `Process exited with code null`, no task console output | If the computer running node-opendronemap is using an old or 32bit CPU, you need to compile [OpenDroneMap](https://github.com/OpenDroneMap/OpenDroneMap) from sources and setup node-opendronemap natively. You cannot use docker. Docker images work with CPUs with 64-bit extensions, MMX, SSE, SSE2, SSE3 and SSSE3 instruction set support or higher.
On Windows, docker-compose fails with `Failed to execute the script docker-compose` | Make sure you have enabled VT-x virtualization in the BIOS
@ -78,6 +89,50 @@ If you want to run WebODM in production, make sure to change the `SECRET_KEY` va
See the [API documentation page](https://opendronemap.github.io/WebODM/).
## Run the docker version as a Linux Service
If you wish to run the docker version with auto start/monitoring/stop, etc, as a systemd style Linux Service, a systemd unit file is included in the service folder of the repo.
This should work on any Linux OS capable of running WebODM, and using a SystemD based service daemon (such as Ubuntu 16.04 server for example).
This has only been tested on Ubuntu 16.04 server.
The following pre-requisites are required:
* Requires odm user
* Requires docker installed via system (ubuntu: `sudo apt-get install docker.io`)
* Requires screen to be installed
* Requires odm user member of docker group
* Required WebODM directory checked out to /opt/WebODM
* Requires that /opt/WebODM is recursively owned by odm:odm
If all pre-requisites have been met, and repository is checked out to /opt/WebODM folder, then you can use the following steps to enable and manage the service:
First, to install the service, and enable the service to run at startup from now on:
```bash
sudo systemctl enable /opt/WebODM/service/webodm.service
```
To manually stop the service:
```bash
sudo systemctl stop webodm
```
To manually start the service:
```bash
sudo systemctl start webodm
```
To manually check service status:
```bash
sudo systemctl status webodm
```
The service runs within a screen session, so as the odm user you can easily jump into the screen session by using:
```bash
screen -r webodm
```
(if you wish to exit the screen session, don't use ctrl+c, that will kill webodm, use `CTRL+A` then hit the `D` key)
## Run it natively
If you want to run WebODM natively, you will need to install:
@ -120,7 +175,7 @@ DATABASES = {
}
```
From psql or [pgadmin](https://www.pgadmin.org), connect to the database and set the [postgis.enable_outdb_rasters](http://postgis.net/docs/manual-2.2/postgis_enable_outdb_rasters.html) and [postgis.gdal_enabled_drivers](http://postgis.net/docs/postgis_gdal_enabled_drivers.html) settings:
From psql or [pgadmin](https://www.pgadmin.org), connect to PostgreSQL, create a new database (name it `webodm_dev`), connect to it and set the [postgis.enable_outdb_rasters](http://postgis.net/docs/manual-2.2/postgis_enable_outdb_rasters.html) and [postgis.gdal_enabled_drivers](http://postgis.net/docs/postgis_gdal_enabled_drivers.html) settings:
```sql
ALTER SYSTEM SET postgis.enable_outdb_rasters TO True;
@ -162,6 +217,27 @@ gdalinfo --version
```
Should all work without errors.
## OpenDroneMap, node-OpenDroneMap, WebODM... what?
The [OpenDroneMap project](https://github.com/OpenDroneMap/) is composed of several components.
- [OpenDroneMap](https://github.com/OpenDroneMap/OpenDroneMap) is a command line toolkit that processes aerial images. Users comfortable with the command line are probably OK using this component alone.
- [node-OpenDroneMap](https://github.com/OpenDroneMap/node-OpenDroneMap) is a lightweight interface and API (Application Program Interface) built directly on top of [OpenDroneMap](https://github.com/OpenDroneMap/OpenDroneMap). Users not comfortable with the command line can use this interface to process aerial images and developers can use the API to build applications. Features such as user authentication, map displays, etc. are not provided.
- [WebODM](https://github.com/OpenDroneMap/WebODM) adds more features such as user authentication, map displays, 3D displays, a higher level API and the ability to orchestrate multiple processing nodes (run jobs in parallel). Processing nodes are simply servers running [node-OpenDroneMap](https://github.com/OpenDroneMap/node-OpenDroneMap).
![webodm](https://cloud.githubusercontent.com/assets/1951843/25567386/5aeec7aa-2dba-11e7-9169-aca97b70db79.png)
In general, follow these guidelines to find out what you should use:
I am a... | Best choice
--------- | -----------
End user, I'm not really comfortable with the command line | [WebODM](https://github.com/OpenDroneMap/WebODM)
End user, I like shell commands, I need to process images for myself. I use other software to display processing results | [OpenDroneMap](https://github.com/OpenDroneMap/OpenDroneMap)
End user, I can work with the command line, but I'd rather not. I use other software to display processing results | [node-OpenDroneMap](https://github.com/OpenDroneMap/node-OpenDroneMap)
End user, I need a drone mapping application for my organization that everyone can use. | [WebODM](https://github.com/OpenDroneMap/WebODM)
Developer, I'm looking to build an app that displays map results and takes care of things like permissions | [WebODM](https://github.com/OpenDroneMap/WebODM)
Developer, I'm looking to build an app that will stay behind a firewall and just needs raw results | [node-OpenDroneMap](https://github.com/OpenDroneMap/node-OpenDroneMap)
## Roadmap
- [X] User Registration / Authentication
- [X] UI mockup

Wyświetl plik

@ -2,6 +2,11 @@ import React from 'react';
import './css/Dashboard.scss';
import ProjectList from './components/ProjectList';
import EditProjectDialog from './components/EditProjectDialog';
import Utils from './classes/Utils';
import {
BrowserRouter as Router,
Route
} from 'react-router-dom';
import $ from 'jquery';
class Dashboard extends React.Component {
@ -32,7 +37,20 @@ class Dashboard extends React.Component {
}
render() {
const projectList = ({ location, history }) => {
let q = Utils.queryParams(location),
page = parseInt(q.page !== undefined ? q.page : 1);
return <ProjectList
source={`/api/projects/?ordering=-created_at&page=${page}`}
ref={(domNode) => { this.projectList = domNode; }}
currentPage={page}
history={history}
/>;
};
return (
<Router basename="/dashboard">
<div>
<div className="text-right add-button">
<button type="button"
@ -47,10 +65,9 @@ class Dashboard extends React.Component {
saveAction={this.addNewProject}
ref={(domNode) => { this.projectDialog = domNode; }}
/>
<ProjectList
source="/api/projects/?ordering=-created_at&page=#{PAGE}"
ref={(domNode) => { this.projectList = domNode; }} />
<Route path="/" component={projectList} />
</div>
</Router>
);
}
}

Wyświetl plik

@ -0,0 +1,55 @@
import Utils from './Utils';
const SEP = ",";
class HistoryNav{
constructor(history){
this.history = history;
}
changeQS(param, value){
this.history.replace(
this.history.location.pathname +
Utils.replaceSearchQueryParam(this.history.location, param, value) +
this.history.location.hash);
}
isValueInQSList(param, value){
let q = Utils.queryParams(this.history.location);
if (q[param]){
return q[param].split(SEP).find(v => v == value);
}else return false;
}
addToQSList(param, value){
let q = Utils.queryParams(this.history.location);
if (q[param]){
if (!this.isValueInQSList(param, value)){
this.changeQS(param, q[param] + SEP + value);
}
}else{
this.changeQS(param, value);
}
}
removeFromQSList(param, value){
let q = Utils.queryParams(this.history.location);
if (q[param]){
let parts = q[param].split(SEP);
this.changeQS(param,
parts.filter(p => p != value).join(SEP)
);
}
}
toggleQSListItem(param, value, add){
if (add){
this.addToQSList(param, value);
}else{
this.removeFromQSList(param, value);
}
}
}
export default HistoryNav;

Wyświetl plik

@ -25,5 +25,30 @@ export default {
return result;
},
queryParams: function(location){
let params = {};
let paramsRaw = (location.search.replace("?", "").match(/([^&=]+)=?([^&]*)/g) || []);
for (let i in paramsRaw){
let parts = paramsRaw[i].split("=");
params[parts[0]] = parts[1];
}
return params;
},
toSearchQuery: function(params){
let parts = [];
for (let k in params){
parts.push(k + "=" + params[k]);
}
if (parts.length > 0) return "?" + parts.join("&");
else return "";
},
replaceSearchQueryParam: function(location, param, value){
let q = this.queryParams(location);
q[param] = value;
return this.toSearchQuery(q);
}
};

Wyświetl plik

@ -1,27 +1,33 @@
import React from 'react';
import update from 'immutability-helper';
import HistoryNav from '../classes/HistoryNav';
class Paginated extends React.Component{
constructor(){
super();
this.handlePageChange = this.handlePageChange.bind(this);
static defaultProps = {
currentPage: 1
};
static propTypes = {
history: React.PropTypes.object.isRequired, // reference to the history object coming from the route this component is bound to
currentPage: React.PropTypes.number
};
constructor(props){
super(props);
this.historyNav = new HistoryNav(props.history);
}
updatePagination(itemsPerPage, totalItems){
let currentPage = 1;
let currentPage = this.props.currentPage;
const totalPages = this.totalPages(itemsPerPage, totalItems);
if (this.state.pagination && this.state.pagination.currentPage !== undefined){
currentPage = this.state.pagination.currentPage;
}
if (currentPage > totalPages) currentPage = totalPages;
this.setState({pagination: {
switchingPages: false,
switchingPages: false,
itemsPerPage: itemsPerPage,
totalItems: totalItems,
currentPage: currentPage
totalItems: totalItems
}
});
}
@ -30,14 +36,6 @@ class Paginated extends React.Component{
return Math.ceil(totalItems / itemsPerPage);
}
getPaginatedUrl(base){
const page = this.state.pagination && this.state.pagination.currentPage !== undefined
? this.state.pagination.currentPage
: 1;
return base.replace(/#\{PAGE\}/g, page);
}
setPaginationState(props, done){
this.setState(update(this.state, {
pagination: {
@ -46,29 +44,17 @@ class Paginated extends React.Component{
}), done);
}
handlePageChange(pageNum){
return (e) => {
// Update current page, once rendering is completed, raise
// on page changed event
this.setPaginationState({
currentPage: pageNum,
switchingPages: true
}, () => {
if (this.onPageChanged) this.onPageChanged(pageNum);
});
}
}
handlePageItemsNumChange(delta, needsRefreshCallback){
let currentPage = this.props.currentPage;
const pagesBefore = this.totalPages(this.state.pagination.itemsPerPage, this.state.pagination.totalItems),
pagesAfter = this.totalPages(this.state.pagination.itemsPerPage, this.state.pagination.totalItems + delta);
let currentPage = this.state.pagination.currentPage;
if (currentPage > pagesAfter) currentPage = pagesAfter;
this.historyNav.changeQS('page', currentPage);
this.setPaginationState({
totalItems: this.state.pagination.totalItems + delta,
currentPage: currentPage
totalItems: this.state.pagination.totalItems + delta
}, () => {
if (pagesBefore !== pagesAfter) needsRefreshCallback(pagesAfter);
});

Wyświetl plik

@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import $ from 'jquery';
class Paginator extends React.Component {
@ -14,20 +15,20 @@ class Paginator extends React.Component {
<div className={this.props.className}>
<ul className="pagination pagination-sm">
<li className={currentPage === 1 ? "disabled" : ""}>
<a href="javascript:void(0);" onClick={this.props.handlePageChange(1)}>
<Link to={{search: "?page=1"}}>
<span>&laquo;</span>
</a>
</Link>
</li>
{pages.map(page => {
return (<li
key={page + 1}
className={currentPage === (page + 1) ? "active" : ""}
><a href="javascript:void(0);" onClick={this.props.handlePageChange(page + 1)}>{page + 1}</a></li>);
><Link to={{search: "?page=" + (page + 1)}}>{page + 1}</Link></li>);
})}
<li className={currentPage === numPages ? "disabled" : ""}>
<a href="javascript:void(0);" onClick={this.props.handlePageChange(numPages)}>
<Link to={{search: "?page=" + numPages}}>
<span>&raquo;</span>
</a>
</Link>
</li>
</ul>
</div>

Wyświetl plik

@ -6,10 +6,11 @@ import ProjectListItem from './ProjectListItem';
import Paginated from './Paginated';
import Paginator from './Paginator';
import ErrorMessage from './ErrorMessage';
import { Route } from 'react-router-dom';
class ProjectList extends Paginated {
constructor(){
super();
constructor(props){
super(props);
this.state = {
loading: true,
@ -27,12 +28,18 @@ class ProjectList extends Paginated {
this.refresh();
}
componentDidUpdate(prevProps){
if (prevProps.source !== this.props.source){
this.refresh();
}
}
refresh(){
this.setState({refreshing: true});
// Load projects from API
this.serverRequest =
$.getJSON(this.getPaginatedUrl(this.props.source), json => {
$.getJSON(this.props.source, json => {
if (json.results){
this.setState({
projects: json.results,
@ -78,11 +85,15 @@ class ProjectList extends Paginated {
return (<div className="project-list">Loading projects... <i className="fa fa-refresh fa-spin fa-fw"></i></div>);
}else{
return (<div className="project-list">
<ErrorMessage bind={[this, 'error']} />
<Paginator className="text-right" {...this.state.pagination} handlePageChange={this.handlePageChange.bind(this)}>
<ErrorMessage bind={[this, 'error']} />
<Paginator className="text-right" {...this.state.pagination} {...this.props}>
<ul className={"list-group project-list " + (this.state.refreshing ? "refreshing" : "")}>
{this.state.projects.map(p => (
<ProjectListItem key={p.id} data={p} onDelete={this.handleDelete} />
<ProjectListItem
key={p.id}
data={p}
onDelete={this.handleDelete}
history={this.props.history} />
))}
</ul>
</Paginator>

Wyświetl plik

@ -8,14 +8,17 @@ import ErrorMessage from './ErrorMessage';
import EditProjectDialog from './EditProjectDialog';
import Dropzone from '../vendor/dropzone';
import csrf from '../django/csrf';
import HistoryNav from '../classes/HistoryNav';
import $ from 'jquery';
class ProjectListItem extends React.Component {
constructor(props){
super(props);
this.historyNav = new HistoryNav(props.history);
this.state = {
showTaskList: false,
showTaskList: this.historyNav.isValueInQSList("project_task_open", props.data.id),
updatingTask: false,
upload: this.getDefaultUploadState(),
error: "",
@ -201,8 +204,12 @@ class ProjectListItem extends React.Component {
}
toggleTaskList(){
const showTaskList = !this.state.showTaskList;
this.historyNav.toggleQSListItem("project_task_open", this.state.data.id, showTaskList);
this.setState({
showTaskList: !this.state.showTaskList
showTaskList: showTaskList
});
}
@ -358,6 +365,7 @@ class ProjectListItem extends React.Component {
ref={this.setRef("taskList")}
source={`/api/projects/${data.id}/tasks/?ordering=-created_at`}
onDelete={this.taskDeleted}
history={this.props.history}
/> : ""}
</div>

Wyświetl plik

@ -3,8 +3,8 @@ import '../css/TaskList.scss';
import TaskListItem from './TaskListItem';
class TaskList extends React.Component {
constructor(){
super();
constructor(props){
super(props);
this.state = {
tasks: [],
@ -75,7 +75,12 @@ class TaskList extends React.Component {
{message}
{this.state.tasks.map(task => (
<TaskListItem data={task} key={task.id} refreshInterval={3000} onDelete={this.deleteTask} />
<TaskListItem
data={task}
key={task.id}
refreshInterval={3000}
onDelete={this.deleteTask}
history={this.props.history} />
))}
</div>
);

Wyświetl plik

@ -6,13 +6,16 @@ import pendingActions from '../classes/PendingActions';
import ErrorMessage from './ErrorMessage';
import EditTaskDialog from './EditTaskDialog';
import AssetDownloadButtons from './AssetDownloadButtons';
import HistoryNav from '../classes/HistoryNav';
class TaskListItem extends React.Component {
constructor(props){
super();
this.historyNav = new HistoryNav(props.history);
this.state = {
expanded: false,
expanded: this.historyNav.isValueInQSList("project_task_expanded", props.data.id),
task: {},
time: props.data.processing_time,
actionError: "",
@ -111,8 +114,12 @@ class TaskListItem extends React.Component {
}
toggleExpanded(){
const expanded = !this.state.expanded;
this.historyNav.toggleQSListItem("project_task_expanded", this.props.data.id, expanded);
this.setState({
expanded: !this.state.expanded
expanded: expanded
});
}

Wyświetl plik

@ -44,6 +44,8 @@
"react": "^15.3.2",
"react-dom": "^15.3.2",
"react-hot-loader": "^3.0.0-beta.5",
"react-router": "^4.1.0",
"react-router-dom": "^4.1.0",
"sass-loader": "^4.0.2",
"style-loader": "^0.13.1",
"tween.js": "^16.6.0",

Wyświetl plik

@ -0,0 +1,15 @@
[Unit]
Description=Start WebODM OpenDroneMap Service Container
Requires=docker.service
[Service]
Type=forking
User=odm
Group=odm
WorkingDirectory=/opt/WebODM
ExecStart=/bin/bash -c 'screen -dmS webodm /opt/WebODM/webodm.sh start'
ExecStop=/bin/bash -c '/opt/WebODM/webodm.sh stop'
Restart=on-failure
[Install]
WantedBy=multi-user.target

Wyświetl plik

@ -88,7 +88,7 @@ if [[ $1 = "start" ]]; then
elif [[ $1 = "stop" ]]; then
environment_check
echo "Stopping WebODM..."
run "docker-compose down"
run "docker-compose down --remove-orphans"
elif [[ $1 = "rebuild" ]]; then
environment_check
echo "Rebuilding WebODM..."