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