Merge pull request #418 from OpenDroneMap/volume

Volume
pull/421/head v0.5.1
Piero Toffanin 2018-03-30 09:15:23 -04:00 zatwierdzone przez GitHub
commit 7b6efd6031
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
58 zmienionych plików z 662 dodań i 94 usunięć

Wyświetl plik

@ -18,7 +18,7 @@ RUN printf "deb http://mirror.steadfast.net/debian/ stable main contrib n
RUN printf "deb http://mirror.steadfast.net/debian/ testing main contrib non-free\ndeb-src http://mirror.steadfast.net/debian/ testing main contrib non-free" > /etc/apt/sources.list.d/testing.list
# Install Node.js GDAL, nginx, letsencrypt, psql
RUN apt-get -qq update && apt-get -qq install -t testing -y binutils libproj-dev gdal-bin nginx && apt-get -qq install -y gettext-base cron certbot postgresql-client-9.6
RUN apt-get -qq update && apt-get -qq install -t testing -y binutils libproj-dev gdal-bin nginx grass-core && apt-get -qq install -y gettext-base cron certbot postgresql-client-9.6
# Install pip reqs

Wyświetl plik

@ -189,7 +189,7 @@ Developer, I'm looking to build an app that will stay behind a firewall and just
- [X] 2D Map Display
- [X] 3D Model Display
- [ ] NDVI display
- [ ] Volumetric Measurements
- [X] Volumetric Measurements
- [X] Cluster management and setup.
- [ ] Mission Planner
- [X] Plugins/Webhooks System

Wyświetl plik

@ -21,7 +21,6 @@ class TaskIDsSerializer(serializers.BaseSerializer):
def to_representation(self, obj):
return obj.id
class TaskSerializer(serializers.ModelSerializer):
project = serializers.PrimaryKeyRelatedField(queryset=models.Project.objects.all())
processing_node = serializers.PrimaryKeyRelatedField(queryset=ProcessingNode.objects.all())
@ -193,15 +192,15 @@ class TaskNestedView(APIView):
queryset = models.Task.objects.all().defer('orthophoto_extent', 'dtm_extent', 'dsm_extent', 'console_output', )
permission_classes = (IsAuthenticatedOrReadOnly, )
def get_and_check_task(self, request, pk, project_pk, annotate={}):
def get_and_check_task(self, request, pk, annotate={}):
try:
task = self.queryset.annotate(**annotate).get(pk=pk, project=project_pk)
task = self.queryset.annotate(**annotate).get(pk=pk)
except (ObjectDoesNotExist, ValidationError):
raise exceptions.NotFound()
# Check for permissions, unless the task is public
if not task.public:
get_and_check_project(request, project_pk)
get_and_check_project(request, task.project.id)
return task
@ -211,7 +210,7 @@ class TaskTiles(TaskNestedView):
"""
Get a tile image
"""
task = self.get_and_check_task(request, pk, project_pk)
task = self.get_and_check_task(request, pk)
tile_path = task.get_tile_path(tile_type, z, x, y)
if os.path.isfile(tile_path):
tile = open(tile_path, "rb")
@ -225,7 +224,7 @@ class TaskTilesJson(TaskNestedView):
"""
Get tile.json for this tasks's asset type
"""
task = self.get_and_check_task(request, pk, project_pk)
task = self.get_and_check_task(request, pk)
extent_map = {
'orthophoto': task.orthophoto_extent,
@ -256,7 +255,7 @@ class TaskDownloads(TaskNestedView):
"""
Downloads a task asset (if available)
"""
task = self.get_and_check_task(request, pk, project_pk)
task = self.get_and_check_task(request, pk)
# Check and download
try:
@ -284,7 +283,7 @@ class TaskAssets(TaskNestedView):
"""
Downloads a task asset (if available)
"""
task = self.get_and_check_task(request, pk, project_pk)
task = self.get_and_check_task(request, pk)
# Check for directory traversal attacks
try:

Wyświetl plik

@ -1,6 +1,7 @@
from django.conf.urls import url, include
from app.api.presets import PresetViewSet
from app.plugins import get_api_url_patterns
from .projects import ProjectViewSet
from .tasks import TaskViewSet, TaskTiles, TaskTilesJson, TaskDownloads, TaskAssets
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
@ -31,4 +32,6 @@ urlpatterns = [
url(r'^auth/', include('rest_framework.urls')),
url(r'^token-auth/', obtain_jwt_token),
]
]
urlpatterns += get_api_url_patterns()

Wyświetl plik

@ -3,7 +3,7 @@ import os
import kombu
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User, Group
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.core.files import File
from django.db.utils import ProgrammingError
from guardian.shortcuts import assign_perm
@ -35,6 +35,10 @@ def boot():
if settings.DEBUG:
logger.warning("Debug mode is ON (for development this is OK)")
# Make sure our app/media/tmp folder exists
if not os.path.exists(settings.MEDIA_TMP):
os.mkdir(settings.MEDIA_TMP)
# Check default group
try:
default_group, created = Group.objects.get_or_create(name='Default')
@ -60,18 +64,7 @@ def boot():
# Add permission to view processing nodes
default_group.permissions.add(Permission.objects.get(codename="view_processingnode"))
# Add default presets
Preset.objects.get_or_create(name='DSM + DTM', system=True,
options=[{'name': 'dsm', 'value': True}, {'name': 'dtm', 'value': True}, {'name': 'mesh-octree-depth', 'value': 11}])
Preset.objects.get_or_create(name='Fast Orthophoto', system=True,
options=[{'name': 'fast-orthophoto', 'value': True}])
Preset.objects.get_or_create(name='High Quality', system=True,
options=[{'name': 'dsm', 'value': True},
{'name': 'mesh-octree-depth', 'value': "12"},
{'name': 'dem-resolution', 'value': "0.04"},
{'name': 'orthophoto-resolution', 'value': "40"},
])
Preset.objects.get_or_create(name='Default', system=True, options=[{'name': 'dsm', 'value': True}, {'name': 'mesh-octree-depth', 'value': 11}])
add_default_presets()
# Add settings
default_theme, created = Theme.objects.get_or_create(name='Default')
@ -101,4 +94,29 @@ def boot():
except ProgrammingError:
logger.warning("Could not touch the database. If running a migration, this is expected.")
logger.warning("Could not touch the database. If running a migration, this is expected.")
def add_default_presets():
try:
Preset.objects.update_or_create(name='DSM + DTM', system=True,
defaults={
'options': [{'name': 'dsm', 'value': True}, {'name': 'dtm', 'value': True},
{'name': 'mesh-octree-depth', 'value': 6}]})
Preset.objects.update_or_create(name='Fast Orthophoto', system=True,
defaults={'options': [{'name': 'fast-orthophoto', 'value': True}]})
Preset.objects.update_or_create(name='High Quality', system=True,
defaults={'options': [{'name': 'dsm', 'value': True},
{'name': 'mesh-octree-depth', 'value': "12"},
{'name': 'dem-resolution', 'value': "0.04"},
{'name': 'orthophoto-resolution', 'value': "40"},
]})
Preset.objects.update_or_create(name='Default', system=True,
defaults={'options': [{'name': 'dsm', 'value': True},
{'name': 'mesh-octree-depth', 'value': 6}]})
except MultipleObjectsReturned:
# Mostly to handle a legacy code problem where
# multiple system presets with the same name were
# created if we changed the options
Preset.objects.filter(system=True).delete()
add_default_presets()

Wyświetl plik

@ -578,7 +578,7 @@ class Task(models.Model):
self.status = status_codes.FAILED
self.pending_action = None
self.save()
def find_all_files_matching(self, regex):
directory = full_task_directory_path(self.id, self.project.id)
return [os.path.join(directory, f) for f in os.listdir(directory) if

Wyświetl plik

@ -1,6 +1,7 @@
import os
import logging
import importlib
import subprocess
import django
import json
@ -13,30 +14,57 @@ logger = logging.getLogger('app.logger')
def register_plugins():
for plugin in get_active_plugins():
# Check for package.json in public directory
# and run npm install if needed
if plugin.path_exists("public/package.json") and not plugin.path_exists("public/node_modules"):
logger.info("Running npm install for {}".format(plugin.get_name()))
subprocess.call(['npm', 'install'], cwd=plugin.get_path("public"))
# Check for webpack.config.js (if we need to build it)
if plugin.path_exists("public/webpack.config.js") and not plugin.path_exists("public/build"):
logger.info("Running webpack for {}".format(plugin.get_name()))
subprocess.call(['webpack'], cwd=plugin.get_path("public"))
plugin.register()
logger.info("Registered {}".format(plugin))
def get_url_patterns():
def get_app_url_patterns():
"""
@return the patterns to expose the /public directory of each plugin (if needed)
@return the patterns to expose the /public directory of each plugin (if needed) and
each mount point
"""
url_patterns = []
for plugin in get_active_plugins():
for mount_point in plugin.mount_points():
for mount_point in plugin.app_mount_points():
url_patterns.append(url('^plugins/{}/{}'.format(plugin.get_name(), mount_point.url),
mount_point.view,
*mount_point.args,
**mount_point.kwargs))
if plugin.has_public_path():
if plugin.path_exists("public"):
url_patterns.append(url('^plugins/{}/(.*)'.format(plugin.get_name()),
django.views.static.serve,
{'document_root': plugin.get_path("public")}))
return url_patterns
def get_api_url_patterns():
"""
@return the patterns to expose the plugin API mount points (if any)
"""
url_patterns = []
for plugin in get_active_plugins():
for mount_point in plugin.api_mount_points():
url_patterns.append(url('^plugins/{}/{}'.format(plugin.get_name(), mount_point.url),
mount_point.view,
*mount_point.args,
**mount_point.kwargs))
return url_patterns
plugins = None
def get_active_plugins():
# Cache plugins search
@ -86,6 +114,12 @@ def get_active_plugins():
return plugins
def get_plugin_by_name(name):
plugins = get_active_plugins()
res = list(filter(lambda p: p.get_name() == name, plugins))
return res[0] if res else None
def get_plugins_path():
current_path = os.path.dirname(os.path.realpath(__file__))
return os.path.abspath(os.path.join(current_path, "..", "..", "plugins"))

Wyświetl plik

@ -0,0 +1,117 @@
import logging
import shutil
import tempfile
import subprocess
import os
import geojson
from string import Template
from webodm import settings
logger = logging.getLogger('app.logger')
class GrassEngine:
def __init__(self):
self.grass_binary = shutil.which('grass7') or \
shutil.which('grass72') or \
shutil.which('grass74') or \
shutil.which('grass76')
if self.grass_binary is None:
logger.warning("Could not find a GRASS 7 executable. GRASS scripts will not work.")
else:
logger.info("Initializing GRASS engine using {}".format(self.grass_binary))
def create_context(self, serialized_context = {}):
if self.grass_binary is None: raise GrassEngineException("GRASS engine is unavailable")
return GrassContext(self.grass_binary, **serialized_context)
class GrassContext:
def __init__(self, grass_binary, tmpdir = None, template_args = {}, location = None):
self.grass_binary = grass_binary
if tmpdir is None:
tmpdir = os.path.basename(tempfile.mkdtemp('_grass_engine', dir=settings.MEDIA_TMP))
self.tmpdir = tmpdir
self.template_args = template_args
self.location = location
def get_cwd(self):
return os.path.join(settings.MEDIA_TMP, self.tmpdir)
def add_file(self, filename, source, use_as_location=False):
param = os.path.splitext(filename)[0] # filename without extension
dst_path = os.path.abspath(os.path.join(self.get_cwd(), filename))
with open(dst_path, 'w') as f:
f.write(source)
self.template_args[param] = dst_path
if use_as_location:
self.set_location(self.template_args[param])
return dst_path
def add_param(self, param, value):
self.template_args[param] = value
def set_location(self, location):
"""
:param location: either a "epsg:XXXXX" string or a path to a geospatial file defining the location
"""
if not location.startswith('epsg:'):
location = os.path.abspath(location)
self.location = location
def execute(self, script):
"""
:param script: path to .grass script
:return: script output
"""
if self.location is None: raise GrassEngineException("Location is not set")
script = os.path.abspath(script)
# Create grass script via template substitution
try:
with open(script) as f:
script_content = f.read()
except FileNotFoundError:
raise GrassEngineException("Script does not exist: {}".format(script))
tmpl = Template(script_content)
# Write script to disk
with open(os.path.join(self.get_cwd(), 'script.sh'), 'w') as f:
f.write(tmpl.substitute(self.template_args))
# Execute it
p = subprocess.Popen([self.grass_binary, '-c', self.location, 'location', '--exec', 'sh', 'script.sh'],
cwd=self.get_cwd(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
out = out.decode('utf-8').strip()
err = err.decode('utf-8').strip()
if p.returncode == 0:
return out
else:
raise GrassEngineException("Could not execute GRASS script {} from {}: {}".format(script, self.get_cwd(), err))
def serialize(self):
return {
'tmpdir': self.tmpdir,
'template_args': self.template_args,
'location': self.location
}
def __del__(self):
# Cleanup
if os.path.exists(self.get_cwd()):
shutil.rmtree(self.get_cwd())
class GrassEngineException(Exception):
pass
grass = GrassEngine()

Wyświetl plik

@ -5,7 +5,7 @@ class MountPoint:
"""
:param url: path to mount this view to, relative to plugins directory
:param view: Django view
:param view: Django/DjangoRestFramework view
:param args: extra args to pass to url() call
:param kwargs: extra kwargs to pass to url() call
"""

Wyświetl plik

@ -46,8 +46,8 @@ class PluginBase(ABC):
"""
return "plugins/{}/templates/{}".format(self.get_name(), path)
def has_public_path(self):
return os.path.isdir(self.get_path("public"))
def path_exists(self, path):
return os.path.exists(self.get_path(path))
def include_js_files(self):
"""
@ -73,7 +73,7 @@ class PluginBase(ABC):
"""
return []
def mount_points(self):
def app_mount_points(self):
"""
Should be overriden by plugins that want to connect
custom Django views
@ -81,5 +81,13 @@ class PluginBase(ABC):
"""
return []
def api_mount_points(self):
"""
Should be overriden by plugins that want to add
new API mount points
:return: [] of MountPoint objects
"""
return []
def __str__(self):
return "[{}]".format(self.get_module_name())

Wyświetl plik

@ -200,7 +200,7 @@ pre.prettyprint,
}
/* Failed */
.task-list-item .status-label.error{
.task-list-item .status-label.error, .theme-background-failed{
background-color: theme("failed");
}

Wyświetl plik

@ -11,10 +11,17 @@ if (!window.PluginsAPI){
SystemJS.config({
baseURL: '/plugins',
map: {
css: '/static/app/js/vendor/css.js'
'css': '/static/app/js/vendor/css.js',
'globals-loader': '/static/app/js/vendor/globals-loader.js'
},
meta: {
'*.css': { loader: 'css' }
'*.css': { loader: 'css' },
// Globals always available in the window object
'jQuery': { loader: 'globals-loader', exports: '$' },
'leaflet': { loader: 'globals-loader', exports: 'L' },
'ReactDOM': { loader: 'globals-loader', exports: 'ReactDOM' },
'React': { loader: 'globals-loader', exports: 'React' }
}
});

Wyświetl plik

@ -41,11 +41,16 @@ export default class ApiFactory{
};
}
const obj = {};
let obj = {};
api.endpoints.forEach(endpoint => {
if (!Array.isArray(endpoint)) endpoint = [endpoint];
addEndpoint(obj, ...endpoint);
});
if (api.helpers){
obj = Object.assign(obj, api.helpers);
}
return obj;
}

Wyświetl plik

@ -1,4 +1,5 @@
import Utils from '../Utils';
import L from 'leaflet';
const { assert } = Utils;

Wyświetl plik

@ -268,7 +268,7 @@ class Map extends React.Component {
handleMapMouseDown(e){
// Make sure the share popup closes
this.shareButton.hidePopup();
if (this.sharePopup) this.shareButton.hidePopup();
}
render() {

Wyświetl plik

@ -16,10 +16,17 @@
}
}
.leaflet-right .leaflet-control,
.leaflet-control-measure.leaflet-control{
margin-right: 12px;
}
.leaflet-touch .leaflet-control-layers-toggle{
width: 30px;
height: 30px;
background-size: 20px;
}
.popup-opacity-slider{
margin-bottom: 6px;
}

Wyświetl plik

@ -1,10 +1,13 @@
import '../css/main.scss';
import './django/csrf';
import ReactDOM from 'react-dom';
import React from 'react';
import PluginsAPI from './classes/plugins/API';
// Main is always executed first in the page
// We share the ReactDOM object to avoid having to include it
// We share some objects to avoid having to include them
// as a dependency in each component (adds too much space overhead)
window.ReactDOM = ReactDOM;
window.React = React;

Wyświetl plik

@ -0,0 +1,15 @@
/*
SystemJS Globals loader plugin
Piero Toffanin 2018
*/
// this code simply allows loading of global modules
// that are already defined in the window object
exports.fetch = function(load) {
var moduleName = load.name.split("/").pop();
return moduleName;
}
exports.instantiate = function(load){
return window[load.source] || window[load.metadata.exports];
}

Wyświetl plik

@ -1,6 +1,9 @@
import os
from django.test import Client
from rest_framework import status
from app.plugins import get_plugin_by_name
from .classes import BootTestCase
class TestPlugins(BootTestCase):
@ -37,6 +40,10 @@ class TestPlugins(BootTestCase):
# And our menu entry
self.assertContains(res, '<li><a href="/plugins/test/menu_url/"><i class="test-icon"></i> Test</a></li>', html=True)
# A node_modules directory has been created as a result of npm install
# because we have a package.json in the public director
test_plugin = get_plugin_by_name("test")
self.assertTrue(os.path.exists(test_plugin.get_path("public/node_modules")))
# TODO:
# test API endpoints
# test python hooks
# test GRASS engine

Wyświetl plik

@ -4,7 +4,7 @@ from django.shortcuts import render_to_response
from django.template import RequestContext
from .views import app as app_views, public as public_views
from .plugins import get_url_patterns
from .plugins import get_app_url_patterns
from app.boot import boot
from webodm import settings
@ -30,7 +30,7 @@ urlpatterns = [
# TODO: is there a way to place plugins /public directories
# into the static build directories and let nginx serve them?
urlpatterns += get_url_patterns()
urlpatterns += get_app_url_patterns()
handler404 = app_views.handler404
handler500 = app_views.handler500

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "WebODM",
"version": "0.5.0",
"version": "0.5.1",
"description": "Open Source Drone Image Processing",
"main": "index.js",
"scripts": {
@ -42,7 +42,7 @@
"immutability-helper": "^2.0.0",
"jest": "^21.0.1",
"json-loader": "^0.5.4",
"leaflet": "^1.0.1",
"leaflet": "^1.3.1",
"node-sass": "^3.10.1",
"object.values": "^1.0.3",
"proj4": "^2.4.3",

Wyświetl plik

@ -0,0 +1,55 @@
import os
import json
from rest_framework import serializers
from rest_framework import status
from rest_framework.response import Response
from app.api.tasks import TaskNestedView
from worker.tasks import execute_grass_script
from app.plugins.grass_engine import grass, GrassEngineException
from geojson import Feature, Point, FeatureCollection
class GeoJSONSerializer(serializers.Serializer):
area = serializers.JSONField(help_text="Polygon contour defining the volume area to compute")
class TaskVolume(TaskNestedView):
def post(self, request, pk=None):
task = self.get_and_check_task(request, pk)
if task.dsm_extent is None:
return Response({'error': 'No surface model available'})
serializer = GeoJSONSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
area = serializer['area'].value
points = FeatureCollection([Feature(geometry=Point(coords)) for coords in area['geometry']['coordinates'][0]])
dsm = os.path.abspath(task.get_asset_download_path("dsm.tif"))
try:
context = grass.create_context()
context.add_file('area_file.geojson', json.dumps(area))
context.add_file('points_file.geojson', str(points))
context.add_param('dsm_file', dsm)
context.set_location(dsm)
output = execute_grass_script.delay(os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"calc_volume.grass"
), context.serialize()).get()
if isinstance(output, dict) and 'error' in output: raise GrassEngineException(output['error'])
cols = output.split(':')
if len(cols) == 7:
return Response({'volume': str(abs(float(cols[6])))}, status=status.HTTP_200_OK)
else:
raise GrassEngineException(output)
except GrassEngineException as e:
return Response({'error': str(e)}, status=status.HTTP_200_OK)

Wyświetl plik

@ -0,0 +1,26 @@
# area_file: Geospatial file containing the area to measure
# points_file: Geospatial file containing the points defining the area
# dsm_file: GeoTIFF DEM containing the surface
# ------
# output: prints the volume to stdout
v.import input=${area_file} output=polygon_area --overwrite
v.import input=${points_file} output=polygon_points --overwrite
v.buffer -s --overwrite input=polygon_area type=area output=region distance=3 minordistance=3
r.external input=${dsm_file} output=dsm --overwrite
g.region rast=dsm
g.region vector=region
# prevent : removing eventual existing mask
r.mask -r
r.mask vect=region
v.what.rast map=polygon_points raster=dsm column=height
v.to.rast input=polygon_area output=r_polygon_area use=val value=255 --overwrite
#v.surf.rst --overwrite input=polygon_points zcolumn=height elevation=dsm_below_pile mask=r_polygon_area
v.surf.bspline --overwrite input=polygon_points column=height raster_output=dsm_below_pile lambda_i=100
r.mapcalc expression='pile_height_above_dsm=dsm-dsm_below_pile' --overwrite
r.volume -f input=pile_height_above_dsm clump=r_polygon_area

Wyświetl plik

@ -1,13 +1,13 @@
{
"name": "Area/Length Measurements",
"name": "Volume/Area/Length Measurements",
"webodmMinVersion": "0.5.0",
"description": "A plugin to compute area and length measurements on Leaflet",
"description": "A plugin to compute volume, area and length measurements on Leaflet",
"version": "0.1.0",
"author": "Piero Toffanin",
"author": "Abdelkoddouss Izem, Piero Toffanin",
"email": "pt@masseranolabs.com",
"repository": "https://github.com/OpenDroneMap/WebODM",
"tags": ["area", "length", "measurements"],
"tags": ["volume", "area", "length", "measurements"],
"homepage": "https://github.com/OpenDroneMap/WebODM",
"experimental": false,
"experimental": true,
"deprecated": false
}

Wyświetl plik

@ -1,5 +1,12 @@
from app.plugins import MountPoint
from app.plugins import PluginBase
from .api import TaskVolume
class Plugin(PluginBase):
def include_js_files(self):
return ['main.js']
return ['main.js']
def api_mount_points(self):
return [
MountPoint('task/(?P<pk>[^/.]+)/volume', TaskVolume.as_view())
]

Wyświetl plik

@ -0,0 +1,103 @@
import React from 'react';
import PropTypes from 'prop-types';
import './MeasurePopup.scss';
import $ from 'jquery';
import L from 'leaflet';
module.exports = class MeasurePopup extends React.Component {
static defaultProps = {
map: {},
model: {},
resultFeature: {}
};
static propTypes = {
map: PropTypes.object.isRequired,
model: PropTypes.object.isRequired,
resultFeature: PropTypes.object.isRequired
}
constructor(props){
super(props);
this.state = {
volume: null, // to be calculated
error: ""
};
}
componentDidMount(){
this.calculateVolume();
}
calculateVolume(){
const { lastCoord } = this.props.model;
let layers = this.getLayersAtCoords(L.latLng(
lastCoord.dd.y,
lastCoord.dd.x
));
console.log(layers);
// Did we select a layer?
if (layers.length > 0){
const layer = layers[layers.length - 1];
const meta = layer[Symbol.for("meta")];
if (meta){
const task = meta.task;
$.ajax({
type: 'POST',
url: `/api/plugins/measure/task/${task.id}/volume`,
data: JSON.stringify({'area': this.props.resultFeature.toGeoJSON()}),
contentType: "application/json"
}).done(result => {
if (result.volume){
this.setState({volume: parseFloat(result.volume)});
}else if (result.error){
this.setState({error: result.error});
}else{
this.setState({error: "Invalid response: " + result});
}
}).fail(error => {
this.setState({error});
});
}else{
console.warn("Cannot find [meta] symbol for layer: ", layer);
this.setState({volume: false});
}
}else{
this.setState({volume: false});
}
}
// @return the layers in the map
// at a specific lat/lon
getLayersAtCoords(latlng){
const targetBounds = L.latLngBounds(latlng, latlng);
const intersects = [];
for (let l in this.props.map._layers){
const layer = this.props.map._layers[l];
if (layer.options && layer.options.bounds){
if (targetBounds.intersects(layer.options.bounds)){
intersects.push(layer);
}
}
}
return intersects;
}
render(){
const { volume, error } = this.state;
return (<div className="plugin-measure popup">
<p>Area: {this.props.model.areaDisplay}</p>
<p>Perimeter: {this.props.model.lengthDisplay}</p>
{volume === null && !error && <p>Volume: <i>computing...</i> <i className="fa fa-cog fa-spin fa-fw" /></p>}
{typeof volume === "number" && <p>Volume: {volume.toFixed("2")} Cubic Meters ({(volume * 35.3147).toFixed(2)} Cubic Feet)</p>}
{error && <p>Volume: <span className={"error theme-background-failed " + (error.length > 200 ? 'long' : '')}>{error}</span></p>}
</div>);
}
}

Wyświetl plik

@ -0,0 +1,11 @@
.plugin-measure.popup{
p{
margin: 0;
}
.error.long{
overflow: scroll;
display: block;
max-height: 200px;
}
}

Wyświetl plik

@ -0,0 +1,41 @@
import L from 'leaflet';
import './app.scss';
import 'leaflet-measure-ex/dist/leaflet-measure';
import 'leaflet-measure-ex/dist/leaflet-measure.css';
import MeasurePopup from './MeasurePopup';
import ReactDOM from 'ReactDOM';
import React from 'react';
import $ from 'jquery';
module.exports = class App{
constructor(map){
this.map = map;
L.control.measure({
labels:{
measureDistancesAndAreas: 'Measure volume, area and length',
areaMeasurement: 'Measurement'
},
primaryLengthUnit: 'meters',
secondaryLengthUnit: 'feet',
primaryAreaUnit: 'sqmeters',
secondaryAreaUnit: 'acres'
}).addTo(map);
map.on('measurepopupshown', ({popupContainer, model, resultFeature}) => {
// Only modify area popup, length popup is fine as default
if (model.area !== 0){
const $container = $("<div/>"),
$popup = $(popupContainer);
$popup.children("p").empty();
$popup.children("h3:first-child").after($container);
ReactDOM.render(<MeasurePopup
model={model}
resultFeature={resultFeature}
map={map} />, $container.get(0));
}
});
}
}

Wyświetl plik

@ -0,0 +1,19 @@
.leaflet-control-measure,
.leaflet-measure-resultpopup{
h3{
font-size: 120%;
}
}
.leaflet-control-measure-interaction{
a{
width: auto !important;
height: auto !important;
line-height: auto !important;
display: initial !important;
&:hover{
background-color: inherit !important;
}
}
}

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 397 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 762 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 387 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 692 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 326 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 462 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 192 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 277 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 491 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1003 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 279 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 460 B

Wyświetl plik

@ -1 +0,0 @@
.leaflet-control-measure h3,.leaflet-measure-resultpopup h3{margin:0 0 12px 0;padding-bottom:10px;line-height:1em;font-weight:normal;font-size:1.1em;border-bottom:solid 1px #DDD}.leaflet-control-measure p,.leaflet-measure-resultpopup p{margin:10px 0 0;line-height:1em}.leaflet-control-measure p:first-child,.leaflet-measure-resultpopup p:first-child{margin-top:0}.leaflet-control-measure a,.leaflet-measure-resultpopup a{color:#5E66CC;text-decoration:none}.leaflet-control-measure a:hover,.leaflet-measure-resultpopup a:hover{opacity:0.5;text-decoration:none}.leaflet-control-measure .tasks,.leaflet-measure-resultpopup .tasks{margin:12px 0 0;padding:10px 0 0;border-top:solid 1px #DDD;list-style:none;list-style-image:none}.leaflet-control-measure .tasks li,.leaflet-measure-resultpopup .tasks li{display:inline;margin:0 10px 0 0}.leaflet-control-measure .tasks li:last-child,.leaflet-measure-resultpopup .tasks li:last-child{margin-right:0}.leaflet-control-measure .coorddivider,.leaflet-measure-resultpopup .coorddivider{color:#999}.leaflet-control-measure{background:#fff;border-radius:5px;box-shadow:0 1px 5px rgba(0,0,0,0.4)}.leaflet-control-measure .leaflet-control-measure-toggle,.leaflet-control-measure .leaflet-control-measure-toggle:hover{display:block;width:36px;height:36px;background-position:50% 50%;background-repeat:no-repeat;background-image:url(images/rulers.png);border-radius:5px;text-indent:100%;white-space:nowrap;overflow:hidden}.leaflet-retina .leaflet-control-measure .leaflet-control-measure-toggle,.leaflet-retina .leaflet-control-measure .leaflet-control-measure-toggle:hover{background-image:url(images/rulers_@2X.png);background-size:16px 16px}.leaflet-touch .leaflet-control-measure .leaflet-control-measure-toggle,.leaflet-touch .leaflet-control-measure .leaflet-control-measure-toggle:hover{width:44px;height:44px}.leaflet-control-measure .startprompt h3{margin-bottom:10px}.leaflet-control-measure .startprompt .tasks{margin-top:0;padding-top:0;border-top:0}.leaflet-control-measure .leaflet-control-measure-interaction{padding:10px 12px}.leaflet-control-measure .results .group{margin-top:10px;padding-top:10px;border-top:dotted 1px #eaeaea}.leaflet-control-measure .results .group:first-child{margin-top:0;padding-top:0;border-top:0}.leaflet-control-measure .results .heading{margin-right:5px;color:#999}.leaflet-control-measure a.start{padding-left:18px;background-repeat:no-repeat;background-position:0% 50%;background-image:url(images/start.png)}.leaflet-retina .leaflet-control-measure a.start{background-image:url(images/start_@2X.png);background-size:12px 12px}.leaflet-control-measure a.cancel{padding-left:18px;background-repeat:no-repeat;background-position:0% 50%;background-image:url(images/cancel.png)}.leaflet-retina .leaflet-control-measure a.cancel{background-image:url(images/cancel_@2X.png);background-size:12px 12px}.leaflet-control-measure a.finish{padding-left:18px;background-repeat:no-repeat;background-position:0% 50%;background-image:url(images/check.png)}.leaflet-retina .leaflet-control-measure a.finish{background-image:url(images/check_@2X.png);background-size:12px 12px}.leaflet-measure-resultpopup a.zoomto{padding-left:18px;background-repeat:no-repeat;background-position:0% 50%;background-image:url(images/focus.png)}.leaflet-retina .leaflet-measure-resultpopup a.zoomto{background-image:url(images/focus_@2X.png);background-size:12px 12px}.leaflet-measure-resultpopup a.deletemarkup{padding-left:18px;background-repeat:no-repeat;background-position:0% 50%;background-image:url(images/trash.png)}.leaflet-retina .leaflet-measure-resultpopup a.deletemarkup{background-image:url(images/trash_@2X.png);background-size:11px 12px}

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -1,11 +1,6 @@
PluginsAPI.Map.willAddControls([
'measure/leaflet-measure.css',
'measure/leaflet-measure.min.js'
], function(options){
L.control.measure({
primaryLengthUnit: 'meters',
secondaryLengthUnit: 'feet',
primaryAreaUnit: 'sqmeters',
secondaryAreaUnit: 'acres'
}).addTo(options.map);
'measure/build/app.js',
'measure/build/app.css'
], function(options, App){
new App(options.map);
});

Wyświetl plik

@ -0,0 +1,14 @@
{
"name": "measure",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"leaflet-measure-ex": "^3.0.4"
}
}

Wyświetl plik

@ -0,0 +1,76 @@
// Magic to include node_modules of root WebODM's directory
process.env.NODE_PATH = "../../../node_modules";
require("module").Module._initPaths();
let path = require("path");
let webpack = require('webpack');
let ExtractTextPlugin = require('extract-text-webpack-plugin');
let LiveReloadPlugin = require('webpack-livereload-plugin');
module.exports = {
context: __dirname,
entry: {
app: ['./app.jsx']
},
output: {
path: path.join(__dirname, './build'),
filename: "[name].js",
libraryTarget: "amd"
},
plugins: [
new LiveReloadPlugin(),
new ExtractTextPlugin('[name].css', {
allChunks: true
})
],
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: 'babel-loader',
query: {
"plugins": [
'syntax-class-properties',
'transform-class-properties'
],
presets: ['es2015', 'react']
}
}
],
},
{
test: /\.s?css$/,
use: ExtractTextPlugin.extract({
use: 'css-loader!sass-loader'
})
},
{
test: /\.(png|jpg|jpeg|svg)/,
loader: "url-loader?limit=100000"
}
]
},
resolve: {
modules: ['node_modules', 'bower_components'],
extensions: ['.js', '.jsx']
},
externals: {
// require("jquery") is external and available
// on the global let jQuery
"jquery": "jQuery",
"SystemJS": "SystemJS",
"PluginsAPI": "PluginsAPI",
"leaflet": "leaflet",
"ReactDOM": "ReactDOM",
"React": "React"
}
}

Wyświetl plik

@ -6,7 +6,7 @@ class Plugin(PluginBase):
def main_menu(self):
return [Menu("GCP Interface", self.public_url(""), "fa fa-map-marker fa-fw")]
def mount_points(self):
def app_mount_points(self):
return [
MountPoint('$', lambda request: render(request, self.template_path("app.html"), {'title': 'GCP Editor'}))
]

Wyświetl plik

@ -12,7 +12,7 @@ class Plugin(PluginBase):
def include_css_files(self):
return ['test.css']
def mount_points(self):
def app_mount_points(self):
return [
MountPoint('/app_mountpoint/$', lambda request: render(request, self.template_path("app.html"), {'title': 'Test'}))
]

Wyświetl plik

@ -0,0 +1,14 @@
{
"name": "public",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"pad-left": "^2.1.0"
}
}

Wyświetl plik

@ -1 +0,0 @@
from .plugin import *

Wyświetl plik

@ -1,13 +0,0 @@
{
"name": "Volume Measurements",
"webodmMinVersion": "0.5.0",
"description": "A plugin to compute volume measurements from a DSM",
"version": "0.1.0",
"author": "Piero Toffanin",
"email": "pt@masseranolabs.com",
"repository": "https://github.com/OpenDroneMap/WebODM",
"tags": ["volume", "measurements"],
"homepage": "https://github.com/OpenDroneMap/WebODM",
"experimental": true,
"deprecated": false
}

Wyświetl plik

@ -1,5 +0,0 @@
from app.plugins import PluginBase
class Plugin(PluginBase):
def include_js_files(self):
return ['hello.js']

Wyświetl plik

@ -1,6 +0,0 @@
PluginsAPI.Map.willAddControls(function(options){
console.log("GOT: ", options);
});
PluginsAPI.Map.didAddControls(function(options){
console.log("GOT2: ", options);
});

Wyświetl plik

@ -21,6 +21,7 @@ djangorestframework-jwt==1.9.0
drf-nested-routers==0.11.1
funcsigs==1.0.2
futures==3.0.5
geojson==2.3.0
gunicorn==19.7.1
itypes==1.1.0
kombu==4.1.0

Wyświetl plik

@ -249,6 +249,7 @@ CORS_ORIGIN_ALLOW_ALL = True
# File uploads
MEDIA_ROOT = os.path.join(BASE_DIR, 'app', 'media')
MEDIA_TMP = os.path.join(MEDIA_ROOT, 'tmp')
# Store flash messages in cookies
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'

Wyświetl plik

@ -73,6 +73,7 @@ module.exports = {
// require("jquery") is external and available
// on the global let jQuery
"jquery": "jQuery",
"SystemJS": "SystemJS"
"SystemJS": "SystemJS",
"React": "React"
}
}

Wyświetl plik

@ -8,6 +8,7 @@ from django.db.models import Q
from app.models import Project
from app.models import Task
from app.plugins.grass_engine import grass, GrassEngineException
from nodeodm import status_codes
from nodeodm.models import ProcessingNode
from webodm import settings
@ -77,3 +78,12 @@ def process_pending_tasks():
for task in tasks:
process_task.delay(task.id)
@app.task
def execute_grass_script(script, serialized_context = {}):
try:
ctx = grass.create_context(serialized_context)
return ctx.execute(script)
except GrassEngineException as e:
return {'error': str(e)}