|
@ -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
|
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
|
# 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
|
# Install pip reqs
|
||||||
|
|
|
@ -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] 2D Map Display
|
||||||
- [X] 3D Model Display
|
- [X] 3D Model Display
|
||||||
- [ ] NDVI display
|
- [ ] NDVI display
|
||||||
- [ ] Volumetric Measurements
|
- [X] Volumetric Measurements
|
||||||
- [X] Cluster management and setup.
|
- [X] Cluster management and setup.
|
||||||
- [ ] Mission Planner
|
- [ ] Mission Planner
|
||||||
- [X] Plugins/Webhooks System
|
- [X] Plugins/Webhooks System
|
||||||
|
|
|
@ -21,7 +21,6 @@ class TaskIDsSerializer(serializers.BaseSerializer):
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
return obj.id
|
return obj.id
|
||||||
|
|
||||||
|
|
||||||
class TaskSerializer(serializers.ModelSerializer):
|
class TaskSerializer(serializers.ModelSerializer):
|
||||||
project = serializers.PrimaryKeyRelatedField(queryset=models.Project.objects.all())
|
project = serializers.PrimaryKeyRelatedField(queryset=models.Project.objects.all())
|
||||||
processing_node = serializers.PrimaryKeyRelatedField(queryset=ProcessingNode.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', )
|
queryset = models.Task.objects.all().defer('orthophoto_extent', 'dtm_extent', 'dsm_extent', 'console_output', )
|
||||||
permission_classes = (IsAuthenticatedOrReadOnly, )
|
permission_classes = (IsAuthenticatedOrReadOnly, )
|
||||||
|
|
||||||
def get_and_check_task(self, request, pk, project_pk, annotate={}):
|
def get_and_check_task(self, request, pk, annotate={}):
|
||||||
try:
|
try:
|
||||||
task = self.queryset.annotate(**annotate).get(pk=pk, project=project_pk)
|
task = self.queryset.annotate(**annotate).get(pk=pk)
|
||||||
except (ObjectDoesNotExist, ValidationError):
|
except (ObjectDoesNotExist, ValidationError):
|
||||||
raise exceptions.NotFound()
|
raise exceptions.NotFound()
|
||||||
|
|
||||||
# Check for permissions, unless the task is public
|
# Check for permissions, unless the task is public
|
||||||
if not task.public:
|
if not task.public:
|
||||||
get_and_check_project(request, project_pk)
|
get_and_check_project(request, task.project.id)
|
||||||
|
|
||||||
return task
|
return task
|
||||||
|
|
||||||
|
@ -211,7 +210,7 @@ class TaskTiles(TaskNestedView):
|
||||||
"""
|
"""
|
||||||
Get a tile image
|
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)
|
tile_path = task.get_tile_path(tile_type, z, x, y)
|
||||||
if os.path.isfile(tile_path):
|
if os.path.isfile(tile_path):
|
||||||
tile = open(tile_path, "rb")
|
tile = open(tile_path, "rb")
|
||||||
|
@ -225,7 +224,7 @@ class TaskTilesJson(TaskNestedView):
|
||||||
"""
|
"""
|
||||||
Get tile.json for this tasks's asset type
|
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 = {
|
extent_map = {
|
||||||
'orthophoto': task.orthophoto_extent,
|
'orthophoto': task.orthophoto_extent,
|
||||||
|
@ -256,7 +255,7 @@ class TaskDownloads(TaskNestedView):
|
||||||
"""
|
"""
|
||||||
Downloads a task asset (if available)
|
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
|
# Check and download
|
||||||
try:
|
try:
|
||||||
|
@ -284,7 +283,7 @@ class TaskAssets(TaskNestedView):
|
||||||
"""
|
"""
|
||||||
Downloads a task asset (if available)
|
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
|
# Check for directory traversal attacks
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
|
||||||
from app.api.presets import PresetViewSet
|
from app.api.presets import PresetViewSet
|
||||||
|
from app.plugins import get_api_url_patterns
|
||||||
from .projects import ProjectViewSet
|
from .projects import ProjectViewSet
|
||||||
from .tasks import TaskViewSet, TaskTiles, TaskTilesJson, TaskDownloads, TaskAssets
|
from .tasks import TaskViewSet, TaskTiles, TaskTilesJson, TaskDownloads, TaskAssets
|
||||||
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
|
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
|
||||||
|
@ -31,4 +32,6 @@ urlpatterns = [
|
||||||
|
|
||||||
url(r'^auth/', include('rest_framework.urls')),
|
url(r'^auth/', include('rest_framework.urls')),
|
||||||
url(r'^token-auth/', obtain_jwt_token),
|
url(r'^token-auth/', obtain_jwt_token),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
urlpatterns += get_api_url_patterns()
|
46
app/boot.py
|
@ -3,7 +3,7 @@ import os
|
||||||
import kombu
|
import kombu
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User, Group
|
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.core.files import File
|
||||||
from django.db.utils import ProgrammingError
|
from django.db.utils import ProgrammingError
|
||||||
from guardian.shortcuts import assign_perm
|
from guardian.shortcuts import assign_perm
|
||||||
|
@ -35,6 +35,10 @@ def boot():
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
logger.warning("Debug mode is ON (for development this is OK)")
|
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
|
# Check default group
|
||||||
try:
|
try:
|
||||||
default_group, created = Group.objects.get_or_create(name='Default')
|
default_group, created = Group.objects.get_or_create(name='Default')
|
||||||
|
@ -60,18 +64,7 @@ def boot():
|
||||||
# Add permission to view processing nodes
|
# Add permission to view processing nodes
|
||||||
default_group.permissions.add(Permission.objects.get(codename="view_processingnode"))
|
default_group.permissions.add(Permission.objects.get(codename="view_processingnode"))
|
||||||
|
|
||||||
# Add default presets
|
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 settings
|
# Add settings
|
||||||
default_theme, created = Theme.objects.get_or_create(name='Default')
|
default_theme, created = Theme.objects.get_or_create(name='Default')
|
||||||
|
@ -101,4 +94,29 @@ def boot():
|
||||||
|
|
||||||
|
|
||||||
except ProgrammingError:
|
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()
|
||||||
|
|
|
@ -578,7 +578,7 @@ class Task(models.Model):
|
||||||
self.status = status_codes.FAILED
|
self.status = status_codes.FAILED
|
||||||
self.pending_action = None
|
self.pending_action = None
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def find_all_files_matching(self, regex):
|
def find_all_files_matching(self, regex):
|
||||||
directory = full_task_directory_path(self.id, self.project.id)
|
directory = full_task_directory_path(self.id, self.project.id)
|
||||||
return [os.path.join(directory, f) for f in os.listdir(directory) if
|
return [os.path.join(directory, f) for f in os.listdir(directory) if
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import importlib
|
import importlib
|
||||||
|
import subprocess
|
||||||
|
|
||||||
import django
|
import django
|
||||||
import json
|
import json
|
||||||
|
@ -13,30 +14,57 @@ logger = logging.getLogger('app.logger')
|
||||||
|
|
||||||
def register_plugins():
|
def register_plugins():
|
||||||
for plugin in get_active_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()
|
plugin.register()
|
||||||
logger.info("Registered {}".format(plugin))
|
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 = []
|
url_patterns = []
|
||||||
for plugin in get_active_plugins():
|
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),
|
url_patterns.append(url('^plugins/{}/{}'.format(plugin.get_name(), mount_point.url),
|
||||||
mount_point.view,
|
mount_point.view,
|
||||||
*mount_point.args,
|
*mount_point.args,
|
||||||
**mount_point.kwargs))
|
**mount_point.kwargs))
|
||||||
|
|
||||||
if plugin.has_public_path():
|
if plugin.path_exists("public"):
|
||||||
url_patterns.append(url('^plugins/{}/(.*)'.format(plugin.get_name()),
|
url_patterns.append(url('^plugins/{}/(.*)'.format(plugin.get_name()),
|
||||||
django.views.static.serve,
|
django.views.static.serve,
|
||||||
{'document_root': plugin.get_path("public")}))
|
{'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
|
return url_patterns
|
||||||
|
|
||||||
|
|
||||||
plugins = None
|
plugins = None
|
||||||
def get_active_plugins():
|
def get_active_plugins():
|
||||||
# Cache plugins search
|
# Cache plugins search
|
||||||
|
@ -86,6 +114,12 @@ def get_active_plugins():
|
||||||
return 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():
|
def get_plugins_path():
|
||||||
current_path = os.path.dirname(os.path.realpath(__file__))
|
current_path = os.path.dirname(os.path.realpath(__file__))
|
||||||
return os.path.abspath(os.path.join(current_path, "..", "..", "plugins"))
|
return os.path.abspath(os.path.join(current_path, "..", "..", "plugins"))
|
||||||
|
|
|
@ -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()
|
|
@ -5,7 +5,7 @@ class MountPoint:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
:param url: path to mount this view to, relative to plugins directory
|
: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 args: extra args to pass to url() call
|
||||||
:param kwargs: extra kwargs to pass to url() call
|
:param kwargs: extra kwargs to pass to url() call
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -46,8 +46,8 @@ class PluginBase(ABC):
|
||||||
"""
|
"""
|
||||||
return "plugins/{}/templates/{}".format(self.get_name(), path)
|
return "plugins/{}/templates/{}".format(self.get_name(), path)
|
||||||
|
|
||||||
def has_public_path(self):
|
def path_exists(self, path):
|
||||||
return os.path.isdir(self.get_path("public"))
|
return os.path.exists(self.get_path(path))
|
||||||
|
|
||||||
def include_js_files(self):
|
def include_js_files(self):
|
||||||
"""
|
"""
|
||||||
|
@ -73,7 +73,7 @@ class PluginBase(ABC):
|
||||||
"""
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def mount_points(self):
|
def app_mount_points(self):
|
||||||
"""
|
"""
|
||||||
Should be overriden by plugins that want to connect
|
Should be overriden by plugins that want to connect
|
||||||
custom Django views
|
custom Django views
|
||||||
|
@ -81,5 +81,13 @@ class PluginBase(ABC):
|
||||||
"""
|
"""
|
||||||
return []
|
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):
|
def __str__(self):
|
||||||
return "[{}]".format(self.get_module_name())
|
return "[{}]".format(self.get_module_name())
|
|
@ -200,7 +200,7 @@ pre.prettyprint,
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Failed */
|
/* Failed */
|
||||||
.task-list-item .status-label.error{
|
.task-list-item .status-label.error, .theme-background-failed{
|
||||||
background-color: theme("failed");
|
background-color: theme("failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,17 @@ if (!window.PluginsAPI){
|
||||||
SystemJS.config({
|
SystemJS.config({
|
||||||
baseURL: '/plugins',
|
baseURL: '/plugins',
|
||||||
map: {
|
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: {
|
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' }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -41,11 +41,16 @@ export default class ApiFactory{
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = {};
|
let obj = {};
|
||||||
api.endpoints.forEach(endpoint => {
|
api.endpoints.forEach(endpoint => {
|
||||||
if (!Array.isArray(endpoint)) endpoint = [endpoint];
|
if (!Array.isArray(endpoint)) endpoint = [endpoint];
|
||||||
addEndpoint(obj, ...endpoint);
|
addEndpoint(obj, ...endpoint);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (api.helpers){
|
||||||
|
obj = Object.assign(obj, api.helpers);
|
||||||
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Utils from '../Utils';
|
import Utils from '../Utils';
|
||||||
|
import L from 'leaflet';
|
||||||
|
|
||||||
const { assert } = Utils;
|
const { assert } = Utils;
|
||||||
|
|
||||||
|
|
|
@ -268,7 +268,7 @@ class Map extends React.Component {
|
||||||
|
|
||||||
handleMapMouseDown(e){
|
handleMapMouseDown(e){
|
||||||
// Make sure the share popup closes
|
// Make sure the share popup closes
|
||||||
this.shareButton.hidePopup();
|
if (this.sharePopup) this.shareButton.hidePopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -16,10 +16,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaflet-right .leaflet-control,
|
||||||
.leaflet-control-measure.leaflet-control{
|
.leaflet-control-measure.leaflet-control{
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle{
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-opacity-slider{
|
.popup-opacity-slider{
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import '../css/main.scss';
|
import '../css/main.scss';
|
||||||
import './django/csrf';
|
import './django/csrf';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import React from 'react';
|
||||||
import PluginsAPI from './classes/plugins/API';
|
import PluginsAPI from './classes/plugins/API';
|
||||||
|
|
||||||
// Main is always executed first in the page
|
// 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)
|
// as a dependency in each component (adds too much space overhead)
|
||||||
window.ReactDOM = ReactDOM;
|
window.ReactDOM = ReactDOM;
|
||||||
|
window.React = React;
|
||||||
|
|
||||||
|
|
|
@ -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];
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
|
import os
|
||||||
|
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
from app.plugins import get_plugin_by_name
|
||||||
from .classes import BootTestCase
|
from .classes import BootTestCase
|
||||||
|
|
||||||
class TestPlugins(BootTestCase):
|
class TestPlugins(BootTestCase):
|
||||||
|
@ -37,6 +40,10 @@ class TestPlugins(BootTestCase):
|
||||||
# And our menu entry
|
# And our menu entry
|
||||||
self.assertContains(res, '<li><a href="/plugins/test/menu_url/"><i class="test-icon"></i> Test</a></li>', html=True)
|
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:
|
# TODO:
|
||||||
# test API endpoints
|
# test GRASS engine
|
||||||
# test python hooks
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.shortcuts import render_to_response
|
||||||
from django.template import RequestContext
|
from django.template import RequestContext
|
||||||
|
|
||||||
from .views import app as app_views, public as public_views
|
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 app.boot import boot
|
||||||
from webodm import settings
|
from webodm import settings
|
||||||
|
@ -30,7 +30,7 @@ urlpatterns = [
|
||||||
|
|
||||||
# TODO: is there a way to place plugins /public directories
|
# TODO: is there a way to place plugins /public directories
|
||||||
# into the static build directories and let nginx serve them?
|
# into the static build directories and let nginx serve them?
|
||||||
urlpatterns += get_url_patterns()
|
urlpatterns += get_app_url_patterns()
|
||||||
|
|
||||||
handler404 = app_views.handler404
|
handler404 = app_views.handler404
|
||||||
handler500 = app_views.handler500
|
handler500 = app_views.handler500
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "WebODM",
|
"name": "WebODM",
|
||||||
"version": "0.5.0",
|
"version": "0.5.1",
|
||||||
"description": "Open Source Drone Image Processing",
|
"description": "Open Source Drone Image Processing",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
"immutability-helper": "^2.0.0",
|
"immutability-helper": "^2.0.0",
|
||||||
"jest": "^21.0.1",
|
"jest": "^21.0.1",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"leaflet": "^1.0.1",
|
"leaflet": "^1.3.1",
|
||||||
"node-sass": "^3.10.1",
|
"node-sass": "^3.10.1",
|
||||||
"object.values": "^1.0.3",
|
"object.values": "^1.0.3",
|
||||||
"proj4": "^2.4.3",
|
"proj4": "^2.4.3",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "Area/Length Measurements",
|
"name": "Volume/Area/Length Measurements",
|
||||||
"webodmMinVersion": "0.5.0",
|
"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",
|
"version": "0.1.0",
|
||||||
"author": "Piero Toffanin",
|
"author": "Abdelkoddouss Izem, Piero Toffanin",
|
||||||
"email": "pt@masseranolabs.com",
|
"email": "pt@masseranolabs.com",
|
||||||
"repository": "https://github.com/OpenDroneMap/WebODM",
|
"repository": "https://github.com/OpenDroneMap/WebODM",
|
||||||
"tags": ["area", "length", "measurements"],
|
"tags": ["volume", "area", "length", "measurements"],
|
||||||
"homepage": "https://github.com/OpenDroneMap/WebODM",
|
"homepage": "https://github.com/OpenDroneMap/WebODM",
|
||||||
"experimental": false,
|
"experimental": true,
|
||||||
"deprecated": false
|
"deprecated": false
|
||||||
}
|
}
|
|
@ -1,5 +1,12 @@
|
||||||
|
from app.plugins import MountPoint
|
||||||
from app.plugins import PluginBase
|
from app.plugins import PluginBase
|
||||||
|
from .api import TaskVolume
|
||||||
|
|
||||||
class Plugin(PluginBase):
|
class Plugin(PluginBase):
|
||||||
def include_js_files(self):
|
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())
|
||||||
|
]
|
||||||
|
|
|
@ -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>);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
.plugin-measure.popup{
|
||||||
|
p{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error.long{
|
||||||
|
overflow: scroll;
|
||||||
|
display: block;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Przed Szerokość: | Wysokość: | Rozmiar: 397 B |
Przed Szerokość: | Wysokość: | Rozmiar: 762 B |
Przed Szerokość: | Wysokość: | Rozmiar: 387 B |
Przed Szerokość: | Wysokość: | Rozmiar: 692 B |
Przed Szerokość: | Wysokość: | Rozmiar: 326 B |
Przed Szerokość: | Wysokość: | Rozmiar: 462 B |
Przed Szerokość: | Wysokość: | Rozmiar: 192 B |
Przed Szerokość: | Wysokość: | Rozmiar: 277 B |
Przed Szerokość: | Wysokość: | Rozmiar: 491 B |
Przed Szerokość: | Wysokość: | Rozmiar: 1003 B |
Przed Szerokość: | Wysokość: | Rozmiar: 279 B |
Przed Szerokość: | Wysokość: | Rozmiar: 460 B |
|
@ -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}
|
|
|
@ -1,11 +1,6 @@
|
||||||
PluginsAPI.Map.willAddControls([
|
PluginsAPI.Map.willAddControls([
|
||||||
'measure/leaflet-measure.css',
|
'measure/build/app.js',
|
||||||
'measure/leaflet-measure.min.js'
|
'measure/build/app.css'
|
||||||
], function(options){
|
], function(options, App){
|
||||||
L.control.measure({
|
new App(options.map);
|
||||||
primaryLengthUnit: 'meters',
|
|
||||||
secondaryLengthUnit: 'feet',
|
|
||||||
primaryAreaUnit: 'sqmeters',
|
|
||||||
secondaryAreaUnit: 'acres'
|
|
||||||
}).addTo(options.map);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ class Plugin(PluginBase):
|
||||||
def main_menu(self):
|
def main_menu(self):
|
||||||
return [Menu("GCP Interface", self.public_url(""), "fa fa-map-marker fa-fw")]
|
return [Menu("GCP Interface", self.public_url(""), "fa fa-map-marker fa-fw")]
|
||||||
|
|
||||||
def mount_points(self):
|
def app_mount_points(self):
|
||||||
return [
|
return [
|
||||||
MountPoint('$', lambda request: render(request, self.template_path("app.html"), {'title': 'GCP Editor'}))
|
MountPoint('$', lambda request: render(request, self.template_path("app.html"), {'title': 'GCP Editor'}))
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,7 +12,7 @@ class Plugin(PluginBase):
|
||||||
def include_css_files(self):
|
def include_css_files(self):
|
||||||
return ['test.css']
|
return ['test.css']
|
||||||
|
|
||||||
def mount_points(self):
|
def app_mount_points(self):
|
||||||
return [
|
return [
|
||||||
MountPoint('/app_mountpoint/$', lambda request: render(request, self.template_path("app.html"), {'title': 'Test'}))
|
MountPoint('/app_mountpoint/$', lambda request: render(request, self.template_path("app.html"), {'title': 'Test'}))
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
from .plugin import *
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
from app.plugins import PluginBase
|
|
||||||
|
|
||||||
class Plugin(PluginBase):
|
|
||||||
def include_js_files(self):
|
|
||||||
return ['hello.js']
|
|
|
@ -1,6 +0,0 @@
|
||||||
PluginsAPI.Map.willAddControls(function(options){
|
|
||||||
console.log("GOT: ", options);
|
|
||||||
});
|
|
||||||
PluginsAPI.Map.didAddControls(function(options){
|
|
||||||
console.log("GOT2: ", options);
|
|
||||||
});
|
|
|
@ -21,6 +21,7 @@ djangorestframework-jwt==1.9.0
|
||||||
drf-nested-routers==0.11.1
|
drf-nested-routers==0.11.1
|
||||||
funcsigs==1.0.2
|
funcsigs==1.0.2
|
||||||
futures==3.0.5
|
futures==3.0.5
|
||||||
|
geojson==2.3.0
|
||||||
gunicorn==19.7.1
|
gunicorn==19.7.1
|
||||||
itypes==1.1.0
|
itypes==1.1.0
|
||||||
kombu==4.1.0
|
kombu==4.1.0
|
||||||
|
|
|
@ -249,6 +249,7 @@ CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
|
||||||
# File uploads
|
# File uploads
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'app', 'media')
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'app', 'media')
|
||||||
|
MEDIA_TMP = os.path.join(MEDIA_ROOT, 'tmp')
|
||||||
|
|
||||||
# Store flash messages in cookies
|
# Store flash messages in cookies
|
||||||
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
|
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
|
||||||
|
|
|
@ -73,6 +73,7 @@ module.exports = {
|
||||||
// require("jquery") is external and available
|
// require("jquery") is external and available
|
||||||
// on the global let jQuery
|
// on the global let jQuery
|
||||||
"jquery": "jQuery",
|
"jquery": "jQuery",
|
||||||
"SystemJS": "SystemJS"
|
"SystemJS": "SystemJS",
|
||||||
|
"React": "React"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,6 +8,7 @@ from django.db.models import Q
|
||||||
|
|
||||||
from app.models import Project
|
from app.models import Project
|
||||||
from app.models import Task
|
from app.models import Task
|
||||||
|
from app.plugins.grass_engine import grass, GrassEngineException
|
||||||
from nodeodm import status_codes
|
from nodeodm import status_codes
|
||||||
from nodeodm.models import ProcessingNode
|
from nodeodm.models import ProcessingNode
|
||||||
from webodm import settings
|
from webodm import settings
|
||||||
|
@ -77,3 +78,12 @@ def process_pending_tasks():
|
||||||
|
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
process_task.delay(task.id)
|
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)}
|