kopia lustrzana https://github.com/OpenDroneMap/WebODM
Support for requirements.txt, tweaks to elevationmap plugin
rodzic
f55c112d67
commit
11e3e42b06
19
app/admin.py
19
app/admin.py
|
|
@ -1,5 +1,6 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
|
|
@ -8,7 +9,7 @@ from guardian.admin import GuardedModelAdmin
|
|||
from app.models import PluginDatum
|
||||
from app.models import Preset
|
||||
from app.models import Plugin
|
||||
from app.plugins import get_plugin_by_name
|
||||
from app.plugins import get_plugin_by_name, enable_plugin, disable_plugin
|
||||
from .models import Project, Task, ImageUpload, Setting, Theme
|
||||
from django import forms
|
||||
from codemirror2.widgets import CodeMirrorEditor
|
||||
|
|
@ -108,15 +109,19 @@ class PluginAdmin(admin.ModelAdmin):
|
|||
return custom_urls + urls
|
||||
|
||||
def plugin_enable(self, request, plugin_name, *args, **kwargs):
|
||||
p = Plugin.objects.get(pk=plugin_name)
|
||||
p.enabled = True
|
||||
p.save()
|
||||
try:
|
||||
enable_plugin(plugin_name)
|
||||
except Exception as e:
|
||||
messages.warning(request, "Cannot enable plugin {}: {}".format(plugin_name, str(e)))
|
||||
|
||||
return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
|
||||
|
||||
def plugin_disable(self, request, plugin_name, *args, **kwargs):
|
||||
p = Plugin.objects.get(pk=plugin_name)
|
||||
p.enabled = False
|
||||
p.save()
|
||||
try:
|
||||
disable_plugin(plugin_name)
|
||||
except Exception as e:
|
||||
messages.warning(request, "Cannot disable plugin {}: {}".format(plugin_name, str(e)))
|
||||
|
||||
return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
|
||||
|
||||
def plugin_actions(self, obj):
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class Plugin(models.Model):
|
||||
name = models.CharField(max_length=255, primary_key=True, blank=False, null=False, help_text="Plugin name")
|
||||
enabled = models.BooleanField(db_index=True, default=True, help_text="Whether this plugin is enabled.")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return self.name
|
||||
|
||||
def enable(self):
|
||||
self.enabled = True
|
||||
self.save()
|
||||
|
||||
def disable(self):
|
||||
self.enabled = False
|
||||
self.save()
|
||||
|
|
@ -3,5 +3,3 @@ from .plugin_base import PluginBase
|
|||
from .menu import Menu
|
||||
from .mount_point import MountPoint
|
||||
from .functions import *
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import logging
|
||||
import importlib
|
||||
import subprocess
|
||||
import traceback
|
||||
|
||||
import django
|
||||
import json
|
||||
|
|
@ -18,6 +19,10 @@ from webodm import settings
|
|||
logger = logging.getLogger('app.logger')
|
||||
|
||||
def init_plugins():
|
||||
# Make sure app/media/plugins exists
|
||||
if not os.path.exists(get_plugins_persistent_path()):
|
||||
os.mkdir(get_plugins_persistent_path())
|
||||
|
||||
build_plugins()
|
||||
sync_plugin_db()
|
||||
register_plugins()
|
||||
|
|
@ -105,8 +110,12 @@ def build_plugins():
|
|||
|
||||
def register_plugins():
|
||||
for plugin in get_active_plugins():
|
||||
plugin.register()
|
||||
logger.info("Registered {}".format(plugin))
|
||||
try:
|
||||
plugin.register()
|
||||
logger.info("Registered {}".format(plugin))
|
||||
except Exception as e:
|
||||
disable_plugin(plugin.get_name())
|
||||
logger.warning("Cannot register {}: {}".format(plugin, str(e)))
|
||||
|
||||
|
||||
def get_app_url_patterns():
|
||||
|
|
@ -115,7 +124,7 @@ def get_app_url_patterns():
|
|||
each mount point
|
||||
"""
|
||||
url_patterns = []
|
||||
for plugin in get_active_plugins():
|
||||
for plugin in get_plugins():
|
||||
for mount_point in plugin.app_mount_points():
|
||||
url_patterns.append(url('^plugins/{}/{}'.format(plugin.get_name(), mount_point.url),
|
||||
mount_point.view,
|
||||
|
|
@ -134,7 +143,7 @@ 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 plugin in get_plugins():
|
||||
for mount_point in plugin.api_mount_points():
|
||||
url_patterns.append(url('^plugins/{}/{}'.format(plugin.get_name(), mount_point.url),
|
||||
mount_point.view,
|
||||
|
|
@ -228,10 +237,28 @@ def get_plugin_by_name(name, only_active=True, refresh_cache_if_none=False):
|
|||
else:
|
||||
return res
|
||||
|
||||
def get_current_plugin():
|
||||
"""
|
||||
When called from a python module inside a plugin's directory,
|
||||
it returns the plugin that this python module belongs to
|
||||
:return: Plugin instance
|
||||
"""
|
||||
caller_filename = traceback.extract_stack()[-2][0]
|
||||
|
||||
relp = os.path.relpath(caller_filename, get_plugins_path())
|
||||
parts = relp.split(os.sep)
|
||||
if len(parts) > 0:
|
||||
plugin_name = parts[0]
|
||||
return get_plugin_by_name(plugin_name, only_active=False, refresh_cache_if_none=True)
|
||||
|
||||
return None
|
||||
|
||||
def get_plugins_path():
|
||||
current_path = os.path.dirname(os.path.realpath(__file__))
|
||||
return os.path.abspath(os.path.join(current_path, "..", "..", "plugins"))
|
||||
|
||||
def get_plugins_persistent_path(*paths):
|
||||
return os.path.join(settings.MEDIA_ROOT, "plugins", *paths)
|
||||
|
||||
def get_dynamic_script_handler(script_path, callback=None, **kwargs):
|
||||
def handleRequest(request):
|
||||
|
|
@ -251,11 +278,17 @@ def get_dynamic_script_handler(script_path, callback=None, **kwargs):
|
|||
|
||||
return handleRequest
|
||||
|
||||
def enable_plugin(plugin_name):
|
||||
p = get_plugin_by_name(plugin_name, only_active=False)
|
||||
p.register()
|
||||
Plugin.objects.get(pk=plugin_name).enable()
|
||||
|
||||
def disable_plugin(plugin_name):
|
||||
Plugin.objects.get(pk=plugin_name).disable()
|
||||
|
||||
def get_site_settings():
|
||||
return Setting.objects.first()
|
||||
|
||||
|
||||
def versionToInt(version):
|
||||
"""
|
||||
Converts a WebODM version string (major.minor.build) to a integer value
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import importlib
|
||||
import json
|
||||
import logging, os, sys
|
||||
import logging, os, sys, subprocess
|
||||
from abc import ABC
|
||||
from app.plugins import UserDataStore, GlobalDataStore
|
||||
from app.plugins.functions import get_plugins_persistent_path
|
||||
from contextlib import contextmanager
|
||||
|
||||
from app.plugins.pyutils import requirements_installed, compute_file_md5
|
||||
|
||||
logger = logging.getLogger('app.logger')
|
||||
|
||||
|
|
@ -11,7 +16,64 @@ class PluginBase(ABC):
|
|||
self.manifest = None
|
||||
|
||||
def register(self):
|
||||
pass
|
||||
self.check_requirements()
|
||||
|
||||
|
||||
def check_requirements(self):
|
||||
"""
|
||||
Check if Python requirements need to be installed
|
||||
"""
|
||||
req_file = self.get_path("requirements.txt")
|
||||
if os.path.exists(req_file):
|
||||
reqs_installed = requirements_installed(req_file, self.get_python_packages_path())
|
||||
|
||||
md5_file = self.get_python_packages_path("install_md5")
|
||||
md5_mismatch = False
|
||||
req_md5 = compute_file_md5(req_file)
|
||||
|
||||
if os.path.exists(md5_file):
|
||||
with open(md5_file, 'r') as f:
|
||||
md5_mismatch = f.read().strip() != req_md5
|
||||
|
||||
|
||||
if not reqs_installed or md5_mismatch:
|
||||
logger.info("Installing requirements.txt for {}".format(self))
|
||||
|
||||
if not os.path.exists(self.get_python_packages_path()):
|
||||
os.makedirs(self.get_python_packages_path(), exist_ok=True)
|
||||
|
||||
p = subprocess.Popen(['pip', 'install', '-U', '-r', 'requirements.txt',
|
||||
'--target', self.get_python_packages_path()],
|
||||
cwd=self.get_path())
|
||||
p.wait()
|
||||
|
||||
# Verify
|
||||
if requirements_installed(self.get_path("requirements.txt"), self.get_python_packages_path()):
|
||||
logger.info("Installed requirements.txt for {}".format(self))
|
||||
|
||||
# Write MD5
|
||||
if req_md5:
|
||||
with open(md5_file, 'w') as f:
|
||||
f.write(req_md5)
|
||||
else:
|
||||
logger.warning("Failed to install requirements.txt for {}".format(self))
|
||||
|
||||
def get_persistent_path(self, *paths):
|
||||
return get_plugins_persistent_path(self.name, *paths)
|
||||
|
||||
def get_python_packages_path(self, *paths):
|
||||
return self.get_persistent_path("site-packages", *paths)
|
||||
|
||||
@contextmanager
|
||||
def python_imports(self):
|
||||
# Add python path
|
||||
sys.path.insert(0, self.get_python_packages_path())
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# Remove python path
|
||||
sys.path.remove(self.get_python_packages_path())
|
||||
|
||||
|
||||
def get_path(self, *paths):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import hashlib
|
||||
|
||||
def parse_requirements(requirements_file):
|
||||
"""
|
||||
Parse a requirements.txt file
|
||||
:param requirements_file: path to requirements.txt file
|
||||
:return: package names
|
||||
"""
|
||||
if os.path.exists(requirements_file):
|
||||
with open(requirements_file, 'r') as f:
|
||||
deps = list(filter(lambda x: len(x) > 0, map(str.strip, f.read().split('\n'))))
|
||||
return [re.split('==|<=|>=|<|>', d)[0] for d in deps]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def requirements_installed(requirements_file, python_path):
|
||||
"""
|
||||
Checks if the packages in requirements.txt have been installed in the specified
|
||||
python path. Note that this does NOT check for versions, just package names
|
||||
:param requirements_file: path to requirements.txt
|
||||
:param python_path: path to directory where packages are installed
|
||||
:return: True if all requirements are installed, false otherwise
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = env.get("PYTHONPATH", "") + ":" + python_path
|
||||
reqs = subprocess.check_output(['pip', 'freeze'], env=env)
|
||||
installed_packages = [r.decode().split('==')[0] for r in reqs.split()]
|
||||
deps = parse_requirements(requirements_file)
|
||||
|
||||
return set(deps) & set(installed_packages) == set(deps)
|
||||
|
||||
def compute_file_md5(filename):
|
||||
return hashlib.md5(open(filename, 'rb').read()).hexdigest()
|
||||
|
|
@ -9,7 +9,7 @@ const leafletPreCheck = (options) => {
|
|||
};
|
||||
|
||||
const layersControlPreCheck = (options) => {
|
||||
assert(options.layersControl !== undefined);
|
||||
assert(options.controls !== undefined);
|
||||
leafletPreCheck(options);
|
||||
}
|
||||
|
||||
|
|
@ -17,8 +17,8 @@ export default {
|
|||
namespace: "Map",
|
||||
|
||||
endpoints: [
|
||||
["willAddControls", layersControlPreCheck],
|
||||
["didAddControls", leafletPreCheck],
|
||||
["willAddControls", leafletPreCheck],
|
||||
["didAddControls", layersControlPreCheck],
|
||||
["addActionButton", leafletPreCheck],
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -206,12 +206,17 @@ class Map extends React.Component {
|
|||
zoomControl: false
|
||||
});
|
||||
|
||||
Leaflet.control.scale({
|
||||
PluginsAPI.Map.triggerWillAddControls({
|
||||
map: this.map,
|
||||
tiles
|
||||
});
|
||||
|
||||
let scaleControl = Leaflet.control.scale({
|
||||
maxWidth: 250,
|
||||
}).addTo(this.map);
|
||||
|
||||
//add zoom control with your options
|
||||
Leaflet.control.zoom({
|
||||
let zoomControl = Leaflet.control.zoom({
|
||||
position:'bottomleft'
|
||||
}).addTo(this.map);
|
||||
|
||||
|
|
@ -258,12 +263,6 @@ https://a.tile.openstreetmap.org/{z}/{x}/{y}.png
|
|||
selectedOverlays: [],
|
||||
baseLayers: this.basemaps
|
||||
}).addTo(this.map);
|
||||
|
||||
PluginsAPI.Map.triggerWillAddControls({
|
||||
map: this.map,
|
||||
layersControl: this.autolayers,
|
||||
tiles
|
||||
});
|
||||
|
||||
this.map.fitWorld();
|
||||
this.map.attributionControl.setPrefix("");
|
||||
|
|
@ -316,7 +315,12 @@ https://a.tile.openstreetmap.org/{z}/{x}/{y}.png
|
|||
|
||||
PluginsAPI.Map.triggerDidAddControls({
|
||||
map: this.map,
|
||||
tiles: tiles
|
||||
tiles: tiles,
|
||||
controls:{
|
||||
autolayers: this.autolayers,
|
||||
scale: scaleControl,
|
||||
zoom: zoomControl
|
||||
}
|
||||
});
|
||||
|
||||
PluginsAPI.Map.triggerAddActionButton({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "WebODM",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"description": "Open Source Drone Image Processing",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
webpack.config.js
|
||||
disabled
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ from app.plugins.views import TaskView
|
|||
from worker.tasks import execute_grass_script
|
||||
from app.plugins.grass_engine import grass, GrassEngineException, cleanup_grass_context
|
||||
from worker.celery import app as celery
|
||||
from app.plugins import get_current_plugin
|
||||
|
||||
class TaskElevationMapGenerate(TaskView):
|
||||
def post(self, request, pk=None):
|
||||
task = self.get_and_check_task(request, pk)
|
||||
plugin = get_current_plugin()
|
||||
|
||||
if task.dsm_extent is None:
|
||||
return Response({'error': 'No DSM layer is available.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
|
@ -41,6 +43,8 @@ class TaskElevationMapGenerate(TaskView):
|
|||
context.add_param('noise_filter_size', noise_filter_size)
|
||||
context.add_param('epsg', epsg)
|
||||
context.add_param('python_script_path', os.path.join(current_dir, "elevationmap.py"))
|
||||
context.add_param('python_path', plugin.get_python_packages_path())
|
||||
|
||||
if dtm != None:
|
||||
context.add_param('dtm', '--dtm {}'.format(dtm))
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
# noise_filter_size: Area in meters where we will clean up noise in the contours
|
||||
# epsg: target EPSG code
|
||||
# python_script_path: Path of the python script
|
||||
# python_path: Path to python modules
|
||||
# dtm: Optional text to include the GeoTIFF DTM
|
||||
#
|
||||
# ------
|
||||
|
|
@ -20,7 +21,7 @@ elif [ "${format}" = "ESRI Shapefile" ]; then
|
|||
ext="shp"
|
||||
fi
|
||||
|
||||
${python_script_path} "${dsm_file}" ${interval} --epsg ${epsg} --noise_filter_size ${noise_filter_size} ${dtm} -o output.json
|
||||
PYTHONPATH="${python_path}" "${python_script_path}" "${dsm_file}" ${interval} --epsg ${epsg} --noise_filter_size ${noise_filter_size} ${dtm} -o output.json
|
||||
|
||||
if [ $$ext != "json" ]; then
|
||||
ogr2ogr -f "${format}" output.$$ext output.json > /dev/null
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ElevationMap",
|
||||
"webodmMinVersion": "1.1.0",
|
||||
"webodmMinVersion": "1.1.1",
|
||||
"description": "Calculate and draw an elevation map based on a task's DEMs",
|
||||
"version": "1.0.0",
|
||||
"author": "Nicolas Chamo",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from .api import TaskElevationMapGenerate
|
|||
from .api import TaskElevationMapCheck
|
||||
from .api import TaskElevationMapDownload
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def include_js_files(self):
|
||||
return ['main.js']
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
PluginsAPI.Map.willAddControls([
|
||||
PluginsAPI.Map.didAddControls([
|
||||
'elevationmap/build/ElevationMap.js',
|
||||
'elevationmap/build/ElevationMap.css'
|
||||
], function(args, ElevationMap){
|
||||
|
|
@ -9,6 +9,6 @@ PluginsAPI.Map.willAddControls([
|
|||
|
||||
// TODO: add support for map view where multiple tasks are available?
|
||||
if (tasks.length === 1){
|
||||
args.map.addControl(new ElevationMap({map: args.map, layersControl: args.layersControl, tasks: tasks}));
|
||||
args.map.addControl(new ElevationMap({map: args.map, layersControl: args.controls.autolayers, tasks: tasks}));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue