diff --git a/Dockerfile b/Dockerfile
index b53b9fead..cab4dd12c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
diff --git a/README.md b/README.md
index 45032e7a7..4709b4330 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/app/api/tasks.py b/app/api/tasks.py
index 994e502be..e5d3b1cd1 100644
--- a/app/api/tasks.py
+++ b/app/api/tasks.py
@@ -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 @@ def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="")
"""
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 @@ def get(self, request, pk=None, project_pk=None, tile_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 = {
'orthophoto': task.orthophoto_extent,
@@ -256,7 +255,7 @@ def get(self, request, pk=None, project_pk=None, asset=""):
"""
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 @@ def get(self, request, pk=None, project_pk=None, unsafe_asset_path=""):
"""
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:
diff --git a/app/api/urls.py b/app/api/urls.py
index 4b57ec80f..f9e1b1d60 100644
--- a/app/api/urls.py
+++ b/app/api/urls.py
@@ -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 @@
url(r'^auth/', include('rest_framework.urls')),
url(r'^token-auth/', obtain_jwt_token),
-]
\ No newline at end of file
+]
+
+urlpatterns += get_api_url_patterns()
\ No newline at end of file
diff --git a/app/boot.py b/app/boot.py
index 0e0a4dd79..ea8987154 100644
--- a/app/boot.py
+++ b/app/boot.py
@@ -3,7 +3,7 @@
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.")
\ No newline at end of file
+ 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()
diff --git a/app/models/task.py b/app/models/task.py
index 942aef783..569df1183 100644
--- a/app/models/task.py
+++ b/app/models/task.py
@@ -578,7 +578,7 @@ def set_failure(self, error_message):
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
diff --git a/app/plugins/functions.py b/app/plugins/functions.py
index e49e50138..28e2ab43d 100644
--- a/app/plugins/functions.py
+++ b/app/plugins/functions.py
@@ -1,6 +1,7 @@
import os
import logging
import importlib
+import subprocess
import django
import json
@@ -13,30 +14,57 @@
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"))
diff --git a/app/plugins/grass_engine.py b/app/plugins/grass_engine.py
new file mode 100644
index 000000000..5d400945c
--- /dev/null
+++ b/app/plugins/grass_engine.py
@@ -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()
\ No newline at end of file
diff --git a/app/plugins/mount_point.py b/app/plugins/mount_point.py
index b1cd8fba6..e6cf4f2b4 100644
--- a/app/plugins/mount_point.py
+++ b/app/plugins/mount_point.py
@@ -5,7 +5,7 @@ def __init__(self, url, view, *args, **kwargs):
"""
: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
"""
diff --git a/app/plugins/plugin_base.py b/app/plugins/plugin_base.py
index d9b5a996d..83244aa9f 100644
--- a/app/plugins/plugin_base.py
+++ b/app/plugins/plugin_base.py
@@ -46,8 +46,8 @@ def template_path(self, path):
"""
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 @@ def main_menu(self):
"""
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 @@ def mount_points(self):
"""
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())
\ No newline at end of file
diff --git a/app/static/app/css/theme.scss b/app/static/app/css/theme.scss
index b9c7b1483..ada5b17d8 100644
--- a/app/static/app/css/theme.scss
+++ b/app/static/app/css/theme.scss
@@ -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");
}
diff --git a/app/static/app/js/classes/plugins/API.js b/app/static/app/js/classes/plugins/API.js
index c604cc82e..d469304d0 100644
--- a/app/static/app/js/classes/plugins/API.js
+++ b/app/static/app/js/classes/plugins/API.js
@@ -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' }
}
});
diff --git a/app/static/app/js/classes/plugins/ApiFactory.js b/app/static/app/js/classes/plugins/ApiFactory.js
index 03205c680..239b49fec 100644
--- a/app/static/app/js/classes/plugins/ApiFactory.js
+++ b/app/static/app/js/classes/plugins/ApiFactory.js
@@ -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;
}
diff --git a/app/static/app/js/classes/plugins/Map.js b/app/static/app/js/classes/plugins/Map.js
index 368c861df..bb32622d9 100644
--- a/app/static/app/js/classes/plugins/Map.js
+++ b/app/static/app/js/classes/plugins/Map.js
@@ -1,4 +1,5 @@
import Utils from '../Utils';
+import L from 'leaflet';
const { assert } = Utils;
diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx
index 9d96478fc..a385b5fa6 100644
--- a/app/static/app/js/components/Map.jsx
+++ b/app/static/app/js/components/Map.jsx
@@ -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() {
diff --git a/app/static/app/js/css/Map.scss b/app/static/app/js/css/Map.scss
index ae2b1758d..56fcc89c3 100644
--- a/app/static/app/js/css/Map.scss
+++ b/app/static/app/js/css/Map.scss
@@ -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;
}
diff --git a/app/static/app/js/main.jsx b/app/static/app/js/main.jsx
index a867a0948..6bad3c376 100644
--- a/app/static/app/js/main.jsx
+++ b/app/static/app/js/main.jsx
@@ -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;
+
diff --git a/app/static/app/js/vendor/globals-loader.js b/app/static/app/js/vendor/globals-loader.js
new file mode 100644
index 000000000..7bf598062
--- /dev/null
+++ b/app/static/app/js/vendor/globals-loader.js
@@ -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];
+}
\ No newline at end of file
diff --git a/app/tests/test_plugins.py b/app/tests/test_plugins.py
index 349e8def2..004d6e9cc 100644
--- a/app/tests/test_plugins.py
+++ b/app/tests/test_plugins.py
@@ -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 @@ def test_core_plugins(self):
# And our menu entry
self.assertContains(res, '
Test', 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
diff --git a/app/urls.py b/app/urls.py
index 248e80e30..2e86eeb66 100644
--- a/app/urls.py
+++ b/app/urls.py
@@ -4,7 +4,7 @@
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 @@
# 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
diff --git a/package.json b/package.json
index b68ed5417..719a73625 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/plugins/measure/api.py b/plugins/measure/api.py
new file mode 100644
index 000000000..d86d321cf
--- /dev/null
+++ b/plugins/measure/api.py
@@ -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)
+
+
+
+
diff --git a/plugins/measure/calc_volume.grass b/plugins/measure/calc_volume.grass
new file mode 100755
index 000000000..b990287ed
--- /dev/null
+++ b/plugins/measure/calc_volume.grass
@@ -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
diff --git a/plugins/measure/manifest.json b/plugins/measure/manifest.json
index 8cc635de7..727b790f7 100644
--- a/plugins/measure/manifest.json
+++ b/plugins/measure/manifest.json
@@ -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
}
\ No newline at end of file
diff --git a/plugins/measure/plugin.py b/plugins/measure/plugin.py
index ac51ce355..9576fd0b4 100644
--- a/plugins/measure/plugin.py
+++ b/plugins/measure/plugin.py
@@ -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[^/.]+)/volume', TaskVolume.as_view())
+ ]
diff --git a/plugins/measure/public/MeasurePopup.jsx b/plugins/measure/public/MeasurePopup.jsx
new file mode 100644
index 000000000..a59dae383
--- /dev/null
+++ b/plugins/measure/public/MeasurePopup.jsx
@@ -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 (
+
Area: {this.props.model.areaDisplay}
+
Perimeter: {this.props.model.lengthDisplay}
+ {volume === null && !error &&
Volume: computing...
}
+ {typeof volume === "number" &&
Volume: {volume.toFixed("2")} Cubic Meters ({(volume * 35.3147).toFixed(2)} Cubic Feet)
}
+ {error &&
Volume: 200 ? 'long' : '')}>{error}
}
+
);
+ }
+}
\ No newline at end of file
diff --git a/plugins/measure/public/MeasurePopup.scss b/plugins/measure/public/MeasurePopup.scss
new file mode 100644
index 000000000..b8241e1b2
--- /dev/null
+++ b/plugins/measure/public/MeasurePopup.scss
@@ -0,0 +1,11 @@
+.plugin-measure.popup{
+ p{
+ margin: 0;
+ }
+
+ .error.long{
+ overflow: scroll;
+ display: block;
+ max-height: 200px;
+ }
+}
\ No newline at end of file
diff --git a/plugins/measure/public/app.jsx b/plugins/measure/public/app.jsx
new file mode 100644
index 000000000..338fdeba0
--- /dev/null
+++ b/plugins/measure/public/app.jsx
@@ -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 = $(""),
+ $popup = $(popupContainer);
+
+ $popup.children("p").empty();
+ $popup.children("h3:first-child").after($container);
+
+ ReactDOM.render(, $container.get(0));
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/plugins/measure/public/app.scss b/plugins/measure/public/app.scss
new file mode 100644
index 000000000..afb4fe8a6
--- /dev/null
+++ b/plugins/measure/public/app.scss
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/measure/public/images/cancel.png b/plugins/measure/public/images/cancel.png
deleted file mode 100644
index a4e7c492e..000000000
Binary files a/plugins/measure/public/images/cancel.png and /dev/null differ
diff --git a/plugins/measure/public/images/cancel_@2X.png b/plugins/measure/public/images/cancel_@2X.png
deleted file mode 100644
index dcc72f0c1..000000000
Binary files a/plugins/measure/public/images/cancel_@2X.png and /dev/null differ
diff --git a/plugins/measure/public/images/check.png b/plugins/measure/public/images/check.png
deleted file mode 100644
index 55f274b1d..000000000
Binary files a/plugins/measure/public/images/check.png and /dev/null differ
diff --git a/plugins/measure/public/images/check_@2X.png b/plugins/measure/public/images/check_@2X.png
deleted file mode 100644
index df8032e49..000000000
Binary files a/plugins/measure/public/images/check_@2X.png and /dev/null differ
diff --git a/plugins/measure/public/images/focus.png b/plugins/measure/public/images/focus.png
deleted file mode 100644
index 5a87d2433..000000000
Binary files a/plugins/measure/public/images/focus.png and /dev/null differ
diff --git a/plugins/measure/public/images/focus_@2X.png b/plugins/measure/public/images/focus_@2X.png
deleted file mode 100644
index 1eb7dd4ce..000000000
Binary files a/plugins/measure/public/images/focus_@2X.png and /dev/null differ
diff --git a/plugins/measure/public/images/rulers.png b/plugins/measure/public/images/rulers.png
deleted file mode 100644
index 5d6339d9d..000000000
Binary files a/plugins/measure/public/images/rulers.png and /dev/null differ
diff --git a/plugins/measure/public/images/rulers_@2X.png b/plugins/measure/public/images/rulers_@2X.png
deleted file mode 100644
index 7247a0c9d..000000000
Binary files a/plugins/measure/public/images/rulers_@2X.png and /dev/null differ
diff --git a/plugins/measure/public/images/start.png b/plugins/measure/public/images/start.png
deleted file mode 100644
index b8ca942b5..000000000
Binary files a/plugins/measure/public/images/start.png and /dev/null differ
diff --git a/plugins/measure/public/images/start_@2X.png b/plugins/measure/public/images/start_@2X.png
deleted file mode 100644
index 01da494c8..000000000
Binary files a/plugins/measure/public/images/start_@2X.png and /dev/null differ
diff --git a/plugins/measure/public/images/trash.png b/plugins/measure/public/images/trash.png
deleted file mode 100644
index 7ff478a45..000000000
Binary files a/plugins/measure/public/images/trash.png and /dev/null differ
diff --git a/plugins/measure/public/images/trash_@2X.png b/plugins/measure/public/images/trash_@2X.png
deleted file mode 100644
index fea11a8c4..000000000
Binary files a/plugins/measure/public/images/trash_@2X.png and /dev/null differ
diff --git a/plugins/measure/public/leaflet-measure.css b/plugins/measure/public/leaflet-measure.css
deleted file mode 100644
index 10e16b086..000000000
--- a/plugins/measure/public/leaflet-measure.css
+++ /dev/null
@@ -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}
diff --git a/plugins/measure/public/leaflet-measure.min.js b/plugins/measure/public/leaflet-measure.min.js
deleted file mode 100644
index 1dfe9e231..000000000
--- a/plugins/measure/public/leaflet-measure.min.js
+++ /dev/null
@@ -1,4 +0,0 @@
-!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g.04045?Math.pow((b+.055)/1.055,2.4):b/12.92,c=c>.04045?Math.pow((c+.055)/1.055,2.4):c/12.92,d=d>.04045?Math.pow((d+.055)/1.055,2.4):d/12.92;var e=.4124*b+.3576*c+.1805*d,f=.2126*b+.7152*c+.0722*d,g=.0193*b+.1192*c+.9505*d;return[100*e,100*f,100*g]}function l(a){var b,c,d,e=k(a),f=e[0],g=e[1],h=e[2];return f/=95.047,g/=100,h/=108.883,f=f>.008856?Math.pow(f,1/3):7.787*f+16/116,g=g>.008856?Math.pow(g,1/3):7.787*g+16/116,h=h>.008856?Math.pow(h,1/3):7.787*h+16/116,b=116*g-16,c=500*(f-g),d=200*(g-h),[b,c,d]}function m(a){return M(l(a))}function n(a){var b,c,d,e,f,g=a[0]/360,h=a[1]/100,i=a[2]/100;if(0==h)return f=255*i,[f,f,f];c=i<.5?i*(1+h):i+h-i*h,b=2*i-c,e=[0,0,0];for(var j=0;j<3;j++)d=g+1/3*-(j-1),d<0&&d++,d>1&&d--,f=6*d<1?b+6*(c-b)*d:2*d<1?c:3*d<2?b+(c-b)*(2/3-d)*6:b,e[j]=255*f;return e}function o(a){var b,c,d=a[0],e=a[1]/100,f=a[2]/100;return 0===f?[0,0,0]:(f*=2,e*=f<=1?f:2-f,c=(f+e)/2,b=2*e/(f+e),[d,100*b,100*c])}function p(a){return h(n(a))}function q(a){return i(n(a))}function s(a){return j(n(a))}function t(a){var b=a[0]/60,c=a[1]/100,d=a[2]/100,e=Math.floor(b)%6,f=b-Math.floor(b),g=255*d*(1-c),h=255*d*(1-c*f),i=255*d*(1-c*(1-f)),d=255*d;switch(e){case 0:return[d,i,g];case 1:return[h,d,g];case 2:return[g,d,i];case 3:return[g,h,d];case 4:return[i,g,d];case 5:return[d,g,h]}}function u(a){var b,c,d=a[0],e=a[1]/100,f=a[2]/100;return c=(2-e)*f,b=e*f,b/=c<=1?c:2-c,b=b||0,c/=2,[d,100*b,100*c]}function v(a){return h(t(a))}function w(a){return i(t(a))}function x(a){return j(t(a))}function y(a){var c,d,e,f,h=a[0]/360,i=a[1]/100,j=a[2]/100,k=i+j;switch(k>1&&(i/=k,j/=k),c=Math.floor(6*h),d=1-j,e=6*h-c,0!=(1&c)&&(e=1-e),f=i+e*(d-i),c){default:case 6:case 0:r=d,g=f,b=i;break;case 1:r=f,g=d,b=i;break;case 2:r=i,g=d,b=f;break;case 3:r=i,g=f,b=d;break;case 4:r=f,g=i,b=d;break;case 5:r=d,g=i,b=f}return[255*r,255*g,255*b]}function z(a){return e(y(a))}function A(a){return f(y(a))}function B(a){return i(y(a))}function C(a){return j(y(a))}function D(a){var b,c,d,e=a[0]/100,f=a[1]/100,g=a[2]/100,h=a[3]/100;return b=1-Math.min(1,e*(1-h)+h),c=1-Math.min(1,f*(1-h)+h),d=1-Math.min(1,g*(1-h)+h),[255*b,255*c,255*d]}function E(a){return e(D(a))}function F(a){return f(D(a))}function G(a){return h(D(a))}function H(a){return j(D(a))}function I(a){var b,c,d,e=a[0]/100,f=a[1]/100,g=a[2]/100;return b=3.2406*e+f*-1.5372+g*-.4986,c=e*-.9689+1.8758*f+.0415*g,d=.0557*e+f*-.204+1.057*g,b=b>.0031308?1.055*Math.pow(b,1/2.4)-.055:b*=12.92,c=c>.0031308?1.055*Math.pow(c,1/2.4)-.055:c*=12.92,d=d>.0031308?1.055*Math.pow(d,1/2.4)-.055:d*=12.92,b=Math.min(Math.max(0,b),1),c=Math.min(Math.max(0,c),1),d=Math.min(Math.max(0,d),1),[255*b,255*c,255*d]}function J(a){var b,c,d,e=a[0],f=a[1],g=a[2];return e/=95.047,f/=100,g/=108.883,e=e>.008856?Math.pow(e,1/3):7.787*e+16/116,f=f>.008856?Math.pow(f,1/3):7.787*f+16/116,g=g>.008856?Math.pow(g,1/3):7.787*g+16/116,b=116*f-16,c=500*(e-f),d=200*(f-g),[b,c,d]}function K(a){return M(J(a))}function L(a){var b,c,d,e,f=a[0],g=a[1],h=a[2];return f<=8?(c=100*f/903.3,e=7.787*(c/100)+16/116):(c=100*Math.pow((f+16)/116,3),e=Math.pow(c/100,1/3)),b=b/95.047<=.008856?b=95.047*(g/500+e-16/116)/7.787:95.047*Math.pow(g/500+e,3),d=d/108.883<=.008859?d=108.883*(e-h/200-16/116)/7.787:108.883*Math.pow(e-h/200,3),[b,c,d]}function M(a){var b,c,d,e=a[0],f=a[1],g=a[2];return b=Math.atan2(g,f),c=360*b/2/Math.PI,c<0&&(c+=360),d=Math.sqrt(f*f+g*g),[e,d,c]}function N(a){return I(L(a))}function O(a){var b,c,d,e=a[0],f=a[1],g=a[2];return d=g/360*2*Math.PI,b=f*Math.cos(d),c=f*Math.sin(d),[e,b,c]}function P(a){return L(O(a))}function Q(a){return N(O(a))}function R(a){return Y[a]}function S(a){return e(R(a))}function T(a){return f(R(a))}function U(a){return h(R(a))}function V(a){return i(R(a))}function W(a){return l(R(a))}function X(a){return k(R(a))}c.exports={rgb2hsl:e,rgb2hsv:f,rgb2hwb:h,rgb2cmyk:i,rgb2keyword:j,rgb2xyz:k,rgb2lab:l,rgb2lch:m,hsl2rgb:n,hsl2hsv:o,hsl2hwb:p,hsl2cmyk:q,hsl2keyword:s,hsv2rgb:t,hsv2hsl:u,hsv2hwb:v,hsv2cmyk:w,hsv2keyword:x,hwb2rgb:y,hwb2hsl:z,hwb2hsv:A,hwb2cmyk:B,hwb2keyword:C,cmyk2rgb:D,cmyk2hsl:E,cmyk2hsv:F,cmyk2hwb:G,cmyk2keyword:H,keyword2rgb:R,keyword2hsl:S,keyword2hsv:T,keyword2hwb:U,keyword2cmyk:V,keyword2lab:W,keyword2xyz:X,xyz2rgb:I,xyz2lab:J,xyz2lch:K,lab2xyz:L,lab2rgb:N,lab2lch:M,lch2lab:O,lch2xyz:P,lch2rgb:Q};var Y={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},Z={};for(var $ in Y)Z[JSON.stringify(Y[$])]=$},{}],3:[function(a,b,c){var d=a("./conversions"),e=function(){return new j};for(var f in d){e[f+"Raw"]=function(a){return function(b){return"number"==typeof b&&(b=Array.prototype.slice.call(arguments)),d[a](b)}}(f);var g=/(\w+)2(\w+)/.exec(f),h=g[1],i=g[2];e[h]=e[h]||{},e[h][i]=e[f]=function(a){return function(b){"number"==typeof b&&(b=Array.prototype.slice.call(arguments));var c=d[a](b);if("string"==typeof c||void 0===c)return c;for(var e=0;ec?(b+.05)/(c+.05):(c+.05)/(b+.05)},level:function(a){var b=this.contrast(a);return b>=7.1?"AAA":b>=4.5?"AA":""},dark:function(){var a=this.values.rgb,b=(299*a[0]+587*a[1]+114*a[2])/1e3;return b<128},light:function(){return!this.dark()},negate:function(){for(var a=[],b=0;b<3;b++)a[b]=255-this.values.rgb[b];return this.setValues("rgb",a),this},lighten:function(a){return this.values.hsl[2]+=this.values.hsl[2]*a,this.setValues("hsl",this.values.hsl),this},darken:function(a){return this.values.hsl[2]-=this.values.hsl[2]*a,this.setValues("hsl",this.values.hsl),this},saturate:function(a){return this.values.hsl[1]+=this.values.hsl[1]*a,this.setValues("hsl",this.values.hsl),this},desaturate:function(a){return this.values.hsl[1]-=this.values.hsl[1]*a,this.setValues("hsl",this.values.hsl),this},whiten:function(a){return this.values.hwb[1]+=this.values.hwb[1]*a,this.setValues("hwb",this.values.hwb),this},blacken:function(a){return this.values.hwb[2]+=this.values.hwb[2]*a,this.setValues("hwb",this.values.hwb),this},greyscale:function(){var a=this.values.rgb,b=.3*a[0]+.59*a[1]+.11*a[2];return this.setValues("rgb",[b,b,b]),this},clearer:function(a){return this.setValues("alpha",this.values.alpha-this.values.alpha*a),this},opaquer:function(a){return this.setValues("alpha",this.values.alpha+this.values.alpha*a),this},rotate:function(a){var b=this.values.hsl[0];return b=(b+a)%360,b=b<0?360+b:b,this.values.hsl[0]=b,this.setValues("hsl",this.values.hsl),this},mix:function(a,b){b=1-(null==b?.5:b);for(var c=2*b-1,d=this.alpha()-a.alpha(),e=((c*d==-1?c:(c+d)/(1+c*d))+1)/2,f=1-e,g=this.rgbArray(),h=a.rgbArray(),i=0;i0&&(c+=h(this._coords[a-1],this._coords[a]));this._calcedDistance=c}},distance:function(a){var b=d.extend({units:"meters"},a);if(this._internalDistanceCalc(),d.isFunction(g[b.units]))return g[b.units](this._calcedDistance)}}},{"./constants":9,"./units":14,underscore:15}],11:[function(a,b,c){var d=a("underscore");b.exports=function(a){return d.map(a,function(a){return[a[1],a[0]]})}},{underscore:15}],12:[function(a,b,c){var d=a("underscore"),e=a("./path"),f=a("./distance"),g=a("./area");d.extend(e.prototype,f,g),c.path=function(a,b){return new e(a,b)}},{"./area":8,"./distance":10,"./path":13,underscore:15}],13:[function(a,b,c){var d=a("./flipcoords"),e=function(a,b){this._options=b||{},a=a||[],this._coords=this._options.imBackwards===!0?d(a):a};b.exports=e},{"./flipcoords":11}],14:[function(a,b,c){c.meters={toFeet:function(a){return 3.28084*a},toKilometers:function(a){return.001*a},toMiles:function(a){return 621371e-9*a}},c.sqMeters={toSqMiles:function(a){return 3.86102e-7*a},toAcres:function(a){return 247105e-9*a}},c.degrees={toRadians:function(a){return a*Math.PI/180}}},{}],15:[function(a,b,c){(function(){var a=this,d=a._,e={},f=Array.prototype,g=Object.prototype,h=Function.prototype,i=f.push,j=f.slice,k=f.concat,l=g.toString,m=g.hasOwnProperty,n=f.forEach,o=f.map,p=f.reduce,q=f.reduceRight,r=f.filter,s=f.every,t=f.some,u=f.indexOf,v=f.lastIndexOf,w=Array.isArray,x=Object.keys,y=h.bind,z=function(a){return a instanceof z?a:this instanceof z?void(this._wrapped=a):new z(a)};"undefined"!=typeof c?("undefined"!=typeof b&&b.exports&&(c=b.exports=z),c._=z):a._=z,z.VERSION="1.5.2";var A=z.each=z.forEach=function(a,b,c){if(null!=a)if(n&&a.forEach===n)a.forEach(b,c);else if(a.length===+a.length){for(var d=0,f=a.length;d2;if(null==a&&(a=[]),p&&a.reduce===p)return d&&(b=z.bind(b,d)),e?a.reduce(b,c):a.reduce(b);if(A(a,function(a,f,g){e?c=b.call(d,c,a,f,g):(c=a,e=!0)}),!e)throw new TypeError(B);return c},z.reduceRight=z.foldr=function(a,b,c,d){var e=arguments.length>2;if(null==a&&(a=[]),q&&a.reduceRight===q)return d&&(b=z.bind(b,d)),e?a.reduceRight(b,c):a.reduceRight(b);var f=a.length;if(f!==+f){var g=z.keys(a);f=g.length}if(A(a,function(h,i,j){i=g?g[--f]:--f,e?c=b.call(d,c,a[i],i,j):(c=a[i],e=!0)}),!e)throw new TypeError(B);return c},z.find=z.detect=function(a,b,c){var d;return C(a,function(a,e,f){if(b.call(c,a,e,f))return d=a,!0}),d},z.filter=z.select=function(a,b,c){var d=[];return null==a?d:r&&a.filter===r?a.filter(b,c):(A(a,function(a,e,f){b.call(c,a,e,f)&&d.push(a)}),d)},z.reject=function(a,b,c){return z.filter(a,function(a,d,e){return!b.call(c,a,d,e)},c)},z.every=z.all=function(a,b,c){b||(b=z.identity);var d=!0;return null==a?d:s&&a.every===s?a.every(b,c):(A(a,function(a,f,g){if(!(d=d&&b.call(c,a,f,g)))return e}),!!d)};var C=z.some=z.any=function(a,b,c){b||(b=z.identity);var d=!1;return null==a?d:t&&a.some===t?a.some(b,c):(A(a,function(a,f,g){if(d||(d=b.call(c,a,f,g)))return e}),!!d)};z.contains=z.include=function(a,b){return null!=a&&(u&&a.indexOf===u?a.indexOf(b)!=-1:C(a,function(a){return a===b}))},z.invoke=function(a,b){var c=j.call(arguments,2),d=z.isFunction(b);return z.map(a,function(a){return(d?b:a[b]).apply(a,c)})},z.pluck=function(a,b){return z.map(a,function(a){return a[b]})},z.where=function(a,b,c){return z.isEmpty(b)?c?void 0:[]:z[c?"find":"filter"](a,function(a){for(var c in b)if(b[c]!==a[c])return!1;return!0})},z.findWhere=function(a,b){return z.where(a,b,!0)},z.max=function(a,b,c){if(!b&&z.isArray(a)&&a[0]===+a[0]&&a.length<65535)return Math.max.apply(Math,a);if(!b&&z.isEmpty(a))return-(1/0);var d={computed:-(1/0),value:-(1/0)};return A(a,function(a,e,f){var g=b?b.call(c,a,e,f):a;g>d.computed&&(d={value:a,computed:g})}),d.value},z.min=function(a,b,c){if(!b&&z.isArray(a)&&a[0]===+a[0]&&a.length<65535)return Math.min.apply(Math,a);if(!b&&z.isEmpty(a))return 1/0;var d={computed:1/0,value:1/0};return A(a,function(a,e,f){var g=b?b.call(c,a,e,f):a;gd||void 0===c)return 1;if(c>>1;c.call(d,a[h])=0});
-})},z.difference=function(a){var b=k.apply(f,j.call(arguments,1));return z.filter(a,function(a){return!z.contains(b,a)})},z.zip=function(){for(var a=z.max(z.pluck(arguments,"length").concat(0)),b=new Array(a),c=0;c=0;c--)b=[a[c].apply(this,b)];return b[0]}},z.after=function(a,b){return function(){if(--a<1)return b.apply(this,arguments)}},z.keys=x||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var b=[];for(var c in a)z.has(a,c)&&b.push(c);return b},z.values=function(a){for(var b=z.keys(a),c=b.length,d=new Array(c),e=0;e":">",'"':""","'":"'"}};I.unescape=z.invert(I.escape);var J={escape:new RegExp("["+z.keys(I.escape).join("")+"]","g"),unescape:new RegExp("("+z.keys(I.unescape).join("|")+")","g")};z.each(["escape","unescape"],function(a){z[a]=function(b){return null==b?"":(""+b).replace(J[a],function(b){return I[a][b]})}}),z.result=function(a,b){if(null!=a){var c=a[b];return z.isFunction(c)?c.call(a):c}},z.mixin=function(a){A(z.functions(a),function(b){var c=z[b]=a[b];z.prototype[b]=function(){var a=[this._wrapped];return i.apply(a,arguments),O.call(this,c.apply(z,a))}})};var K=0;z.uniqueId=function(a){var b=++K+"";return a?a+b:b},z.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var L=/(.)^/,M={"'":"'","\\":"\\","\r":"r","\n":"n","\t":"t","\u2028":"u2028","\u2029":"u2029"},N=/\\|'|\r|\n|\t|\u2028|\u2029/g;z.template=function(a,b,c){var d;c=z.defaults({},c,z.templateSettings);var e=new RegExp([(c.escape||L).source,(c.interpolate||L).source,(c.evaluate||L).source].join("|")+"|$","g"),f=0,g="__p+='";a.replace(e,function(b,c,d,e,h){return g+=a.slice(f,h).replace(N,function(a){return"\\"+M[a]}),c&&(g+="'+\n((__t=("+c+"))==null?'':_.escape(__t))+\n'"),d&&(g+="'+\n((__t=("+d+"))==null?'':__t)+\n'"),e&&(g+="';\n"+e+"\n__p+='"),f=h+b.length,b}),g+="';\n",c.variable||(g="with(obj||{}){\n"+g+"}\n"),g="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+g+"return __p;\n";try{d=new Function(c.variable||"obj","_",g)}catch(a){throw a.source=g,a}if(b)return d(b,z);var h=function(a){return d.call(this,a,z)};return h.source="function("+(c.variable||"obj")+"){\n"+g+"}",h},z.chain=function(a){return z(a).chain()};var O=function(a){return this._chain?z(a).chain():a};z.mixin(z),A(["pop","push","reverse","shift","sort","splice","unshift"],function(a){var b=f[a];z.prototype[a]=function(){var c=this._wrapped;return b.apply(c,arguments),"shift"!=a&&"splice"!=a||0!==c.length||delete c[0],O.call(this,c)}}),A(["concat","join","slice"],function(a){var b=f[a];z.prototype[a]=function(){return O.call(this,b.apply(this._wrapped,arguments))}}),z.extend(z.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this)},{}],16:[function(a,b,c){(function(){var a=this,d=a.humanize,e={};"undefined"!=typeof c?("undefined"!=typeof b&&b.exports&&(c=b.exports=e),c.humanize=e):("function"==typeof define&&define.amd&&define("humanize",function(){return e}),a.humanize=e),e.noConflict=function(){return a.humanize=d,this},e.pad=function(a,b,c,d){if(a+="",c?c.length>1&&(c=c.charAt(0)):c=" ",d=void 0===d?"left":"right","right"===d)for(;a.length4&&a<21?"th":{1:"st",2:"nd",3:"rd"}[a%10]||"th"},w:function(){return c.getDay()},z:function(){return(k.L()?g[k.n()]:f[k.n()])+k.j()-1},W:function(){var a=k.z()-k.N()+1.5;return e.pad(1+Math.floor(Math.abs(a)/7)+(a%7>3.5?1:0),2,"0")},F:function(){return j[c.getMonth()]},m:function(){return e.pad(k.n(),2,"0")},M:function(){return k.F().slice(0,3)},n:function(){return c.getMonth()+1},t:function(){return new Date(k.Y(),k.n(),0).getDate()},L:function(){return 1===new Date(k.Y(),1,29).getMonth()?1:0},o:function(){var a=k.n(),b=k.W();return k.Y()+(12===a&&b<9?-1:1===a&&b>9)},Y:function(){return c.getFullYear()},y:function(){return String(k.Y()).slice(-2)},a:function(){return c.getHours()>11?"pm":"am"},A:function(){return k.a().toUpperCase()},B:function(){var a=c.getTime()/1e3,b=a%86400+3600;b<0&&(b+=86400);var d=b/86.4%1e3;return a<0?Math.ceil(d):Math.floor(d)},g:function(){return k.G()%12||12},G:function(){return c.getHours()},h:function(){return e.pad(k.g(),2,"0")},H:function(){return e.pad(k.G(),2,"0")},i:function(){return e.pad(c.getMinutes(),2,"0")},s:function(){return e.pad(c.getSeconds(),2,"0")},u:function(){return e.pad(1e3*c.getMilliseconds(),6,"0")},O:function(){var a=c.getTimezoneOffset(),b=Math.abs(a);return(a>0?"-":"+")+e.pad(100*Math.floor(b/60)+b%60,4,"0")},P:function(){var a=k.O();return a.substr(0,3)+":"+a.substr(3,2)},Z:function(){return 60*-c.getTimezoneOffset()},c:function(){return"Y-m-d\\TH:i:sP".replace(d,h)},r:function(){return"D, d M Y H:i:s O".replace(d,h)},U:function(){return c.getTime()/1e3||0}};return a.replace(d,h)},e.numberFormat=function(a,b,c,d){b=isNaN(b)?2:Math.abs(b),c=void 0===c?".":c,d=void 0===d?",":d;var e=a<0?"-":"";a=Math.abs(+a||0);var f=parseInt(a.toFixed(b),10)+"",g=f.length>3?f.length%3:0;return e+(g?f.substr(0,g)+d:"")+f.substr(g).replace(/(\d{3})(?=\d)/g,"$1"+d)+(b?c+Math.abs(a-f).toFixed(b).slice(2):"")},e.naturalDay=function(a,b){a=void 0===a?e.time():a,b=void 0===b?"Y-m-d":b;var c=86400,d=new Date,f=new Date(d.getFullYear(),d.getMonth(),d.getDate()).getTime()/1e3;return a=f-c?"yesterday":a>=f&&a=f+c&&a-2)return(c>=0?"just ":"")+"now";if(c<60&&c>-60)return c>=0?Math.floor(c)+" seconds ago":"in "+Math.floor(-c)+" seconds";if(c<120&&c>-120)return c>=0?"about a minute ago":"in about a minute";if(c<3600&&c>-3600)return c>=0?Math.floor(c/60)+" minutes ago":"in "+Math.floor(-c/60)+" minutes";if(c<7200&&c>-7200)return c>=0?"about an hour ago":"in about an hour";if(c<86400&&c>-86400)return c>=0?Math.floor(c/3600)+" hours ago":"in "+Math.floor(-c/3600)+" hours";var d=172800;if(c-d)return c>=0?"1 day ago":"in 1 day";var f=2505600;if(c-f)return c>=0?Math.floor(c/86400)+" days ago":"in "+Math.floor(-c/86400)+" days";var g=5184e3;if(c-g)return c>=0?"about a month ago":"in about a month";var h=parseInt(e.date("Y",b),10),i=parseInt(e.date("Y",a),10),j=12*h+parseInt(e.date("n",b),10),k=12*i+parseInt(e.date("n",a),10),l=j-k;if(l<12&&l>-12)return l>=0?l+" months ago":"in "+-l+" months";var m=h-i;return m<2&&m>-2?m>=0?"a year ago":"in a year":m>=0?m+" years ago":"in "+-m+" years"},e.ordinal=function(a){a=parseInt(a,10),a=isNaN(a)?0:a;var b=a<0?"-":"";a=Math.abs(a);var c=a%100;return b+a+(c>4&&c<21?"th":{1:"st",2:"nd",3:"rd"}[a%10]||"th")},e.filesize=function(a,b,c,d,f,g){return b=void 0===b?1024:b,a<=0?"0 bytes":(a"),a=a.replace(/\n/g,"
"),"
"+a+"
"},e.nl2br=function(a){return a.replace(/(\r\n|\n|\r)/g,"
")},e.truncatechars=function(a,b){return a.length<=b?a:a.substr(0,b)+"…"},e.truncatewords=function(a,b){var c=a.split(" ");return c.length1&&(a=e(a,Array.prototype.slice.call(arguments,1))),a},__n:function(a,b,c){var d;if("number"==typeof b){var f=a,g=b;d=this.translate(this.locale,f),d=e(parseInt(g,10)>1?d.other:d.one,Array.prototype.slice.call(arguments,1))}else{var h=a,i=b,g=c;d=this.translate(this.locale,h,i),d=e(parseInt(g,10)>1?d.other:d.one,[g]),arguments.length>3&&(d=e(d,Array.prototype.slice.call(arguments,3)))}return d},setLocale:function(a){if(a)return this.locales[a]||(this.devMode&&console.warn("Locale ("+a+") not found."),a=this.defaultLocale),this.locale=a},getLocale:function(){return this.locale},isPreferredLocale:function(){return!this.prefLocale||this.prefLocale===this.getLocale()},setLocaleFromSessionVar:function(a){if(a=a||this.request,a&&a.session&&a.session[this.sessionVarName]){var b=a.session[this.sessionVarName];this.locales[b]&&(this.devMode&&console.log("Overriding locale from query: "+b),this.setLocale(b))}},setLocaleFromQuery:function(a){if(a=a||this.request,a&&a.query&&a.query.lang){var b=(a.query.lang+"").toLowerCase();this.locales[b]&&(this.devMode&&console.log("Overriding locale from query: "+b),this.setLocale(b))}},setLocaleFromSubdomain:function(a){a=a||this.request,a&&a.headers&&a.headers.host&&/^([^.]+)/.test(a.headers.host)&&this.locales[RegExp.$1]&&(this.devMode&&console.log("Overriding locale from host: "+RegExp.$1),this.setLocale(RegExp.$1))},setLocaleFromCookie:function(a){if(a=a||this.request,a&&a.cookies&&this.cookieName&&a.cookies[this.cookieName]){var b=a.cookies[this.cookieName].toLowerCase();this.locales[b]&&(this.devMode&&console.log("Overriding locale from cookie: "+b),this.setLocale(b))}},setLocaleFromEnvironmentVariable:function(){if(c.env.LANG){var a=c.env.LANG.split("_")[0];this.locales[a]&&(this.devMode&&console.log("Overriding locale from environment variable: "+a),this.setLocale(a))}},preferredLocale:function(a){if(a=a||this.request,a&&a.headers){for(var b,c=a.headers["accept-language"]||"",d=/(^|,\s*)([a-z0-9-]+)/gi,e=this;!b&&(match=d.exec(c));){var f=match[2].toLowerCase(),g=f.split("-");e.locales[f]?b=f:g.length>1&&e.locales[g[0]]&&(b=g[0])}return b||this.defaultLocale}},translate:function(a,b,c){return a&&this.locales[a]||(this.devMode&&console.warn("WARN: No locale found. Using the default ("+this.defaultLocale+") as current locale"),a=this.defaultLocale,this.initLocale(a,{})),this.locales[a][b]||this.devMode&&(d(this.locales[a],b,c?{one:b,other:c}:void 0),this.writeFile(a)),d(this.locales[a],b,c?{one:b,other:c}:void 0)},readFile:function(a){var b=this.locateFile(a);if(!this.devMode&&h.localeCache[b])return void this.initLocale(a,h.localeCache[b]);try{var c,d=f.readFileSync(b);if("function"==typeof this.base){var e;try{e=this.base(a)}catch(b){console.error("base function threw exception for locale %s",a,b)}if("string"==typeof e)try{c=this.parse(f.readFileSync(this.locateFile(e)))}catch(b){console.error("unable to read or parse base file %s for locale %s",e,a,b)}}try{var g=this.parse(d);if(c){for(var i in g)c[i]=g[i];g=c}this.initLocale(a,g)}catch(a){console.error("unable to parse locales from file (maybe "+b+" is empty or invalid "+this.extension+"?): ",a)}}catch(c){f.existsSync(b)||this.writeFile(a)}},writeFile:function(a){if(!this.devMode)return void this.initLocale(a,{});try{f.lstatSync(this.directory)}catch(a){this.devMode&&console.log("creating locales dir in: "+this.directory),f.mkdirSync(this.directory,493)}this.initLocale(a,{});try{var b=this.locateFile(a),c=b+".tmp";f.writeFileSync(c,this.dump(this.locales[a],this.indent),"utf8"),f.statSync(c).isFile()?f.renameSync(c,b):console.error("unable to write locales to file (either "+c+" or "+b+" are not writeable?): ")}catch(a){console.error("unexpected error writing files (either "+c+" or "+b+" are not writeable?): ",a)}},locateFile:function(a){return g.normalize(this.directory+"/"+a+this.extension)},initLocale:function(a,b){if(!this.locales[a]&&(this.locales[a]=b,!this.devMode)){var c=this.locateFile(a);h.localeCache[c]||(h.localeCache[c]=b)}}}}).call(this,a("_process"))},{_process:20,fs:1,path:19,sprintf:21}],18:[function(a,b,c){b.exports=a("./i18n")},{"./i18n":17}],19:[function(a,b,c){(function(a){function b(a,b){for(var c=0,d=a.length-1;d>=0;d--){var e=a[d];"."===e?a.splice(d,1):".."===e?(a.splice(d,1),c++):c&&(a.splice(d,1),c--)}if(b)for(;c--;c)a.unshift("..");return a}function d(a,b){if(a.filter)return a.filter(b);for(var c=[],d=0;d=-1&&!e;f--){var g=f>=0?arguments[f]:a.cwd();if("string"!=typeof g)throw new TypeError("Arguments to path.resolve must be strings");g&&(c=g+"/"+c,e="/"===g.charAt(0))}return c=b(d(c.split("/"),function(a){return!!a}),!e).join("/"),(e?"/":"")+c||"."},c.normalize=function(a){var e=c.isAbsolute(a),f="/"===g(a,-1);return a=b(d(a.split("/"),function(a){return!!a}),!e).join("/"),a||e||(a="."),a&&f&&(a+="/"),(e?"/":"")+a},c.isAbsolute=function(a){return"/"===a.charAt(0)},c.join=function(){var a=Array.prototype.slice.call(arguments,0);return c.normalize(d(a,function(a,b){if("string"!=typeof a)throw new TypeError("Arguments to path.join must be strings");return a}).join("/"))},c.relative=function(a,b){function d(a){for(var b=0;b=0&&""===a[c];c--);return b>c?[]:a.slice(b,c-b+1)}a=c.resolve(a).substr(1),b=c.resolve(b).substr(1);for(var e=d(a.split("/")),f=d(b.split("/")),g=Math.min(e.length,f.length),h=g,i=0;i1)for(var c=1;c0;c[--b]=a);return c.join("")}var c=function(){return c.cache.hasOwnProperty(arguments[0])||(c.cache[arguments[0]]=c.parse(arguments[0])),c.format.call(null,c.cache[arguments[0]],arguments)};return c.object_stringify=function(a,b,d,e){var f="";if(null!=a)switch(typeof a){case"function":return"[Function"+(a.name?": "+a.name:"")+"]";case"object":if(a instanceof Error)return"["+a.toString()+"]";if(b>=d)return"[Object]";if(e&&(e=e.slice(0),e.push(a)),null!=a.length){f+="[";var g=[];for(var h in a)e&&e.indexOf(a[h])>=0?g.push("[Circular]"):g.push(c.object_stringify(a[h],b+1,d,e));f+=g.join(", ")+"]"}else{if("getMonth"in a)return"Date("+a+")";f+="{";var g=[];for(var i in a)a.hasOwnProperty(i)&&(e&&e.indexOf(a[i])>=0?g.push(i+": [Circular]"):g.push(i+": "+c.object_stringify(a[i],b+1,d,e)));f+=g.join(", ")+"}"}return f;case"string":return'"'+a+'"'}return""+a},c.format=function(e,f){var g,h,i,j,k,l,m,n=1,o=e.length,p="",q=[];for(h=0;h=0?"+"+g:g,l=j[4]?"0"==j[4]?"0":j[4].charAt(1):" ",m=j[6]-String(g).length,k=j[6]?b(l,m):"",q.push(j[5]?g+k:k+g)}return q.join("")},c.cache={},c.parse=function(a){for(var b=a,c=[],d=[],e=0;b;){if(null!==(c=/^[^\x25]+/.exec(b)))d.push(c[0]);else if(null!==(c=/^\x25{2}/.exec(b)))d.push("%");else{if(null===(c=/^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosOuxX])/.exec(b)))throw new Error("[sprintf] "+b);if(c[2]){e|=1;var f=[],g=c[2],h=[];if(null===(h=/^([a-z_][a-z_\d]*)/i.exec(g)))throw new Error("[sprintf] "+g);for(f.push(h[1]);""!==(g=g.substring(h[0].length));)if(null!==(h=/^\.([a-z_][a-z_\d]*)/i.exec(g)))f.push(h[1]);else{if(null===(h=/^\[(\d+)\]/.exec(g)))throw new Error("[sprintf] "+g);f.push(h[1])}c[2]=f}else e|=2;if(3===e)throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported");d.push(c)}b=b.substring(c[0].length)}return d},c}(),e=function(a,b){var c=b.slice();return c.unshift(a),d.apply(null,c)};b.exports=d,d.sprintf=d,d.vsprintf=e},{}],22:[function(a,b,c){(function(){function a(a){function b(b,c,d,e,f,g){for(;f>=0&&f0?0:h-1;return arguments.length<3&&(e=c[g?g[i]:i],i+=a),b(c,d,e,g,i,h)}}function d(a){return function(b,c,d){c=w(c,d);for(var e=B(b),f=a>0?0:e-1;f>=0&&f0?g=f>=0?f:Math.max(f+h,g):h=f>=0?Math.min(f+1,h):f+h+1;else if(c&&f&&h)return f=c(d,e),d[f]===e?f:-1;if(e!==e)return f=b(m.call(d,g,h),u.isNaN),f>=0?f+g:-1;for(f=a>0?g:h-1;f>=0&&f=0&&b<=A};u.each=u.forEach=function(a,b,c){b=v(b,c);var d,e;if(C(a))for(d=0,e=a.length;d=0},u.invoke=function(a,b){var c=m.call(arguments,2),d=u.isFunction(b);return u.map(a,function(a){var e=d?b:a[b];return null==e?e:e.apply(a,c)})},u.pluck=function(a,b){return u.map(a,u.property(b))},u.where=function(a,b){return u.filter(a,u.matcher(b))},u.findWhere=function(a,b){return u.find(a,u.matcher(b))},u.max=function(a,b,c){var d,e,f=-(1/0),g=-(1/0);if(null==b&&null!=a){a=C(a)?a:u.values(a);for(var h=0,i=a.length;hf&&(f=d)}else b=w(b,c),u.each(a,function(a,c,d){e=b(a,c,d),(e>g||e===-(1/0)&&f===-(1/0))&&(f=a,g=e)});return f},u.min=function(a,b,c){var d,e,f=1/0,g=1/0;if(null==b&&null!=a){a=C(a)?a:u.values(a);for(var h=0,i=a.length;hd||void 0===c)return 1;if(cb?(g&&(clearTimeout(g),g=null),h=j,f=a.apply(d,e),g||(d=e=null)):g||c.trailing===!1||(g=setTimeout(i,k)),f}},u.debounce=function(a,b,c){var d,e,f,g,h,i=function(){var j=u.now()-g;j=0?d=setTimeout(i,b-j):(d=null,c||(h=a.apply(f,e),d||(f=e=null)))};return function(){f=this,e=arguments,g=u.now();var j=c&&!d;return d||(d=setTimeout(i,b)),j&&(h=a.apply(f,e),f=e=null),h}},u.wrap=function(a,b){return u.partial(b,a)},u.negate=function(a){return function(){return!a.apply(this,arguments)}},u.compose=function(){var a=arguments,b=a.length-1;return function(){for(var c=b,d=a[b].apply(this,arguments);c--;)d=a[c].call(this,d);return d}},u.after=function(a,b){return function(){if(--a<1)return b.apply(this,arguments)}},u.before=function(a,b){var c;return function(){return--a>0&&(c=b.apply(this,arguments)),a<=1&&(b=null),c}},u.once=u.partial(u.before,2);var G=!{toString:null}.propertyIsEnumerable("toString"),H=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];u.keys=function(a){if(!u.isObject(a))return[];if(q)return q(a);var b=[];for(var c in a)u.has(a,c)&&b.push(c);return G&&f(a,b),b},u.allKeys=function(a){if(!u.isObject(a))return[];var b=[];for(var c in a)b.push(c);return G&&f(a,b),b},u.values=function(a){for(var b=u.keys(a),c=b.length,d=Array(c),e=0;e":">",'"':""","'":"'","`":"`"},K=u.invert(J),L=function(a){var b=function(b){return a[b]},c="(?:"+u.keys(a).join("|")+")",d=RegExp(c),e=RegExp(c,"g");return function(a){return a=null==a?"":""+a,d.test(a)?a.replace(e,b):a}};u.escape=L(J),u.unescape=L(K),u.result=function(a,b,c){var d=null==a?void 0:a[b];return void 0===d&&(d=c),u.isFunction(d)?d.call(a):d};var M=0;u.uniqueId=function(a){var b=++M+"";return a?a+b:b},u.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var N=/(.)^/,O={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},P=/\\|'|\r|\n|\u2028|\u2029/g,Q=function(a){return"\\"+O[a]};u.template=function(a,b,c){!b&&c&&(b=c),b=u.defaults({},b,u.templateSettings);var d=RegExp([(b.escape||N).source,(b.interpolate||N).source,(b.evaluate||N).source].join("|")+"|$","g"),e=0,f="__p+='";a.replace(d,function(b,c,d,g,h){return f+=a.slice(e,h).replace(P,Q),e=h+b.length,c?f+="'+\n((__t=("+c+"))==null?'':_.escape(__t))+\n'":d?f+="'+\n((__t=("+d+"))==null?'':__t)+\n'":g&&(f+="';\n"+g+"\n__p+='"),b}),f+="';\n",b.variable||(f="with(obj||{}){\n"+f+"}\n"),f="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+f+"return __p;\n";try{var g=new Function(b.variable||"obj","_",f)}catch(a){throw a.source=f,a}var h=function(a){return g.call(this,a,u)},i=b.variable||"obj";return h.source="function("+i+"){\n"+f+"}",h},u.chain=function(a){var b=u(a);return b._chain=!0,b};var R=function(a,b){return a._chain?u(b).chain():b};u.mixin=function(a){u.each(u.functions(a),function(b){var c=u[b]=a[b];u.prototype[b]=function(){var a=[this._wrapped];return l.apply(a,arguments),R(this,c.apply(u,a))}})},u.mixin(u),u.each(["pop","push","reverse","shift","sort","splice","unshift"],function(a){var b=i[a];u.prototype[a]=function(){var c=this._wrapped;return b.apply(c,arguments),"shift"!==a&&"splice"!==a||0!==c.length||delete c[0],R(this,c)}}),u.each(["concat","join","slice"],function(a){var b=i[a];u.prototype[a]=function(){return R(this,b.apply(this._wrapped,arguments))}}),u.prototype.value=function(){return this._wrapped},u.prototype.valueOf=u.prototype.toJSON=u.prototype.value,u.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return u})}).call(this)},{}],23:[function(a,b,c){var d=a("underscore"),e=a("geocrunch"),f=function(a){return a<10?"0"+a.toString():a.toString()},g=function(a,b,c){var d=Math.abs(a),e=Math.floor(d),g=Math.floor(60*(d-e)),h=Math.round(3600*(d-e-g/60)*100)/100,i=d===a?b:c;return f(e)+"° "+f(g)+"' "+f(h)+'" '+i},h=function(a){var b=d.last(a),c=e.path(d.map(a,function(a){return[a.lng,a.lat]})),f=c.distance({units:"meters"}),h=c.area({units:"sqmeters"});return{lastCoord:{dd:{x:b.lng,y:b.lat},dms:{x:g(b.lng,"E","W"),y:g(b.lat,"N","S")}},length:f,area:h}};b.exports={measure:h}},{geocrunch:7,underscore:22}],24:[function(a,b,c){var d=function(a,b){return b||(b=document),b.querySelector(a)},e=function(a,b){return b||(b=document),Array.prototype.slice.call(b.querySelectorAll(a))},f=function(a){if(a)return a.setAttribute("style","display:none;"),a},g=function(a){if(a)return a.removeAttribute("style"),a};b.exports={$:d,$$:e,hide:f,show:g}},{}],25:[function(a,b,c){b.exports={measure:"Medir",measureDistancesAndAreas:"Medeix distancies i àreas",createNewMeasurement:"Crear nova medicio",startCreating:"Començi a crear la medicio afegint punts al mapa",finishMeasurement:"Acabar la medició",lastPoint:"Últim punt",area:"Área",perimeter:"Perómetre",pointLocation:"Localizació del punt",areaMeasurement:"Medició d'área",linearMeasurement:"Medició lineal",pathDistance:"Distancia de ruta",centerOnArea:"Centrar en aquesta área",centerOnLine:"Centrar en aquesta línia",centerOnLocation:"Centrar en aquesta localizació",cancel:"Cancel·lar",delete:"Eliminar",acres:"Acres",feet:"Peus",kilometers:"Quilòmetres",hectares:"Hectàreas",meters:"Metros",miles:"Milles",sqfeet:"Peus cuadrats",sqmeters:"Metres cuadrats",sqmiles:"Milles cuadrades",decPoint:".",thousandsSep:" "}},{}],26:[function(a,b,c){b.exports={measure:"测量",measureDistancesAndAreas:"同时测量距离和面积",createNewMeasurement:"开始一次新的测量",startCreating:"点击地图加点以开始创建测量",finishMeasurement:"完成测量",lastPoint:"最后点的坐标",area:"面积",perimeter:"周长",pointLocation:"点的坐标",areaMeasurement:"面积测量",linearMeasurement:"距离测量",pathDistance:"路径长度",centerOnArea:"该面积居中",centerOnLine:"该线段居中",centerOnLocation:"该位置居中",cancel:"取消",delete:"删除",acres:"公亩",feet:"英尺",kilometers:"公里",hectares:"公顷",meters:"米",miles:"英里",sqfeet:"平方英尺",sqmeters:"平方米",sqmiles:"平方英里",decPoint:".",thousandsSep:","}},{}],27:[function(a,b,c){b.exports={measure:"Mål",measureDistancesAndAreas:"Mål afstande og arealer",createNewMeasurement:"Lav en ny måling",startCreating:"Begynd målingen ved at tilføje punkter på kortet",finishMeasurement:"Afslut måling",lastPoint:"Sidste punkt",area:"Areal",perimeter:"Omkreds",pointLocation:"Punkt",areaMeasurement:"Areal",linearMeasurement:"Linje",pathDistance:"Sti afstand",centerOnArea:"Centrér dette område",centerOnLine:"Centrér denne linje",centerOnLocation:"Centrér dette punkt",cancel:"Annuller",delete:"Slet",acres:"acre",feet:"fod",kilometers:"km",hectares:"ha",meters:"m",miles:"mil",sqfeet:"kvadratfod",sqmeters:"m²",sqmiles:"kvadratmil",decPoint:",",thousandsSep:"."}},{}],28:[function(a,b,c){b.exports={measure:"Messung",measureDistancesAndAreas:"Messung von Abständen und Flächen",createNewMeasurement:"Eine neue Messung durchführen",startCreating:"Führen Sie die Messung durch, indem Sie der Karte Punkte hinzufügen.",finishMeasurement:"Messung beenden",lastPoint:"Letzter Punkt",area:"Fläche",perimeter:"Rand",pointLocation:"Lage des Punkts",areaMeasurement:"Gemessene Fläche",linearMeasurement:"Gemessener Abstand",pathDistance:"Abstand entlang des Pfads",centerOnArea:"Auf diese Fläche zentrieren",centerOnLine:"Auf diesen Linienzug zentrieren",centerOnLocation:"Auf diesen Ort zentrieren",cancel:"Abbrechen",delete:"Löschen",acres:"Morgen",feet:"Fuß",kilometers:"Kilometer",hectares:"Hektar",meters:"Meter",miles:"Meilen",sqfeet:"Quadratfuß",sqmeters:"Quadratmeter",sqmiles:"Quadratmeilen",decPoint:",",thousandsSep:"."}},{}],29:[function(a,b,c){b.exports={measure:"Messung",measureDistancesAndAreas:"Abstände und Flächen messen",createNewMeasurement:"Eine neue Messung durchführen",startCreating:"Messen sie, indem Sie der Karte Punkte hinzufügen",finishMeasurement:"Messung beenden",lastPoint:"Letzter Punkt",area:"Fläche",perimeter:"Umfang",pointLocation:"Lage des Punkts",areaMeasurement:"Fläche",linearMeasurement:"Abstand",pathDistance:"Umfang",centerOnArea:"Auf diese Fläche zentrieren",centerOnLine:"Auf diese Linie zentrieren",centerOnLocation:"Auf diesen Ort zentrieren",cancel:"Abbrechen",delete:"Löschen",acres:"Morgen",feet:"Fuß",kilometers:"Kilometer",hectares:"Hektar",meters:"Meter",miles:"Meilen",sqfeet:"Quadratfuß",sqmeters:"Quadratmeter",sqmiles:"Quadratmeilen",decPoint:".",thousandsSep:"'"}},{}],30:[function(a,b,c){b.exports={measure:"Measure",measureDistancesAndAreas:"Measure distances and areas",createNewMeasurement:"Create a new measurement",startCreating:"Start creating a measurement by adding points to the map",finishMeasurement:"Finish measurement",lastPoint:"Last point",area:"Area",perimeter:"Perimeter",pointLocation:"Point location",areaMeasurement:"Area measurement",linearMeasurement:"Linear measurement",pathDistance:"Path distance",centerOnArea:"Center on this area",centerOnLine:"Center on this line",centerOnLocation:"Center on this location",cancel:"Cancel",delete:"Delete",acres:"Acres",feet:"Feet",kilometers:"Kilometers",hectares:"Hectares",meters:"Meters",miles:"Miles",sqfeet:"Sq Feet",sqmeters:"Sq Meters",sqmiles:"Sq Miles",decPoint:".",thousandsSep:","}},{}],31:[function(a,b,c){b.exports={measure:"Measure",measureDistancesAndAreas:"Measure distances and areas",createNewMeasurement:"Create a new measurement",startCreating:"Start creating a measurement by adding points to the map",finishMeasurement:"Finish measurement",lastPoint:"Last point",area:"Area",perimeter:"Perimeter",pointLocation:"Point location",areaMeasurement:"Area measurement",linearMeasurement:"Linear measurement",pathDistance:"Path distance",centerOnArea:"Centre on this area",centerOnLine:"Centre on this line",centerOnLocation:"Centre on this location",cancel:"Cancel",delete:"Delete",acres:"Acres",feet:"Feet",kilometers:"Kilometres",hectares:"Hectares",meters:"Meters",miles:"Miles",sqfeet:"Sq Feet",sqmeters:"Sq Meters",sqmiles:"Sq Miles",decPoint:".",thousandsSep:","}},{}],32:[function(a,b,c){b.exports={measure:"Medición",measureDistancesAndAreas:"Mida distancias y áreas",createNewMeasurement:"Crear nueva medición",startCreating:"Empiece a crear la medición añadiendo puntos al mapa",finishMeasurement:"Terminar medición",lastPoint:"Último punto",area:"Área",perimeter:"Perímetro",pointLocation:"Localización del punto",areaMeasurement:"Medición de área",linearMeasurement:"Medición linear",pathDistance:"Distancia de ruta",centerOnArea:"Centrar en este área",centerOnLine:"Centrar en esta línea",centerOnLocation:"Centrar en esta localización",cancel:"Cancelar",delete:"Eliminar",acres:"Acres",feet:"Pies",kilometers:"Kilómetros",hectares:"Hectáreas",meters:"Metros",miles:"Millas",sqfeet:"Pies cuadrados",sqmeters:"Metros cuadrados",sqmiles:"Millas cuadradas",decPoint:".",thousandsSep:" "}},{}],33:[function(a,b,c){b.exports={measure:"اندازه گیری",measureDistancesAndAreas:"اندازه گیری فاصله و مساحت",createNewMeasurement:"ثبت اندازه گیری جدید",startCreating:"برای ثبت اندازه گیری جدید نقاطی را به نقشه اضافه کنید.",finishMeasurement:"پایان اندازه گیری",lastPoint:"آخرین نقطه",area:"مساحت",perimeter:"محیط",pointLocation:"مکان نقطه",areaMeasurement:"اندازه گیری مساحت",linearMeasurement:"اندازه گیری خطی",pathDistance:"فاصله مسیر",centerOnArea:"مرکز این سطح",centerOnLine:"مرکز این خط",centerOnLocation:"مرکز این مکان",cancel:"لغو",delete:"حذف",acres:"ایکر",feet:"پا",kilometers:"کیلومتر",hectares:"هکتار",meters:"متر",miles:"مایل",sqfeet:"پا مربع",sqmeters:"متر مربع",sqmiles:"مایل مربع",decPoint:"/",thousandsSep:","}},{}],34:[function(a,b,c){b.exports={measure:"Sukat",measureDistancesAndAreas:"Kalkulahin ang tamang distansya at sukat",createNewMeasurement:"Lumikha ng isang bagong pagsukat",startCreating:"Simulan ang paglikha ng isang pagsukat sa pamamagitan ng pagdaragdag ng mga puntos sa mapa",finishMeasurement:"Tapusin ang pagsukat",lastPoint:"Huling punto sa mapa",area:"Sukat",perimeter:"Palibot",pointLocation:"Lokasyon ng punto",areaMeasurement:"Kabuuang sukat",linearMeasurement:"Pagsukat ng guhit",pathDistance:"Distansya ng daanan",centerOnArea:"I-sentro sa lugar na ito",centerOnLine:"I-sentro sa linya na ito",centerOnLocation:"I-sentro sa lokasyong ito",cancel:"Kanselahin",delete:"Tanggalin",acres:"Acres",feet:"Talampakan",kilometers:"Kilometro",hectares:"Hektarya",meters:"Metro",miles:"Milya",sqfeet:"Talampakang Kwadrado",sqmeters:"Metro Kwadrado",sqmiles:"Milya Kwadrado",decPoint:".",thousandsSep:","}},{}],35:[function(a,b,c){b.exports={measure:"Mesure",measureDistancesAndAreas:"Mesurer les distances et superficies",createNewMeasurement:"Créer une nouvelle mesure",startCreating:"Débuter la création d'une nouvelle mesure en ajoutant des points sur la carte",finishMeasurement:"Finir la mesure",lastPoint:"Dernier point",area:"Superficie",perimeter:"Périmètre",pointLocation:"Placement du point",areaMeasurement:"Mesure de superficie",linearMeasurement:"Mesure linéaire",pathDistance:"Distance du chemin",centerOnArea:"Centrer sur cette zone",centerOnLine:"Centrer sur cette ligne",centerOnLocation:"Centrer à cet endroit",cancel:"Annuler",delete:"Supprimer",acres:"Acres",feet:"Pieds",kilometers:"Kilomètres",hectares:"Hectares",meters:"Mètres",miles:"Miles",sqfeet:"Pieds carrés",sqmeters:"Mètres carrés",sqmiles:"Miles carrés",decPoint:",",thousandsSep:" "}},{}],36:[function(a,b,c){b.exports={measure:"Misura",measureDistancesAndAreas:"Misura distanze e aree",createNewMeasurement:"Crea una nuova misurazione",startCreating:"Comincia a creare una misurazione aggiungendo punti alla mappa",finishMeasurement:"Misurazione conclusa",lastPoint:"Ultimo punto",area:"Area",perimeter:"Perimetro",pointLocation:"Posizione punto",areaMeasurement:"Misura area",linearMeasurement:"Misura lineare",pathDistance:"Distanza percorso",centerOnArea:"Centra su questa area",centerOnLine:"Centra su questa linea",centerOnLocation:"Centra su questa posizione",cancel:"Annulla",delete:"Cancella",acres:"Acri",feet:"Piedi",kilometers:"Chilometri",hectares:"Ettari",meters:"Metri",miles:"Miglia",sqfeet:"Piedi quadri",sqmeters:"Metri quadri",sqmiles:"Miglia quadre",decPoint:".",thousandsSep:","}},{}],37:[function(a,b,c){b.exports={measure:"Meet",measureDistancesAndAreas:"Meet afstanden en oppervlakten",createNewMeasurement:"Maak een nieuwe meting",startCreating:"Begin een meting door punten toe te voegen aan de kaart",finishMeasurement:"Beëindig meting",lastPoint:"Laatste punt",area:"Oppervlakte",perimeter:"Omtrek",pointLocation:"Locatie punt",areaMeasurement:"Oppervlakte meting",linearMeasurement:"Gemeten afstand",pathDistance:"Afstand over de lijn",centerOnArea:"Centreer op dit gebied",centerOnLine:"Centreer op deze lijn",centerOnLocation:"Centreer op deze locatie",cancel:"Annuleer",delete:"Wis",acres:"are",feet:"Voet",kilometers:"km",hectares:"ha",meters:"m",miles:"Mijl",sqfeet:"Vierkante Feet",sqmeters:"m2",sqmiles:"Vierkante Mijl",decPoint:",",thousandsSep:"."}},{}],38:[function(a,b,c){b.exports={measure:"Pomiar",measureDistancesAndAreas:"Pomiar odległości i powierzchni",createNewMeasurement:"Utwórz nowy pomiar",startCreating:"Rozpocznij tworzenie nowego pomiaru poprzez dodanie punktów na mapie",finishMeasurement:"Zakończ pomiar",lastPoint:"Ostatni punkt",area:"Powierzchnia",perimeter:"Obwód",pointLocation:"Punkt lokalizacji",areaMeasurement:"Pomiar powierzchni",linearMeasurement:"Pomiar liniowy",pathDistance:"Długość ścieżki",centerOnArea:"Środek tego obszaru",centerOnLine:"Środek tej linii",centerOnLocation:"Środek w tej lokalizacji",cancel:"Anuluj",delete:"Skasuj",acres:"akrów",feet:"stóp",kilometers:"kilometrów",hectares:"hektarów",meters:"metrów",miles:"mil",sqfeet:"stóp kwadratowych",sqmeters:"metrów kwadratowych",sqmiles:"mil kwadratowych",decPoint:",",thousandsSep:"."}},{}],39:[function(a,b,c){b.exports={measure:"Medidas",measureDistancesAndAreas:"Mede distâncias e áreas",createNewMeasurement:"Criar nova medida",startCreating:"Comece criando uma medida, adicionando pontos no mapa",finishMeasurement:"Finalizar medida",lastPoint:"Último ponto",area:"Área",perimeter:"Perímetro",pointLocation:"Localização do ponto",areaMeasurement:"Medida de área",linearMeasurement:"Medida linear",pathDistance:"Distância",centerOnArea:"Centralizar nesta área",centerOnLine:"Centralizar nesta linha",centerOnLocation:"Centralizar nesta localização",cancel:"Cancelar",delete:"Excluir",acres:"Acres",feet:"Pés",kilometers:"Quilômetros",hectares:"Hectares",meters:"Metros",miles:"Milhas",sqfeet:"Pés²",sqmeters:"Metros²",sqmiles:"Milhas²",decPoint:",",thousandsSep:"."}},{}],40:[function(a,b,c){b.exports={measure:"Medições",measureDistancesAndAreas:"Medir distâncias e áreas",createNewMeasurement:"Criar uma nova medição",startCreating:"Adicione pontos no mapa, para criar uma nova medição",finishMeasurement:"Finalizar medição",lastPoint:"Último ponto",area:"Área",perimeter:"Perímetro",pointLocation:"Localização do ponto",areaMeasurement:"Medição da área",linearMeasurement:"Medição linear",pathDistance:"Distância",centerOnArea:"Centrar nesta área",centerOnLine:"Centrar nesta linha",centerOnLocation:"Centrar nesta localização",cancel:"Cancelar",delete:"Eliminar",acres:"Acres",feet:"Pés",kilometers:"Kilômetros",hectares:"Hectares",meters:"Metros",miles:"Milhas",sqfeet:"Pés²",sqmeters:"Metros²",sqmiles:"Milhas²",decPoint:",",thousandsSep:"."}},{}],41:[function(a,b,c){b.exports={measure:"Измерение",measureDistancesAndAreas:"Измерение расстояний и площади",createNewMeasurement:"Создать новое измерение",startCreating:"Для начала измерения добавьте точку на карту",finishMeasurement:"Закончить измерение",lastPoint:"Последняя точка",area:"Область",perimeter:"Периметр",pointLocation:"Местоположение точки",areaMeasurement:"Измерение области",linearMeasurement:"Линейное измерение",pathDistance:"Расстояние",centerOnArea:"Сфокусироваться на данной области",centerOnLine:"Сфокусироваться на данной линии",centerOnLocation:"Сфокусироваться на данной местности",cancel:"Отменить",delete:"Удалить",acres:"акры",feet:"фут",kilometers:"км",hectares:"га",meters:"м",miles:"миль",sqfeet:"футов²",sqmeters:"м²",sqmiles:"миль²",decPoint:".",thousandsSep:","}},{}],42:[function(a,b,c){b.exports={measure:"Mäta",measureDistancesAndAreas:"Mäta avstånd och yta",createNewMeasurement:"Skapa ny mätning",startCreating:"Börja mätning genom att lägga till punkter på kartan",finishMeasurement:"Avsluta mätning",lastPoint:"Sista punkt",area:"Yta",perimeter:"Omkrets",pointLocation:"Punktens Läge",areaMeasurement:"Arealmätning",linearMeasurement:"Längdmätning",pathDistance:"Total linjelängd",centerOnArea:"Centrera på detta område",centerOnLine:"Centrera på denna linje",centerOnLocation:"Centrera på denna punkt",cancel:"Avbryt",delete:"Radera",acres:"Tunnland",feet:"Fot",kilometers:"Kilometer",hectares:"Hektar",meters:"Meter",miles:"Miles",sqfeet:"Kvadratfot",sqmeters:"Kvadratmeter",sqmiles:"Kvadratmiles",decPoint:",",thousandsSep:" "}},{}],43:[function(a,b,c){b.exports={measure:"Hesapla",measureDistancesAndAreas:"Uzaklık ve alan hesapla",createNewMeasurement:"Yeni hesaplama",startCreating:"Yeni nokta ekleyerek hesaplamaya başla",finishMeasurement:"Hesaplamayı bitir",lastPoint:"Son nokta",area:"Alan",perimeter:"Çevre uzunluğu",pointLocation:"Nokta yeri",areaMeasurement:"Alan hesaplaması",linearMeasurement:"Doğrusal hesaplama",pathDistance:"Yol uzunluğu",centerOnArea:"Bu alana odaklan",centerOnLine:"Bu doğtuya odaklan",centerOnLocation:"Bu yere odaklan",cancel:"Çıkış",delete:"Sil",acres:"Dönüm",feet:"Feet",kilometers:"Kilometre",hectares:"Hektar",meters:"Metre",miles:"Mil",sqfeet:"Feet kare",sqmeters:"Metre kare",sqmiles:"Mil kare",decPoint:".",thousandsSep:","}},{}],44:[function(a,b,c){(function(b){var c=a("underscore"),d="undefined"!=typeof window?window.L:"undefined"!=typeof b?b.L:null,e=a("humanize"),f=a("./units"),g=a("./calc"),h=a("./dom"),i=h.$,j=a("./mapsymbology"),k=c.template('<%= i18n.__(\'measure\') %>\n\n
\n
<%= i18n.__(\'measureDistancesAndAreas\') %>
\n
\n
\n
\n
<%= i18n.__(\'measureDistancesAndAreas\') %>
\n
<%= i18n.__(\'startCreating\') %>
\n
\n
\n
\n
'),l=c.template('\n
<%= i18n.__(\'lastPoint\') %>
\n
<%= model.lastCoord.dms.y %> / <%= model.lastCoord.dms.x %>
\n
<%= humanize.numberFormat(model.lastCoord.dd.y, 6) %> / <%= humanize.numberFormat(model.lastCoord.dd.x, 6) %>
\n
\n<% if (model.pointCount > 1) { %>\n\n
<%= i18n.__(\'pathDistance\') %> <%= model.lengthDisplay %>
\n
\n<% } %>\n<% if (model.pointCount > 2) { %>\n\n
<%= i18n.__(\'area\') %> <%= model.areaDisplay %>
\n
\n<% } %>'),m=c.template('<%= i18n.__(\'pointLocation\') %>
\n<%= model.lastCoord.dms.y %> / <%= model.lastCoord.dms.x %>
\n<%= humanize.numberFormat(model.lastCoord.dd.y, 6) %> / <%= humanize.numberFormat(model.lastCoord.dd.x, 6) %>
\n'),n=c.template('<%= i18n.__(\'linearMeasurement\') %>
\n<%= model.lengthDisplay %>
\n'),o=c.template('<%= i18n.__(\'areaMeasurement\') %>
\n<%= model.areaDisplay %>
\n<%= model.lengthDisplay %> <%= i18n.__(\'perimeter\') %>
\n'),p=new(a("i18n-2"))({devMode:!1,locales:{ca:a("./i18n/ca"),cn:a("./i18n/cn"),da:a("./i18n/da"),de:a("./i18n/de"),de_CH:a("./i18n/de_CH"),en:a("./i18n/en"),en_UK:a("./i18n/en_UK"),es:a("./i18n/es"),fa:a("./i18n/fa"),fil_PH:a("./i18n/fil_PH"),fr:a("./i18n/fr"),it:a("./i18n/it"),nl:a("./i18n/nl"),pl:a("./i18n/pl"),pt_BR:a("./i18n/pt_BR"),pt_PT:a("./i18n/pt_PT"),ru:a("./i18n/ru"),sv:a("./i18n/sv"),tr:a("./i18n/tr")}});d.Control.Measure=d.Control.extend({_className:"leaflet-control-measure",options:{units:{},position:"topright",primaryLengthUnit:"feet",secondaryLengthUnit:"miles",primaryAreaUnit:"acres",activeColor:"#ABE67E",completedColor:"#C8F2BE",captureZIndex:1e4,popupOptions:{className:"leaflet-measure-resultpopup",autoPanPadding:[10,10]}},initialize:function(a){d.setOptions(this,a),this.options.units=d.extend({},f,this.options.units),this._symbols=new j(c.pick(this.options,"activeColor","completedColor")),p.setLocale(this.options.localization)},onAdd:function(a){return this._map=a,this._latlngs=[],this._initLayout(),a.on("click",this._collapse,this),this._layer=d.layerGroup().addTo(a),this._container},onRemove:function(a){a.off("click",this._collapse,this),a.removeLayer(this._layer)},_initLayout:function(){var a,b,c,e,f=this._className,g=this._container=d.DomUtil.create("div",f);g.innerHTML=k({model:{className:f},i18n:p}),g.setAttribute("aria-haspopup",!0),d.Browser.touch?d.DomEvent.on(g,"click",d.DomEvent.stopPropagation):(d.DomEvent.disableClickPropagation(g),d.DomEvent.disableScrollPropagation(g)),a=this.$toggle=i(".js-toggle",g),this.$interaction=i(".js-interaction",g),b=i(".js-start",g),c=i(".js-cancel",g),e=i(".js-finish",g),this.$startPrompt=i(".js-startprompt",g),this.$measuringPrompt=i(".js-measuringprompt",g),this.$startHelp=i(".js-starthelp",g),this.$results=i(".js-results",g),this.$measureTasks=i(".js-measuretasks",g),this._collapse(),this._updateMeasureNotStarted(),d.Browser.android||(d.DomEvent.on(g,"mouseenter",this._expand,this),d.DomEvent.on(g,"mouseleave",this._collapse,this)),d.DomEvent.on(a,"click",d.DomEvent.stop),d.Browser.touch?d.DomEvent.on(a,"click",this._expand,this):d.DomEvent.on(a,"focus",this._expand,this),d.DomEvent.on(b,"click",d.DomEvent.stop),d.DomEvent.on(b,"click",this._startMeasure,this),d.DomEvent.on(c,"click",d.DomEvent.stop),d.DomEvent.on(c,"click",this._finishMeasure,this),d.DomEvent.on(e,"click",d.DomEvent.stop),d.DomEvent.on(e,"click",this._handleMeasureDoubleClick,this);
-},_expand:function(){h.hide(this.$toggle),h.show(this.$interaction)},_collapse:function(){this._locked||(h.hide(this.$interaction),h.show(this.$toggle))},_updateMeasureNotStarted:function(){h.hide(this.$startHelp),h.hide(this.$results),h.hide(this.$measureTasks),h.hide(this.$measuringPrompt),h.show(this.$startPrompt)},_updateMeasureStartedNoPoints:function(){h.hide(this.$results),h.show(this.$startHelp),h.show(this.$measureTasks),h.hide(this.$startPrompt),h.show(this.$measuringPrompt)},_updateMeasureStartedWithPoints:function(){h.hide(this.$startHelp),h.show(this.$results),h.show(this.$measureTasks),h.hide(this.$startPrompt),h.show(this.$measuringPrompt)},_startMeasure:function(){this._locked=!0,this._measureVertexes=d.featureGroup().addTo(this._layer),this._captureMarker=d.marker(this._map.getCenter(),{clickable:!0,zIndexOffset:this.options.captureZIndex,opacity:0}).addTo(this._layer),this._setCaptureMarkerIcon(),this._captureMarker.on("mouseout",this._handleMapMouseOut,this).on("dblclick",this._handleMeasureDoubleClick,this).on("click",this._handleMeasureClick,this),this._map.on("mousemove",this._handleMeasureMove,this).on("mouseout",this._handleMapMouseOut,this).on("move",this._centerCaptureMarker,this).on("resize",this._setCaptureMarkerIcon,this),d.DomEvent.on(this._container,"mouseenter",this._handleMapMouseOut,this),this._updateMeasureStartedNoPoints(),this._map.fire("measurestart",null,!1)},_finishMeasure:function(){var a=c.extend({},this._resultsModel,{points:this._latlngs});this._locked=!1,d.DomEvent.off(this._container,"mouseover",this._handleMapMouseOut,this),this._clearMeasure(),this._captureMarker.off("mouseout",this._handleMapMouseOut,this).off("dblclick",this._handleMeasureDoubleClick,this).off("click",this._handleMeasureClick,this),this._map.off("mousemove",this._handleMeasureMove,this).off("mouseout",this._handleMapMouseOut,this).off("move",this._centerCaptureMarker,this).off("resize",this._setCaptureMarkerIcon,this),this._layer.removeLayer(this._measureVertexes).removeLayer(this._captureMarker),this._measureVertexes=null,this._updateMeasureNotStarted(),this._collapse(),this._map.fire("measurefinish",a,!1)},_clearMeasure:function(){this._latlngs=[],this._resultsModel=null,this._measureVertexes.clearLayers(),this._measureDrag&&this._layer.removeLayer(this._measureDrag),this._measureArea&&this._layer.removeLayer(this._measureArea),this._measureBoundary&&this._layer.removeLayer(this._measureBoundary),this._measureDrag=null,this._measureArea=null,this._measureBoundary=null},_centerCaptureMarker:function(){this._captureMarker.setLatLng(this._map.getCenter())},_setCaptureMarkerIcon:function(){this._captureMarker.setIcon(d.divIcon({iconSize:this._map.getSize().multiplyBy(2)}))},_getMeasurementDisplayStrings:function(a){function b(a,b,e,f,g){var h;return b&&d[b]?(h=c(a,d[b],f,g),e&&d[e]&&(h=h+" ("+c(a,d[e],f,g)+")")):h=c(a,null,f,g),h}function c(a,b,c,d){return b&&b.factor&&b.display?e.numberFormat(a*b.factor,b.decimals||0,c||p.__("decPoint"),d||p.__("thousandsSep"))+" "+p.__([b.display])||b.display:e.numberFormat(a,0,c||p.__("decPoint"),d||p.__("thousandsSep"))}var d=this.options.units;return{lengthDisplay:b(a.length,this.options.primaryLengthUnit,this.options.secondaryLengthUnit,this.options.decPoint,this.options.thousandsSep),areaDisplay:b(a.area,this.options.primaryAreaUnit,this.options.secondaryAreaUnit,this.options.decPoint,this.options.thousandsSep)}},_updateResults:function(){var a=g.measure(this._latlngs),b=this._resultsModel=c.extend({},a,this._getMeasurementDisplayStrings(a),{pointCount:this._latlngs.length});this.$results.innerHTML=l({model:b,humanize:e,i18n:p})},_handleMeasureMove:function(a){this._measureDrag?this._measureDrag.setLatLng(a.latlng):this._measureDrag=d.circleMarker(a.latlng,this._symbols.getSymbol("measureDrag")).addTo(this._layer),this._measureDrag.bringToFront()},_handleMeasureDoubleClick:function(){var a,b,f,h,j,k,l=this._latlngs;this._finishMeasure(),l.length&&(l.length>2&&l.push(c.first(l)),a=g.measure(l),1===l.length?(b=d.circleMarker(l[0],this._symbols.getSymbol("resultPoint")),h=m({model:a,humanize:e,i18n:p})):2===l.length?(b=d.polyline(l,this._symbols.getSymbol("resultLine")),h=n({model:c.extend({},a,this._getMeasurementDisplayStrings(a)),humanize:e,i18n:p})):(b=d.polygon(l,this._symbols.getSymbol("resultArea")),h=o({model:c.extend({},a,this._getMeasurementDisplayStrings(a)),humanize:e,i18n:p})),f=d.DomUtil.create("div",""),f.innerHTML=h,j=i(".js-zoomto",f),j&&(d.DomEvent.on(j,"click",d.DomEvent.stop),d.DomEvent.on(j,"click",function(){b.getBounds?this._map.fitBounds(b.getBounds(),{padding:[20,20],maxZoom:17}):b.getLatLng&&this._map.panTo(b.getLatLng())},this)),k=i(".js-deletemarkup",f),k&&(d.DomEvent.on(k,"click",d.DomEvent.stop),d.DomEvent.on(k,"click",function(){this._layer.removeLayer(b)},this)),b.addTo(this._layer),b.bindPopup(f,this.options.popupOptions),b.getBounds?b.openPopup(b.getBounds().getCenter()):b.getLatLng&&b.openPopup(b.getLatLng()))},_handleMeasureClick:function(a){var b=this._map.mouseEventToLatLng(a.originalEvent),d=c.last(this._latlngs),e=this._symbols.getSymbol("measureVertex");d&&b.equals(d)||(this._latlngs.push(b),this._addMeasureArea(this._latlngs),this._addMeasureBoundary(this._latlngs),this._measureVertexes.eachLayer(function(a){a.setStyle(e),a._path.setAttribute("class",e.className)}),this._addNewVertex(b),this._measureBoundary&&this._measureBoundary.bringToFront(),this._measureVertexes.bringToFront()),this._updateResults(),this._updateMeasureStartedWithPoints()},_handleMapMouseOut:function(){this._measureDrag&&(this._layer.removeLayer(this._measureDrag),this._measureDrag=null)},_addNewVertex:function(a){d.circleMarker(a,this._symbols.getSymbol("measureVertexActive")).addTo(this._measureVertexes)},_addMeasureArea:function(a){return a.length<3?void(this._measureArea&&(this._layer.removeLayer(this._measureArea),this._measureArea=null)):void(this._measureArea?this._measureArea.setLatLngs(a):this._measureArea=d.polygon(a,this._symbols.getSymbol("measureArea")).addTo(this._layer))},_addMeasureBoundary:function(a){return a.length<2?void(this._measureBoundary&&(this._layer.removeLayer(this._measureBoundary),this._measureBoundary=null)):void(this._measureBoundary?this._measureBoundary.setLatLngs(a):this._measureBoundary=d.polyline(a,this._symbols.getSymbol("measureBoundary")).addTo(this._layer))}}),d.Map.mergeOptions({measureControl:!1}),d.Map.addInitHook(function(){this.options.measureControl&&(this.measureControl=(new d.Control.Measure).addTo(this))}),d.control.measure=function(a){return new d.Control.Measure(a)}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./calc":23,"./dom":24,"./i18n/ca":25,"./i18n/cn":26,"./i18n/da":27,"./i18n/de":28,"./i18n/de_CH":29,"./i18n/en":30,"./i18n/en_UK":31,"./i18n/es":32,"./i18n/fa":33,"./i18n/fil_PH":34,"./i18n/fr":35,"./i18n/it":36,"./i18n/nl":37,"./i18n/pl":38,"./i18n/pt_BR":39,"./i18n/pt_PT":40,"./i18n/ru":41,"./i18n/sv":42,"./i18n/tr":43,"./mapsymbology":45,"./units":46,humanize:16,"i18n-2":18,underscore:22}],45:[function(a,b,c){var d=a("underscore"),e=a("color"),f=function(a){this.setOptions(a)};f.DEFAULTS={activeColor:"#ABE67E",completedColor:"#C8F2BE"},d.extend(f.prototype,{setOptions:function(a){return this._options=d.extend({},f.DEFAULTS,this._options,a),this},getSymbol:function(a){var b={measureDrag:{clickable:!1,radius:4,color:this._options.activeColor,weight:2,opacity:.7,fillColor:this._options.activeColor,fillOpacity:.5,className:"layer-measuredrag"},measureArea:{clickable:!1,stroke:!1,fillColor:this._options.activeColor,fillOpacity:.2,className:"layer-measurearea"},measureBoundary:{clickable:!1,color:this._options.activeColor,weight:2,opacity:.9,fill:!1,className:"layer-measureboundary"},measureVertex:{clickable:!1,radius:4,color:this._options.activeColor,weight:2,opacity:1,fillColor:this._options.activeColor,fillOpacity:.7,className:"layer-measurevertex"},measureVertexActive:{clickable:!1,radius:4,color:this._options.activeColor,weight:2,opacity:1,fillColor:e(this._options.activeColor).darken(.15),fillOpacity:.7,className:"layer-measurevertex active"},resultArea:{clickable:!0,color:this._options.completedColor,weight:2,opacity:.9,fillColor:this._options.completedColor,fillOpacity:.2,className:"layer-measure-resultarea"},resultLine:{clickable:!0,color:this._options.completedColor,weight:3,opacity:.9,fill:!1,className:"layer-measure-resultline"},resultPoint:{clickable:!0,radius:4,color:this._options.completedColor,weight:2,opacity:1,fillColor:this._options.completedColor,fillOpacity:.7,className:"layer-measure-resultpoint"}};return b[a]}}),b.exports=f},{color:6,underscore:22}],46:[function(a,b,c){b.exports={acres:{factor:24711e-8,display:"acres",decimals:2},feet:{factor:3.2808,display:"feet",decimals:0},kilometers:{factor:.001,display:"kilometers",decimals:2},hectares:{factor:1e-4,display:"hectares",decimals:2},meters:{factor:1,display:"meters",decimals:0},miles:{factor:3.2808/5280,display:"miles",decimals:2},sqfeet:{factor:10.7639,display:"sqfeet",decimals:0},sqmeters:{factor:1,display:"sqmeters",decimals:0},sqmiles:{factor:3.86102e-7,display:"sqmiles",decimals:2}}},{}]},{},[44]);
\ No newline at end of file
diff --git a/plugins/measure/public/main.js b/plugins/measure/public/main.js
index 5fbc55a58..d89eb536d 100644
--- a/plugins/measure/public/main.js
+++ b/plugins/measure/public/main.js
@@ -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);
});
diff --git a/plugins/measure/public/package.json b/plugins/measure/public/package.json
new file mode 100644
index 000000000..5066d7c5e
--- /dev/null
+++ b/plugins/measure/public/package.json
@@ -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"
+ }
+}
diff --git a/plugins/measure/public/webpack.config.js b/plugins/measure/public/webpack.config.js
new file mode 100644
index 000000000..391672a94
--- /dev/null
+++ b/plugins/measure/public/webpack.config.js
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/plugins/posm-gcpi/plugin.py b/plugins/posm-gcpi/plugin.py
index 9d713846b..b09cd720a 100644
--- a/plugins/posm-gcpi/plugin.py
+++ b/plugins/posm-gcpi/plugin.py
@@ -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'}))
]
diff --git a/plugins/test/plugin.py b/plugins/test/plugin.py
index 6fcb6ef59..68dc25899 100644
--- a/plugins/test/plugin.py
+++ b/plugins/test/plugin.py
@@ -12,7 +12,7 @@ def include_js_files(self):
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'}))
]
diff --git a/plugins/test/public/package.json b/plugins/test/public/package.json
new file mode 100644
index 000000000..46cba507d
--- /dev/null
+++ b/plugins/test/public/package.json
@@ -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"
+ }
+}
diff --git a/plugins/volume/__init__.py b/plugins/volume/__init__.py
deleted file mode 100644
index 48aad58ec..000000000
--- a/plugins/volume/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .plugin import *
diff --git a/plugins/volume/disabled b/plugins/volume/disabled
deleted file mode 100644
index e69de29bb..000000000
diff --git a/plugins/volume/manifest.json b/plugins/volume/manifest.json
deleted file mode 100644
index c4d68fe42..000000000
--- a/plugins/volume/manifest.json
+++ /dev/null
@@ -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
-}
\ No newline at end of file
diff --git a/plugins/volume/plugin.py b/plugins/volume/plugin.py
deleted file mode 100644
index d9513cd19..000000000
--- a/plugins/volume/plugin.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from app.plugins import PluginBase
-
-class Plugin(PluginBase):
- def include_js_files(self):
- return ['hello.js']
\ No newline at end of file
diff --git a/plugins/volume/public/hello.js b/plugins/volume/public/hello.js
deleted file mode 100644
index cf511f6a1..000000000
--- a/plugins/volume/public/hello.js
+++ /dev/null
@@ -1,6 +0,0 @@
-PluginsAPI.Map.willAddControls(function(options){
- console.log("GOT: ", options);
-});
-PluginsAPI.Map.didAddControls(function(options){
- console.log("GOT2: ", options);
-});
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 923ddd50a..b81604fcc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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
diff --git a/webodm/settings.py b/webodm/settings.py
index 4682fea94..7adfbe7fe 100644
--- a/webodm/settings.py
+++ b/webodm/settings.py
@@ -249,6 +249,7 @@
# 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'
diff --git a/webpack.config.js b/webpack.config.js
index a00512680..c729b948b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -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"
}
}
\ No newline at end of file
diff --git a/worker/tasks.py b/worker/tasks.py
index 672993e70..ad84d38dc 100644
--- a/worker/tasks.py
+++ b/worker/tasks.py
@@ -8,6 +8,7 @@
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)}
\ No newline at end of file