diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..ddd5496a5f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,12 @@ +{ + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + }, + "env": { + "browser": true, + }, + "rules": { + "semi": 2 + } +} diff --git a/bin/run-celery.sh b/bin/run-celery.sh index c45bedac56..564cd0896f 100755 --- a/bin/run-celery.sh +++ b/bin/run-celery.sh @@ -2,4 +2,5 @@ # Run Celery as the DesignSafe Community Account su tg458981 -c "celery -A designsafe beat -l info --pidfile= --schedule=/tmp/celerybeat-schedule" & -su tg458981 -c "celery -A designsafe worker -l info --autoscale=10,2" +su tg458981 -c "celery -A designsafe worker -l info --autoscale=15,5 -Q indexing,files" & +su tg458981 -c "celery -A designsafe worker -l info --autoscale=10,3 -Q default,api" diff --git a/bower.json b/bower.json index d5b845e080..62ca44b090 100644 --- a/bower.json +++ b/bower.json @@ -35,8 +35,12 @@ "angular-drag-and-drop-lists": "1.4.0", "angular-xeditable": "0.1.12", "angular-httpi": "*", + "angular-slick-carousel": "^3.1.7", + "exif-js": "^2.1.1", "d3": "^4.4.0", - "angular-slick-carousel": "^3.1.7" + "angular-native-dragdrop": "^1.2.2", + "leaflet-measure": "^2.1.7", + "tether": "^1.4.0" }, "resolutions": { "angular": "~1.4.8", diff --git a/conf/nginx.conf b/conf/nginx.conf index 4eef987a97..a094a1824c 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -52,6 +52,10 @@ http { ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + location /uploads { + alias /corral-repl/tacc/NHERI/uploads; + } + location /media { alias /var/www/designsafe-ci.org/media; } diff --git a/designsafe.env.sample b/designsafe.env.sample index d62107c242..fa05036022 100644 --- a/designsafe.env.sample +++ b/designsafe.env.sample @@ -78,12 +78,24 @@ AGAVE_TENANT_BASEURL=https://agave.designsafe-ci.org AGAVE_CLIENT_KEY= AGAVE_CLIENT_SECRET= AGAVE_TOKEN_SESSION_ID= +AGAVE_JWT_PUBKEY= # Long-lived token used by the portal for "admin" actions; obtain from API store interface AGAVE_SUPER_TOKEN= +AGAVE_JWT_ISSUER= +AGAVE_JWT_HEADER= +AGAVE_JWT_USER_CLAIM_FIELD= AGAVE_STORAGE_SYSTEM=designsafe.storage.default +PROJECT_SYSTEM_STORAGE_CREDENTIALS= + +BOX_APP_CLIENT_ID= +BOX_APP_CLIENT_SECRET= + +DROPBOX_APP_KEY= +DROPBOX_APP_SECRET= + # djangoRT RT_HOST=https://hostname.example.com/REST/1.0/ RT_USERNAME=username @@ -100,3 +112,27 @@ NEES_DATABASE_HOST= NEES_DATABASE_PORT= NEES_DATABASE_USER= NEES_DATABASE_PASSWORD= + + +# FOR CMS/RECAPTCHA +DJANGOCMS_FORMS_RECAPTCHA_PUBLIC_KEY= +DJANGOCMS_FORMS_RECAPTCHA_SECRET_KEY= + +FLOWER_BROKER= +FLOWER_BROKER_API= + +#CELERY config + +BROKER_URL_PROTOCOL=amqp:// +BROKER_URL_USERNAME= +BROKER_URL_PWD= +BROKER_URL_HOST= +BROKER_URL_PORT= +BROKER_URL_VHOST= +#BROKER_URL = 'amqp://designsafe:pwd@rabbitmq:5672//' +CELERY_RESULT_BACKEND_PROTOCOL= +CELERY_RESULT_BACKEND_USERNAME= +CELERY_RESULT_BACKEND_PWD= +CELERY_RESULT_BACKEND_HOST= +CELERY_RESULT_BACKEND_PORT= +CELERY_RESULT_BACKEND_DB= diff --git a/designsafe/apps/accounts/templates/designsafe/apps/accounts/base.html b/designsafe/apps/accounts/templates/designsafe/apps/accounts/base.html index 8c639dc731..6f28625388 100644 --- a/designsafe/apps/accounts/templates/designsafe/apps/accounts/base.html +++ b/designsafe/apps/accounts/templates/designsafe/apps/accounts/base.html @@ -26,7 +26,7 @@

{{title}}

  • Licenses
  • {% url 'designsafe_accounts:manage_notifications' as item_url %} -
  • Notifications
  • +
  • Notification Settings
  • {% url 'designsafe_accounts:manage_applications' as item_url %}
  • 3rd Party Apps
  • diff --git a/designsafe/apps/api/agave/filemanager/agave.py b/designsafe/apps/api/agave/filemanager/agave.py index dcb20ed147..4938122b4b 100644 --- a/designsafe/apps/api/agave/filemanager/agave.py +++ b/designsafe/apps/api/agave/filemanager/agave.py @@ -28,6 +28,8 @@ class AgaveFileManager(BaseFileManager): 'path': '/corral-repl/tacc/NHERI/shared'}, {'regex': r'^designsafe.storage.community$', 'path': '/corral-repl/tacc/NHERI/community'}, + {'regex': r'^designsafe.storage.published$', + 'path': '/corral-repl/tacc/NHERI/published'}, {'regex': r'^project\-', 'path': '/corral-repl/tacc/NHERI/projects'} ] @@ -53,7 +55,8 @@ def import_data(self, system, file_path, from_system, from_file_path): res = f.import_data(from_system, from_file_path) file_name = from_file_path.split('/')[-1] reindex_agave.apply_async(kwargs={'username': 'ds_admin', - 'file_id': '{}/{}'.format(system, os.path.join(file_path, file_name))}) + 'file_id': '{}/{}'.format(system, os.path.join(file_path, file_name))}, + queue='indexing') return res def copy(self, system, file_path, dest_path=None, dest_name=None): @@ -75,7 +78,8 @@ def copy(self, system, file_path, dest_path=None, dest_name=None): # schedule celery task to index new copy reindex_agave.apply_async(kwargs = {'username': 'ds_admin', - 'file_id': '{}/{}/{}'.format(system, dest_path.strip('/'), dest_name)}) + 'file_id': '{}/{}/{}'.format(system, dest_path.strip('/'), dest_name)}, + queue='indexing') return copied_file @@ -84,7 +88,8 @@ def delete(self, system, path): parent_path = '/'.join(path.strip('/').split('/')[:-1]) reindex_agave.apply_async(kwargs = {'username': 'ds_admin', 'file_id': '{}/{}'.format(system, parent_path), - 'levels': 1}) + 'levels': 1}, + queue='indexing') return resp def download(self, system, path): @@ -109,7 +114,8 @@ def mkdir(self, system, file_path, dir_name): f = BaseFileResource(self._ag, system, file_path) resp = f.mkdir(dir_name) reindex_agave.apply_async(kwargs = {'username': 'ds_admin', - 'file_id': '{}/{}'.format(system, file_path)}) + 'file_id': '{}/{}'.format(system, file_path)}, + queue='indexing') return resp def move(self, system, file_path, dest_path, dest_name=None): @@ -119,10 +125,12 @@ def move(self, system, file_path, dest_path, dest_name=None): parent_path = parent_path.strip('/') or '/' reindex_agave.apply_async(kwargs = {'username': 'ds_admin', 'file_id': '{}/{}'.format(system, parent_path), - 'levels': 1}) + 'levels': 1}, + queue='indexing') reindex_agave.apply_async(kwargs = {'username': 'ds_admin', 'file_id': '{}/{}'.format(system, os.path.join(dest_path, resp.name)), - 'levels': 1}) + 'levels': 1}, + queue='indexing') return resp def rename(self, system, file_path, rename_to): @@ -131,7 +139,8 @@ def rename(self, system, file_path, rename_to): parent_path = '/'.join(file_path.strip('/').split('/')[:-1]) reindex_agave.apply_async(kwargs = {'username': 'ds_admin', 'file_id': '{}/{}'.format(system, parent_path), - 'levels': 1}) + 'levels': 1}, + queue='indexing') return resp def share(self, system, file_path, username, permission): @@ -142,7 +151,8 @@ def share(self, system, file_path, username, permission): pem.permission_bit = permission resp = pem.save() reindex_agave.apply_async(kwargs = {'username': 'ds_admin', - 'file_id': '{}/{}'.format(system, file_path)}) + 'file_id': '{}/{}'.format(system, file_path)}, + queue='indexing') return resp def trash(self, system, file_path, trash_path): @@ -171,10 +181,12 @@ def trash(self, system, file_path, trash_path): parent_path = parent_path.strip('/') or '/' reindex_agave.apply_async(kwargs = {'username': 'ds_admin', 'file_id': '{}/{}'.format(system, trash_path), - 'levels': 1}) + 'levels': 1}, + queue='indexing') reindex_agave.apply_async(kwargs = {'username': 'ds_admin', 'file_id': '{}/{}'.format(system, parent_path), - 'levels': 1}) + 'levels': 1}, + queue='indexing') return resp def upload(self, system, file_path, upload_file): @@ -182,5 +194,6 @@ def upload(self, system, file_path, upload_file): resp = f.upload(upload_file) reindex_agave.apply_async(kwargs = {'username': 'ds_admin', 'file_id': '{}/{}'.format(system, file_path), - 'levels': 1}) + 'levels': 1}, + queue='indexing') return resp diff --git a/designsafe/apps/api/agave/filemanager/public_search_index.py b/designsafe/apps/api/agave/filemanager/public_search_index.py index 7feb71f9b7..5ac1f331cb 100644 --- a/designsafe/apps/api/agave/filemanager/public_search_index.py +++ b/designsafe/apps/api/agave/filemanager/public_search_index.py @@ -12,6 +12,7 @@ import os import re import datetime +import itertools from django.conf import settings from elasticsearch import TransportError from elasticsearch_dsl import Search, DocType @@ -38,6 +39,120 @@ except KeyError as e: logger.exception('ELASTIC_SEARCH missing %s' % e) +class PublicationIndexed(DocType): + class Meta: + index = 'published' + doc_type = 'publication' + +class Publication(object): + def __init__(self, wrap=None, project_id=None, *args, **kwargs): + if wrap is not None: + if isinstance(wrap, PublicationIndexed): + self._wrap = wrap + else: + s = PublicationIndexed.search() + s.query = Q({"term": + {"projectId._exact": wrap['projectId']} + }) + try: + res = s.execute() + except TransportError as e: + if e.status_code != 404: + res = s.execute() + + if res.hits.total: + self._wrap = res[0] + raise Exception('Initializing from existent publication ' + 'and a publication object was given. ' + 'Are you sure you want to do this? ') + else: + self._wrap = PublicationIndexed(**wrap) + + elif project_id is not None: + s = PublicationIndexed.search() + s.query = Q({"term": {"projectId._exact": project_id }}) + logger.debug('p serach query: {}'.format(s.to_dict())) + try: + res = s.execute() + except TransportError as e: + if e.status_code == 404: + raise + res = s.execute() + + if res.hits.total: + self._wrap = res[0] + else: + self._wrap = PublicationIndexed(projectId=project_id) + else: + self._wrap = PublicationIndexed() + + @classmethod + def listing(cls): + list_search = PublicSearchManager(cls, + PublicationIndexed.search(), + page_size=100) + list_search._search.query = Q({"match_all":{}}) + list_search.sort({'projectId._exact': 'asc'}) + #s = PublicationIndexed.search() + #s.query = Q({"match_all":{}}) + #try: + # res = s.execute() + #except TransportError as err: + # if err.satus_code == 404: + # raise + # res = s.execute() + + return list_search.results(0) + + @property + def id(self): + return self._wrap.meta.id + + def save(self, **kwargs): + self._wrap.save(**kwargs) + return self + + def to_dict(self): + return self._wrap.to_dict() + + def to_file(self): + dict_obj = {'agavePath': 'agave://designsafe.storage.published/{}'.\ + format(self.project.value.projectId), + 'children': [], + 'deleted': False, + 'format': 'folder', + 'length': 24731027, + 'meta': { + 'title': self.project['value']['title'] + }, + 'name': self.project.value.projectId, + 'path': '/{}'.format(self.project.value.projectId), + 'permissions': 'READ', + 'project': self.project.value.projectId, + 'system': 'designsafe.storage.published', + 'systemId': 'designsafe.storage.published', + 'type': 'dir'} + return dict_obj + + def related_file_paths(self): + dict_obj = self._wrap.to_dict() + related_objs = dict_obj.get('modelConfigs', []) + \ + dict_obj.get('analysisList', []) + \ + dict_obj.get('sensorsList', []) + \ + dict_obj.get('eventsList', []) + file_paths = [] + proj_sys = 'project-{}'.format(dict_obj['project']['uuid']) + for obj in related_objs: + file_paths += obj['_filePaths'] + + return file_paths + + def __getattr__(self, name): + val = getattr(self._wrap, name, None) + if val: + return val + else: + raise AttributeError('\'Publication\' has no attribute \'{}\''.format(name)) class CMSIndexed(DocType): class Meta: @@ -307,7 +422,7 @@ def to_dict(self): obj_dict = self._doc.to_dict() obj_dict['system'] = self.system obj_dict['path'] = self.path - obj_dict['children'] = [doc.to_dict() for doc in self.children] + obj_dict['children'] = [doc.to_dict() if not hasattr(doc, 'projectId') else doc.to_file() for doc in self.children] obj_dict['metadata'] = self.metadata() obj_dict['permissions'] = 'READ' obj_dict['trail'] = self.trail() @@ -338,6 +453,23 @@ def __init__(self): def listing(self, system, file_path, offset=0, limit=100): file_path = file_path or '/' listing = PublicObject.listing(system, file_path, offset, limit) + publications = Publication.listing() + #children = [{'agavePath': 'agave://designsafe.storage.published/{}'.format(pub.project.value.projectId), + # 'children': [], + # 'deleted': False, + # 'format': 'folder', + # 'length': 24731027, + # 'meta': { + # 'title': pub.project['value']['title'] + # }, + # 'name': pub.project.value.projectId, + # 'path': '/{}'.format(pub.project.value.projectId), + # 'permissions': 'READ', + # 'project': pub.project.value.projectId, + # 'system': 'designsafe.storage.published', + # 'systemId': 'designsafe.storage.published', + # 'type': 'dir'} for pub in publications] + listing.children = itertools.chain(publications, listing.children) return listing def search(self, system, query_string, @@ -442,3 +574,13 @@ def search(self, system, query_string, 'permissions': 'READ' } return result + +class PublicationManager(object): + def save_publication(self, publication): + publication['projectId'] = publication['project']['value']['projectId'] + publication['created'] = datetime.datetime.now().isoformat() + publication['status'] = 'publishing' + pub = Publication(publication) + pub.save() + return pub + diff --git a/designsafe/apps/api/agave/filemanager/published.py b/designsafe/apps/api/agave/filemanager/published.py new file mode 100644 index 0000000000..22d1c60913 --- /dev/null +++ b/designsafe/apps/api/agave/filemanager/published.py @@ -0,0 +1,52 @@ +"""File Manager for published Data +""" + +import logging +import json +import os +import re +import datetime +from django.conf import settings +from .base import BaseFileManager +from designsafe.apps.api.agave.filemanager.agave import AgaveFileManager +from designsafe.apps.api.exceptions import ApiException +from designsafe.apps.api.agave.filemanager.public_search_index import Publication + +logger = logging.getLogger(__name__) + +class PublishedFileManager(AgaveFileManager): + NAME = 'community' + DEFAULT_SYSTEM_ID = 'designsafe.storage.published' + + def listing(self, system, file_path, offset=0, limit=100): + path_comps = file_path.strip('/').split('/') + if len(path_comps) < 1: + raise ApiException(messsage='Invalid Action', status=400) + elif len(path_comps) == 1: + project_id = path_comps[0] + publication = Publication(project_id=project_id) + return publication + else: + return super(PublishedFileManager, self).\ + listing(system, file_path, offset, limit) + + def delete(self, *args, **kwargs): + return ApiException(messsage='Invalid Action', status=400) + + def mkdir(self, *args, **kwargs): + return ApiException(messsage='Invalid Action', status=400) + + def move(self, *args, **kwargs): + return ApiException(messsage='Invalid Action', status=400) + + def rename(self, *args, **kwargs): + return ApiException(messsage='Invalid Action', status=400) + + def share(self, *args, **kwargs): + return ApiException(messsage='Invalid Action', status=400) + + def trash(self, *args, **kwargs): + return ApiException(messsage='Invalid Action', status=400) + + def upload(self, *args, **kwargs): + return ApiException(messsage='Invalid Action', status=400) diff --git a/designsafe/apps/api/agave/models/base.py b/designsafe/apps/api/agave/models/base.py new file mode 100644 index 0000000000..ea988ffe4b --- /dev/null +++ b/designsafe/apps/api/agave/models/base.py @@ -0,0 +1,406 @@ +""" Base classes to handle agave metadata objects """ +import inspect +import six +import json +import re +import logging +import datetime + +logger = logging.getLogger(__name__) + +REGISTRY = {} +LAZY_OPS = [] + +class RelatedQuery(object): + def __init__(self, uuid=None, uuids=None, related_obj_name=None, rel_cls=None): + self.uuid = uuid + self.uuids = uuids or [] + self.related_obj_name = related_obj_name + self.rel_cls = rel_cls + query = {'name': related_obj_name, 'associationIds': []} + self._query = query + + def __call__(self, agave_client): + metas = agave_client.meta.listMetadata(q=json.dumps(self.query)) + return [self.rel_cls(**meta) for meta in metas] + + def add(self, uuid): + self.uuids.append(uuid) + + @property + def query(self): + """JSON query to submit to agave metadata endpoint. + + This class represents both a forward-lookup field and a reverse-lookup field. + There are two class attributes ``uuid`` and ``uuids`` (notice the 's'). + IF ``self.uuid`` has a valid value then it means this is a reverse-lookup field + and we need to retrieve all the objects related to this object's specific UUID + AND with a specific object name: ``{"name": "some.name", "associationIds": "UUID"}``. + IF ``self.uuids`` has a valid value (it could be a string or an array of strings) means + this is a forward-lookup field and we need to retrieve every object for every UUID + in the ``self.uuids`` attribute: ``{"uuid": {"$in": ["UUID1", "UUID2"]}}``. + + ..todo:: This class should be separated in two classes, one for reverse-lookup fields + and another one for forward-lookup fields. The reason why it was first implemented like this + is because the implementation of a reverse-lookup field was not completely clear. + This TODO is mainly for readability. + """ + if self.uuid: + self._query['associationIds'] = self.uuid + elif self.uuids is not None: + if isinstance(self.uuids, basestring): + self.uuid = [self.uuids] + elif len(self.uuids) == 0: + return [] + + self._query = {'uuid': {'$in': self.uuids}} + else: + raise ValueError('Cannot create query') + + return self._query + +def register_lazy_rel(cls, field_name, related_obj_name, multiple, rel_cls): + reg_key = '{}.{}'.format(cls.model_name, cls.__name__) + LAZY_OPS.append((reg_key, + field_name, + RelatedQuery(related_obj_name=related_obj_name, rel_cls=rel_cls)) + ) + +def set_lazy_rels(): + for lazy_args in LAZY_OPS: + cls = REGISTRY[lazy_args[0]] + #lazy_args[2].rel_cls = cls + cls._meta._reverse_fields.append(lazy_args[1]) + setattr(cls, lazy_args[1], lazy_args[2]) + + del LAZY_OPS[:] + +def register_class(cls, name, model_name): + registry_key = '{}.{}'.format(model_name, name) + if REGISTRY.get(registry_key) is None: + REGISTRY[registry_key] = cls + +def camelcase_to_spinal(string): + rec = re.compile('([A-Z])+') + return rec.sub(r'_\1', string).lower() + +def spinal_to_camelcase(string): + comps = string.split('_') + if string.startswith('_'): + comps = comps[1:] + first = ''.join(['_', comps[0]]) + else: + first = comps[0] + camel = ''.join(x.capitalize() or '_' for x in comps[1:]) + camel = ''.join([first, camel]) + return camel + +class Manager(object): + def __init__(self, model_cls): + self.model_cls = model_cls + self.agave_client = None + + def set_client(self, agave_client): + self.agave_client = agave_client + setattr(self.model_cls._meta, 'agave_client', agave_client) + + def get(self, agave_client, uuid): + meta = agave_client.meta.getMetadata(uuid=uuid) + return self.model_cls(**meta) + + def list(self, agave_client, association_id=None): + if association_id is None: + metas = agave_client.meta.listMetadata(q=json.dumps({'name': self.model_cls.model_name})) + else: + metas = agave_client.meta.listMetadata(q=json.dumps({'name': self.model_cls.model_name, + 'associationIds': association_id})) + for meta in metas: + yield self.model_cls(**meta) + +class Links(object): + def __init__(self, values): + for attrname, val in six.iteritems(values): + setattr(self, attrname, val) + +class Options(object): + """Options class to store model's _meta data + """ + _model = None + _schema_fields = ['uuid', 'schema_id', 'internal_username', + 'association_ids', 'last_updated', 'created', + 'owner', 'name', '_links'] + + def __init__(self, model_name): + self._nested_fields = {} + self._related_fields = {} + self._reverse_fields = [] + self._fields_map = {} + self._fields = [] + self.name = model_name + self.model_name = model_name + self.model_manager = None + + def add_field(self, field): + if field.nested_cls: + self._nested_fields[field.attname] = field + elif field.related: + self._related_fields[field.attname] = field + else: + self._fields_map[field.attname] = field + self._fields.append(field) + + def contribute_to_class(self, cls, name): + cls._meta = self + self._model = cls + + + +class BaseModel(type): + """ + Metaclass for models + """ + def __new__(cls, name, bases, attrs): + super_new = super(BaseModel, cls).__new__ + + # Also ensure initialization is only performed for subclasses of Model + # (excluding Model class itself). + parents = [b for b in bases if isinstance(b, BaseModel)] + if not parents: + return super_new(cls, name, bases, attrs) + + module = attrs.pop('__module__') + new_attrs = {'__module__': module} + classcell = attrs.pop('__classcell__', None) + if classcell is not None: + new_attrs['__classcell__'] = classcell + + new_class = super_new(cls, name, bases, new_attrs) + model_name = attrs.get('model_name') + new_class.add_to_class('_meta', Options(model_name)) + if attrs.get('_is_nested', False): + setattr(new_class, 'model_name', None) + else: + setattr(new_class, 'model_name', model_name) + + setattr(new_class, '_is_nested', attrs.pop('_is_nested', False)) + for obj_name, obj in attrs.items(): + new_class.add_to_class(obj_name, obj) + + new_class._prepare() + register_class(new_class, name, model_name) + return new_class + + def add_to_class(cls, name, value): + if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'): + value.contribute_to_class(cls, name) + else: + setattr(cls, name, value) + + def _prepare(cls): + opts = cls._meta + if cls.__doc__ is None: + cls.__doc__ = "%s(%s)" % (cls.__name__, ", ".join(f.name for f in opts._fields)) + + #if not opts.manager: + # if any(f.name == 'objects' for f in opts.fields): + # raise ValueError( + # "Model %s must specify a custom Manager, because it has a " + # "field name 'objects'." % cls.__name__ + # ) + # manager = Manager() + # manager.auto_created = True + # cls.add_to_class('objects', manager) + if not opts.model_manager: + setattr(cls._meta, 'model_manager', Manager(cls)) + + if not isinstance(opts.model_manager, Manager): + raise ValueError("Model Manager must be a Manager class.") + + +class Model(object): + __metaclass__ = BaseModel + + def __init__(self, **kwargs): + if not self._is_nested: + self.schema_id = None + self.internal_username = None + self.last_updated = None + self.created = None + self.owner = None + self.__links = None + + self._uuid = None + self._association_ids = [] + self.name = None + self.parent = None + #logger.debug('kwargs: %s', json.dumps(kwargs, indent=4)) + cls = self.__class__ + opts = self._meta + _setattr = setattr + #logger.debug('_is_nested: %s', self._is_nested) + if self._is_nested: + obj_value = kwargs + else: + obj_value = kwargs.pop('value', {}) + links = kwargs.pop('_links', {}) + self._links = Links(links) + for attrname, val in six.iteritems(kwargs): + attrname = camelcase_to_spinal(attrname) + setattr(self, attrname, val) + + for attrname, field in six.iteritems(opts._fields_map): + _setattr(self, attrname, self._get_init_value(field, obj_value, attrname)) + + for attrname, field in six.iteritems(opts._nested_fields): + val = self._get_init_value(field, obj_value, attrname) + nested_obj = field.nested_cls(**val) + nested_obj.parent = self + _setattr(self, attrname, nested_obj) + + for attrname, field in six.iteritems(opts._related_fields): + value = self._get_init_value(field, obj_value, attrname) + if not value and attrname.endswith('_UUID'): + _attr = spinal_to_camelcase(attrname) + _attr = ''.join([_attr[:-4], 'UUID']) + value = obj_value.get(_attr, None) or value + + _setattr(self, attrname, RelatedQuery(uuids=value, rel_cls=field.related)) + + for attrname in opts._reverse_fields: + field = getattr(self, attrname) + field.uuid = self.uuid + + if self.name is None: + self.name = self._meta.model_name + + super(Model, self).__init__() + + def __getattribute__(self, name): + opts = object.__getattribute__(self, '_meta') + _cls = opts._fields_map.get(name, None) + if _cls is not None and hasattr(_cls, 'to_python'): + return _cls.to_python(object.__getattribute__(self, name)) + + return object.__getattribute__(self, name) + + def _get_init_value(self, field, values, name): + attrname = spinal_to_camelcase(name) + return values.get(attrname, field.get_default()) + + @property + def uuid(self): + if not self._uuid and self.parent: + return self.parent.uuid + + return self._uuid + + @uuid.setter + def uuid(self, val): + if not self._uuid and self.parent: + self.parent.uuid = val + else: + self._uuid = val + + @property + def association_ids(self): + if not self._association_ids and self.parent: + return self.parent.association_ids + + return self._association_ids + + @association_ids.setter + def association_ids(self, val): + if not self._association_ids and self.parent: + self.parent.association_ids = val + else: + self._association_ids = val + + def to_dict(self): + dict_obj = {} + for attrname, value in six.iteritems(self._meta.__dict__): + if not attrname.startswith('_'): + dict_obj[attrname] = value + + dict_obj['_links'] = {} + for attrname, value in six.iteritems(self._meta._links.__dict__): + dict_obj['_links'][attrname] = value + + dict_obj['value'] = {} + for field in self._meta._fields: + dict_obj['value'][field.attname] = getattr(self, field.attname) + + dict_obj.pop('model_manager', None) + return dict_obj + + def to_body_dict(self): + from designsafe.apps.api.agave.models.fields import ListField + dict_obj = {} + + if not self._is_nested: + for attrname in self._meta._schema_fields: + value = getattr(self, attrname, None) + if not inspect.isclass(value): + dict_obj[spinal_to_camelcase(attrname)] = value + dict_obj['associationIds'] = list(set(self.association_ids)) + + dict_obj['_links'] = {} + for attrname, value in six.iteritems(self._links.__dict__): + dict_obj['_links'][spinal_to_camelcase(attrname)] = value + + #dict_obj['value'] = {} + value_dict = {} + for field in self._meta._fields: + value = getattr(self, field.attname) + attrname = spinal_to_camelcase(field.attname) + if isinstance(value, RelatedQuery): + value_dict[attrname] = list(set(value.uuids)) + elif isinstance(value, Model): + value_dict[attrname] = value.to_body_dict() + elif isinstance(field, ListField) and field.list_cls is not None: + value_dict[attrname] = [o.to_body_dict() for o in set(value)] + elif isinstance(field, ListField): + value_dict[attrname] = list(set(value)) + else: + value_dict[attrname] = value + if not self._is_nested: + dict_obj['value'] = value_dict + else: + dict_obj = value_dict + + dict_obj.pop('modelManager', None) + dict_obj.pop('modelName', None) + if ('created' in dict_obj and isinstance(dict_obj['created'], datetime.datetime)): + dict_obj['created'] = dict_obj['created'].isoformat() + + if ('lastUpdated' in dict_obj and isinstance(dict_obj['lastUpdated'], datetime.datetime)): + dict_obj['lastUpdated'] = dict_obj['lastUpdated'].isoformat() + + return dict_obj + + def save(self, agave_client): + if self.parent: + self.parent.save(agave_client) + + body = self.to_body_dict() + body.pop('_relatedFields', None) + if self.uuid is None: + logger.debug('Adding Metadata: %s, with: %s', self.name, body) + ret = agave_client.meta.addMetadata(body=body) + else: + logger.debug('Updating Metadata: %s, with: %s', self.uuid, body) + ret = agave_client.meta.updateMetadata(uuid=self.uuid, body=body) + return ret + + def associate(self, value): + _aids = self.association_ids[:] + if isinstance(value, basestring): + _aids.append(value) + else: + _aids += value + + self.association_ids = list(set(_aids)) + return self.association_ids + + @property + def manager(self): + return self._meta.model_manager diff --git a/designsafe/apps/api/agave/models/fields.py b/designsafe/apps/api/agave/models/fields.py new file mode 100644 index 0000000000..eaecb9a3b9 --- /dev/null +++ b/designsafe/apps/api/agave/models/fields.py @@ -0,0 +1,149 @@ +""" Base field classes """ +import six +import datetime +import dateutil.parser +import collections +from decimal import Decimal +from designsafe.apps.api.agave.models.base import register_lazy_rel + +import logging + +logger = logging.getLogger(__name__) + +class NOT_PROVIDED(object): + pass + +class BaseField(object): + """ Base field class """ + related_model = None + + def __init__(self, verbose_name=None, name=None, + max_length=None, blank=False, null=False, + related=None, default=None, choices=None, + help_text='', validators=(), error_messages=None, + nested_cls=None, related_name=None, list_cls=None): + self.verbose_name = verbose_name + self.name = name + self.max_length = max_length + self.blank = blank + self.null = null + self.related = related + self.default = default + self.choices = choices + self.help_text = help_text + self.validators = validators + self.error_messages = error_messages + self.nested_cls = nested_cls + self.attname = None + self.related_name = related_name + self.list_cls = list_cls + + if not isinstance(self.choices, collections.Iterator): + self.choices = [] + else: + self.choices = list( self.choices) + + if not self.name and self.verbose_name: + self.name = self.verbose_name.lower() + self.name = self.name.replace(' ', '_') + + self.choices = self.choices or [] + + def contribute_to_class(self, cls, name): + """ Register the field with the model class it belongs to. """ + if not self.name: + self.name = name + + self.attname = name + if self.verbose_name is None: + self.verbose_name = name.replace('_', ' ') + self.model = cls + cls._meta.add_field(self) + + def get_default(self): + if not isinstance(self.default, NOT_PROVIDED): + return self.default + else: + raise ValueError('No default set') + + def to_python(self, value): + return value + + def clean(self, value): + return self.to_python(value) + +class CharField(BaseField): + """ Char Field """ + def __init__(self, *args, **kwargs): + super(CharField, self).__init__(*args, **kwargs) + + def to_python(self, value): + return unicode(value) + +class UuidField(CharField): + """ Uuid Field """ + def __init__(self, *args, **kwargs): + kwargs['schema_field'] = True + super(UuidField, self).__init__(*args, **kwargs) + +class DateTimeField(BaseField): + """ Date Time Field """ + def __init__(self, *args, **kwargs): + super(DateTimeField, self).__init__(*args, **kwargs) + + def to_python(self, value): + if not isinstance(value, datetime): + return dateutil.parser.parse(value) + + return value + +class IntField(BaseField): + """ Int Field """ + def __init__(self, *args, **kwargs): + super(IntField, self).__init__(*args, **kwargs) + + def to_python(self, value): + return int(value) + +class DecimalField(BaseField): + """ Decimal Field """ + def __init__(self, *args, **kwargs): + super(DecimalField, self).__init__(*args, **kwargs) + + def to_python(self, value): + return Decimal(value) + +class ListField(BaseField): + """ List Field """ + def __init__(self, verbose_name, list_cls=None, *args, **kwargs): + kwargs['default'] = kwargs.get('default', []) + kwargs['list_cls'] = list_cls + kwargs['verbose_name'] = verbose_name + super(ListField, self).__init__(*args, **kwargs) + + def to_python(self, value): + if self.list_cls is not None: + return [self.list_cls(**val) for val in value] + + return list(value) + +class NestedObjectField(BaseField): + """ Nested Object Field """ + def __init__(self, nested_cls, *args, **kwargs): + kwargs['nested_cls'] = nested_cls + kwargs['default'] = kwargs.get('default', {}) + super(NestedObjectField, self).__init__(*args, **kwargs) + +class RelatedObjectField(BaseField): + """ Related Object Field """ + def __init__(self, related, multiple=False, related_name=None, *args, **kwargs): + kwargs['related_name'] = related_name + kwargs['related'] = related + super(RelatedObjectField, self).__init__(*args, **kwargs) + self.multiple = multiple + + def contribute_to_class(self, cls, name): + """ Register the field with the model class it belongs to. """ + super(RelatedObjectField, self).contribute_to_class(cls, name) + related_name = self.related_name or '%s_set' % cls.__name__.lower() + register_lazy_rel(self.related, related_name, cls.model_name, self.multiple, cls) diff --git a/designsafe/apps/api/agave/models/files.py b/designsafe/apps/api/agave/models/files.py index 13766c16fa..b1e809d7ef 100644 --- a/designsafe/apps/api/agave/models/files.py +++ b/designsafe/apps/api/agave/models/files.py @@ -31,14 +31,14 @@ def __init__(self, agave_client, file_obj=None, **kwargs): if not meta_objs: defaults = kwargs defaults['name'] = 'designsafe.file' - defaults['value'] = {'fileUUID': file_obj.uuid, + defaults['value'] = {'fileUuid': file_obj.uuid, 'keywords': []} defaults['associationIds'] = [file_obj.uuid] project_uuid = kwargs.get('project_uuid') if re.search(r'^project-', file_obj.system) or project_uuid: project_uuid = project_uuid or file_obj.system.replace('project-', '', 1) - defaults['value'].update({'projectUUID': project_uuid}) + defaults['value'].update({'projectUuid': project_uuid}) defaults['associationIds'].append(project_uuid) else: defaults = kwargs @@ -98,7 +98,7 @@ def _update_pems_with_system_roles(self, system_roles, meta_pems): return meta_pems_users def match_pems_to_project(self, project_uuid = None): - project_uuid = project_uuid or self.value.get('projectUUID') + project_uuid = project_uuid or self.value.get('projectUUID', self.value.get('projectUuid')) logger.debug('matchins pems to project: %s', project_uuid) if not project_uuid: return self @@ -117,7 +117,7 @@ def save(self): super(BaseFileMetadata, self).save() #self.match_pems_to_project() if self.value.get('projectUUID'): - tasks.check_project_meta_pems.apply_async(args=[self.uuid]) + tasks.check_project_meta_pems.apply_async(args=[self.uuid], queue='api') else: super(BaseFileMetadata, self).save() diff --git a/designsafe/apps/api/agave/views.py b/designsafe/apps/api/agave/views.py index 9c23628324..9a4ac4f318 100644 --- a/designsafe/apps/api/agave/views.py +++ b/designsafe/apps/api/agave/views.py @@ -250,7 +250,8 @@ def put(self, request, file_mgr_name, system_id, file_path): 'dest_resource': external, 'src_file_id': os.path.join(system_id, file_path.strip('/')), 'dest_file_id': body.get('path') - }) + }, + queue='files') event_data[Notification.MESSAGE] = 'Data copy has been scheduled. This will take a few minutes.' event_data[Notification.EXTRA] = { 'resource': external, @@ -414,7 +415,8 @@ def put(self, request, file_mgr_name, system_id, file_path): if file_listing.previewable: preview_url = reverse('designsafe_api:files_media', args=[file_mgr_name, system_id, file_path]) - return JsonResponse({'href': '{}?preview=true'.format(preview_url)}) + return JsonResponse({'href': '{}?preview=true'.format(preview_url), + 'postit': file_listing.download_postit(force=False, lifetime=360)}) else: return HttpResponseBadRequest('Preview not available for this item') except HTTPError as e: @@ -611,7 +613,7 @@ def post(self, request, file_mgr_name, system_id, file_path): Notification.OPERATION: 'data_depot_share', Notification.STATUS: Notification.SUCCESS, Notification.USER: request.user.username, - Notification.MESSAGE: 'Permissions for a file/folder has been updated.', + Notification.MESSAGE: 'Permissions for a file/folder is being updated.', Notification.EXTRA: {'system': system_id, 'path': file_path, 'username': username, @@ -629,7 +631,7 @@ def post(self, request, file_mgr_name, system_id, file_path): Notification.EXTRA: {'message': err.response.text} } Notification.objects.create(**event_data) - return HttpResponseBadRequest(e.response.text) + return HttpResponseBadRequest(err.response.text) return JsonResponse(pem, encoder=AgaveJSONEncoder, safe=False) return HttpResponseBadRequest("Unsupported operation") diff --git a/designsafe/apps/api/data/agave/filemanager.py b/designsafe/apps/api/data/agave/filemanager.py index d72adab2db..7007856bf9 100644 --- a/designsafe/apps/api/data/agave/filemanager.py +++ b/designsafe/apps/api/data/agave/filemanager.py @@ -295,7 +295,8 @@ def listing(self, file_id=None, **kwargs): listing = self._agave_listing(system, file_path, **kwargs) reindex_agave.apply_async(kwargs = {'username': self.username, 'file_id': file_id, - 'levels': 1}) + 'levels': 1}, + queue='indexing') except IndexError: listing = es_listing return listing @@ -724,7 +725,8 @@ def share(self, file_id, permissions, recursive = True, **kwargs): #esf = Object.from_file_path(system, self.username, file_path) #esf.share(self.username, permissions) share_agave.apply_async(args=(self.username, file_id, permissions, - recursive)) + recursive), + queue='indexing') return f.to_dict() def transfer(self, file_id, dest_resource, dest_file_id): @@ -735,7 +737,8 @@ def transfer(self, file_id, dest_resource, dest_file_id): self.resource, file_id, dest_resource, - dest_file_id)) + dest_file_id), + queue='indexing') return {'message': 'The requested transfer has been scheduled'} else: message = 'The requested transfer from %s to %s ' \ @@ -803,7 +806,8 @@ def upload(self, file_id, files, **kwargs): 'file_id': file_id, 'full_indexing': False, 'pems_indexing': True, - 'index_full_path': True}) + 'index_full_path': True}, + queue='indexing') return u_file.to_dict() def from_file_real_path(self, file_real_path): diff --git a/designsafe/apps/api/data/agave/public_filemanager.py b/designsafe/apps/api/data/agave/public_filemanager.py index 0b5fd8e0a4..9f01829cd6 100644 --- a/designsafe/apps/api/data/agave/public_filemanager.py +++ b/designsafe/apps/api/data/agave/public_filemanager.py @@ -273,7 +273,7 @@ def copy(self, file_id, dest_resource, dest_file_id, **kwargs): service = lookup_transfer_service(self.resource, dest_resource) if service: args = (self.username, self.resource, file_id, dest_resource, dest_file_id) - service.apply_async(args=args) + service.apply_async(args=args, queue='files') return {'message': 'The requested transfer has been scheduled'} else: message = 'The requested transfer from %s to %s ' \ diff --git a/designsafe/apps/api/data/agave/tests/test_filemanager.py b/designsafe/apps/api/data/agave/tests/test_filemanager.py index eaa705c191..4a29e13cb3 100644 --- a/designsafe/apps/api/data/agave/tests/test_filemanager.py +++ b/designsafe/apps/api/data/agave/tests/test_filemanager.py @@ -713,5 +713,5 @@ def test_rename(self, mock_agave_from_file_path, mock_share_task): self.user.username, path, agave_client=fm.agave_client) mock_share_task.assert_called_with( - args=(self.user.username, file_id, permissions, True)) + args=(self.user.username, file_id, permissions, True), queue='indexing') diff --git a/designsafe/apps/api/data/box/filemanager.py b/designsafe/apps/api/data/box/filemanager.py index d2e6b1a1dc..f7ee0b5849 100644 --- a/designsafe/apps/api/data/box/filemanager.py +++ b/designsafe/apps/api/data/box/filemanager.py @@ -167,7 +167,7 @@ def copy(self, file_id, dest_resource, dest_file_id, **kwargs): args = (self._user.username, self.resource, file_id, dest_resource, dest_file_id) - service.apply_async(args=args) + service.apply_async(args=args, queue='files') return {'message': 'The requested transfer has been scheduled'} else: message = 'The requested transfer from %s to %s ' \ @@ -201,7 +201,8 @@ def import_file(self, file_id, from_resource, import_file_id, **kwargs): file_id, from_resource, import_file_id), - countdown=10) + countdown=10, + queue='files') return {'message': 'Your file(s) have been scheduled for upload to box.'} diff --git a/designsafe/apps/api/data/views.py b/designsafe/apps/api/data/views.py index 3549d428af..705f8a52c1 100644 --- a/designsafe/apps/api/data/views.py +++ b/designsafe/apps/api/data/views.py @@ -190,8 +190,10 @@ def get(self, request, pk, *args, **kwargs): logger.info('extra: {}'.format(extra)) try: - target_path = extra['target_path'] + # target_path = extra['target_path'] + file_id = '%s%s' % (extra['system'], extra['trail'][-2]['path']) #path of the containing folder except KeyError as e: file_id = extra['id'] - target_path = reverse('designsafe_data:data_depot') + 'agave/' + file_id + '/' + + target_path = reverse('designsafe_data:data_depot') + 'agave/' + file_id + '/' return redirect(target_path) diff --git a/designsafe/apps/api/decorators.py b/designsafe/apps/api/decorators.py new file mode 100644 index 0000000000..4d79995349 --- /dev/null +++ b/designsafe/apps/api/decorators.py @@ -0,0 +1,80 @@ +""" Decorators used for the api TODO: This is Django specific. We should either move this into some kind of + django specific utils or try and make it as general as possible. +""" +import logging +from functools import wraps +from base64 import b64decode +from django.utils.six import text_type +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth import login +import jwt as pyjwt +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.serialization import load_der_public_key + +#pylint: disable=invalid-name +logger = logging.getLogger(__name__) +#pylint: enable=invalid-name + +def _decode_jwt(jwt): + """Verified signature on a jwt + + Uses public key to decode the jwt message. + + :param str jwt: JWT string + :return: base64-decoded message + """ + #pubkey = settings.AGAVE_JWT_PUBKEY + pubkey = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCUp/oV1vWc8/TkQSiAvTousMzO\nM4asB2iltr2QKozni5aVFu818MpOLZIr8LMnTzWllJvvaA5RAAdpbECb+48FjbBe\n0hseUdN5HpwvnH/DW8ZccGvk53I6Orq7hLCv1ZHtuOCokghz/ATrhyPq+QktMfXn\nRS4HrKGJTzxaCcU7OQIDAQAB' + key_der = b64decode(pubkey) + key = load_der_public_key(key_der, backend=default_backend()) + return pyjwt.decode(jwt, key, issuer=settings.AGAVE_JWT_ISSUER) + #return pyjwt.decode(jwt, key, verify=False) + +def _get_jwt_payload(request): + """Return JWT payload as a string + + :param django.http.request request: Django Request + :return: JWT payload + :rtype: str + """ + payload = request.META.get(settings.AGAVE_JWT_HEADER) + if payload and isinstance(payload, text_type): + # Header encoding (see RFC5987) + payload = payload.encode('iso-8859-1') + + return payload + +def agave_jwt_login(func): + """Decorator to login user with a jwt + + ..note:: + It will sliently fail and continue executing the wrapped function + if the JWT payload header IS NOT present in the request. If the JWT payload + header IS present then it will continue executing the wrapped function passing + the request object with the correct user logged-in. + Because of this it is assumed that this decorator will be used together with + :func:`django.contrib.auth.decorators.login_required` decorator. This way we do + not disrupt your usual Django login config. + """ + #pylint: disable=missing-docstring + @wraps(func) + def decorated_function(request, *args, **kwargs): + if request.user.is_authenticated(): + return func(request, *args, **kwargs) + + payload = _get_jwt_payload(request) + if not payload: + logger.debug('No JWT payload found. Falling back') + return func(request, *args, **kwargs) + + jwt_payload = _decode_jwt(payload) + username = jwt_payload.get(settings.AGAVE_JWT_USER_CLAIM_FIELD, '') + user = get_user_model().objects.get(username=username) + user.backend = 'django.contrib.auth.backends.ModelBackend', + login(request, user) + return func(request, *args, **kwargs) + + return decorated_function + #pylint: enable=missing-docstring diff --git a/designsafe/apps/api/external_resources/box/filemanager/manager.py b/designsafe/apps/api/external_resources/box/filemanager/manager.py index 2349ee9719..40257b57e3 100644 --- a/designsafe/apps/api/external_resources/box/filemanager/manager.py +++ b/designsafe/apps/api/external_resources/box/filemanager/manager.py @@ -31,8 +31,7 @@ def __init__(self, user_obj, **kwargs): try: self.box_api = user_obj.box_user_token.client except BoxUserToken.DoesNotExist: - message = 'You need to connect your Box.com account ' \ - 'before you can access your Box.com files.' + message = 'Connect your Box account here' raise ApiException(status=400, message=message, extra={ 'action_url': reverse('box_integration:index'), 'action_label': 'Connect Box.com Account' @@ -220,7 +219,8 @@ def copy(self, username, src_file_id, dest_file_id, **kwargs): reindex_agave.apply_async(kwargs={ 'username': user.username, 'file_id': '{}/{}'.format(agave_system_id, agave_file_path) - }) + }, + queue='indexing') except: logger.exception('Unexpected task failure: box_download', extra={ 'username': username, diff --git a/designsafe/apps/api/external_resources/dropbox/filemanager/manager.py b/designsafe/apps/api/external_resources/dropbox/filemanager/manager.py index 7a05d98b97..b1a1a8adcb 100644 --- a/designsafe/apps/api/external_resources/dropbox/filemanager/manager.py +++ b/designsafe/apps/api/external_resources/dropbox/filemanager/manager.py @@ -38,8 +38,7 @@ def __init__(self, user_obj, **kwargs): self.dropbox_api = Dropbox(dropbox_token.access_token) except DropboxUserToken.DoesNotExist: - message = 'You need to connect your Dropbox.com account ' \ - 'before you can access your Dropbox.com files.' + message = 'Connect your Dropbox account here' raise ApiException(status=400, message=message, extra={ 'action_url': reverse('dropbox_integration:index'), 'action_label': 'Connect Dropbox.com Account' @@ -95,7 +94,7 @@ def listing(self, file_id='', **kwargs): entries = dropbox_item.entries while True: - children.extend([DropboxFile(item, item.path_display, parent=dropbox_item).to_dict(default_pems=default_pems) + children.extend([DropboxFile(item, item.path_display.encode('utf-8'), parent=dropbox_item).to_dict(default_pems=default_pems) for item in entries]) if has_more: folder = self.dropbox_api.files_list_folder_continue(cursor) @@ -202,7 +201,8 @@ def copy(self, username, src_file_id, dest_file_id, **kwargs): reindex_agave.apply_async(kwargs={ 'username': user.username, 'file_id': '{}/{}'.format(agave_system_id, agave_file_path) - }) + }, + queue='indexing') except: logger.exception('Unexpected task failure: dropbox_download', extra={ 'username': username, diff --git a/designsafe/apps/api/external_resources/views.py b/designsafe/apps/api/external_resources/views.py index 077db08c39..86d8491f53 100644 --- a/designsafe/apps/api/external_resources/views.py +++ b/designsafe/apps/api/external_resources/views.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) -class FilesListView(BaseApiView, SecureMixin): +class FilesListView(SecureMixin, BaseApiView): """Listing view""" def get(self, request, file_mgr_name, file_id=None): @@ -37,7 +37,7 @@ def get(self, request, file_mgr_name, file_id=None): listing = fmgr.listing(file_id) return JsonResponse(listing, safe=False) -class FileMediaView(BaseApiView, SecureMixin): +class FileMediaView(SecureMixin, BaseApiView): """File Media View""" def get(self, request, file_mgr_name, file_id): @@ -91,7 +91,8 @@ def put(self, request, file_mgr_name, file_id): 'file_mgr_name': file_mgr_name, 'username': request.user.username, 'src_file_id': file_id, - 'dest_file_id': os.path.join(body['system'], body['path'].strip('/'))}) + 'dest_file_id': os.path.join(body['system'], body['path'].strip('/'))}, + queue='files') return JsonResponse({'status': 200, 'message': 'OK'}) except HTTPError as e: logger.exception('Unable to copy file') @@ -110,6 +111,6 @@ def put(self, request, file_mgr_name, file_id): return HttpResponseBadRequest("Operation not implemented.") -class FilePermissionsView(BaseApiView, SecureMixin): +class FilePermissionsView(SecureMixin, BaseApiView): """File Permissions View""" pass diff --git a/designsafe/apps/api/fixtures/agave-experiment-meta.json b/designsafe/apps/api/fixtures/agave-experiment-meta.json new file mode 100644 index 0000000000..d46d2019a8 --- /dev/null +++ b/designsafe/apps/api/fixtures/agave-experiment-meta.json @@ -0,0 +1,26 @@ +{ + "uuid" : "9620095064311852570-242ac11e-0001-012", + "schemaId" : null, + "internalUsername" : null, + "associationIds" : [ ], + "lastUpdated" : "2017-01-26T09:48:46.760-06:00", + "name" : "designsafe.project", + "value" : { + "title": "experiment title", + "experimentType": "other" + }, + "created" : "2016-10-30T15:57:58.970-05:00", + "owner" : "ds_admin", + "_links" : { + "self" : { + "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012" + }, + "permissions" : { + "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012/pems" + }, + "owner" : { + "href" : "https://agave.designsafe-ci.org/profiles/v2/ds_admin" + }, + "associationIds" : [ ] + } +} diff --git a/designsafe/apps/api/fixtures/agave-file-meta.json b/designsafe/apps/api/fixtures/agave-file-meta.json new file mode 100644 index 0000000000..e31bfa129d --- /dev/null +++ b/designsafe/apps/api/fixtures/agave-file-meta.json @@ -0,0 +1,26 @@ +{ + "uuid" : "8510095064311852681-242ac11e-0001-012", + "schemaId" : null, + "internalUsername" : null, + "associationIds" : [ ], + "lastUpdated" : "2017-01-26T09:48:46.760-06:00", + "name" : "designsafe.project", + "value" : { + "keywords": ["one", "two", "three", "four"], + "projectUUID": "8510095064311852570-242ac11e-0001-012" + }, + "created" : "2016-10-30T15:57:58.970-05:00", + "owner" : "ds_admin", + "_links" : { + "self" : { + "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012" + }, + "permissions" : { + "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012/pems" + }, + "owner" : { + "href" : "https://agave.designsafe-ci.org/profiles/v2/ds_admin" + }, + "associationIds" : [ ] + } +} diff --git a/designsafe/apps/api/fixtures/agave-model-config-meta.json b/designsafe/apps/api/fixtures/agave-model-config-meta.json new file mode 100644 index 0000000000..46a3359cdd --- /dev/null +++ b/designsafe/apps/api/fixtures/agave-model-config-meta.json @@ -0,0 +1,28 @@ +{ + "uuid" : "8510095064311852681-242ac11e-0001-015", + "schemaId" : null, + "internalUsername" : null, + "associationIds" : [ ], + "lastUpdated" : "2017-01-26T09:48:46.760-06:00", + "name" : "designsafe.project", + "value" : { + "title": "Model config title", + "description": "description", + "coverage": "coverage", + "files": ["adsfasfd", "qewrqwerqwe", "zxczxcvzvxc"] + }, + "created" : "2016-10-30T15:57:58.970-05:00", + "owner" : "ds_admin", + "_links" : { + "self" : { + "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012" + }, + "permissions" : { + "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012/pems" + }, + "owner" : { + "href" : "https://agave.designsafe-ci.org/profiles/v2/ds_admin" + }, + "associationIds" : [ ] + } +} diff --git a/designsafe/apps/api/fixtures/agave-project-meta.json b/designsafe/apps/api/fixtures/agave-project-meta.json new file mode 100644 index 0000000000..0627e1b634 --- /dev/null +++ b/designsafe/apps/api/fixtures/agave-project-meta.json @@ -0,0 +1,32 @@ +{ + "uuid" : "8511125064311852570-242ac11e-0001-012", + "schemaId" : null, + "internalUsername" : null, + "associationIds" : [ ], + "lastUpdated" : "2017-01-26T09:48:46.760-06:00", + "name" : "designsafe.project", + "value" : { + "teamMember" : [ "jcoronel", "jcoronel", "jcoronel" ], + "projectType": "experimental", + "description": "description", + "pi" : "erathje", + "title" : "Projekt Drei", + "awardNumber": "NSF-04897", + "associatedProjects": ["asdfasdf", "qewrqwerqwerqew", "zcvzxcvzxcvzxcv"], + "ef": "Experimental Facility" + }, + "created" : "2016-10-30T15:57:58.970-05:00", + "owner" : "ds_admin", + "_links" : { + "self" : { + "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012" + }, + "permissions" : { + "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012/pems" + }, + "owner" : { + "href" : "https://agave.designsafe-ci.org/profiles/v2/ds_admin" + }, + "associationIds" : [ ] + } +} diff --git a/designsafe/apps/api/mixins.py b/designsafe/apps/api/mixins.py index 6d8c4a7695..f6225e3270 100644 --- a/designsafe/apps/api/mixins.py +++ b/designsafe/apps/api/mixins.py @@ -1,5 +1,6 @@ from django.utils.decorators import method_decorator from django.contrib.auth.decorators import login_required +from designsafe.apps.api.decorators import agave_jwt_login from django.http import HttpResponse from django.core.serializers.json import DjangoJSONEncoder import logging @@ -20,12 +21,18 @@ def render_to_json_response(self, context, **response_kwargs): class SecureMixin(object): - """ - View mixin to use login_required + """View mixin to ensure the user has access to a secured view + + This Mixin first checks if the request is done using a JWT. If this is not the case + then it will continue and check if the request is using a regular django session cookie. + Either way the request will be correctly authenticated. This way we can easily + use the same API endpoints and put them behind WSO2. + TODO: When moving into Django 1.9 @method_decorator(login_required) should be a class wrapper @method_decorator(login_required, name='dispatch') as per: https://docs.djangoproject.com/en/1.9/topics/class-based-views/intro/#decorating-the-class """ + @method_decorator(agave_jwt_login) @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): return super(SecureMixin, self).dispatch(request, *args, **kwargs) diff --git a/designsafe/apps/api/models.py b/designsafe/apps/api/models.py index 71a8362390..a9c3dc5038 100644 --- a/designsafe/apps/api/models.py +++ b/designsafe/apps/api/models.py @@ -1,3 +1,8 @@ from django.db import models # Create your models here. +from designsafe.apps.api.agave.models.fields import * +from designsafe.apps.api.projects.models import * + +from designsafe.apps.api.agave.models.base import set_lazy_rels +set_lazy_rels() diff --git a/designsafe/apps/api/notifications/views/api.py b/designsafe/apps/api/notifications/views/api.py index 18c2193037..2e96abc9ae 100644 --- a/designsafe/apps/api/notifications/views/api.py +++ b/designsafe/apps/api/notifications/views/api.py @@ -18,22 +18,34 @@ class ManageNotificationsView(SecureMixin, JSONResponseMixin, BaseApiView): def get(self, request, event_type = None, *args, **kwargs): - limit = request.GET.get('limit', None) + limit = request.GET.get('limit', 0) + page = request.GET.get('page', 0) + if event_type is not None: notifs = Notification.objects.filter(event_type = event_type, deleted = False, user = request.user.username).order_by('-datetime') + total = Notification.objects.filter(event_type = event_type, + deleted = False, + user = request.user.username).count() else: notifs = Notification.objects.filter(deleted = False, user = request.user.username).order_by('-datetime') + total = Notification.objects.filter(deleted = False, + user = request.user.username).count() if limit: - notifs = notifs[0:limit] + limit = int(limit) + page = int(page) + offset = page * limit + notifs = notifs[offset:offset+limit] + for n in notifs: if not n.read: n.mark_read() notifs = [n.to_dict() for n in notifs] - return self.render_to_json_response(notifs) + return self.render_to_json_response({'notifs':notifs, 'page':page, 'total': total}) + # return self.render_to_json_response(notifs) def post(self, request, *args, **kwargs): body_json = json.loads(request.body) diff --git a/designsafe/apps/api/projects/managers/__init__.py b/designsafe/apps/api/projects/managers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/designsafe/apps/api/projects/managers/publication.py b/designsafe/apps/api/projects/managers/publication.py new file mode 100644 index 0000000000..8f81fa742e --- /dev/null +++ b/designsafe/apps/api/projects/managers/publication.py @@ -0,0 +1,386 @@ +import re +import logging +import codecs +import xml.etree.ElementTree as ET +from xml.dom import minidom +from django.contrib.auth import get_user_model +from django.conf import settings +import datetime +import dateutil.parser +import requests + +logger = logging.getLogger(__name__) + +USER = settings.EZID_USER +PASSWORD = settings.EZID_PASS +CREDS = (USER, PASSWORD) +BASE_URI = 'https://ezid.cdlib.org/' +SHOULDER = 'doi:10.5072/FK2' + +def pretty_print(xml): + """Return a pretty-printed XML string for the Element. + """ + rough_string = ET.tostring(xml, 'utf-8') + reparsed = minidom.parseString(rough_string) + return reparsed.toprettyxml(indent=" ") + +def _project_header(): + xml_obj = ET.Element("resource") + xml_obj.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance" + xml_obj.attrib["xmlns"] = "http://datacite.org/schema/kernel-4" + xml_obj.attrib["xsi:schemaLocation"] = "http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd" + return xml_obj + +def _unescape(s): + return re.sub("%([0-9A-Fa-f][0-9A-Fa-f])", + lambda m: chr(int(m.group(1), 16)), s) + +def parse_response(res): + response = dict(tuple(_unescape(v).strip() for v in l.split(":", 1)) \ + for l in res.decode("UTF-8").splitlines()) + return response + +def _escape(s, key=True): + if key: + return re.sub("[%:\r\n]", lambda c: "%%%02X" % ord(c.group(0)), s) + else: + return re.sub("[%\r\n]", lambda c: "%%%02X" % ord(c.group(0)), s) + +def format_req(metadata): + anvl = [] + for key, value in metadata.items(): + key = _escape(key) + if value.startswith("@") and len(value) > 1: + f = codecs.open(value[1:], encoding="UTF-8") + value = f.read() + f.close() + value = _escape(value, False) + anvl.append("%s: %s" % (key, value)) + return "\n".join(anvl) + +def _reserve_doi(xml_obj): + xml_str = ET.tostring(xml_obj, encoding="UTF-8", method="xml") + metadata = {'_status': 'reserved', 'datacite': xml_str} + response = requests.post('{}/shoulder/{}'.format(BASE_URI, SHOULDER), + data=format_req(metadata), + auth=CREDS, + headers={'Content-Type': 'text/plain'}) + res = parse_response(response.text) + if 'success' in res: + return res['success'] + else: + raise Exception(res['error']) + +def _update_doi(doi, xml_obj=None, status='reserved'): + if xml_obj is not None: + xml_str = ET.tostring(xml_obj, encoding="UTF-8", method="xml") + metadata = {'_status': status, 'datacite': xml_str} + else: + metadata = {'_status': status} + + response = requests.post('{}/id/{}'.format(BASE_URI, doi), + data=format_req(metadata), + auth=CREDS, + headers={'Content-Type': 'text/plain'}) + res = parse_response(response.text) + if 'success' in res: + return res['success'] + else: + raise Exception(res['error']) + +def _project_required_xml(publication): + project_body = publication['project'] + proj = project_body['value'] + xml_obj = _project_header() + + resource = xml_obj + identifier = ET.SubElement(resource, 'identifier') + identifier.attrib['identifierType'] = 'DOI' + identifier.text = SHOULDER.replace('doi:', '') + creators = ET.SubElement(resource, 'creators') + um = get_user_model() + for author in [proj['pi']] + proj['coPis'] + proj['teamMembers']: + try: + user = um.objects.get(username=author) + creator = ET.SubElement(creators, 'creator') + creator_name = ET.SubElement(creator, 'creatorName') + creator_name.text = '{}, {}'.format(user.last_name, user.first_name) + except um.DoesNotExist as err: + logger.debug('User not found: %s', author) + + titles = ET.SubElement(resource, 'titles') + title = ET.SubElement(titles, 'title') + title.text = proj['title'] + publisher = ET.SubElement(resource, 'publisher') + publisher.text = 'Designsafe-CI' + + now = dateutil.parser.parse(publication['created']) + publication_year = ET.SubElement(resource, 'publicationYear') + publication_year.text = str(now.year) + + resource_type = ET.SubElement(resource, 'resourceType') + resource_type.text = "Project/{}".format(proj['projectType'].title()) + resource_type.attrib['resourceTypeGeneral'] = 'Dataset' + descriptions = ET.SubElement(resource, 'descriptions') + desc = ET.SubElement(descriptions, 'description') + desc.attrib['descriptionType'] = 'Abstract' + desc.text = proj['description'] + return xml_obj + +def _experiment_required_xml(users, experiment, created): + exp = experiment['value'] + xml_obj = _project_header() + + resource = xml_obj + identifier = ET.SubElement(resource, 'identifier') + identifier.attrib['identifierType'] = 'DOI' + identifier.text = SHOULDER.replace('doi:', '') + creators = ET.SubElement(resource, 'creators') + um = get_user_model() + authors = exp.get('authors') + authors = authors or users + for author in authors: + try: + if isinstance(author, basestring): + user = um.objects.get(username=author) + else: + user = um.objects.get(username=author['username']) + + creator = ET.SubElement(creators, 'creator') + creator_name = ET.SubElement(creator, 'creatorName') + creator_name.text = '{}, {}'.format(user.last_name, user.first_name) + except um.DoesNotExist as err: + logger.debug('User not found: %s', author) + + titles = ET.SubElement(resource, 'titles') + title = ET.SubElement(titles, 'title') + title.text = exp['title'] + publisher = ET.SubElement(resource, 'publisher') + publisher.text = 'Designsafe-CI' + + now = dateutil.parser.parse(created) + publication_year = ET.SubElement(resource, 'publicationYear') + publication_year.text = str(now.year) + + resource_type = ET.SubElement(resource, 'resourceType') + resource_type.text = "Experiment/{}".format(exp['experimentType'].title()) + resource_type.attrib['resourceTypeGeneral'] = 'Dataset' + descriptions = ET.SubElement(resource, 'descriptions') + desc = ET.SubElement(descriptions, 'description') + desc.attrib['descriptionType'] = 'Abstract' + desc.text = exp['description'] + return xml_obj + +def _analysis_required_xml(users, analysis, created): + anl = analysis['value'] + xml_obj = _project_header() + + resource = xml_obj + identifier = ET.SubElement(resource, 'identifier') + identifier.attrib['identifierType'] = 'DOI' + identifier.text = SHOULDER.replace('doi:', '') + creators = ET.SubElement(resource, 'creators') + um = get_user_model() + for author in users: + try: + if isinstance(author, basestring): + user = um.objects.get(username=author) + else: + user = um.objects.get(username=author['username']) + creator = ET.SubElement(creators, 'creator') + creator_name = ET.SubElement(creator, 'creatorName') + creator_name.text = '{}, {}'.format(user.last_name, user.first_name) + except um.DoesNotExist as err: + logger.debug('User not found: %s', author) + + titles = ET.SubElement(resource, 'titles') + title = ET.SubElement(titles, 'title') + title.text = anl['title'] + publisher = ET.SubElement(resource, 'publisher') + publisher.text = 'Designsafe-CI' + + now = dateutil.parser.parse(created) + publication_year = ET.SubElement(resource, 'publicationYear') + publication_year.text = str(now.year) + + resource_type = ET.SubElement(resource, 'resourceType') + resource_type.text = 'Analysis' + resource_type.attrib['resourceTypeGeneral'] = 'Other' + descriptions = ET.SubElement(resource, 'descriptions') + desc = ET.SubElement(descriptions, 'description') + desc.attrib['descriptionType'] = 'Abstract' + desc.text = anl['description'] + return xml_obj + +def analysis_reserve_xml(publication, analysis, created): + anl = analysis['value'] + xml_obj = _analysis_required_xml(publication['users'], analysis, + created) + now = dateutil.parser.parse(created) + reserve_res = _reserve_doi(xml_obj) + doi, ark = reserve_res.split('|') + doi = doi.strip() + ark = ark.strip() + identifier = xml_obj.find('identifier') + identifier.text = doi + resource = xml_obj + _update_doi(doi, xml_obj) + return (doi, ark, xml_obj) + +def experiment_reserve_xml(publication, experiment, created): + exp = experiment['value'] + xml_obj = _experiment_required_xml(publication['users'], experiment, + created) + now = dateutil.parser.parse(created) + reserve_res = _reserve_doi(xml_obj) + doi, ark = reserve_res.split('|') + doi = doi.strip() + ark = ark.strip() + identifier = xml_obj.find('identifier') + identifier.text = doi + resource = xml_obj + contributors = ET.SubElement(resource, 'contributors') + contributor = ET.SubElement(contributors, 'contributor') + contributor.attrib['contributorType'] = 'HostingInstitution' + name = ET.SubElement(contributor, 'contributorName') + name.text = exp['experimentalFacility'] + subjects = ET.SubElement(resource, 'subjects') + exp_type = ET.SubElement(subjects, 'subject') + exp_type.text = exp['experimentType'].title() + eq_type = ET.SubElement(subjects, 'subject') + eq_type.text = exp['equipmentType'] + events = [event for event in publication['eventsList'] if \ + experiment['uuid'] in event['associationIds']] + + for event in events: + event_subj = ET.SubElement(subjects, 'subject') + event_subj.text = event['value']['title'] + + mcfs = [mcf for mcf in publication['modelConfigs'] if \ + experiment['uuid'] in mcf['associationIds']] + + for mcf in mcfs: + mcf_subj = ET.SubElement(subjects, 'subject') + mcf_subj.text = mcf['value']['title'] + + slts = [slt for slt in publication['sensorLists'] if \ + experiment['uuid'] in slt['associationIds']] + + for slt in slts: + slt_subj = ET.SubElement(subjects, 'subject') + slt_subj.text = slt['value']['title'] + + _update_doi(doi, xml_obj) + return (doi, ark, xml_obj) + +def project_reserve_xml(publication): + project_body = publication['project'] + proj = project_body['value'] + xml_obj = _project_required_xml(publication) + logger.debug('required xml: %s', pretty_print(xml_obj)) + now = dateutil.parser.parse(publication['created']) + reserve_resp = _reserve_doi(xml_obj) + doi, ark = reserve_resp.split('|') + doi = doi.strip() + ark = ark.strip() + #logger.debug('doi: %s', doi) + identifier = xml_obj.find('identifier') + identifier.text = doi + + #Optional stuff + resource = xml_obj + subjects = ET.SubElement(resource, 'subjects') + for keyword in proj['keywords'].split(','): + subject = ET.SubElement(subjects, 'subject') + subject.text = keyword.strip().title() + + institutions = publication['institutions'] + contributors = ET.SubElement(resource, 'contributors') + for institution in institutions: + contrib = ET.SubElement(contributors, 'contributor') + name = ET.SubElement(contrib, 'contributorName') + name.text = institution['label'] + contrib.attrib['contributorType'] = 'HostingInstitution' + + dates = ET.SubElement(resource, 'dates') + date_publication = ET.SubElement(dates, 'date') + date_publication.attrib['dateType'] = 'Accepted' + date_publication.text = '{}-{}-{}'.format(now.year, now.month, now.day) + + language = ET.SubElement(resource, 'language') + language.text = 'English' + + alternate_ids = ET.SubElement(resource, 'alternateIdentifiers') + if proj['awardNumber']: + award_number = ET.SubElement(alternate_ids, 'alternateIdentifier') + award_number.attrib['alternateIdentifierType'] = 'NSF Award Number' + award_number.text = proj['awardNumber'] + + project_id = ET.SubElement(alternate_ids, 'alternateIdentifier') + project_id.attrib['alternateIdentifierType'] = 'Project ID' + project_id.text = proj['projectId'] + + rights_list = ET.SubElement(resource, 'rightsList') + rights = ET.SubElement(rights_list, 'rights') + rights.attrib['rightsURI'] = 'http://opendatacommons.org/licenses/by/1-0/' + rights.text = 'ODC-BY 1.0' + logger.debug(pretty_print(xml_obj)) + _update_doi(doi, xml_obj) + return (doi, ark, xml_obj) + +def add_related(xml_obj, dois): + doi = xml_obj.find('identifier').text + resource = xml_obj + related_ids = ET.SubElement(resource, 'relatedIdentifiers') + for _doi in dois: + related = ET.SubElement(related_ids, 'relatedIdentifier') + related.attrib['relatedIdentifierType'] = 'DOI' + related.attrib['relationType'] = 'IsPartOf' + related.text = _doi + + _update_doi(doi, xml_obj) + return (doi, xml_obj) + +def publish_project(doi, xml_obj): + #doi, ark, xml_obj = _project_publish_xml(publication) + logger.debug(pretty_print(xml_obj)) + xml_str = ET.tostring(xml_obj, encoding="UTF-8", method="xml") + metadata = {'_status': 'public', 'datacite': xml_str} + res = requests.post('{}/id/{}'.format(BASE_URI, doi), + format_req(metadata), + auth=CREDS, + headers={'Content-Type': 'text/plain'}) + res = parse_response(res.text) + if 'success' in res: + return res['success'].split('|') + else: + logger.exception(res['error']) + raise Exception(res['error']) + +def reserve_publication(publication): + proj_doi, proj_ark, proj_xml = project_reserve_xml(publication) + exps_dois = [] + anl_dois = [] + xmls = {proj_doi: proj_xml} + publication['project']['doi'] = proj_doi + for exp in publication.get('experimentsList', []): + exp_doi, exp_ark, exp_xml = experiment_reserve_xml(publication, + exp, publication['created']) + add_related(exp_xml, [proj_doi]) + exps_dois.append(exp_doi) + exp['doi'] = exp_doi + xmls[exp_doi] = exp_xml + + for anl in publication.get('analysisList', []): + anl_doi, anl_ark, anl_xml = analysis_reserve_xml(publication, + anl, publication['created']) + add_related(anl_xml, [proj_doi]) + anl_dois.append(anl_doi) + anl['doi'] = anl_doi + xmls[anl_doi] = anl_xml + + add_related(proj_xml, exps_dois + anl_dois) + for _doi in [proj_doi] + exps_dois + anl_dois: + logger.debug(_doi) + _update_doi(_doi, xmls[_doi], status='public') + return publication diff --git a/designsafe/apps/api/projects/managers/yamz.py b/designsafe/apps/api/projects/managers/yamz.py new file mode 100644 index 0000000000..a5ee3bdfe0 --- /dev/null +++ b/designsafe/apps/api/projects/managers/yamz.py @@ -0,0 +1,30 @@ +import re +import logging +import json +import requests +from bs4 import BeautifulSoup +from designsafe.apps.api.mixins import SecureMixin +from designsafe.apps.api.views import BaseApiView +from django.http import JsonResponse, HttpResponseBadRequest + +logger = logging.getLogger(__name__) + +YAMZ_BASE_URL = 'http://www.yamz.net/term/concept=' + +class YamzBaseView(BaseApiView): + def get(self, request, term_id): + res = requests.get(''.join([YAMZ_BASE_URL, term_id])) + soup = BeautifulSoup(res.text) + definition_trs = soup.find_all(string=re.compile(r'^\s*Definition\:\s*$')) + if definition_trs: + definition = definition_trs[0].findParent('tr').text.encode('utf8').replace('Definition:', '', 1).strip() + else: + definition = '' + + examples_trs = soup.find_all(string=re.compile(r'^\s*Examples\:\s*$')) + if examples_trs: + examples = examples_trs[0].findParent('tr').text.encode('utf8').replace('Examples:', '', 1).strip() + else: + examples = '' + + return JsonResponse({'definition': definition, 'examples': examples}) diff --git a/designsafe/apps/api/projects/models.py b/designsafe/apps/api/projects/models.py index 6186f8492b..5ea36b76f4 100644 --- a/designsafe/apps/api/projects/models.py +++ b/designsafe/apps/api/projects/models.py @@ -1,3 +1,8 @@ +import six +import json +import logging +import xml.etree.ElementTree as ET + from designsafe.apps.api.agave.models.metadata import (BaseMetadataResource, BaseMetadataPermissionResource) from designsafe.apps.api.agave.models.files import (BaseFileResource, @@ -6,9 +11,9 @@ from designsafe.apps.api.agave.models.systems import BaseSystemResource from designsafe.apps.api.agave.models.systems import roles as system_roles from designsafe.apps.api.agave import to_camel_case -import six -import json -import logging +from designsafe.apps.api.agave.models.base import Model as MetadataModel +from designsafe.apps.api.agave.models import fields +from django.contrib.auth import get_user_model logger = logging.getLogger(__name__) @@ -49,16 +54,26 @@ def list_projects(cls, agave_client): records = agave_client.meta.listMetadata(q=json.dumps(query), privileged=False) return [cls(agave_client=agave_client, **r) for r in records] + @classmethod + def search(cls, q, agave_client): + """ + Search projects + """ + if isinstance(q, basestring): + query = q + else: + query = json.dumps(q) + records = agave_client.meta.listMetadata(q=query, privileged=False) + return [cls(agave_client=agave_client, **r) for r in records] + def team_members(self): permissions = BaseMetadataPermissionResource.list_permissions( self.uuid, self._agave) - logger.debug('self.value: %s', self.value) pi = self.pi - co_pis_list = getattr(self, 'co_pis', []) - co_pis = [] - if co_pis_list: - co_pis = [x.username for x in permissions if x.username in co_pis_list] + co_pis = getattr(self, 'co_pis', []) + logger.info(co_pis) + # co_pis = [x.username for x in permissions if x.username in co_pis_list] team_members_list = [x.username for x in permissions if x.username not in co_pis + [pi]] return {'pi': pi, @@ -136,6 +151,28 @@ def pi(self, value): def co_pis(self): return self.value.get('coPis', []) + def add_co_pi(self, username): + logger.info('Adding Co PI "{}" to project "{}"'.format(username, self.uuid)) + + coPis = self.value.get('coPis', []) + + coPis.append(username) + self.value['coPis'] = list(set(coPis)) + self.add_collaborator(username) + + def remove_co_pi(self, username): + logger.info('Removing Co PI "{}" to project "{}"'.format(username, self.uuid)) + + coPis = self.value.get('coPis', []) + # logger.info(coPis) + coPis = [uname for uname in coPis if uname != username] + + self.value['coPis'] = coPis + # logger.info(self.value) + # Set permissions on the metadata record + self.remove_collaborator(username) + + @co_pis.setter def co_pis(self, value): # TODO is this assertion valuable? @@ -179,3 +216,350 @@ def project_data_listing(self, path='/'): return BaseFileResource.listing(system=self.project_system_id, path=path, agave_client=self._agave) + + +class RelatedEntity(MetadataModel): + def to_body_dict(self): + body_dict = super(RelatedEntity, self).to_body_dict() + body_dict['_relatedFields'] = [] + for attrname, field in six.iteritems(self._meta._related_fields): + body_dict['_relatedFields'].append(attrname) + return body_dict + +class ExperimentalProject(MetadataModel): + model_name = 'designsafe.project' + team_members = fields.ListField('Team Members') + co_pis = fields.ListField('Co PIs') + project_type = fields.CharField('Project Type', max_length=255, default='other') + project_id = fields.CharField('Project Id') + description = fields.CharField('Description', max_length=1024, default='') + title = fields.CharField('Title', max_length=255, default='') + pi = fields.CharField('PI', max_length=255) + award_number = fields.CharField('Award Number', max_length=255) + associated_projects = fields.ListField('Associated Project') + ef = fields.CharField('Experimental Facility', max_length=512) + keywords = fields.CharField('Keywords') + + def to_body_dict(self): + body_dict = super(ExperimentalProject, self).to_body_dict() + body_dict['_related'] = {} + for attrname, field in six.iteritems(self._meta._related_fields): + body_dict['_related'][attrname] = field.rel_cls.model_name + + for attrname in self._meta._reverse_fields: + field = getattr(self, attrname) + body_dict['_related'][attrname] = field.related_obj_name + + return body_dict + + +class FileModel(MetadataModel): + model_name = 'designsafe.file' + keywords = fields.ListField('Keywords') + project_UUID = fields.RelatedObjectField(ExperimentalProject, default=[]) + +class DataTag(MetadataModel): + _is_nested = True + file = fields.RelatedObjectField(FileModel, default=[]) + desc = fields.CharField('Description', max_length=512, default='') + + def __eq__(self, other): + return self.file == other.file and self.desc == other.desc + + def __hash__(self): + return hash(('file', self.file, 'desc', self.desc)) + +class Experiment(RelatedEntity): + model_name = 'designsafe.project.experiment' + experiment_type = fields.CharField('Experiment Type', max_length=255, default='other') + description = fields.CharField('Description', max_length=1024, default='') + title = fields.CharField('Title', max_length=1024) + experimental_facility = fields.CharField('Experimental Facility', max_length=1024) + equipment_type = fields.CharField('Equipment Type') + authors = fields.ListField('Authors') + project = fields.RelatedObjectField(ExperimentalProject) + +class AnalysisTagGeneral(MetadataModel): + _is_nested = True + analysis_data_graph = fields.ListField('Analysis Data Graph', list_cls=DataTag) + analysis_data_visualization = fields.ListField('Analysis Data Visualization', list_cls=DataTag) + analysis_data_table = fields.ListField('Analysis Data Table', list_cls=DataTag) + application = fields.ListField('Application', list_cls=DataTag) + application_matlab = fields.ListField('Application MATLAB', list_cls=DataTag) + application_r = fields.ListField('Application R', list_cls=DataTag) + application_jupiter_notebook = fields.ListField('Application Notebook', list_cls=DataTag) + application_other = fields.ListField('Application Other', list_cls=DataTag) + application_script = fields.ListField('Application Script', list_cls=DataTag) + +class AnalysisTag(MetadataModel): + _is_nested = True + general = fields.NestedObjectField(AnalysisTagGeneral) + +class Analysis(RelatedEntity): + model_name = 'designsafe.project.analysis' + analysis_type = fields.CharField('Analysis Type', max_length=255, default='other') + title = fields.CharField('Title', max_length=1024) + description = fields.CharField('Description', max_length=1024, default='') + analysis_data = fields.CharField('Analysis Data', max_length=1024, default='') + application = fields.CharField('Analysis Data', max_length=1024, default='') + script = fields.RelatedObjectField(FileModel, multiple=True) + tags = fields.NestedObjectField(AnalysisTag) + project = fields.RelatedObjectField(ExperimentalProject) + experiments = fields.RelatedObjectField(Experiment) + #events = fields.RelatedObjectField(Event) + files = fields.RelatedObjectField(FileModel, multiple=True) + +class ModelConfigTagCentrifuge(MetadataModel): + _is_nested = True + triaxial_test = fields.ListField('Triaxal Test', list_cls=DataTag) + soil_strenght = fields.ListField('Soil Strength', list_cls=DataTag) + hinged_plate_container = fields.ListField('Hinged Plate Container', list_cls=DataTag) + rigid_container = fields.ListField('Rigid Container', list_cls=DataTag) + flexible_shear_beam_container = fields.ListField('Flexible Shear Beam Container', list_cls=DataTag) + structural_model = fields.ListField('Structural Model', list_cls=DataTag) + gravel = fields.ListField('Gravel', list_cls=DataTag) + sand = fields.ListField('Sand', list_cls=DataTag) + silt = fields.ListField('Silt', list_cls=DataTag) + clay = fields.ListField('Clay', list_cls=DataTag) + pit = fields.ListField('pit', list_cls=DataTag) + +class ModelConfigTagShakeTable(MetadataModel): + _is_nested = True + numerical_model = fields.ListField('Numerical Model', list_cls=DataTag) + loading_protocol_intensity = fields.ListField('Loading Protocol', list_cls=DataTag) + loading_protocol_ground_motions = fields.ListField('Loading Protocol Ground Motions', list_cls=DataTag) + material_test = fields.ListField('Material Test', list_cls=DataTag) + structural_model = fields.ListField('Structural Model', list_cls=DataTag) + soil = fields.ListField('Soil', list_cls=DataTag) + steel = fields.ListField('Steel', list_cls=DataTag) + concrete = fields.ListField('Concrete', list_cls=DataTag) + wood = fields.ListField('Wood', list_cls=DataTag) + masonry = fields.ListField('Masonry', list_cls=DataTag) + protective_system_isolation = fields.ListField('Protective System Isolation', list_cls=DataTag) + protective_system_rocking = fields.ListField('Protective System Rocking', list_cls=DataTag) + protective_system_damping = fields.ListField('Protective System Damping', list_cls=DataTag) + +class ModelConfigTagWave(MetadataModel): + _is_nested = True + large_wave_flume = fields.ListField('Large Wave Flume', list_cls=DataTag) + directional_wave_basin = fields.ListField('Directional Wave Basin', list_cls=DataTag) + wavemaker_input_file = fields.ListField('Wavemaker Input File', list_cls=DataTag) + board_siplacement = fields.ListField('Board Siplacement', list_cls=DataTag) + free_surface_height = fields.ListField('Free Surface Height', list_cls=DataTag) + hydrodynamic_conditions = fields.ListField('Hydrodynamic Conditions', list_cls=DataTag) + +class ModelConfigTagWind(MetadataModel): + _is_nested = True + bridge = fields.ListField('Bridge', list_cls=DataTag) + building_low_rise = fields.ListField('Building Low Rise', list_cls=DataTag) + building_tall = fields.ListField('Building Tall', list_cls=DataTag) + chimney = fields.ListField('Chimney', list_cls=DataTag) + mast = fields.ListField('Mast', list_cls=DataTag) + model_aeroelastic = fields.ListField('Model Aeroelastic', list_cls=DataTag) + model_full = fields.ListField('Model Full', list_cls=DataTag) + model_rigid = fields.ListField('Model Rigid', list_cls=DataTag) + model_section = fields.ListField('Model Section', list_cls=DataTag) + scale_full = fields.ListField('Scale Full', list_cls=DataTag) + scale_large = fields.ListField('Scale Large', list_cls=DataTag) + scale_small = fields.ListField('Scale Small', list_cls=DataTag) + tower = fields.ListField('Tower', list_cls=DataTag) + +class ModelConfigTagGeneral(MetadataModel): + _is_nested = True + model_drawing = fields.ListField('Model Drawing', list_cls=DataTag) + image = fields.ListField('Model Drawing', list_cls=DataTag) + video = fields.ListField('Model Drawing', list_cls=DataTag) + +class ModelConfigTag(MetadataModel): + _is_nested = True + centrifuge = fields.NestedObjectField(ModelConfigTagCentrifuge) + general = fields.NestedObjectField(ModelConfigTagGeneral) + shake_table = fields.NestedObjectField(ModelConfigTagShakeTable) + wave = fields.NestedObjectField(ModelConfigTagWave) + wind = fields.NestedObjectField(ModelConfigTagWind) + +class ModelConfiguration(RelatedEntity): + model_name = 'designsafe.project.model_config' + title = fields.CharField('Title', max_length=512) + description = fields.CharField('Description', max_length=1024, default='') + #spatial = fields.CharField('Spatial', max_length=1024) + #lat = fields.CharField('Lat', max_length=1024) + #lon = fields.CharField('Lon', max_length=1024) + #model_drawing = fields.RelatedObjectField(FileModel, multiple=True) + #image = fields.NestedObjectField(DataTag) + #video = fields.NestedObjectField(DataTag) + tags = fields.NestedObjectField(ModelConfigTag) + project = fields.RelatedObjectField(ExperimentalProject) + experiments = fields.RelatedObjectField(Experiment) + #events = fields.RelatedObjectField(Event) + files = fields.RelatedObjectField(FileModel, multiple=True) + +class SensorListTagCentrifuge(MetadataModel): + _is_nested = True + strain_gauge = fields.ListField('Strain Gauge', list_cls=DataTag) + bender_element = fields.ListField('Bender Element', list_cls=DataTag) + load_cell = fields.ListField('Load Cell', list_cls=DataTag) + lineal_potentiometer = fields.ListField('Lineal Potentiometer', list_cls=DataTag) + tactile_pressure = fields.ListField('Tactile Pressure', list_cls=DataTag) + pore_pressure_transducer = fields.ListField('Pore Pressure Transducer', list_cls=DataTag) + linear_variable_differential_transformer = fields.ListField('Linear Variable Differential Transformer', list_cls=DataTag) + accelerometer = fields.ListField('Accelerometer', list_cls=DataTag) + sensor_calibration = fields.ListField('Sensor Calibration', list_cls=DataTag) + +class SensorListTagShakeTable(MetadataModel): + _is_nested = True + accelerometer = fields.ListField('Accelerometer', list_cls=DataTag) + linear_potentiometer = fields.ListField('Linear Potentiometer', list_cls=DataTag) + displacement_sensor = fields.ListField('Displacement Sensor', list_cls=DataTag) + load_cell = fields.ListField('Load Cell', list_cls=DataTag) + soil_sensor = fields.ListField('Soil Sensor', list_cls=DataTag) + strain_gauge = fields.ListField('Strain Gauge', list_cls=DataTag) + +class SensorListTagWave(MetadataModel): + _is_nested = True + wave_gauge_calibration = fields.ListField('Wave Gauge Calibration', list_cls=DataTag) + synchronization = fields.ListField('Synchronization', list_cls=DataTag) + sample_synchronization = fields.ListField('Sample Synchronization', list_cls=DataTag) + project_instrumentation_locations = fields.ListField('Project Instrumentation Locations', list_cls=DataTag) + self_calibrating = fields.ListField('Self Calibrating', list_cls=DataTag) + instrument_survey = fields.ListField('Instrument Survey', list_cls=DataTag) + absolute_timing = fields.ListField('Absolute Timing', list_cls=DataTag) + wiring_details = fields.ListField('Wiring Details', list_cls=DataTag) + calibration_summary = fields.ListField('Calibration Summary', list_cls=DataTag) + +class SensorListTagWind(MetadataModel): + _is_nested = True + accelerometer = fields.ListField('Accelerometer', list_cls=DataTag) + component_velocity_and_statistic_pressure_robes = fields.ListField('Component Velocity And Stastic Pressure Robes', list_cls=DataTag) + inertial = fields.ListField('Intertial', list_cls=DataTag) + laser = fields.ListField('Laser', list_cls=DataTag) + linear_variable_differential_transformer = fields.ListField('Linear Variable Differential Transformer', list_cls=DataTag) + load_cells = fields.ListField('Load Cells', list_cls=DataTag) + particle_image_velocimetry = fields.ListField('Particle Image Velocimetry', list_cls=DataTag) + pitot_tube = fields.ListField('Pitot Tube', list_cls=DataTag) + pressure_scanner = fields.ListField('Pressure Scanner', list_cls=DataTag) + strain_gauge = fields.ListField('Strain Gauge', list_cls=DataTag) + string_potentiometer = fields.ListField('String Potentiometer', list_cls=DataTag) + +class SensorListTagGeneral(MetadataModel): + _is_nested = True + sensor_list = fields.ListField('Sensor List', list_cls=DataTag) + sensor_drawing = fields.ListField('Sensor Drawing', list_cls=DataTag) + +class SensorListTag(MetadataModel): + _is_nested = True + centrifuge = fields.NestedObjectField(SensorListTagCentrifuge) + general = fields.NestedObjectField(SensorListTagGeneral) + shake_table = fields.NestedObjectField(SensorListTagShakeTable) + wave = fields.NestedObjectField(SensorListTagWave) + wind = fields.NestedObjectField(SensorListTagWind) + +class SensorList(RelatedEntity): + model_name = 'designsafe.project.sensor_list' + sensor_list_type = fields.CharField('Sensor List Type', max_length=255, default='other') + title = fields.CharField('Title', max_length=1024) + description = fields.CharField('Description', max_length=1024, default='') + #sensor_drawing = fields.RelatedObjectField(FileModel, multiple=True) + tags = fields.NestedObjectField(SensorListTag) + project = fields.RelatedObjectField(ExperimentalProject) + experiments = fields.RelatedObjectField(Experiment) + #events = fields.RelatedObjectField(Event) + model_configs = fields.RelatedObjectField(ModelConfiguration) + files = fields.RelatedObjectField(FileModel, multiple=True) + +class EventTagCentrifuge(MetadataModel): + _is_nested=True + centrifuge_speed = fields.ListField('Centrifuge Speed', list_cls=DataTag) + slow_data = fields.ListField('Slow Data', list_cls=DataTag) + fast_data = fields.ListField('Fast Data', list_cls=DataTag) + t_bar_test = fields.ListField('T-Bar Test', list_cls=DataTag) + bender_element_test = fields.ListField('Bender Element Test', list_cls=DataTag) + actuator = fields.ListField('Actuator', list_cls=DataTag) + cone_penetrometer = fields.ListField('Cone Penetrometer', list_cls=DataTag) + shaking = fields.ListField('Shaking', list_cls=DataTag) + raw = fields.ListField('Raw', list_cls=DataTag) + calibrated = fields.ListField('Calibrated', list_cls=DataTag) + +class EventTagShakeTable(MetadataModel): + _is_nested=True + shake_table_test = fields.ListField('Shaek Table Test', list_cls=DataTag) + +class EventTagWave(MetadataModel): + _is_nested=True + bathymetric_survey_data = fields.ListField('Bathymetric Survey Data', list_cls=DataTag) + instrumet_calibration_data = fields.ListField('Instrument Calibration Data', list_cls=DataTag) + experimental_conditions = fields.ListField('Experimental Conditions', list_cls=DataTag) + raw = fields.ListField('Raw', list_cls=DataTag) + physical_units = fields.ListField('Physical Units', list_cls=DataTag) + channel_name = fields.ListField('Channel Name', list_cls=DataTag) + matlab_toolbox_source_code = fields.ListField('Matlab Toolbox Source Code', list_cls=DataTag) + +class EventTagWind(MetadataModel): + _is_nested=True + aerodynamic_roughness = fields.ListField('Aerodynamic', list_cls=DataTag) + flow_boundary_layer = fields.ListField('Flow Boundary Layer', list_cls=DataTag) + flow_profile = fields.ListField('Flow Profile', list_cls=DataTag) + flow_steady_gusting = fields.ListField('Flow Steady Gusting', list_cls=DataTag) + incident_flow = fields.ListField('Incident Flow', list_cls=DataTag) + reynolds_number = fields.ListField('Reynolds Number', list_cls=DataTag) + reynolds_stress = fields.ListField('Reynolds Stress', list_cls=DataTag) + scale_integral_length = fields.ListField('Scale Inetgral Length', list_cls=DataTag) + terrain_open = fields.ListField('Terrain Open', list_cls=DataTag) + terrain_urban = fields.ListField('Terrain Urban', list_cls=DataTag) + test_aerodynamic = fields.ListField('Test Aerodynamic', list_cls=DataTag) + test_complex_topography = fields.ListField('Test Complex Topography', list_cls=DataTag) + test_destructive = fields.ListField('Test Destructive', list_cls=DataTag) + test_dispersion = fields.ListField('Test Dispersion', list_cls=DataTag) + test_environmental = fields.ListField('Test Environmental', list_cls=DataTag) + test_external_pressure = fields.ListField('Test External Pressure', list_cls=DataTag) + test_high_frequency_force_balance = fields.ListField('Test high Frequency Force Balance', list_cls=DataTag) + test_internal_pressure = fields.ListField('Test Internal Pressure', list_cls=DataTag) + test_pedestrian_level_winds = fields.ListField('Test Pedestrian Level Winds', list_cls=DataTag) + turbulence_profile = fields.ListField('Turbulance Profile', list_cls=DataTag) + turbulence_spectrum = fields.ListField('Turbulence Spectrum', list_cls=DataTag) + uniform_flow = fields.ListField('Uniform Flow', list_cls=DataTag) + velocity_mean = fields.ListField('Velocity Mean', list_cls=DataTag) + velocity_profile = fields.ListField('Velocity Profile', list_cls=DataTag) + wind_direction = fields.ListField('Wind Direction', list_cls=DataTag) + wind_duration = fields.ListField('Wind Duration', list_cls=DataTag) + wind_speed = fields.ListField('Wind Speed', list_cls=DataTag) + wind_tunnel_open_circuit = fields.ListField('Wind Tunnel Open Circuit', list_cls=DataTag) + wind_tunnel_open_jet = fields.ListField('Wind Tunnel Open Jet', list_cls=DataTag) + wind_tunnel_closed_circuit = fields.ListField('Wind Tunnel closed Circuit', list_cls=DataTag) + three_sec_gust = fields.ListField('Three Sec Gust', list_cls=DataTag) + +class EventTagGeneral(MetadataModel): + _is_nested=True + data_units = fields.ListField('Data Units', list_cls=DataTag) + image = fields.ListField('Image', list_cls=DataTag) + video = fields.ListField('Video', list_cls=DataTag) + +class EventTag(MetadataModel): + _is_nested = True + centrifuge = fields.NestedObjectField(EventTagCentrifuge) + general = fields.NestedObjectField(EventTagGeneral) + shake_table = fields.NestedObjectField(EventTagShakeTable) + wave = fields.NestedObjectField(EventTagWave) + wind = fields.NestedObjectField(EventTagWind) + +class Event(RelatedEntity): + model_name = 'designsafe.project.event' + event_type = fields.CharField('Event Type', max_length=255, default='other') + title = fields.CharField('Title', max_length=1024) + description = fields.CharField('Description', max_length=1024, default='') + #load = fields.RelatedObjectField(FileModel, multiple=True) + tags = fields.NestedObjectField(SensorListTag) + analysis = fields.RelatedObjectField(Analysis) + project = fields.RelatedObjectField(ExperimentalProject) + experiments = fields.RelatedObjectField(Experiment) + model_configs = fields.RelatedObjectField(ModelConfiguration) + sensor_lists = fields.RelatedObjectField(SensorList) + files = fields.RelatedObjectField(FileModel, multiple=True) + +class Report(RelatedEntity): + model_name = 'designsafe.project.report' + title = fields.CharField('Title', max_length=1024) + description = fields.CharField('Description', max_length=1024, default='') + project = fields.RelatedObjectField(ExperimentalProject) + experiments = fields.RelatedObjectField(Experiment) + files = fields.RelatedObjectField(FileModel, multiple=True) diff --git a/designsafe/apps/api/projects/urls.py b/designsafe/apps/api/projects/urls.py index 36b814acc0..7426994479 100644 --- a/designsafe/apps/api/projects/urls.py +++ b/designsafe/apps/api/projects/urls.py @@ -11,18 +11,39 @@ """ from django.conf.urls import patterns, url, include -from designsafe.apps.api.projects.views import (ProjectCollectionView, +from designsafe.apps.api.projects.views import (ProjectListingView, + ProjectCollectionView, ProjectDataView, ProjectCollaboratorsView, - ProjectInstanceView) + ProjectInstanceView, + ProjectMetaView, + PublicationView) +from designsafe.apps.api.projects.managers.yamz import YamzBaseView urlpatterns = [ url(r'^$', ProjectCollectionView.as_view(), name='index'), - url(r'^(?P[a-z0-9\-]+)/$', ProjectInstanceView.as_view(), name='project'), + + url(r'^yamz/(?P[a-zA-Z0-9]+)/?$', YamzBaseView.as_view(), name='yamz'), + + url(r'^publication/((?P[a-zA-Z0-9\-\_\.]+)/?)?', PublicationView.as_view(), name='publication'), + + url(r'^listing/(?P[a-zA-Z0-9\-_\.]+)/?$', ProjectListingView.as_view(), name='listing'), + + url(r'^meta/(?P[^ \/]+)/?$', + ProjectMetaView.as_view(), name='project_meta'), + + url(r'^(?P[a-z0-9\-]+)/meta/(?P[a-zA-Z0-9\.\-_]+)/?$', + ProjectMetaView.as_view(), name='project_meta'), + + url(r'^(?P[a-z0-9\-]+)/$', + ProjectInstanceView.as_view(), name='project'), + url(r'^(?P[a-z0-9\-]+)/collaborators/$', ProjectCollaboratorsView.as_view(), name='project_collaborators'), + url(r'^(?P[a-z0-9\-]+)/data/$', ProjectDataView.as_view(), name='project_data'), + url(r'^(?P[a-z0-9\-]+)/data/(?P.*)/$', ProjectDataView.as_view(), name='project_data'), ] diff --git a/designsafe/apps/api/projects/views.py b/designsafe/apps/api/projects/views.py index 60a1f3fdac..59e3082969 100644 --- a/designsafe/apps/api/projects/views.py +++ b/designsafe/apps/api/projects/views.py @@ -1,16 +1,28 @@ from django.core.urlresolvers import reverse from django.conf import settings -from django.http import JsonResponse +from django.http.response import HttpResponseForbidden +from django.http import JsonResponse, HttpResponseBadRequest from django.contrib.auth import get_user_model from django.core.mail import send_mail +from django.utils.decorators import method_decorator +from django.contrib.auth.decorators import login_required +from designsafe.apps.api.decorators import agave_jwt_login from designsafe.apps.api import tasks from designsafe.apps.api.views import BaseApiView from designsafe.apps.api.mixins import SecureMixin from designsafe.apps.api.projects.models import Project from designsafe.apps.api.agave import get_service_account_client +from designsafe.apps.api.agave.models.metadata import BaseMetadataPermissionResource from designsafe.apps.api.agave.models.files import BaseFileResource from designsafe.apps.api.agave.models.util import AgaveJSONEncoder from designsafe.apps.accounts.models import DesignSafeProfile +from requests.exceptions import HTTPError +from designsafe.apps.api.projects.models import (ExperimentalProject, FileModel, + Experiment, ModelConfiguration, + Event, Analysis, SensorList, + Report) +from designsafe.apps.api.agave.filemanager.public_search_index import PublicationManager, Publication +from designsafe.apps.api import tasks import logging import json @@ -27,8 +39,54 @@ def template_project_storage_system(project): system_template['storage']['rootDir'].format(project.uuid) return system_template +class PublicationView(BaseApiView): + def get(self, request, project_id): + pub = Publication(project_id=project_id) + if pub is not None: + return JsonResponse(pub.to_dict()) + else: + return JsonResponse({'status': 404, + 'message': 'Not found'}, + status=404) + + @method_decorator(agave_jwt_login) + @method_decorator(login_required) + def post(self, request, **kwargs): + if request.is_ajax(): + data = json.loads(request.body) + + else: + data = request.POST + + #logger.debug('publication: %s', json.dumps(data, indent=2)) + pub = PublicationManager().save_publication(data['publication']) + tasks.save_publication.apply_async(args=[pub.projectId],queue='files') + return JsonResponse({'status': 200, + 'message': 'Your publication has been ' + 'schedule for publication'}, + status=200) -class ProjectCollectionView(BaseApiView, SecureMixin): +class ProjectListingView(SecureMixin, BaseApiView): + def get(self, request, username): + """Returns a list of Project for a specific user. + + If the requesting user is a super user then we can 'impersonate' + another user. Else this is an unauthorized request. + + """ + if not request.user.is_superuser: + return HttpResponseForbidden() + + user = get_user_model().objects.get(username=username) + ag = user.agave_oauth.client + q = request.GET.get('q', None) + if not q: + projects = Project.list_projects(agave_client=ag) + else: + projects = Project.search(q=q, agave_client=ag) + return JsonResponse({'projects': projects}, encoder=AgaveJSONEncoder) + +class ProjectCollectionView(SecureMixin, BaseApiView): def get(self, request): """ @@ -37,6 +95,7 @@ def get(self, request): :return: A list of Projects to which the current user has access :rtype: JsonResponse """ + #raise HTTPError('Custom Error') ag = request.user.agave_oauth.client projects = Project.list_projects(agave_client=ag) data = {'projects': projects} @@ -79,11 +138,15 @@ def post(self, request): associated_projects = post_data.get('associatedProjects', {}) description = post_data.get('description', '') new_pi = post_data.get('pi') + keywords = post_data.get('keywords', '') + project_id = post_data.get('projectId', '') p.update(title=title, award_number=award_number, project_type=project_type, associated_projects=associated_projects, - description=description) + description=description, + keywords=keywords, + projectId=project_id) p.pi = new_pi p.save() @@ -141,11 +204,11 @@ def post(self, request): try: collab_user.profile.send_mail( "[Designsafe-CI] You have been added to a project!", - "

    You have been added to the project {title} as PI

    You can visit the project using this url {url}".format(title=p.title, + "

    You have been added to the project {title} as PI

    You can visit the project using this url {url}".format(title=p.title, url=request.build_absolute_uri(reverse('designsafe_data:data_depot') + '/projects/%s/' % (p.uuid,)))) except DesignSafeProfile.DoesNotExist as err: logger.info("Could not send email to user %s", collab_user) - body = "

    You have been added to the project {title} as PI

    You can visit the project using this url {url}".format(title=p.title, + body = "

    You have been added to the project {title} as PI

    You can visit the project using this url {url}".format(title=p.title, url=request.build_absolute_uri(reverse('designsafe_data:data_depot') + '/projects/%s/' % (p.uuid,))) send_mail( "[Designsafe-CI] You have been added to a project!", @@ -153,12 +216,30 @@ def post(self, request): settings.DEFAULT_FROM_EMAIL, [collab_user.email], html_message=body) - #logger.exception(err) + tasks.set_project_id.apply_async(args=[p.uuid],queue="api") return JsonResponse(p, encoder=AgaveJSONEncoder, safe=False) +class ProjectMetaLookupMixin(object): + def _lookup_model(self, name): + if name == 'designsafe.project': + return ExperimentalProject + elif name == 'designsafe.project.experiment': + return Experiment + elif name == 'designsafe.project.event': + return Event + elif name == 'designsafe.project.analysis': + return Analysis + elif name == 'designsafe.project.sensor_list': + return SensorList + elif name == 'designsafe.project.model_config': + return ModelConfiguration + elif name == 'designsafe.project.report': + return Report + else: + raise ValueError('No module found with that name.') -class ProjectInstanceView(BaseApiView, SecureMixin): +class ProjectInstanceView(SecureMixin, BaseApiView, ProjectMetaLookupMixin): def get(self, request, project_id): """ @@ -167,8 +248,12 @@ def get(self, request, project_id): :rtype: JsonResponse """ ag = request.user.agave_oauth.client - project = Project.from_uuid(agave_client=ag, uuid=project_id) - return JsonResponse(project, encoder=AgaveJSONEncoder, safe=False) + #project = Project.from_uuid(agave_client=ag, uuid=project_id) + meta_obj = ag.meta.getMetadata(uuid=project_id) + model_cls = self._lookup_model(meta_obj['name']) + project = model_cls(**meta_obj) + logger.info(project.__dict__) + return JsonResponse(project.to_body_dict(), safe=False) def post(self, request, project_id): """ @@ -190,20 +275,27 @@ def post(self, request, project_id): project_type = post_data.get('projectType', 'other') associated_projects = post_data.get('associatedProjects', {}) description = post_data.get('description', '') + team_members = post_data.get('teamMembers', []) + keywords = post_data.get('keywords', '') new_pi = post_data.get('pi') - if p.pi != new_pi: + project_id = post_data.get('projectId', '') + if new_pi and new_pi != 'null' and p.pi != new_pi: p.pi = new_pi p.add_collaborator(new_pi) p.update(title=title, award_number=award_number, project_type=project_type, associated_projects=associated_projects, - description=description) + description=description, + team_members=team_members, + keywords=keywords, + projectId=project_id) p.save() - return JsonResponse(p, encoder=AgaveJSONEncoder, safe=False) + project = ExperimentalProject(**p.to_dict()) + return JsonResponse(project.to_body_dict()) -class ProjectCollaboratorsView(BaseApiView, SecureMixin): +class ProjectCollaboratorsView(SecureMixin, BaseApiView): def get(self, request, project_id): ag = request.user.agave_oauth.client @@ -221,19 +313,32 @@ def post(self, request, project_id): project = Project.from_uuid(agave_client=ag, uuid=project_id) username = post_data.get('username') + member_type = post_data.get('memberType', 'teamMember') - project.add_collaborator(username) + logger.info('member Type: %s', member_type) + if member_type == 'teamMember': + project.add_collaborator(username) + elif member_type == 'coPis': + project.add_co_pi(username) + # members_list = project.value.get(member_type, []) + # members_list.append(username) + # _kwargs = {member_type: members_list} + # project.update(**_kwargs) + project.save() + logger.info(project.value) + tasks.check_project_files_meta_pems.apply_async(args=[project.uuid ], queue='api') collab_users = get_user_model().objects.filter(username=username) + collab_users = [] if collab_users: collab_user = collab_users[0] try: collab_user.profile.send_mail( "[Designsafe-CI] You have been added to a project!", - "

    You have been added to the project {title} as PI

    You can visit the project using this url {url}".format(title=project.title, + "

    You have been added to the project {title} as PI

    You can visit the project using this url {url}".format(title=project.title, url=request.build_absolute_uri(reverse('designsafe_data:data_depot') + '/projects/%s/' % (project.uuid,)))) except DesignSafeProfile.DoesNotExist as err: logger.info("Could not send email to user %s", collab_user) - body = "

    You have been added to the project {title} as PI

    You can visit the project using this url {url}".format(title=project.title, + body = "

    You have been added to the project {title} as PI

    You can visit the project using this url {url}".format(title=project.title, url=request.build_absolute_uri(reverse('designsafe_data:data_depot') + '/projects/%s/' % (project.uuid,))) send_mail( "[Designsafe-CI] You have been added to a project!", @@ -243,13 +348,7 @@ def post(self, request, project_id): html_message=body) #logger.exception(err) - members_list = project.value.get(member_type, []) - members_list.append(username) - _kwargs = {member_type: members_list} - project.update(**_kwargs) - project.save() - tasks.check_project_files_meta_pems.apply_async(args=[project.uuid ]) - return JsonResponse({'status': 'ok'}) + return JsonResponse(project.team_members()) def delete(self, request, project_id): if request.is_ajax(): @@ -260,12 +359,14 @@ def delete(self, request, project_id): ag = get_service_account_client() project = Project.from_uuid(agave_client=ag, uuid=project_id) - project.remove_collaborator(post_data.get('username')) - tasks.check_project_files_meta_pems.apply_async(args=[project.uuid]) + # project.remove_collaborator(post_data.get('username')) + project.remove_co_pi(post_data.get('username')) + project.save() + tasks.check_project_files_meta_pems.apply_async(args=[project.uuid], queue='api') return JsonResponse({'status': 'ok'}) -class ProjectDataView(BaseApiView, SecureMixin): +class ProjectDataView(SecureMixin, BaseApiView): def get(self, request, project_id, file_path=''): """ @@ -279,3 +380,115 @@ def get(self, request, project_id, file_path=''): listing = BaseFileResource.listing(ag, p.project_system_id, list_path) return JsonResponse(listing, encoder=AgaveJSONEncoder, safe=False) + +class ProjectMetaView(BaseApiView, SecureMixin, ProjectMetaLookupMixin): + + def get(self, request, project_id=None, name=None, uuid=None): + """ + + :return: + :rtype: JsonResponse + """ + ag = request.user.agave_oauth.client + try: + if name is not None: + model = self._lookup_model(name) + resp = model._meta.model_manager.list(ag, project_id) + resp_list = [r.to_body_dict() for r in resp] + resp_list = sorted(resp_list, key=lambda x: x['created']) + return JsonResponse(resp_list, safe=False) + elif uuid is not None: + meta = ag.meta.getMetadata(uuid=uuid) + model = self._lookup_model(meta['name']) + resp = model(**meta) + return JsonResponse(resp.to_body_dict(), safe=False) + except ValueError: + return HttpResponseBadRequest('Entity not valid.') + + def delete(self, request, uuid): + """ + + :return: + :rtype: JsonResponse + """ + ag = get_service_account_client() + meta_obj = ag.meta.getMetadata(uuid=uuid) + model = self._lookup_model(meta_obj['name']) + meta = model(**meta_obj) + ag.meta.deleteMetadata(uuid=uuid) + return JsonResponse(meta.to_body_dict(), safe=False) + + def post(self, request, project_id, name): + """ + + :param request: + :return: + """ + ag = get_service_account_client() + + if request.is_ajax(): + post_data = json.loads(request.body) + else: + post_data = request.POST.copy() + entity = post_data.get('entity') + try: + model_cls = self._lookup_model(name) + logger.debug('entity: %s', entity) + model = model_cls(value=entity) + logger.debug('model uuid: %s', model.uuid) + file_uuids = [] + if 'filePaths' in entity: + file_paths = entity.get('filePaths', []) + project_system = ''.join(['project-', project_id]) + user_ag = request.user.agave_oauth.client + for file_path in file_paths: + file_obj = BaseFileResource.listing(user_ag, + project_system, + file_path) + + file_uuids.append(file_obj.uuid) + for file_uuid in file_uuids: + model.files.add(file_uuid) + model.associate(file_uuids) + model.project.add(project_id) + model.associate(project_id) + saved = model.save(ag) + resp = model_cls(**saved) + #TODO: This should happen in a celery task and implemented in a manager + #Get project's metadata permissions + pems = BaseMetadataPermissionResource.list_permissions(project_id, ag) + #Loop permissions and set them in whatever metadata object we're saving + for pem in pems: + _pem = BaseMetadataPermissionResource(resp.uuid, ag) + _pem.username = pem.username + _pem.read = pem.read + _pem.write = pem.write + _pem.save() + + except ValueError: + return HttpResponseBadRequest('Entity not valid.') + + return JsonResponse(resp.to_body_dict(), safe=False) + + def put(self, request, uuid): + """ + + :param request: + :return: + """ + ag = get_service_account_client() + + if request.is_ajax(): + post_data = json.loads(request.body) + else: + post_data = request.POST.copy() + try: + entity = post_data.get('entity') + model_cls = self._lookup_model(entity['name']) + model = model_cls(**entity) + saved = model.save(ag) + resp = model_cls(**saved) + except ValueError: + return HttpResponseBadRequest('Entity not valid.') + + return JsonResponse(resp.to_body_dict(), safe=False) diff --git a/designsafe/apps/api/public_data/views.py b/designsafe/apps/api/public_data/views.py index c41cc082d7..9e50251929 100644 --- a/designsafe/apps/api/public_data/views.py +++ b/designsafe/apps/api/public_data/views.py @@ -31,6 +31,7 @@ from designsafe.apps.api.agave.models.util import AgaveJSONEncoder from designsafe.apps.api.agave.filemanager.public_search_index import PublicElasticFileManager from designsafe.apps.api.agave.filemanager.community import CommunityFileManager +from designsafe.apps.api.agave.filemanager.published import PublishedFileManager from designsafe.apps.api.agave.views import FileMediaView logger = logging.getLogger(__name__) @@ -41,8 +42,9 @@ def get(self, request, file_mgr_name, system_id=None, file_path=None): """GET handler.""" logger.info('file_mgr_name: %s', file_mgr_name) - if file_mgr_name != PublicElasticFileManager.NAME \ - and file_mgr_name != 'community': + if file_mgr_name not in [PublicElasticFileManager.NAME, + 'community', + 'published']: return HttpResponseBadRequest('Wrong Manager') if system_id is None: @@ -53,11 +55,15 @@ def get(self, request, file_mgr_name, elif file_mgr_name == 'community': ag = get_user_model().objects.get(username='envision').agave_oauth.client file_mgr = CommunityFileManager(ag) + elif file_mgr_name == 'published': + ag = get_user_model().objects.get(username='envision').agave_oauth.client + file_mgr = PublishedFileManager(ag) + #listing = file_mgr.listing(system_id, file_path) + #return JsonResponse({'response': 'ok'}) offset = int(request.GET.get('offset', 0)) limit = int(request.GET.get('limit', 100)) listing = file_mgr.listing(system_id, file_path, offset, limit) - return JsonResponse(listing.to_dict()) class PublicMediaView(FileMediaView): @@ -76,7 +82,7 @@ def put(self, request, *args, **kwargs): body = request.POST.copy() action = body.get('action', '') - if action in ['copy', 'mkdir', 'move', 'rename', 'trash']: + if action in ['mkdir', 'move', 'rename', 'trash']: return HttpResponseBadRequest('Invalid Action') return super(PublicMediaView, self).put(request, *args, **kwargs) @@ -114,8 +120,9 @@ class PublicPemsView(BaseApiView): def get(self, request, file_mgr_name, system_id = None, file_path = None): """ GET handler """ - if file_mgr_name != PublicElasticFileManager.NAME \ - and file_mgr_name != 'community': + if file_mgr_name not in [PublicElasticFileManager.NAME, + 'community', + 'published']: return HttpResponseBadRequest() if system_id is None: diff --git a/designsafe/apps/api/search/views.py b/designsafe/apps/api/search/views.py index 9244678ffc..dc6e3255f3 100644 --- a/designsafe/apps/api/search/views.py +++ b/designsafe/apps/api/search/views.py @@ -31,15 +31,16 @@ def get(self, request): # stuff too. es_query = Search(index="nees,cms")\ .query(Q("match", systemId=system_id) | Q("exists", field="django_id"))\ - .query("query_string", query=q, default_operator="and")\ + .query("query_string", query=q, default_operator="and", fields=[])\ .query(~Q('match', type='dir'))\ - .highlight("body", - fragment_size=150, + .highlight('body', + fragment_size=100, pre_tags=[""], post_tags=[""], )\ - .highlight_options(require_field_match=False)\ - .extra(from_=offset, size=limit) + .highlight_options(require_field_match=True)\ + .extra(from_=offset, size=limit)\ + if type_filter == 'files': es_query = es_query.query("match", type="file")\ .filter("term", _type="object")\ @@ -59,11 +60,24 @@ def get(self, request): } ) elif type_filter == 'web': - es_query._index = 'cms' - es_query = es_query.query(~Q('match', body='\n'))\ - .filter("exists", field="body") - # es_query = es_query.query("_source", include=['body']) - + es_query = Search(index="cms")\ + .query("query_string", query=q, default_operator="and", fields=["body"])\ + .highlight('body', + fragment_size=100, + pre_tags=[""], + post_tags=[""], + )\ + .highlight_options(require_field_match=True)\ + .extra(from_=offset, size=limit)\ + # es_query._index = 'cms' + # es_query = es_query.query("query_string", query=q, default_operator="and", fields=["body"]) + # es_query = es_query.highlight('body', + # fragment_size=100, + # pre_tags=[""], + # post_tags=[""], + # )\ + # .highlight_options(require_field_match=True) + # logger.info(es_query.to_dict()) # logger.info(es_query.to_dict()) res = es_query.execute() @@ -71,13 +85,7 @@ def get(self, request): # for the sidebar with the different categories to refine by web_query = Search(index="cms")\ - .query("query_string", query=q, default_operator="and")\ - .highlight("body", - fragment_size=150, - pre_tags=[""], - post_tags=[""], - )\ - .highlight_options(require_field_match=False)\ + .query("query_string", query=q, default_operator="and", fields=['body'])\ .extra(from_=offset, size=limit)\ .execute() # web_query = web_query.execute() diff --git a/designsafe/apps/api/tasks.py b/designsafe/apps/api/tasks.py index 08e0936255..e9105baeda 100644 --- a/designsafe/apps/api/tasks.py +++ b/designsafe/apps/api/tasks.py @@ -3,22 +3,25 @@ import re import os import sys -import subprocess +import json +import urllib from datetime import datetime from celery import shared_task from django.core.urlresolvers import reverse from django.contrib.auth import get_user_model from django.conf import settings +from requests.exceptions import HTTPError from designsafe.apps.api.notifications.models import Notification, Broadcast from designsafe.apps.api.agave import get_service_account_client logger = logging.getLogger(__name__) -@shared_task(bind=True) -def reindex_agave(self, username, file_id, full_indexing = True, - levels = 0, pems_indexing = True, index_full_path = True): +@shared_task(bind=True, max_retries=None) +def reindex_agave(self, username, file_id, full_indexing=True, + levels=1, pems_indexing=True, index_full_path=True): user = get_user_model().objects.get(username=username) + levels=1 from designsafe.apps.api.data import AgaveFileManager agave_fm = AgaveFileManager(user) @@ -37,6 +40,10 @@ def reindex_agave(self, username, file_id, full_indexing = True, pems_indexing = pems_indexing, index_full_path = index_full_path, levels = levels) + parent_path = os.path.join(*file_path.strip('/').split('/')[:-1]) + agave_fm.indexer.index(system_id, parent_path, file_user, + levels = 1) + @shared_task(bind=True) def share_agave(self, username, file_id, permissions, recursive): @@ -695,3 +702,141 @@ def check_project_meta_pems(self, metadata_uuid): service = get_service_account_client() bfm = BaseFileMetadata.from_uuid(service, metadata_uuid) bfm.match_pems_to_project() + +@shared_task(bind=True) +def set_project_id(self, project_uuid): + from designsafe.apps.api.projects.models import ExperimentalProject + logger.debug('Setting project ID') + service = get_service_account_client() + project = ExperimentalProject._meta.model_manager.get(service, project_uuid) + id_metas = service.meta.listMetadata(q='{"name": "designsafe.project.id"}') + logger.debug(json.dumps(id_metas, indent=4)) + if not len(id_metas): + raise Exception('No project Id found') + + id_meta = id_metas[0] + project_id = int(id_meta['value']['id']) + project_id = project_id + 1 + for i in range(10): + _projs = service.meta.listMetadata(q='{{"name": "designsafe.project", "value.projectId": {} }}'.format(project_id)) + if len(_projs): + project_id = project_id + 1 + + project.project_id = 'PRJ-{}'.format(str(project_id)) + project.save(service) + logger.debug('updated project id=%s', project.uuid) + id_meta['value']['id'] = project_id + service.meta.updateMetadata(body=id_meta, uuid=id_meta['uuid']) + logger.debug('updated id record=%s', id_meta['uuid']) + +@shared_task(bind=True, max_retries=5) +def copy_publication_files_to_corral(self, project_id): + from designsafe.apps.api.agave.filemanager.public_search_index import Publication + from designsafe.apps.api.agave.models.files import BaseFileResource + publication = Publication(project_id=project_id) + filepaths = publication.related_file_paths() + base_path = ''.join(['/', publication.projectId]) + service = get_service_account_client() + service.files.manage(systemId=settings.PUBLISHED_SYSTEM, + filePath='/', + body={'action': 'mkdir', + 'path': base_path}) + base_dir = BaseFileResource.listing(system=settings.PUBLISHED_SYSTEM, + path=base_path, + agave_client=service) + proj_system = 'project-{}'.format(publication.project['uuid']) + for filepath in filepaths: + filepath = filepath.strip('/') + logger.info('Copying: {}'.format(filepath)) + path_comps = filepath.split('/') + parent_path = os.path.join(*path_comps[:-1]) + file_obj = BaseFileResource.\ + listing(system=proj_system, + path=filepath, + agave_client=service) + if file_obj.type == 'dir': + logger.info('path is a directory, ensuring path exists') + base_obj = BaseFileResource.\ + ensure_path(service, + settings.PUBLISHED_SYSTEM, + os.path.join(base_path, parent_path)) + else: + logger.info('path is a file, ensuring parent path exists') + base_obj = BaseFileResource.\ + ensure_path(service, + settings.PUBLISHED_SYSTEM, + os.path.join(base_path, parent_path)) + try: + base_obj.import_data(file_obj.system, file_obj.path) + except Exception as err: + logger.error('Error when copying data to published: %s. %s', filepath, err) + #self.retry(exc=err) + + try: + image = BaseFileResource.\ + listing(system=proj_system, + path='projectimage.jpg', + agave_client=service) + base_dir.import_data(image.system, image.path) + except HTTPError as err: + logger.debug('No project image') + save_to_fedora.apply_async(args=[project_id]) + +@shared_task(bind=True, max_retries=5, default_retry_delay=60) +def save_publication(self, project_id): + from designsafe.apps.api.agave.filemanager.public_search_index import Publication + from designsafe.apps.api.projects.managers import publication as PublicationManager + try: + pub = Publication(project_id=project_id) + publication = PublicationManager.reserve_publication(pub.to_dict()) + pub.update(**publication) + copy_publication_files_to_corral.apply_async(args=[pub.projectId],queue="files") + except Exception as exc: + logger.error('Proj Id: %s. %s', project_id, exc) + raise self.retry(exc=exc) + +@shared_task(bind=True, max_retries=5, default_retry_delay=60) +def save_to_fedora(self, project_id): + import requests + import magic + from designsafe.apps.api.agave.filemanager.public_search_index import Publication + try: + pub = Publication(project_id=project_id) + pub.update(status='published') + _root = os.path.join('/corral-repl/tacc/NHERI/published', project_id) + fedora_base = 'http://fedoraweb01.tacc.utexas.edu:8080/fcrepo/rest/publications_01' + res = requests.get(fedora_base) + if res.status_code == 404 or res.status_code == 410: + requests.put(fedora_base) + + fedora_project_base = ''.join([fedora_base, '/', project_id]) + res = requests.get(fedora_project_base) + if res.status_code == 404 or res.status_code == 410: + requests.put(fedora_project_base) + + headers = {'Content-Type': 'text/plain'} + #logger.debug('walking: %s', _root) + for root, dirs, files in os.walk(_root): + for name in files: + mime = magic.Magic(mime=True) + headers['Content-Type'] = mime.from_file(os.path.join(root, name)) + #files + full_path = os.path.join(root, name) + _path = full_path.replace(_root, '', 1) + _path = _path.replace('[', '-') + _path = _path.replace(']', '-') + url = ''.join([fedora_project_base, urllib.quote(_path)]) + #logger.debug('uploading: %s', url) + with open(os.path.join(root, name), 'rb') as _file: + requests.put(url, data=_file, headers=headers) + + for name in dirs: + #dirs + full_path = os.path.join(root, name) + _path = full_path.replace(_root, '', 1) + url = ''.join([fedora_project_base, _path]) + #logger.debug('creating: %s', _path) + requests.put(url) + except Exception as exc: + logger.error('Proj Id: %s. %s', project_id, exc) + raise self.retry(exc=exc) diff --git a/designsafe/apps/api/tests.py b/designsafe/apps/api/tests.py index 7ce503c2dd..dfc1147184 100644 --- a/designsafe/apps/api/tests.py +++ b/designsafe/apps/api/tests.py @@ -1,3 +1,70 @@ -from django.test import TestCase +from django.test import TestCase, RequestFactory +from django.conf import settings +from django.contrib.auth import get_user_model + +from designsafe.apps.api.projects.models import ExperimentalProject, ModelConfiguration, FileModel + +from agavepy.agave import Agave +import mock +import json + +import logging + +logger = logging.getLogger(__name__) # Create your tests here. +class ProjectDataModelsTestCase(TestCase): + fixtures = ['user-data.json', 'agave-oauth-token-data.json'] + + def setUp(self): + user = get_user_model().objects.get(pk=2) + user.set_password('password') + user.save() + self.user = user + with open('designsafe/apps/api/fixtures/agave-model-config-meta.json') as f: + model_config_meta = json.load(f) + self.model_config_meta = model_config_meta + + with open('designsafe/apps/api/fixtures/agave-file-meta.json') as f: + file_meta = json.load(f) + self.file_meta = file_meta + + with open('designsafe/apps/api/fixtures/agave-experiment-meta.json') as f: + experiment_meta = json.load(f) + self.experiment_meta = experiment_meta + + with open('designsafe/apps/api/fixtures/agave-project-meta.json') as f: + project_meta = json.load(f) + self.project_meta = project_meta + +class InitExperimentalModels(ProjectDataModelsTestCase): + def test_init_experimental_project(self): + """Test initializing ExperimentalProject + """ + #logger.debug('project_meta: %s', + # json.dumps(self.project_meta, indent=4)) + exp = ExperimentalProject(**self.project_meta) + #logger.debug('experiment dict: %s', + # json.dumps(exp.to_body_dict(), indent=4)) + +class InitModelConfigurationModel(ProjectDataModelsTestCase): + def test_init_model_configuration(self): + """Test initializing ModelConfiguration + """ + #logger.debug('model_config_meta: %s', + # json.dumps(self.model_config_meta, indent=4)) + model_config = ModelConfiguration(**self.model_config_meta) + #logger.debug('model_config dict: %s', + # json.dumps(model_config.to_body_dict(), indent=4)) + +class InitFileModel(ProjectDataModelsTestCase): + def test_init_file_model(self): + """Test initializing File Model + """ + #logger.debug('file meta: %s', + # json.dumps(self.file_meta, indent=4)) + file_meta = FileModel(**self.file_meta) + #logger.debug('dict: %s', file_meta._meta._reverse_fields) + #file_meta.modelconfiguration_set() + #logger.debug('file meta dict: %s', + # json.dumps(file_meta.to_body_dict(), indent=4)) diff --git a/designsafe/apps/api/users/views.py b/designsafe/apps/api/users/views.py index 4c30b4f857..9849a85381 100644 --- a/designsafe/apps/api/users/views.py +++ b/designsafe/apps/api/users/views.py @@ -5,6 +5,8 @@ from django.forms.models import model_to_dict from django.http import HttpResponseNotFound, JsonResponse, HttpResponse from django.views.generic.base import View +from django.core.exceptions import ObjectDoesNotExist +from pytas.http import TASClient from elasticsearch_dsl import Q, Search @@ -54,8 +56,25 @@ def get(self, request): model = get_user_model() q = request.GET.get('username') if q: - user = model.objects.get(username=q) - return JsonResponse(model_to_dict(user, fields=resp_fields)) + try: + user = model.objects.get(username=q) + except ObjectDoesNotExist as err: + return HttpResponseNotFound(); + res_dict = { + 'first_name': user.first_name, + 'last_name': user.last_name, + 'email': user.email, + 'username': user.username, + } + try: + user_tas = TASClient().get_user(username=q) + res_dict['profile'] = { + 'institution': user_tas['institution'] + } + except Exception as err: + logger.info('No Profile.') + + return JsonResponse(res_dict) q = request.GET.get('q') diff --git a/designsafe/apps/api/utils.py b/designsafe/apps/api/utils.py new file mode 100644 index 0000000000..1f1911913e --- /dev/null +++ b/designsafe/apps/api/utils.py @@ -0,0 +1,24 @@ +"""Utilities for APIs +""" +import logging +from django.conf import settings + +#pylint: disable=invalid-name +logger = logging.getLogger(__name__) +#pylint: enable=invalid-name + +def is_jwt(request): + """Checks if a request is authenticated with a JWT + + :param obj request: Django request object + + ..note:: This function only checks if the correct header + is present and if the value of said header is not empty. + Use :func:`~designsafe.apps.api.decorators.agave_jwt_login` to + decorate protected views. + """ + val = request.META.get(settings.AGAVE_JWT_HEADER) + if val: + return True + + return False diff --git a/designsafe/apps/auth/backends.py b/designsafe/apps/auth/backends.py index a5dbf00ddd..81fcad755d 100644 --- a/designsafe/apps/auth/backends.py +++ b/designsafe/apps/auth/backends.py @@ -7,6 +7,7 @@ from django.dispatch import receiver from designsafe.apps.accounts.models import DesignSafeProfile from pytas.http import TASClient +from .tasks import check_or_create_agave_home_dir import logging import re import requests @@ -84,7 +85,7 @@ def authenticate(self, username=None, password=None, request=None, **kwargs): last_name=tas_user['lastName'], email=tas_user['email'] ) - + check_or_create_agave_home_dir.apply_async(args=(user.username,)) try: profile = DesignSafeProfile.objects.get(user=user) except DesignSafeProfile.DoesNotExist: @@ -138,6 +139,8 @@ def authenticate(self, *args, **kwargs): email=agave_user['email'] ) + check_or_create_agave_home_dir.apply_async(args=(user.username,)) + try: profile = DesignSafeProfile.objects.get(user=user) except DesignSafeProfile.DoesNotExist: diff --git a/designsafe/apps/auth/signals.py b/designsafe/apps/auth/signals.py index 677bd262a4..a68d850278 100644 --- a/designsafe/apps/auth/signals.py +++ b/designsafe/apps/auth/signals.py @@ -5,4 +5,4 @@ @receiver(user_logged_in) def on_user_logged_in(sender, user, request, **kwargs): - check_or_create_agave_home_dir.apply_async(args=(user.username,)) + check_or_create_agave_home_dir.apply_async(args=(user.username,), queue='files') diff --git a/designsafe/apps/auth/tasks.py b/designsafe/apps/auth/tasks.py index 5a0e5b7344..19a593c1fa 100644 --- a/designsafe/apps/auth/tasks.py +++ b/designsafe/apps/auth/tasks.py @@ -26,6 +26,19 @@ def check_or_create_agave_home_dir(username): ag.files.manage(systemId=settings.AGAVE_STORAGE_SYSTEM, filePath='', body=body) + ds_admin_client = Agave(api_server=getattr(settings, 'AGAVE_TENANT_BASEURL'), token=getattr(settings, 'AGAVE_SUPER_TOKEN')) + job_body = { + 'inputs': { + 'username': username, + 'directory': 'shared/{}'.format(username) + }, + 'name': 'setfacl', + 'appId': 'setfacl_corral3-0.1' + } + + response = ds_admin_client.jobs.submit(body=job_body) + logger.debug('setfacl response: {}'.format(response)) + # add dir to index fm = FileManager(agave_client=ag) fm.index(settings.AGAVE_STORAGE_SYSTEM, username, username, levels=1) diff --git a/designsafe/apps/auth/templates/designsafe/apps/auth/login.html b/designsafe/apps/auth/templates/designsafe/apps/auth/login.html index 3007df44d7..1085014c6b 100644 --- a/designsafe/apps/auth/templates/designsafe/apps/auth/login.html +++ b/designsafe/apps/auth/templates/designsafe/apps/auth/login.html @@ -10,9 +10,9 @@

    Log in to DesignSafe-CI

    -

    +

    @@ -36,7 +36,7 @@

    Log in TACC Website

    - @@ -86,10 +82,16 @@

    Recent Apps

    My Jobs

    -
    -

    No recent jobs!

    +
    +

    + No recent jobs! You can submit jobs in the + Workspace +

    +
    + +
    -
    +
    • @@ -132,7 +134,7 @@

      Notifications

      • diff --git a/designsafe/apps/dashboard/static/designsafe/apps/dashboard/js/charts/DS_TSBarChart.js b/designsafe/apps/dashboard/static/designsafe/apps/dashboard/js/charts/DS_TSBarChart.js index ead546fb51..0bc9703175 100644 --- a/designsafe/apps/dashboard/static/designsafe/apps/dashboard/js/charts/DS_TSBarChart.js +++ b/designsafe/apps/dashboard/static/designsafe/apps/dashboard/js/charts/DS_TSBarChart.js @@ -6,14 +6,16 @@ function DS_TSBarChart (element_id) { height = 500 - margin.top - margin.bottom, xSelector = function (d) {return d.datetime_utc;}, ySelector = function (d) {return d.value;}, - start_date = new Date(new Date().getTime() - 14 * 24 * 60 * 60 * 1000), - end_date = new Date(), + start_date = new Date(new Date(new Date().getTime() - 14 * 24 * 60 * 60 * 1000).setHours(0,0,0,0)), + end_date = new Date(new Date().setHours(0,0,0,0)), data, x, y, xAxis, yAxis, axis_label, focus, svg, dispatch = d3.dispatch('bar_click'); + var num_days = (end_date.getTime() - start_date.getTime()) / (24 * 60 * 60 *1000); + console.log(num_days) d3.select(window).on('resize', function () { width = $(element_id).width() - margin.left - margin.right; exports(); @@ -22,7 +24,7 @@ function DS_TSBarChart (element_id) { function exports () { d3.select(element_id).html(''); - x = d3.scaleTime().range([0, width]).domain([start_date, end_date]); + x = d3.scaleTime().range([0, width - margin.left-margin.right]).domain([start_date, end_date]); y = d3.scaleLinear().range([height, 0]); svg = d3.select(element_id).append("svg") @@ -32,11 +34,11 @@ function DS_TSBarChart (element_id) { xAxis = d3.axisBottom(x) .tickPadding(0) // .tickSize(-height, 1, 0) - .ticks(13) + // .ticks(14) .tickFormat(d3.timeFormat("%m/%d")); yAxis = d3.axisLeft(y) - .ticks(3) + // .ticks(3) .tickSize(-width, 0, 0) .tickFormat(function(e){ if(Math.floor(e) != e) { @@ -86,27 +88,13 @@ function DS_TSBarChart (element_id) { .call(yAxis); focus.selectAll('.bar').remove(); - // focus.append("g") - // .selectAll("g") - // .data(data) - // .enter().append("g") - // // .attr("fill", function(d) { return z(d.key); }) - // .attr("x", function (d) { return x(xSelector(d)) - (width / x.ticks().length / 2) + 2.5 ;}) - // .selectAll("rect") - // .data(function(d) { return d.values; }) - // .enter().append("rect") - // .attr("class", "bar") - // - // .attr("y", function(d, i) { return height - y(i); }) - // .attr("height", function(d, i) { return y}) - // .attr("width", 15); - focus.selectAll(".bar") .data(data) .enter().append("rect") .attr("class", "bar") - .attr("x", function (d) { return x(xSelector(d)) - (width / x.ticks().length / 2) + 2.5 ;}) - .attr("width", width / x.ticks().length - 5) + // .attr("x", function (d) { return x(xSelector(d)) - ( (width - margin.right - margin.right) / x.ticks().length / 2) + 2.5 ;}) + .attr("x", function (d) { return x(xSelector(d)) - (width - margin.left - margin.right) / num_days/2 + 2.5 }) + .attr("width", (width - margin.left - margin.right) / num_days - 5) .attr("y", function (d) {return y(ySelector(d));}) .attr("height", function(d) { return height - y(ySelector(d)); }) .on('click', bar_click); @@ -160,13 +148,19 @@ function DS_TSBarChart (element_id) { exports.start_date = function (d) { if (!arguments.length) return start_date; - start_date = d; + //this just gives a little padding for the first bar, so that + //it doesn't go outside the chart + start_date = new Date(new Date(d.getTime() - (24 * 60 * 60 *1000)).setHours(0,0,0,0)); + x.domain([start_date, end_date]) + num_days = (end_date.getTime() - start_date.getTime()) / (24 * 60 * 60 *1000); return exports; } exports.end_date = function (d) { if (!arguments.length) return end_date; - end_date = d; + end_date = new Date(new Date(d.getTime() - (24 * 60 * 60 *1000)).setHours(0,0,0,0)); + x.domain([start_date, end_date]); + num_days = (end_date.getTime() - start_date.getTime()) / (24 * 60 * 60 *1000); return exports; } diff --git a/designsafe/apps/dashboard/static/designsafe/apps/dashboard/js/controllers/dashboardCtrl.js b/designsafe/apps/dashboard/static/designsafe/apps/dashboard/js/controllers/dashboardCtrl.js index 566545fa22..e42c8b638e 100644 --- a/designsafe/apps/dashboard/static/designsafe/apps/dashboard/js/controllers/dashboardCtrl.js +++ b/designsafe/apps/dashboard/static/designsafe/apps/dashboard/js/controllers/dashboardCtrl.js @@ -1,32 +1,47 @@ angular.module('designsafe').controller('DashboardCtrl', ['$scope', 'UserService', 'NotificationService', 'AgaveService', 'TicketsService', function ($scope, UserService, NotificationService, AgaveService, TicketsService) { - $scope.jobs_count = 12; - $scope.storage_count = 151231231231232; - $scope.apps_count = 45; - $scope.activities_count = 42; $scope.display_job_details = false; $scope.loading_tickets = true; + $scope.loading_jobs = true; $scope.today = new Date(); $scope.first_jobs_date = new Date($scope.today.getTime() - (14 * 24 * 60 * 60 * 1000 )) + $scope.first_jobs_date = new Date($scope.first_jobs_date.setHours(0,0,0,0)); + console.log($scope.first_jobs_date.getTime()); + $scope.chart = new DS_TSBarChart('#ds_jobs_chart') + .height(250) + .xSelector(function (d) { return d.key;}) + .ySelector(function (d) { return d.values.length;}) + .start_date($scope.first_jobs_date); + + $scope.chart.on('bar_click', function (ev, toggled) { + (toggled) ? $scope.display_job_details = true : $scope.display_job_details = false; + $scope.jobs_details = ev.values; + $scope.$apply(); + }); NotificationService.list({limit:5}).then(function (resp) { - $scope.notifications = resp; + $scope.notifications = resp.notifs; }) UserService.usage().then(function (resp) { $scope.usage = resp; }); - AgaveService.jobsListing({'created.gt':'2017-01-01'}).then(function (resp) { + AgaveService.jobsListing({'created.gt':moment($scope.first_jobs_date).format('Y-M-D')}).then(function (resp) { $scope.jobs = resp; $scope.chart_data = AgaveService.jobsByDate(resp); + + // console.log($scope.jobs); $scope.chart.data($scope.chart_data); var tmp = _.groupBy($scope.jobs, function (d) {return d.appId}); + // console.log(tmp) $scope.recent_apps = Object.keys(tmp); + $scope.loading_jobs = false; }) - AgaveService.appsListing({filter:'id', limit:99999}).then(function (resp) { - $scope.apps = resp; + AgaveService.appsListing({limit:99999}).then(function (resp) { + var tmp = _.groupBy(resp, function (d) {return d.label}); + $scope.apps = Object.keys(tmp); }) TicketsService.get().then(function (resp) { @@ -36,15 +51,6 @@ function ($scope, UserService, NotificationService, AgaveService, TicketsService $scope.loading_tickets = false; }) - $scope.chart = new DS_TSBarChart('#ds_jobs_chart') - .height(250) - .xSelector(function (d) { return d.key;}) - .ySelector(function (d) { return d.values.length;}); - $scope.chart.on('bar_click', function (ev, toggled) { - (toggled) ? $scope.display_job_details = true : $scope.display_job_details = false; - $scope.jobs_details = ev.values; - $scope.$apply(); - }); }]) diff --git a/designsafe/apps/dashboard/static/designsafe/apps/dashboard/styles/dashboard.css b/designsafe/apps/dashboard/static/designsafe/apps/dashboard/styles/dashboard.css index 209f8a159a..d3ade2a352 100644 --- a/designsafe/apps/dashboard/static/designsafe/apps/dashboard/styles/dashboard.css +++ b/designsafe/apps/dashboard/static/designsafe/apps/dashboard/styles/dashboard.css @@ -26,12 +26,14 @@ .ds-chart .y.axis path { fill: none; + stroke: #d3d3d3; } .dashboard-stat { display: block; margin-bottom: 25px; overflow: hidden; + /*padding: 10px;*/ } .dashboard-stat .visual { width: 80px; @@ -46,13 +48,13 @@ } .dashboard-stat.blue { - background-color: #3598dc; + background-color: #2f89c7; } .dashboard-stat.red { - background-color: #e7505a; + background-color: #cd4750; } .dashboard-stat.green { - background-color: #2daa70; + background-color: #2daaa4; } .dashboard-stat.purple { background-color: #8E44AD; @@ -65,10 +67,10 @@ color: #FFFFFF; } .dashboard-stat .details .number { - padding-top: 25px; + padding-top: 15px; text-align: right; - font-size: 34px; - line-height: 36px; + font-size: 50px; + line-height: 45px; letter-spacing: -1px; margin-bottom: 0px; font-weight: 300; @@ -95,10 +97,12 @@ color: #FFFFFF; opacity: 1; filter: alpha(opacity=100); + font-family: monospace; } .dashboard-stat .details .desc { text-align: right; - font-size: 16px; + font-size: 20px; + line-height: 15px; letter-spacing: 0px; font-weight: 300; } diff --git a/designsafe/apps/dashboard/tests.py b/designsafe/apps/dashboard/tests.py index e69de29bb2..8af99da130 100644 --- a/designsafe/apps/dashboard/tests.py +++ b/designsafe/apps/dashboard/tests.py @@ -0,0 +1,59 @@ +import json +import logging +from unittest import skip +from elasticsearch_dsl import Q +from django.test import TestCase +from django.contrib.auth import get_user_model, signals +from django.contrib.auth.models import Permission +from django.core.urlresolvers import reverse +from designsafe.apps.auth.signals import on_user_logged_in +from designsafe.apps.api.agave.filemanager.search_index import IndexedFile + +logger = logging.getLogger(__name__) + +class DashboardTests(TestCase): + + fixtures = ['user-data.json'] + + def setUp(self): + # configure regular user + user = get_user_model().objects.get(pk=2) + user.set_password('user/password') + user.save() + f1 = IndexedFile( + length=1, + path="ds_user/test", + ) + f1.save(refresh=True) + f2 = IndexedFile( + length=1, + path="ds_user/test", + ) + f2.save(refresh=True) + + # disconnect user_logged_in signal + signals.user_logged_in.disconnect(on_user_logged_in) + + def tearDown(self): + s = IndexedFile.search() + res = s.query('bool', must=[Q("match", **{"path._path": "ds_user"})]).extra(size=10000) + res.execute() + #for doc in res: + # doc.delete(refresh=True, ignore=404) + + def test_dashboard_index(self): + url = reverse('designsafe_dashboard:index') + self.client.login(username='ds_user', password='user/password') + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + + @skip('Skiping for now. TODO: Figure out what\'s up') + def test_dashboard_agg_storage(self): + + url = reverse('designsafe_api:user_usage') + self.client.login(username='ds_user', password='user/password') + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + data = json.loads(resp.content) + logger.debug('data: %s', data) + self.assertEqual(data["total_storage_bytes"], 2) diff --git a/designsafe/apps/dashboard/views.py b/designsafe/apps/dashboard/views.py index ce5b764cc1..f285cf667d 100644 --- a/designsafe/apps/dashboard/views.py +++ b/designsafe/apps/dashboard/views.py @@ -1,21 +1,7 @@ from django.shortcuts import render -from django.contrib import messages -from django.contrib.auth import get_user_model, logout from django.contrib.auth.decorators import login_required, permission_required -from django.core.exceptions import ObjectDoesNotExist -from django.core.urlresolvers import reverse -from django.db.models import Q -from django.http import HttpResponse, HttpResponseRedirect, JsonResponse -from django.shortcuts import render_to_response -from django.utils.translation import ugettext_lazy as _ -from designsafe.apps.accounts import forms, integrations -from designsafe.apps.accounts.models import (NEESUser, DesignSafeProfile, - NotificationPreferences) -from pytas.http import TASClient -from pytas.models import User as TASUser +from django.http import JsonResponse import logging -import json -import re logger = logging.getLogger(__name__) @@ -26,15 +12,5 @@ def index(request): The default accounts view. Provides user settings for managing profile, authentication, notifications, identities, and applications. """ - django_user = request.user - user_profile = TASUser(username=request.user.username) - try: - demographics = django_user.profile - except ObjectDoesNotExist as e: - logger.info('exception e:{} {}'.format(type(e), e)) - - context = { - 'profile': user_profile, - } - return render(request, 'designsafe/apps/dashboard/index.html', context) + return render(request, 'designsafe/apps/dashboard/index.html') diff --git a/designsafe/apps/data/templates/data/data_depot.html b/designsafe/apps/data/templates/data/data_depot.html index b2b8e6139c..4dda8b77fe 100644 --- a/designsafe/apps/data/templates/data/data_depot.html +++ b/designsafe/apps/data/templates/data/data_depot.html @@ -32,6 +32,7 @@ + {% endaddtoblock %} {% addtoblock "js" %} @@ -44,6 +45,7 @@ + {% endaddtoblock %} diff --git a/designsafe/apps/data/views/base.py b/designsafe/apps/data/views/base.py index d47ca1262b..0e2d858856 100644 --- a/designsafe/apps/data/views/base.py +++ b/designsafe/apps/data/views/base.py @@ -228,9 +228,10 @@ def login_redirect(request): return redirect_to_login( path, resolved_login_url) + @method_decorator(ensure_csrf_cookie) def dispatch(self, request, *args, **kwargs): try: - return super(BasePublicTemplate, self).dispatch(request, *args, **kwargs) + return super(DataDepotView, self).dispatch(request, *args, **kwargs) except PermissionDenied: return DataDepotView.login_redirect(request) diff --git a/designsafe/apps/geo/__init__.py b/designsafe/apps/geo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/designsafe/apps/geo/apps.py b/designsafe/apps/geo/apps.py new file mode 100644 index 0000000000..5238dad187 --- /dev/null +++ b/designsafe/apps/geo/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + +class DesignGeoConfig(AppConfig): + name = 'designsafe.apps.geo' + label = 'designsafe_geo' + verbose_name = 'DesignSafe Geo' diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/build/bundle.js b/designsafe/apps/geo/static/designsafe/apps/geo/build/bundle.js new file mode 100644 index 0000000000..de192b9947 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/build/bundle.js @@ -0,0 +1,1648 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.l = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; + +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; + +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; + +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 16); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var DBModalCtrl = function () { + DBModalCtrl.$inject = ["$scope", "$uibModalInstance", "filename"]; + function DBModalCtrl($scope, $uibModalInstance, filename) { + 'ngInject'; + + _classCallCheck(this, DBModalCtrl); + + this.$scope = $scope; + this.$uibModalInstance = $uibModalInstance; + this.selected = null; + this.saveas = { filename: filename }; + } + + _createClass(DBModalCtrl, [{ + key: 'ok', + value: function ok() { + this.$uibModalInstance.close({ selected: this.selected, saveas: this.saveas.filename }); + } + }, { + key: 'cancel', + value: function cancel() { + this.$uibModalInstance.dismiss('cancel'); + } + }]); + + return DBModalCtrl; +}(); + +exports.default = DBModalCtrl; + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var LayerGroup = function () { + function LayerGroup(label, fg) { + _classCallCheck(this, LayerGroup); + + this.label = label; + this.feature_group = fg; + this.show = true; + this.show_contents = true; + } + + _createClass(LayerGroup, [{ + key: 'num_features', + value: function num_features() { + return this.feature_group.getLayers().length; + } + }, { + key: 'get_feature_type', + value: function get_feature_type(f) { + // debugger + if (f.options.image_src) { + return 'Image'; + } else if (f instanceof L.Marker) { + return 'Point'; + } else if (f instanceof L.Polygon) { + return 'Polygon'; + } else { + return 'Path'; + } + } + }]); + + return LayerGroup; +}(); + +exports.default = LayerGroup; + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _L = __webpack_require__(15); + +var _L2 = _interopRequireDefault(_L); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var MapProject = function () { + function MapProject(name) { + _classCallCheck(this, MapProject); + + this.name = name; + this.layer_groups = []; + this.description = null; + } + + _createClass(MapProject, [{ + key: "clear", + value: function clear() { + this.layer_groups.forEach(function (lg) { + lg.feature_group.clearLayers(); + }); + } + }, { + key: "get_bounds", + value: function get_bounds() { + var bounds = []; + this.layer_groups.forEach(function (lg) { + bounds.push(lg.feature_group.getBounds()); + }); + return bounds; + } + }, { + key: "num_features", + value: function num_features() { + total = 0; + this.layer_groups.forEach(function (lg) { + total += lg.num_features(); + }); + } + }, { + key: "to_json", + value: function to_json() { + var out = { + "type": "FeatureCollection", + "features": [], + "ds_map": true, + "name": this.name, + "description": this.description, + "num_layers": this.layer_groups.length, + "layer_groups": [] + }; + this.layer_groups.forEach(function (lg, lg_idx) { + out.layer_groups.push(lg.label); + var tmp = { + "type": "FeatureCollection", + "features": [], + "label": lg.label + }; + lg.feature_group.getLayers().forEach(function (feature) { + var json = feature.toGeoJSON(); + // These are all the keys in the options object that we need to + // re-create the layers in the application after loading. + var opt_keys = ['label', 'color', 'fillColor', 'fillOpacity', 'description', 'image_src', 'thumb_src', 'original_src']; + + // //add in any options + // if (feature.options.image_src) { + // json.properties.image_src = feature.options.image_src; + // } + // if (feature.options.thumb_src) { + // json.properties.thumb_src = feature.options.thumb_src; + // } + for (var key in feature.options) { + if (opt_keys.indexOf(key) !== -1) { + json.properties[key] = feature.options[key]; + } + }; + json.layer_group_index = lg_idx; + out.features.push(json); + }); + }); + return out; + } + }]); + + return MapProject; +}(); + +exports.default = MapProject; + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.get_file_extension = get_file_extension; +function get_file_extension(fname) { + return fname.split('.').pop().toLowerCase(); +} + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _mapSidebar = __webpack_require__(9); + +var _mapSidebar2 = _interopRequireDefault(_mapSidebar); + +var _dbModal = __webpack_require__(0); + +var _dbModal2 = _interopRequireDefault(_dbModal); + +var _imageOverlayModal = __webpack_require__(17); + +var _imageOverlayModal2 = _interopRequireDefault(_imageOverlayModal); + +var _help = __webpack_require__(8); + +var _help2 = _interopRequireDefault(_help); + +var _settingsModal = __webpack_require__(10); + +var _settingsModal2 = _interopRequireDefault(_settingsModal); + +var _confirmClearModal = __webpack_require__(7); + +var _confirmClearModal2 = _interopRequireDefault(_confirmClearModal); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var mod = angular.module('ds.geo.controllers', []); + +mod.controller('MapSidebarCtrl', _mapSidebar2.default); +mod.controller('DBModalCtrl', _dbModal2.default); +mod.controller('HelpCtrl', _help2.default); +mod.controller('SettingsModalCtrl', _settingsModal2.default); +mod.controller('ConfirmClearModalCtrl', _confirmClearModal2.default); +mod.controller('ImageOverlayModalCtrl', _imageOverlayModal2.default); + +exports.default = mod; + +/***/ }), +/* 5 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _customOnChange = __webpack_require__(11); + +var _customOnChange2 = _interopRequireDefault(_customOnChange); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var mod = angular.module('ds.geo.directives', []); + +mod.directive('customOnChange', _customOnChange2.default); +exports.default = mod; + +/***/ }), +/* 6 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _geoStateService = __webpack_require__(14); + +var _geoStateService2 = _interopRequireDefault(_geoStateService); + +var _geoDataService = __webpack_require__(12); + +var _geoDataService2 = _interopRequireDefault(_geoDataService); + +var _geoSettingsService = __webpack_require__(13); + +var _geoSettingsService2 = _interopRequireDefault(_geoSettingsService); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var mod = angular.module('ds.geo.services', []); // import customOnChange from './custom-on-change'; + +mod.service('GeoStateService', _geoStateService2.default); +mod.service('GeoDataService', _geoDataService2.default); +mod.service('GeoSettingsService', _geoSettingsService2.default); + +// mod.directive('customOnChange', customOnChange); + +exports.default = mod; + +/***/ }), +/* 7 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var ConfirmClearModalCtrl = function () { + ConfirmClearModalCtrl.$inject = ["$scope", "$uibModalInstance", "GeoSettingsService"]; + function ConfirmClearModalCtrl($scope, $uibModalInstance, GeoSettingsService) { + 'ngInject'; + + _classCallCheck(this, ConfirmClearModalCtrl); + + this.$scope = $scope; + this.$uibModalInstance = $uibModalInstance; + } + + _createClass(ConfirmClearModalCtrl, [{ + key: 'ok', + value: function ok() { + this.$uibModalInstance.close('ok'); + } + }, { + key: 'cancel', + value: function cancel() { + this.$uibModalInstance.dismiss('cancel'); + } + }]); + + return ConfirmClearModalCtrl; +}(); + +exports.default = ConfirmClearModalCtrl; + +/***/ }), +/* 8 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var HelpCtrl = function HelpCtrl($scope) { + 'ngInject'; + + _classCallCheck(this, HelpCtrl); + + this.$scope = $scope; +}; +HelpCtrl.$inject = ["$scope"]; + +exports.default = HelpCtrl; + +/***/ }), +/* 9 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _layer_group = __webpack_require__(1); + +var _layer_group2 = _interopRequireDefault(_layer_group); + +var _mapProject = __webpack_require__(2); + +var _mapProject2 = _interopRequireDefault(_mapProject); + +var _dbModal = __webpack_require__(0); + +var _dbModal2 = _interopRequireDefault(_dbModal); + +var _geoUtils = __webpack_require__(3); + +var GeoUtils = _interopRequireWildcard(_geoUtils); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var MapSidebarCtrl = function () { + MapSidebarCtrl.$inject = ["$scope", "$window", "$timeout", "$interval", "$q", "$uibModal", "toastr", "DataService", "$http", "GeoDataService", "GeoSettingsService"]; + function MapSidebarCtrl($scope, $window, $timeout, $interval, $q, $uibModal, toastr, DataService, $http, GeoDataService, GeoSettingsService) { + 'ngInject'; + + var _this = this; + + _classCallCheck(this, MapSidebarCtrl); + + this.$scope = $scope; + this.LGeo = $window.LGeo; + this.$timeout = $timeout; + this.$interval = $interval; + this.$window = $window; + this.$q = $q; + this.$uibModal = $uibModal; + this.DataService = DataService; + this.$http = $http; + this.GeoDataService = GeoDataService; + this.GeoSettingsService = GeoSettingsService; + this.toastr = toastr; + + this.settings = this.GeoSettingsService.settings; + + //method binding for callback, sigh... + this.local_file_selected = this.local_file_selected.bind(this); + // this.open_db_modal = this.open_db_modal.bind(this); + + var streets = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }); + + var satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: '©', + maxZoom: 20 + }); + + var basemaps = { + 'Street': streets, + 'Satellite': satellite + }; + this.map = L.map('geo_map', { + layers: [streets, satellite], + preferCanvas: false + }).setView([0, 0], 3); + this.mc = new L.Control.Measure({ primaryLengthUnit: 'meters', primaryAreaUnit: 'meters' }); + this.mc.addTo(this.map); + L.control.layers(basemaps).addTo(this.map); + this.map.zoomControl.setPosition('bottomleft'); + + // Load in a map project from the data service if one does exist, if not + // create a new one from scratch + if (this.GeoDataService.current_project()) { + this.project = this.GeoDataService.current_project(); + + this._init_map_layers(); + this.fit_map_to_project(); + } else { + this.project = new _mapProject2.default('New Map'); + this.project.layer_groups = [new _layer_group2.default('New Group', new L.FeatureGroup())]; + this.map.addLayer(this.project.layer_groups[0].feature_group); + } + + // this._add_click_handlers(); + + // trick to fix the tiles that sometimes don't load for some reason... + $timeout(function () { + _this.map.invalidateSize(); + }, 10); + + // init an active layer group + this.active_layer_group = this.project.layer_groups[0]; + + // Auto keep track of current project in the GeoDataService + // so that if they switch states they will not lose work... + $interval(function () { + _this.GeoDataService.current_project(_this.project); + }, 1000); + + this.add_draw_controls(this.active_layer_group.feature_group); + + // This handles making sure that the features that get created with the draw tool + // are styled with the default colors etc. + this.map.on('draw:created', function (e) { + var object = e.layer; + object.options.color = _this.settings.default_stroke_color; + object.options.fillColor = _this.settings.default_fill_color; + object.options.fillOpacity = _this.settings.default_fill_opacity; + _this.active_layer_group.feature_group.addLayer(object); + _this.$scope.$apply(); + }); + + this.map.on('mousemove', function (e) { + _this.current_mouse_coordinates = e.latlng; + _this.$scope.$apply(); + }); + } // end constructor + + _createClass(MapSidebarCtrl, [{ + key: 'fit_map_to_project', + value: function fit_map_to_project() { + try { + this.map.fitBounds(this.project.get_bounds(), { maxZoom: 16 }); + } catch (e) { + console.log('get_bounds fail', e); + } + } + }, { + key: 'add_draw_controls', + value: function add_draw_controls(fg) { + var dc = new L.Control.Draw({ + position: 'topright', + draw: { + circle: false + }, + edit: { + featureGroup: fg, + remove: true + } + }); + this.map.addControl(dc); + this.drawControl = dc; + } + }, { + key: 'feature_click', + value: function feature_click(layer) { + console.log(layer); + } + }, { + key: '_init_map_layers', + value: function _init_map_layers() { + var _this2 = this; + + // For some reason, need to readd the feature groups for markers to be displayed correctly??? + this.project.layer_groups.forEach(function (lg) { + // lg.feature_group.getLayers().forEach( (layer)=>{ + // this.map.addLayer(layer); + // }); + // this.map.addLayer(lg.feature_group); + _this2.map.removeLayer(lg.feature_group); + _this2.map.addLayer(lg.feature_group); + }); + this.active_layer_group = this.project.layer_groups[0]; + } + + // Adds click handlers to map elements. This does NOT feel + // right to me... + + }, { + key: '_add_click_handlers', + value: function _add_click_handlers() { + var _this3 = this; + + this.project.layer_groups.forEach(function (lg) { + lg.feature_group.on('click', function (ev) { + _this3.project.layer_groups.forEach(function (lg) { + lg.feature_group.getLayers().forEach(function (layer) { + layer.active = false; + if (layer == ev.layer) { + layer.active = true; + } + }); + }); + }); + }); + } + }, { + key: 'create_layer_group', + value: function create_layer_group() { + var lg = new _layer_group2.default("New Group", new L.FeatureGroup()); + this.project.layer_groups.push(lg); + this.active_layer_group = this.project.layer_groups[this.project.layer_groups.length - 1]; + this.map.addLayer(lg.feature_group); + this.select_active_layer_group(this.active_layer_group); + } + }, { + key: 'delete_layer_group', + value: function delete_layer_group(lg, i) { + this.map.removeLayer(lg.feature_group); + this.project.layer_groups.splice(i, 1); + if (this.project.layer_groups.length === 0) { + this.create_layer_group(); + } + this.active_layer_group = this.project.layer_groups[0]; + } + }, { + key: 'delete_feature', + value: function delete_feature(lg, f) { + this.map.removeLayer(f); + lg.feature_group.removeLayer(f); + } + }, { + key: 'show_hide_layer_group', + value: function show_hide_layer_group(lg) { + lg.show ? this.map.addLayer(lg.feature_group) : this.map.removeLayer(lg.feature_group); + } + }, { + key: 'select_active_layer_group', + value: function select_active_layer_group(lg) { + this.map.removeControl(this.drawControl); + this.add_draw_controls(lg.feature_group); + this.active_layer_group = lg; + lg.active = true; + lg.show = true; + } + }, { + key: 'select_feature', + value: function select_feature(lg, feature) { + this.active_layer_group = lg; + this.current_layer == feature ? this.current_layer = null : this.current_layer = feature; + } + }, { + key: 'create_new_project', + value: function create_new_project() { + var _this4 = this; + + var modal = this.$uibModal.open({ + templateUrl: "/static/designsafe/apps/geo/html/confirm-new-modal.html", + controller: "ConfirmClearModalCtrl as vm" + }); + modal.result.then(function (s) { + _this4.map.eachLayer(function (layer) { + console.log(layer); + // this.map.removeLayer(layer); + }); + _this4.project.clear(); + var p = new _mapProject2.default('New Map'); + p.layer_groups = [new _layer_group2.default('New Group', new L.FeatureGroup())]; + _this4.project = p; + _this4.active_layer_group = _this4.project.layer_groups[0]; + _this4.map.addLayer(_this4.active_layer_group.feature_group); + }); + } + }, { + key: 'zoom_to', + value: function zoom_to(feature) { + if (feature instanceof L.Marker) { + var latLngs = [feature.getLatLng()]; + var markerBounds = L.latLngBounds(latLngs); + try { + this.map.fitBounds(markerBounds, { maxZoom: 16 }); + } catch (e) { + console.log(e); + } + } else { + try { + this.map.fitBounds(feature.getBounds(), { maxZoom: 16 }); + } catch (e) { + console.log(e); + } + }; + } + }, { + key: 'on_drop', + value: function on_drop(ev, data, lg) { + var src_lg = this.project.layer_groups[data.pidx]; + var feature = src_lg.feature_group.getLayers()[data.idx]; + src_lg.feature_group.removeLayer(feature); + lg.feature_group.addLayer(feature); + } + }, { + key: 'update_layer_style', + value: function update_layer_style(prop) { + var tmp = this.current_layer; + // debugger; + // this.current_layer.setStyle({prop: this.current_layer.options[prop]}); + var styles = {}; + styles[prop] = this.current_layer.options[prop]; + this.current_layer.setStyle(styles); + } + }, { + key: 'drop_feature_success', + value: function drop_feature_success(ev, data, lg) { + console.log("drag_feature_success", ev, data, lg); + } + }, { + key: '_load_data_success', + value: function _load_data_success(retval) { + var _this5 = this; + + if (this.open_existing) { + if (retval instanceof _mapProject2.default) { + //clear off all the layers from the map + this.project.layer_groups.forEach(function (lg) { + _this5.map.removeLayer(lg.feature_group); + }); + + // set the project to be the return value + this.project = retval; + this._init_map_layers(); + this.fit_map_to_project(); + } else { + this.toastr.error('Load failed! File was not compatible'); + } + this.open_existing = false; + } else if (retval instanceof _mapProject2.default) { + retval.layer_groups.forEach(function (lg) { + _this5.project.layer_groups.push(lg); + _this5.map.addLayer(lg.feature_group); + }); + this.active_layer_group = this.project.layer_groups[0]; + this.fit_map_to_project(); + } else { + + if (this.project.layer_groups.length == 0) { + this.project.layer_groups = [new _layer_group2.default('New Group', new L.FeatureGroup())]; + this.active_layer_group = this.project.layer_groups[0]; + this.map.addLayer(this.project.layer_groups[0].feature_group); + } + //it will be an array of features... + retval.forEach(function (f) { + _this5.active_layer_group.feature_group.addLayer(f); + }); + this.fit_map_to_project(); + } + } + }, { + key: 'open_existing_locally', + value: function open_existing_locally() { + this.open_existing = true; + this.open_file_dialog(); + } + }, { + key: 'open_existing_from_depot', + value: function open_existing_from_depot() { + this.open_existing = true; + this.open_db_modal(); + } + }, { + key: 'open_db_modal', + value: function open_db_modal() { + var _this6 = this; + + var modal = this.$uibModal.open({ + templateUrl: "/static/designsafe/apps/geo/html/db-modal.html", + controller: "DBModalCtrl as vm", + resolve: { + filename: function filename() { + return null; + } + } + }); + modal.result.then(function (f, saveas) { + _this6.load_from_data_depot(f); + }); + } + }, { + key: 'open_settings_modal', + value: function open_settings_modal() { + var _this7 = this; + + var modal = this.$uibModal.open({ + templateUrl: "/static/designsafe/apps/geo/html/settings-modal.html", + controller: "SettingsModalCtrl as vm" + }); + modal.result.then(function (s) { + _this7.settings = _this7.GeoSettingsService.settings; + }); + } + }, { + key: 'open_image_overlay_modal', + value: function open_image_overlay_modal() { + var _this8 = this; + + var modal = this.$uibModal.open({ + templateUrl: "/static/designsafe/apps/geo/html/image-overlay-modal.html", + controller: "ImageOverlayModalCtrl as vm" + }); + modal.result.then(function (res) { + var bounds = void 0; + bounds = [[res.min_lat, res.min_lon], [res.max_lat, res.max_lon]]; + if (res.file) { + _this8.GeoDataService.read_file_as_data_url(res.file).then(function (data) { + console.log(data); + }); + } + var overlay = L.imageOverlay(res.url, bounds).addTo(_this8.map); + console.log(overlay); + }); + } + }, { + key: 'open_file_dialog', + value: function open_file_dialog() { + this.$timeout(function () { + $('#file_picker').click(); + }, 0); + } + }, { + key: 'load_from_data_depot', + value: function load_from_data_depot(f) { + var _this9 = this; + + this.loading = true; + this.GeoDataService.load_from_data_depot(f.selected).then(function (retval) { + _this9._load_data_success(retval); + _this9.loading = false; + }, function (err) { + _this9.toastr.error('Load failed!'); + _this9.loading = false; + _this9.open_existing = false; + }); + } + }, { + key: 'local_file_selected', + value: function local_file_selected(ev) { + var _this10 = this; + + this.loading = true; + var reqs = []; + for (var i = 0; i < ev.target.files.length; i++) { + // debugger + var file = ev.target.files[i]; + var prom = this.GeoDataService.load_from_local_file(file).then(function (retval) { + return _this10._load_data_success(retval); + }); + reqs.push(prom); + // this.loading = false; + }; + this.$q.all(reqs).then(function () { + _this10.loading = false; + //reset the picker + $('#file_picker').val(''); + _this10.toastr.success('Imported file'); + }, function (rej) { + _this10.loading = false; + _this10.toastr.error('Load failed!'); + }).then(function () {}); + } + }, { + key: 'save_locally', + value: function save_locally() { + this.GeoDataService.save_locally(this.project); + } + }, { + key: 'save_to_depot', + value: function save_to_depot() { + var _this11 = this; + + var modal = this.$uibModal.open({ + templateUrl: "/static/designsafe/apps/geo/html/db-modal.html", + controller: "DBModalCtrl as vm", + resolve: { + filename: function filename() { + return _this11.project.name + '.geojson'; + } + } + }); + modal.result.then(function (res) { + var newname = res.saveas; + _this11.project.name = newname.split('.')[0]; + res.selected.name = res.saveas; + _this11.loading = true; + _this11.GeoDataService.save_to_depot(_this11.project, res.selected).then(function (resp) { + _this11.loading = false; + _this11.toastr.success('Saved to data depot'); + }, function (err) { + _this11.toastr.error('Save failed!'); + _this11.loading = false; + }); + }, function (rej) { + _this11.loading = false; + }); + } + }]); + + return MapSidebarCtrl; +}(); + +exports.default = MapSidebarCtrl; + +/***/ }), +/* 10 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var SettingsModalCtrl = function () { + SettingsModalCtrl.$inject = ["$scope", "$uibModalInstance", "GeoSettingsService"]; + function SettingsModalCtrl($scope, $uibModalInstance, GeoSettingsService) { + 'ngInject'; + + _classCallCheck(this, SettingsModalCtrl); + + this.$scope = $scope; + this.$uibModalInstance = $uibModalInstance; + this.GeoSettingsService = GeoSettingsService; + this.settings = GeoSettingsService.settings; + } + + _createClass(SettingsModalCtrl, [{ + key: 'ok', + value: function ok() { + this.GeoSettingsService.settings = this.settings; + this.$uibModalInstance.close(this.settings); + } + }]); + + return SettingsModalCtrl; +}(); + +exports.default = SettingsModalCtrl; + +/***/ }), +/* 11 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = customOnChange; +function customOnChange() { + return { + restrict: 'A', + scope: { + handler: '&' + }, + link: function link(scope, element, attrs) { + element.on('change', function (ev) { + scope.$apply(function () { + scope.handler({ ev: ev }); + }); + }); + } + }; +} + +/***/ }), +/* 12 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _geoUtils = __webpack_require__(3); + +var GeoUtils = _interopRequireWildcard(_geoUtils); + +var _layer_group = __webpack_require__(1); + +var _layer_group2 = _interopRequireDefault(_layer_group); + +var _mapProject = __webpack_require__(2); + +var _mapProject2 = _interopRequireDefault(_mapProject); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var GeoDataService = function () { + GeoDataService.$inject = ["$http", "$q", "UserService", "GeoSettingsService"]; + function GeoDataService($http, $q, UserService, GeoSettingsService) { + 'ngInject'; + + _classCallCheck(this, GeoDataService); + + this.$http = $http; + this.$q = $q; + this.UserService = UserService; + this.GeoSettingsService = GeoSettingsService; + this.active_project = null; + this.previous_project_state = null; + } + + _createClass(GeoDataService, [{ + key: 'current_project', + value: function current_project(project) { + if (!project) { + return this.active_project; + } + this.active_project = project; + } + }, { + key: '_resize_image', + value: function _resize_image(blob) { + var _this = this; + + var max_width = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 400; + var max_height = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 400; + + return this.$q(function (res, rej) { + var base64 = _this._arrayBufferToBase64(blob); + // Create and initialize two canvas + var canvas = document.createElement("canvas"); + var ctx = canvas.getContext("2d"); + var canvasCopy = document.createElement("canvas"); + var copyContext = canvasCopy.getContext("2d"); + + // Create original image + var img = new Image(); + img.src = base64; + img.onload = function () { + // Determine new ratio based on max size + var ratio = 1; + if (img.width > max_width) { + ratio = max_width / img.width; + } else if (img.height > max_height) { + ratio = max_height / img.height; + } + // Draw original image in second canvas + canvasCopy.width = img.width; + canvasCopy.height = img.height; + copyContext.drawImage(img, 0, 0); + + // Copy and resize second canvas to first canvas + canvas.width = img.width * ratio; + canvas.height = img.height * ratio; + ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height); + res(canvas.toDataURL()); + }; + }); + } + }, { + key: '_arrayBufferToBase64', + value: function _arrayBufferToBase64(buffer) { + var binary = ''; + var bytes = new Uint8Array(buffer); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + var encoded = btoa(binary); + return 'data:image/jpg;base64,' + encoded; + } + }, { + key: '_from_kml', + value: function _from_kml(text_blob) { + return this.$q(function (res, rej) { + var features = []; + var l = omnivore.kml.parse(text_blob); + l.getLayers().forEach(function (d) { + // d.feature.properties = {}; + d.options.label = d.feature.properties.name; + features.push(d); + }); + res(features); + }); + } + }, { + key: '_from_kmz', + value: function _from_kmz(blob) { + var _this2 = this; + + return this.$q(function (res, rej) { + var zipper = new JSZip(); + zipper.loadAsync(blob).then(function (zip) { + //loop over all the files in the archive + var proms = []; + for (var key in zip.files) { + var ext = key.split('.').pop(); + if (ext === 'kml') { + return zip.files[key].async('text'); + } + } + }).then(function (txt) { + var features = _this2._from_kml(txt); + res(features); + }); + }); + } + }, { + key: '_from_json', + value: function _from_json(blob) { + var _this3 = this; + + return this.$q(function (res, rej) { + if (blob.ds_map) return res(_this3._from_dsmap(blob)); + + try { + (function () { + var features = []; + L.geoJSON(blob).getLayers().forEach(function (layer) { + for (var key in layer.feature.properties) { + layer.options[key] = layer.feature.properties[key]; + } + features.push(layer); + }); + res(features); + })(); + } catch (e) { + rej('Bad geoJSON'); + } + }); + } + }, { + key: '_from_gpx', + value: function _from_gpx(blob) { + return this.$q(function (res, rej) { + // console.log(text_blob) + var features = []; + var l = omnivore.gpx.parse(blob); + l.getLayers().forEach(function (d) { + features.push(d); + }); + res(features); + }); + } + }, { + key: '_make_image_marker', + value: function _make_image_marker(lat, lon, thumb, preview, original) { + var icon = L.divIcon({ + iconSize: [40, 40], + html: "
        ", + className: 'leaflet-marker-photo' + }); + + var marker = L.marker([lat, lon], { icon: icon }).bindPopup("full res", { + className: 'leaflet-popup-photo', + maxWidth: "auto" + }); + marker.options.image_src = preview; + marker.options.thumb_src = thumb; + marker.options.original_src = original; + return marker; + } + }, { + key: '_from_image', + value: function _from_image(file) { + var _this4 = this; + + return this.$q(function (res, rej) { + try { + (function () { + var exif = EXIF.readFromBinaryFile(file); + var lat = exif.GPSLatitude; + var lon = exif.GPSLongitude; + //Convert coordinates to WGS84 decimal + var latRef = exif.GPSLatitudeRef || "N"; + var lonRef = exif.GPSLongitudeRef || "W"; + lat = (lat[0] + lat[1] / 60 + lat[2] / 3600) * (latRef == "N" ? 1 : -1); + lon = (lon[0] + lon[1] / 60 + lon[2] / 3600) * (lonRef == "W" ? -1 : 1); + if (lat > 90 || lat < -90 || lon > 360 || lon < -360) { + rej('Bad EXIF GPS data'); + } + var encoded = _this4._arrayBufferToBase64(file); + var thumb = null; + var preview = null; + _this4._resize_image(file, 100, 100).then(function (resp) { + thumb = resp; + }).then(function () { + return _this4._resize_image(file, 400, 400); + }).then(function (resp) { + preview = resp; + var marker = _this4._make_image_marker(lat, lon, thumb, preview, encoded); + res([marker]); + }); + })(); + } catch (e) { + rej(e); + } + }); + } + }, { + key: '_from_dsmap', + value: function _from_dsmap(json) { + var _this5 = this; + + return this.$q(function (res, rej) { + // if (json instanceof String) { + var project = new _mapProject2.default(); + project.name = json.name; + project.description = json.description; + json.layer_groups.forEach(function (name) { + project.layer_groups.push(new _layer_group2.default(name, new L.FeatureGroup())); + }); + json.features.forEach(function (d) { + var feature = L.geoJSON(d); + feature.eachLayer(function (layer) { + + // If there were no styles applied, it might be transparent??? + if (!layer.feature.properties.color) { + layer.feature.properties.color = '#ff0000'; + } + if (!layer.feature.properties.fillColor) { + layer.feature.properties.fillColor = '#ff0000'; + }; + if (!layer.feature.properties.opacity) { + layer.feature.properties.opacity = 1.0; + }; + + for (var key in layer.feature.properties) { + layer.options[key] = layer.feature.properties[key]; + } + try { + var styles = { + fillColor: layer.feature.properties.fillColor, + color: layer.feature.properties.color, + opacity: layer.feature.properties.opacity + }; + layer.setStyle(styles); + } catch (e) { + // this can get caught for marker type objects, which for some reason + // do not have a setStyle() method + console.log(e); + } + + var layer_group_index = d.layer_group_index; + if (layer instanceof L.Marker && layer.feature.properties.image_src) { + var latlng = layer.getLatLng(); + layer = _this5._make_image_marker(latlng.lat, latlng.lng, layer.feature.properties.thumb_src, layer.feature.properties.image_src, layer.feature.properties.original_src); + // feat.options.image_src = feat.feature.properties.image_src; + // feat.options.thumb_src = feat.feature.properties.thumb_src; + } + project.layer_groups[layer_group_index].feature_group.addLayer(layer); + layer.options.label = d.properties.label; + }); + }); + res(project); + }); + } + }, { + key: 'read_file_as_data_url', + value: function read_file_as_data_url(file) { + + var reader = new FileReader(); + return this.$q(function (res, rej) { + reader.readAsDataURL(file); + + reader.onload = function (e) { + return res(reader.result); + }; + }); + } + + /* + This will return a promise that resolves to an array of features + that can be added to a LayerGroup + */ + + }, { + key: 'load_from_local_file', + value: function load_from_local_file(file) { + var _this6 = this; + + return this.$q(function (res, rej) { + var ext = GeoUtils.get_file_extension(file.name); + var reader = new FileReader(); + // + if (ext === 'kmz' || ext === 'jpeg' || ext === 'jpg') { + reader.readAsArrayBuffer(file); + } else { + reader.readAsText(file); + } + reader.onload = function (e) { + var p = null; + switch (ext) { + case 'kml': + p = _this6._from_kml(e.target.result); + break; + case 'json': + p = _this6._from_json(JSON.parse(e.target.result)); + break; + case 'geojson': + p = _this6._from_json(JSON.parse(e.target.result)); + break; + case 'kmz': + p = _this6._from_kmz(e.target.result); + break; + case 'gpx': + p = _this6._from_gpx(e.target.result); + break; + case 'jpeg': + p = _this6._from_image(e.target.result); + break; + case 'jpg': + p = _this6._from_image(e.target.result); + break; + case 'dsmap': + p = _this6._from_dsmap(JSON.parse(e.target.result)); + break; + default: + p = _this6._from_json(JSON.parse(e.target.result)); + } + return res(p); + }; + }); + } + + // + // @param f: a file from DataService + // returns a promise with the LayerGroup + + }, { + key: 'load_from_data_depot', + value: function load_from_data_depot(f) { + var _this7 = this; + + var ext = GeoUtils.get_file_extension(f.name); + var responseType = 'text'; + if (ext === 'kmz' || ext === 'jpg' || ext === 'jpeg') { + responseType = 'arraybuffer'; + } + return this.$http.get(f.agaveUrl(), { 'responseType': responseType }).then(function (resp) { + var p = null; + switch (ext) { + case 'kml': + p = _this7._from_kml(resp.data); + break; + case 'json': + p = _this7._from_json(resp.data); + break; + case 'geojson': + p = _this7._from_json(resp.data); + break; + case 'kmz': + p = _this7._from_kmz(resp.data); + break; + case 'gpx': + p = _this7._from_gpx(resp.data); + break; + case 'jpeg': + p = _this7._from_image(resp.data); + break; + case 'jpg': + p = _this7._from_image(resp.data); + break; + case 'dsmap': + p = _this7._from_dsmap(resp.data); + break; + default: + p = _this7._from_json(resp.data); + } + return p; + }); + } + }, { + key: 'save_locally', + value: function save_locally(project) { + var gjson = project.to_json(); + var blob = new Blob([JSON.stringify(gjson)], { type: "application/json" }); + var url = URL.createObjectURL(blob); + + var a = document.createElement('a'); + document.body.appendChild(a); + a.download = project.name + ".geojson"; + a.href = url; + a.textContent = "Download"; + a.click(); + document.body.removeChild(a); + } + }, { + key: 'save_to_depot', + value: function save_to_depot(project, path) { + var form = new FormData(); + var gjson = project.to_json(); + var blob = new Blob([JSON.stringify(gjson)], { type: "application/json" }); + var base_file_url = 'https://agave.designsafe-ci.org/files/v2/media/system/'; + var post_url = base_file_url; + post_url = post_url + path.system; + var file = null; + if (path.type === 'dir') { + post_url = post_url + path.path; + file = new File([blob], path.name); + form.append('fileToUpload', file, path.name); + } else { + // A file was picked, so this WILL replace it + post_url = post_url + path.trail[path.trail.length - 2].path; + file = new File([blob], path.name); + form.append('fileToUpload', file, path.name); + } + return this.$http.post(post_url, form, { headers: { 'Content-Type': undefined } }); + } + }]); + + return GeoDataService; +}(); + +exports.default = GeoDataService; + +/***/ }), +/* 13 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var GeoSettingsService = function GeoSettingsService() { + _classCallCheck(this, GeoSettingsService); + + this.settings = { + default_fill_color: '#ff0000', + default_stroke_color: '#ff0000', + default_fill_opacity: 0.5, + measurement_units: 'si' + }; +}; + +exports.default = GeoSettingsService; + +/***/ }), +/* 14 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var GeoStateService = function GeoStateService($scope, $state) { + _classCallCheck(this, GeoStateService); + + this.$scope = $scope; + this.$state = $state; + this.last_db_path = null; +}; + +exports.default = GeoStateService; + +/***/ }), +/* 15 */ +/***/ (function(module, exports) { + +module.exports = L; + +/***/ }), +/* 16 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +config.$inject = ["$stateProvider", "$uibTooltipProvider", "$urlRouterProvider", "$locationProvider", "toastrConfig"]; +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _directives = __webpack_require__(5); + +var _controllers = __webpack_require__(4); + +var _services = __webpack_require__(6); + +var mod = angular.module('designsafe'); +mod.requires.push('ui.router', 'ang-drag-drop', 'ds.geo.directives', 'ds.geo.controllers', 'ds.geo.services', 'toastr'); + +function config($stateProvider, $uibTooltipProvider, $urlRouterProvider, $locationProvider, toastrConfig) { + 'ngInject'; + + angular.extend(toastrConfig, { + timeOut: 1000 + }); + + $locationProvider.html5Mode({ + enabled: true + }); + + $stateProvider.state('geo', { + url: '', + abstract: true, + templateUrl: '/static/designsafe/apps/geo/html/index.html', + resolve: { + auth: function auth() { + return true; + } + } + }).state('geo.map', { + url: '/hazmapper', + templateUrl: '/static/designsafe/apps/geo/html/map.html', + controller: 'MapSidebarCtrl as vm' + }).state('geo.help', { + url: '/help', + templateUrl: '/static/designsafe/apps/geo/html/help.html', + controller: 'HelpCtrl as vm' + }); + $urlRouterProvider.when('/', '/hazmapper'); + + //config popups etc + $uibTooltipProvider.options({ popupDelay: 1000 }); +} + +mod.config(config); + +exports.default = mod; + +/***/ }), +/* 17 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var ImageOverlayModalCtrl = function () { + ImageOverlayModalCtrl.$inject = ["$scope", "$uibModalInstance"]; + function ImageOverlayModalCtrl($scope, $uibModalInstance) { + 'ngInject'; + + _classCallCheck(this, ImageOverlayModalCtrl); + + this.$scope = $scope; + this.$uibModalInstance = $uibModalInstance; + this.data = { + file: null, + url: null, + min_lat: null, + max_lat: null, + min_lon: null, + max_lon: null + }; + } + + _createClass(ImageOverlayModalCtrl, [{ + key: 'ok', + value: function ok() { + this.$uibModalInstance.close(this.data); + } + }, { + key: 'cancel', + value: function cancel() { + this.$uibModalInstance.dismiss('cancel'); + } + }]); + + return ImageOverlayModalCtrl; +}(); + +exports.default = ImageOverlayModalCtrl; + +/***/ }) +/******/ ]); +//# sourceMappingURL=bundle.js.map \ No newline at end of file diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/build/bundle.js.map b/designsafe/apps/geo/static/designsafe/apps/geo/build/bundle.js.map new file mode 100644 index 0000000000..6cc45fec88 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/build/bundle.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack/bootstrap c626da950b02f60c64e9","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/db-modal.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/models/layer_group.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/models/map-project.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/utils/geo-utils.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/index.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/directives/index.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/index.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/confirm-clear-modal.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/help.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/map-sidebar.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/settings-modal.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/directives/custom-on-change.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-data-service.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-settings-service.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-state-service.js","webpack:///external \"L\"","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/index.js","webpack:///./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/image-overlay-modal.js"],"names":["DBModalCtrl","$scope","$uibModalInstance","filename","selected","saveas","dismiss","label","fg","feature_group","show","show_contents","getLayers","f","options","name","layer_groups","description","forEach","lg","clearLayers","bounds","push","getBounds","total","num_features","out","tmp","json","feature","toGeoJSON","opt_keys","indexOf","key","properties","features","get_file_extension","fname","split","pop","toLowerCase","mod","controller","service","HelpCtrl","$window","$timeout","$interval","$q","$uibModal","toastr","DataService","$http","GeoDataService","GeoSettingsService","settings","streets","L","tileLayer","attribution","satellite","maxZoom","basemaps","map","layers","preferCanvas","mc","Control","Measure","addTo","control","zoomControl","setPosition","current_project","_init_map_layers","fit_map_to_project","project","addLayer","invalidateSize","active_layer_group","add_draw_controls","on","e","object","color","default_stroke_color","fillColor","fillOpacity","$apply","current_mouse_coordinates","fitBounds","console","log","position","draw","circle","edit","featureGroup","remove","addControl","dc","drawControl","removeLayer","layer","active","ev","select_active_layer_group","i","create_layer_group","modal","templateUrl","result","then","s","eachLayer","clear","p","markerBounds","src_lg","prop","styles","current_layer","retval","open_existing","error","length","open","res","min_lat","min_lon","read_file_as_data_url","data","overlay","imageOverlay","url","$","loading","load_from_data_depot","_load_data_success","err","reqs","file","target","prom","success","rej","save_to_depot","SettingsModalCtrl","restrict","scope","handler","link","element","UserService","active_project","blob","base64","canvas","document","createElement","ctx","getContext","copyContext","canvasCopy","img","Image","src","onload","width","max_width","ratio","max_height","height","drawImage","toDataURL","binary","String","fromCharCode","encoded","btoa","l","d","zipper","loadAsync","proms","ext","zip","files","async","txt","_from_kml","geoJSON","icon","divIcon","iconSize","html","className","marker","maxWidth","original_src","original","exif","EXIF","readFromBinaryFile","lat","lonRef","lon","_arrayBufferToBase64","thumb","preview","_resize_image","resp","_make_image_marker","opacity","layer_group_index","Marker","image_src","latlng","getLatLng","reader","FileReader","readAsText","_from_json","JSON","parse","_from_kmz","_from_gpx","_from_image","_from_dsmap","responseType","gjson","a","body","appendChild","download","href","textContent","removeChild","form","to_json","post_url","base_file_url","path","system","type","File","append","default_fill_color","default_fill_opacity","measurement_units","GeoStateService","$state","requires","config","$stateProvider","$uibTooltipProvider","timeOut","$locationProvider","html5Mode","enabled","resolve","auth","state","$urlRouterProvider","when","ImageOverlayModalCtrl","max_lat","max_lon"],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA,mDAA2C,cAAc;;AAEzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;;;;;;;;;;;;IChEqBA,W;AAEnB,uBAAaC,MAAb,EAAqBC,iBAArB,EAAwCC,QAAxC,EAAkD;AAChD;;AADgD;;AAEhD;AACA,SAAKD,gBAAL;AACA,SAAKE,QAAL,GAAgB,IAAhB;AACA,SAAKC,MAAL,GAAc,EAACF,KAAf;AACD;;;;cAEK;AACJ,WAAKD,eAAL;AACD;;;kBAES;AACR,WAAKA,iBAAL,CAAuBI,CAAvB;AACD;;;;;;;;;;;;;;;;;;;;;;;;ACdD,sBAAaC,KAAb,EAAoBC,CAAI;AAAA;;AACtB;AACA,SAAKC,aAAL;AACA,SAAKC,IAAL,GAAY,IAAZ;AACA,SAAKC,YAAL;AACD;;;;wBAEe;AACd,aAAO,KAAKF,aAAL,CAAmBG,IAA1B;AACD;;;4BAEoB;AACnB;AACA,UAAIC,EAAEC,KAAmB;AACvB,eAAO,OAAP;AACD,OAFD,MAEO,IAAID,MAAuB;AAChC,eAAO,OAAP;AACD,OAFM,MAEA,IAAIA,MAAwB;AACjC,eAAO,SAAP;AACD,OAFM,MAEA;AACL;AACD;AACF;;;;;;;;;;;;;;;;;;;;;ACxBH;;;;;;;;;AAIE,sBAAaE,IAAb,EAAmB;AAAA;;AACjB;AACA,SAAKC,YAAL;AACA,SAAKC,WAAL,GAAmB,IAAnB;AACD;;;;iBAEO;AACN,WAAKD,YAAL,CAAkBE,KAAiB;AACjCC,WAAGV,aAAH,CAAiBW,WAAjB;AACD,OAFD;AAGD;;;sBAEY;AACX,UAAIC,SAAS,EAAb;AACA,WAAKL,WAA8B;AACjCK,eAAOC,IAAP,CAAYH,GAAGV,aAAH,CAAiBc,SAAjB,CAAZ;AACD,OAFD;AAGA;AACD;;;wBAEe;AACdC,cAAQ,CAAR;AACA,WAAKR,KAA8B;AACjCQ,iBAASL,GAAGM,YAAH,EAAT;AACD,OAFD;AAGD;;;mBAES;AACR,UAAIC,MAAM;AACR,gBAAQ,CADA;AAER,oBAAY,EAFJ;AAGR,kBAAU,IAHF;AAIR,gBAAQ,KAAKX,EAJL;AAKR,uBAAe,GALP;AAMR,sBAAc,KAAKC,YAAL,CANN;AAOR,wBAAgB;AAPR,OAAV;AASA,QAA2C;AACzCU,YAAIV,YAAJ,CAAiBM,IAAjB,CAAsBH,GAAGZ,KAAzB;AACA,YAAIoB,MAAM;AACR,kBAAQ,CADA;AAER,sBAAY,EAFJ;AAGR,mBAASR,GAAGZ;AAHJ,SAAV;AAKAY,UAAmD;AACjD,cAAIS,OAAOC,QAAQC,SAAR,EAAX;AACA;AACA;AACA,cAAIC,WAAW,CACb,OADa,EAEb,OAFa,EAGb,WAHa,EAIb,UAJF;;AAWA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAiC;AAC/B,gBAAIA,SAASC,OAAT,CAAiBC,GAAjB,MAA0B,CAAC,CAAG;AAChCL,mBAAKM,UAAL,CAAgBD,GAAhB,IAAuBJ,QAAQf,EAA/B;AACD;AACF;AACDc,YAAA;AACAF,cAAIS,QAAJ,CAAab,IAAb,CAAkBM,IAAlB;AACD,SA7BD;AA8BD,OArCD;AAsCA;AACD;;;;;;;;;;;;;;;;;;;AChFI,SAASQ,kBAAT,CAA6BC,KAA7B,EAAoC;AACzC,SAAOA,MAAMC,KAAN,CAAY,GAAZ,EAAiBC,GAAjB,GAAuBC,IAA9B;AACD,C;;;;;;;;;;;;;ACFD;;;;AACA;;;;AACA;;;;AACA;;;;AACA;;;;AACA;;;;;;AAEA;;AAEA;AACAC,IAAIC,UAAJ,CAAe,aAAf;AACAD,IAAIC,UAAJ,CAAe,UAAf;AACAD,IAAIC,UAAJ,CAAe,mBAAf;AACAD,IAAIC,UAAJ,CAAe,uBAAf;AACAD,IAAIC,UAAJ,CAAe,uBAAf;;;;;;;;;;;;;;;ACdA;;;;;;AAEA;;AAGA;kBACeD,G;;;;;;;;;;;;;ACLf;;;;AACA;;;;AACA;;;;;;AAHA;;AAMA;AACAA,IAAIE,OAAJ,CAAY,gBAAZ;AACAF,IAAIE,OAAJ,CAAY,oBAAZ;;AAEA;;;;;;;;;;;;;;;;;;;;ACRE,iCAAa1C,MAAb,EAA4D;AAC1D;;AAD0D;;AAE1D,SAAKA,MAAL,GAAcA,MAAd;AACA;AACD;;;;yBAEK;AACJ,WAAKC,GAAL;AACD;;;MAES;AACR,WAAKA,OAAL;AACD;;;;;;;;;;;;;;;;;;;;;ICdkB0C,Q,GAEnB,kBAAa3C,MAAb,EAAqB;AACnB;;AADmB;;AAEnB,OAAKA,MAAL,GAAcA,MAAd;;;;;;;;;;;;;;;;;;ACJJ;;;;AACA;;;;AACA;;;;AACA;;;;;;;;;;;AAIE,0BAAaA,MAAb,EAAqB4C,OAArB,EAA8BC,QAA9B,EAAwCC,SAAxC,EAAmDC,EAAnD,EAAuDC,SAAvD,EAAkEC,MAAlE,EAA0EC,WAA1E,EAAuFC,KAAvF,EAA8FC,cAA9F,EAA8GC,kBAA9G,EAAkI;AAChI;;AADgI;;AAAA;;AAEhI,SAAKrD,MAAL,GAAcA,GAAd;AACA;AACA,SAAK6C,QAAL,GAAgBA,QAAhB;AACA;AACA,SAAKD,OAAL,GAAeA,MAAf;AACA,SAAKG,EAAL,GAAUA,EAAV;AACA,SAAKC,SAAL,GAAiBA,QAAjB;AACA,SAAKE,WAAL,GAAmBA,QAAnB;AACA,SAAKC,KAAL,GAAaA,KAAb;AACA,SAAKC,QAAL;AACA,SAAKC,kBAAL,GAA0BA,CAA1B;AACA,SAAKJ,MAAL,GAAcA,MAAd;;AAEA,SAAKK,QAAL,GAAgB,KAAKD,gBAArB;;AAEA;AACA;AACA;;AAEA,QAAIE,UAAUC,EAAEC,SAAF,CAAY,YAAsD;AAC5EC,mBAAa;AAD+D,KAAlE,CAAd;;AAIA,QAAIC,YAAYH,EAAEC,SAAF,CACd,qDAAiG;AACjGC,mBAAa,QADoF;AAEjGE,OAAS;AAHX;;AAMA,QAAIC,WAAW;AACb,gBAAUN,CADG;AAEb,OAAaI;AAFf;AAIA,SAAKG,GAAL,GAAWN,EAAEM,GAAe;AACxBC,cAAQ,CAACR,OAAD,EADgB;AAExBS,oBAAc;AAFU,KAAjB,CAAX;AAIA,SAAKC,EAAL,GAAU,IAAIT,EAAEU,OAAF,CAAUC,KAAxB;AACA,SAAKF,EAAL,CAAQG,KAAR,CAAc,KAAKN,GAAnB;AACAN,MAAEa,OAAF,CAAUN,MAAV,CAAiBF,IAAjB;AACA,SAAKC,GAAL,CAASQ,WAAT,CAAqBC,CAArB;;AAEA;AACA;AACA,QAAI,KAAKnB,cAAL,CAAoBoB,eAApB,EAAJ,EAA2C;AACzC;;AAEA,WAAKC,gBAAL;AACA,WAAKC,kBAAL;AAED,KAND,MAMO;AACL;AACA,WAAKC,OAAL,CAAa5D,WAAb;AACA,WAAK+C,GAAL,CAASc,QAAT,CAAkB,KAAKD,GAAvB;AACD;;AAED;;AAEA;AACgB;AAAC,YAAKb,GAAL,CAASe,cAAT;AAAjB;;AAEA;AACA,SAAKC,kBAAL,GAA0B,GAA1B;;AAEA;AACA;AACAhC,cAAW,YAAM;AACf;AACD,KAFD,EAEG,IAFH;;AAIA,SAAKiC,iBAAL;;AAEA;AACA;AACA,SAAKjB,GAAL,CAASkB,EAAT,CAAY,cAAZ,EAA6B,UAACC,CAAD,EAAO;AAClC;AACAC,aAAOrE,OAAP,CAAesE,KAAf,GAAuB,MAAK7B,QAAL,CAAc8B,oBAArC;AACAF,aAAOrE,OAAP,CAAewE,SAAf,GAA2B,MAAK/B,OAAhC;AACA4B,aAAOrE,OAAP,CAAeyE,WAAf,GAA6B,MAAKhC,KAAlC;AACA,YAAKwB,eAAL;AACA,YAAK9E,MAAL,CAAYuF,MAAZ;AACD,KAPD;;AASA,SAAKzB,GAAL,CAASkB,EAAT,CAAY,WAAZ,EAAyB,UAACC,CAAD,EAAO;AAC9B,YAAKO,gBAAL;AACA;AAFF;AAMD,G,CAAC;;;;sBAEmB;AACf;AACF,aAAK1B,GAAL,CAAS2B,SAAT,CAAmB,KAAKd,CAAxB;AACD,OAFD,CAEE,OAAOM,CAAP,EAAU;AACVS,gBAAQC,GAAR,CAAY,iBAAZ,EAA+BV,CAA/B;AACD;AACF;;;OAEsB;AACrB,KAA4B;AAC1BW,MAD0B;AAE1BC,cAAM;AACJC,kBAAQ;AADJ,SAFoB;AAK1BC,cAAM;AACLC,eADK;AAELC,kBAAQ;AAFH;AALoB,OAAnB,CAAT;AAUA,WAAKnC,GAAL,CAASoC,UAAT,CAAoBC,CAApB;AACA,WAAKC,WAAL;AACD;;;4BAEqB;AACpBV,KAAA;AACD;;;yBAEkB;AAAA;;AACjB;AACA,WAAKf,OAAL,CAAa5D,YAAb,CAA0BE,OAA1B,CAAyC;AACvC;AACA;AACA;AACA;AACA,eAAK6C,GAAL,CAASuC,WAAT,CAAqBnF,GAAGV,aAAxB;AACA,eAAKsD,GAAL,CAASc,QAAT,CAAkB1D,GAAGV,OAArB;AACD,OAPD;AAQA,WAAKsE,kBAAL,GAA0B,KAAKH,OAAL,CAAa5D,EAAvC;AACD;;AAED;AACA;;;;qBACuB;AAAA;;AACrB,WAAK4D,OAAL,CAAa5D,YAA8B;AACzCG,WAAGV,aAAH,CAAiBwE,EAAjB,CAAoB,OAApB,EAA6B,MAAO;AAClC,iBAAKL,OAAsC;AACM;AAC7C2B,oBAAMC,MAAN,GAAe,KAAf;AACA,kBAAID,SAASE,GAAGF,KAAhB,EAAuB;AACrBA,sBAAMC,MAAN,GAAe,IAAf;AACD;AACF,aALD;AAMD,WAPD;AAQD,SATD;AAUD,OAXD;AAYD;;;SAEqB;AACpB;AACA;AACA,WAAKzB,kBAAL;AACA,WAAKhB,GAAL,CAASc,QAAT,CAAkB1D,GAAGV,aAArB;AACA,WAAKiG,yBAAL,CAA+B,KAAK3B,kBAApC;AACD;;;uCAEmB5D,E,EAAIwF,C,EAAG;AACzB;AACA;AACA,UAAI,KAAK/B,OAAL,CAAa5D,OAA2B;AAC1C,aAAK4F,kBAAL;AACD;AACD,WAAK7B,kBAAL,GAA0B,KAAKH,OAAL,CAA1B;AACD;;;mCAEezD,E,EAAIN,C,EAAG;AACrB;AACAM,MAAA;AACD;;;sCAE0B;AACzBA,KAAA;AACD;;;8CAEyBA,E,EAAI;AAC5B;AACA;AACA,WAAK4D,kBAAL,GAA0B5D,EAA1B;AACAA,SAAGqF,MAAH,GAAY,IAAZ;AACArF,SAAGT,IAAH,GAAU,IAAV;AACD;;;qBAE2B;AAC1B;AACA;AACD;;;yCAEqB;AAAA;;AACpB,UAAImG,QAAQ,KAAK5D,OAAe;AAC9B6D,qBAAa,qBADiB;AAE9BpE,oBAAY;AAFd;AAIAmE,YAAME,MAAN,CAAaC,IAAb,CAAmB,UAACC,CAAD,EAAO;AACxB,eAAKlD,GAAL,CAASmD,SAAT,CAAmB,UAAUX,KAAV,EAAiB;AAClCZ,kBAAQC,GAAR,CAAYW,KAAZ;AACA;AACD,SAHD;AAIA,eAAK3B,OAAL,CAAauC,KAAb;AACA,YAAIC,IAAI,aAAR;AACAA,UAAEpG,YAAF,GAAiB,CAAC,eAAlB;AACA;AACA,eAAK+D,gBAAL;AACA,eAAKhB,GAAL,CAASc,QAAT,CAAkB,OAAKE,iBAAvB;AACD,OAXD;AAYD;;;SAEgB;AACf,KAAiC;AAC9B;AACA,YAAIsC,OAAJ;AACA,YAAI;AACF,eAAKtD,GAAL,CAAS2B,SAAT,CAAmB2B,WAAnB;AACD,SAFD,CAEE,OAAOnC,CAAP,EAAU;AACVS,kBAAQC,GAAR,CAAYV,CAAZ;AACD;AACH,OARD,MAQO;AACL,YAAI;AACF,eAAKnB,GAAL,CAAS2B,MAAT;AACD,SAAW;AACVC,cAAA;AACD;AACF;AACF;;;QAEsB;AACrB;AACA;AACA2B,aAAO7G,MAAP;AACAU,SAAGV,aAAH,CAAiBoE,QAAjB,CAA0BhD,OAA1B;AACD;;;uCAEmB0F,EAAM;AACxB;AACA;AACA;AACA,UAAIC,SAAS,EAAb;AACAA,aAAOD,IAAP,IAAe,KAAKE,SAApB;AACA,WAAKA,OAAL;AACD;;;yCAEqBhB,CAAc;AAClCd,KAAA;AACD;;;uCAEmB+B,M,EAAQ;AAAA;;AAC1B,UAAI,KAAKC,aAAT,EAAwB;AACtB,YAAID,oCAA8B;AAChC;AAC2C;AACzC,mBAAK3D,GAAL,CAASuC,QAAT;AACD,WAFD;;AAIA;AACA,eAAK1B,OAAL,GAAe8C,MAAf;AACA;AACA;AACD,SAVD,MAUO;AACL,eAAKxE,MAAL,CAAY0E,KAAZ,CAAkB,IAAlB;AACD;AACD,aAAKD,aAAL,GAAqB,KAArB;AACD,OAfD,MAeO,GAAkC;AACvCD,eAAO1G,YAAP,CAAoBE,OAApB,CAA6B,UAACC,EAAD,EAAQ;AACnC;AACA,iBAAK4C,GAAL,CAASc,QAAT,CAAkB1D,GAAGV,EAArB;AACD,SAHD;AAIA,aAAKsE,kBAAL,GAA0B,KAAKH,OAAL,CAAa5D,IAAvC;AACA,aAAK2D,kBAAL;AACD,OAPM,MAOA;;AAEL,YAAI,KAAKC,OAAL,CAAa5D,YAAb,CAA0B6G,MAA1B,IAAoC,CAAxC,EAA2C;AACzC,eAAKjD,OAAL,CAAa5D,WAAb;AACA;AACA;AACD;AACD;AACA0G,eAAOxG,OAAP,CAAgB,UAACL,CAAD,EAAO;AACrB,iBAAKkE,kBAAL,CAAwBtE,aAAxB,CAAsCoE,QAAtC,CAA+ChE,CAA/C;AACD,SAFD;AAGA,aAAK8D,kBAAL;AACD;AACF;;;OAEwB;AACvB;AACA;AACD;;;8BAE0B;AACzB;AACA;AACD;;;2BAEgB;AAAA;;AACf,UAAIkC,QAAQ,KAAK5D,EAAe;AAC9B6D,qBAAa,gBADiB;AAE9BpE,oBAAY,IAFkB;AAGrB;AACPvC,oBAAU,mBAAK;AAAC,mBAAO,IAAP;AAAa;AADtB;AAHqB,OAApB,CAAZ;AAOA0G,YAAME,MAAN,CAAaC,IAAb,CAAkC;AAAC;AAA8B,OAAjE;AACD;;;SAEsB;AAAA;;AACrB,UAAIH,QAAQ,KAAK5D,QAAe;AAC9B6D,qBAAa,sBADiB;AAE9BpE,oBAAY;AAFd;AAIAmE,YAAME,MAAN,CAAaC,IAAb,CAAmB,UAACC,CAAD,EAAO;AACxB,eAAK1D,QAAL,GAAgB,OAAKD,kBAAL,CAAwBC,QAAxC;AAED,OAHD;AAID;;;SAE2B;AAAA;;AAC1B,UAAIsD,QAAQ,KAAK5D,SAAL,CAAe6E,GAAK;AAC9BhB,qBAAa,2BADiB;AAE9BpE,oBAAY;AAFd;AAIAmE,YAAME,MAAN,CAAaC,IAAb,CAAmB,UAACe,GAAD,EAAS;AAC1B,YAAI1G,eAAJ;AACAA,iBAAS,CACP,CAAC0G,IAAIC,OAAL,EAAcD,IAAIE,OAAlB,CADO,EAEP,CAACF,EAFH;AAIA,SAAc;AACZ,iBAAK1E,cAAL,CAAoB6E,QAAgD;AAClEvC,oBAAQC,GAAR,CAAYuC,IAAZ;AACD,WAFD;AAGD;AACD,YAAIC,UAAU3E,EAAE4E,YAAF,CAAeN,IAAIO,GAAnB,EAAwBjH,MAAxB,EAAgCgD,KAAhC,CAAsC,OAAKN,GAA3C,CAAd;AACA4B,gBAAQC,GAAR,CAAYwC,OAAZ;AACD,OAbD;AAcD;;;SAEmB;AAClB,KAAoB;AAClBG,MAAA;AACD,OAFD,EAEG,CAFH;AAGD;;;YAEuB;AAAA;;AACtB,WAAKC,OAAL,GAAe,IAAf;AACA,WAAKnF,cAAL,CAAoBoF,mBAEP;AACT,eAAKC,SAAL;AACA;AACD,OALH,EAME,UAACC,GAAD,EAAQ;AACN,eAAKzF,MAAL,CAAY0E,KAAZ,CAAkB,cAAlB;AACA,eAAKY,OAAL,GAAe,KAAf;AACA,eAAKb,aAAL;AACD,OAVH;AAWD;;;SAEwB;AAAA;;AACvB,WAAKa,OAAL,GAAe,IAAf;AACA,UAAII,OAAO,EAAX;AACA,WAAK,IAAIjC,IAAE,CAAX,EAAcA,GAA+B;AAC3C;AACA,YAAIkC,OAAOpC,GAAGqC,IAAd;AACA,YAAIC,OAAO,CAAgE;AAAC,iBAAO,QAAKL,kBAAL,CAAwBhB,MAAxB,CAAP;AAAwC,SAAzG,CAAX;AACAkB,aAAKtH,IAAL,CAAUyH,IAAV;AACA;AACD;AACD,WAA4B;AAC1B,gBAAKP,OAAL;AACA;AACAD,QAAA;AACA,gBAAKrF,MAAL,CAAY8F,OAAZ,CAAoB,WAApB;AACD,OALD,EAKG,UAACC,GAAD,EAAO;AACR,gBAAKT,OAAL;AACA,gBAAKtF,MAAL,CAAY0E,KAAZ,CAAkB,KAAlB;AACD,OARD,EAQGZ,IARH,CAQS,YAAM,CAEd,CAVD;AAWD;;;8BAEe;AACd;AACD;;;oCAEgB;AAAA;;AACf,UAAIH,QAAQ,KAAK5D,EAAe;AAC9B6D,qBAAa,gBADiB;AAE9BpE,oBAAY,KAFkB;AAGrB;AACPvC,oBAAU,mBAAK;AAAC,mBAAO,QAAKyE,OAAL,CAAa7D,IAAb,GAAoB,UAA3B;AAAuC;AADhD;AAHqB,OAApB,CAAZ;AAOA8F,YAAME,MAAN,CAAaC,IAAb,CAAmB,UAACe,GAAD,EAAS;AAC1B;AACA;AACAA,SAAA;AACA,gBAAKS,OAAL,GAAe,IAAf;AACA,gBAAKnF,cAAL,CAAoB6F,EAA0D;AAC5E,kBAAKV,OAAL,GAAe,KAAf;AACA,kBAAKtF,MAAL,CAAY8F,OAAZ,CAAoB,MAApB;AACD,SAHD,EAGG,UAACL,GAAD,EAAS;AACV,kBAAKzF,MAAL,CAAY0E,KAAZ,CAAkB,cAAlB;AACA,kBAAKY,OAAL,GAAe,KAAf;AACD,SAND;AAOD,OAZD,EAYG,UAACS,GAAD,EAAQ;AACT,gBAAKT,OAAL,GAAe,KAAf;AACD,OAdD;AAgBD;;;;;;;;;;;;;;;;;;;;;;;IChakBW,iB;AAEyC;AAC1D;;AAD0D;;AAE1D,SAAKlJ,MAAL,GAAcA,MAAd;AACA,SAAKC,MAAL;AACA;AACA,SAAKqD,QAAL,GAAgBD,mBAAmBC,MAAnC;AACD;;;;yBAEK;AACJ;AACA;AACD;;;;;;;;;;;;;;;;;;;ACbsC;AACvC,SAAO;AACL6F,aADK;AAELC,GAAO;AACLC,eAAS;AADJ,KAFF;AAKLC,UAAuC;AACrCC,cAAQvE,EAAR,CAAW,CAAwB;AACjCoE,YAAuB;AACrBA,gBAAMC,EAAN;AACD,MAFD;AAGD,OAJD;AAKD;AAXI,GAAP;AAaD,C;;;;;;;;;;;;;;;ACdD;;;;AACA;;;;AACA;;;;;;;;;;;AAKE,0BAAalG,KAAb,EAAoBJ,EAApB,EAAwByG,WAAxB,EAAqCnG,kBAArC,EAAyD;AACvD;;AADuD;;AAEvD,SAAKF,KAAL,GAAaA,KAAb;AACA,SAAKJ,EAAL,GAAUA,EAAV;AACA,SAAKyG,MAAL;AACA;AACA,SAAKC,cAAL,GAAsB,IAAtB;AACA;AACD;;;;+BAEwB;AACvB,UAAI,CAAE9E,OAAN,EAAgB;AACd;AACD;AACD,WAAK8E,cAAL,GAAsB9E,KAAtB;AACD;;;kCAEc+E,CAAqC;AAAA;;AAAA;AAAA;;AAClD,aAAO,KAAK3G,EAAL,CAAS,UAAC+E,GAAD,EAAMkB,GAAN,EAAc;AAC5B,YAAIW,SAAS,EAAb;AACA;AACA,YAAIC,SAASC,SAASC,aAAT,CAAuB,QAAvB,CAAb;AACA,YAAIC,MAAMH,OAAOI,UAAP,CAAkB,IAAlB,CAAV;AACA;AACA,YAAIC,cAAcC,WAAWF,KAA7B;;AAEA;AACA,YAAIG,MAAM,IAAIC,KAAJ,EAAV;AACAD,YAAIE,GAAJ,GAAUV,MAAV;AACAQ,YAAIG,MAAJ,GAAa,YAAI;AACf;AACA;AACA,cAAGH,IAAII,KAAJ,GAAYC,MAAW;AACxBC,oBAAQD,UAAR;AACD,WAFD,MAEO,IAAGL,IAAyB;AACjCM,oBAAQC,aAAaP,CAArB;AACD;AACD;AACAD,qBAAWK,KAAX,GAAmBJ,IAAII,KAAvB;AACAL,qBAAWS,MAAX,GAAoBR,IAAIQ,MAAxB;AACAV,sBAAYW,SAAZ,CAAsBT,GAAtB,EAA2B,CAA3B,EAA8B,CAA9B;;AAEA;AACAP,iBAAOW,KAAP,GAAeJ,IAAII,KAAJ,GAAYE,KAA3B;AACAb,iBAAOe,MAAP,GAAgBR,IAAIQ,MAAJ,GAAhB;AACAZ,cAAIa,SAAJ,CAAcV,UAAd,EAA0B,CAA1B,EAA6B,CAA7B;AACApC,cAAI8B,OAAOiB,SAAP,EAAJ;AAjBF;AAmBD,OA9BM,CAAP;AA+BD;;;kCAE8B;AAC7B;AACA;AACA;AACA,MAA8B;AAC1BC,kBAAUC,OAAOC,OAAjB;AACH;AACD,UAAIC,UAAWC,EAAf;AACA,aAAO,2BAA2BD,CAAlC;AACD;;;OAEoB;AACnB,aAAO,KAAKlI,EAAL,CAAS,UAAC+E,EAAa;AAC5B,YAAI5F,WAAW,EAAf;AACA;AACAiJ,MAA6B;AAC3B;AACAC,YAAEvK,OAAF,CAAUP,KAAV,GAAkB8K,EAAExJ,OAAF,CAAUK,IAA5B;AACAC,mBAASb,IAAT,CAAc+J,CAAd;AACD,SAJD;AAKAtD,YAAI5F,QAAJ;AACD,OATM,CAAP;AAUD;;;WAEgB;AAAA;;AACf,KAA8B;AAC5B;AACAmJ,eAAOC,MAA+B;AACpC;AACA,cAAIC,QAAQ,EAAZ;AAC2B;AACzB,gBAAIC,MAAMxJ,IAAIK,KAAJ,CAAU,GAAV,EAAeC,GAAf,EAAV;AACA,gBAAIkJ,QAAQ,KAAZ,EAAmB;AACjB,qBAAOC,IAAIC,KAAJ,CAAU1J,GAAV,EAAe2J,KAAf,CAAqB,MAArB,CAAP;AACD;AACF;AACF,SATD,EASG5E,IATH,CASS,UAAC6E,GAAD,EAAS;AAChB,cAAI1J,WAAW,OAAK2J,SAAL,CAAeD,CAA9B;AACA9D,cAAI5F,QAAJ;AACD,SAZD;AAaD,OAfM,CAAP;AAgBD;;;wBAEiB;AAAA;;AAChB,KAA8B;AAC5B,MAAiB;;AAEjB,YAAI;AAAA;AACF;AACAsB,cAAEsI,OAAF,CAAUpC,IAAV,EAAgB/I,SAAhB,GAA4BM,EAAoB;AAC9C,mBAAK,IAAIe,GAAT,IAAgBsE,MAAM1E,OAAN,CAAcK,UAA9B,EAA0C;AACxC;AACD;AACDC,uBAASb,CAAT;AACD,aALD;AAMAyG,gBAAI5F,QAAJ;AARE;AASH,SATD,CASE,OAAO+C,CAAP,EAAU;AACV+D,cAAI,CAAJ;AACD;AACF,OAfM,CAAP;AAgBD;;;6BAEgB;AACf,SAA8B;AAC5B;AACA;AACA;AACAmC,UAAExK,SAAF,EAA6B;AAC3BuB,mBAASb,IAAT,CAAc+J,CAAd;AACD,SAFD;AAGAtD,YAAI5F,QAAJ;AACD,OARM,CAAP;AASD;;;WAEuD;AACtD,UAAI6J,OAAOvI,EAAEwI,GAAQ;AACnBC,SADmB;AAEnBC,KAFmB;AAGnBC,MAAW;AAHQ,OAAV,CAAX;;AAMA,UAAIC,SAAS5I,EAAE4I,MAAF,CAEH;AACED,mBAAW,QADb;AAEEE,kBAAU;AAFZ,OAFG,CAAb;AAOAD,SAAA;AACA;AACAA,aAAOvL,OAAP,CAAeyL,YAAf,GAA8BC,QAA9B;AACA,aAAOH,MAAP;AACD;;;gCAEYxD,I,EAAM;AAAA;;AACjB,KAA8B;AAC5B,MAAI;AAAA;AACF,gBAAI4D,OAAOC,KAAKC,WAAhB;AACA,gBAAIC,MAAMH,EAAV;AACA;AACA;AACA;AACA,gBAAII,QAAJ;AACAD,kBAAM,CAACA,IAAI,CAAJ,IAASA,IAAI,CAAJ,IAAO,EAAhB,GAAqBA,IAAI,CAAJ,IAAO,EAAnC;AACAE,kBAAM,CAACA,IAAI,CAAJ,IAASA,IAAI,CAAJ,IAAO,EAAvB;AACA,gBAAKF,MAAM,EAAP,IAAeA,MAAM,CAAC,EAAtB,GAA0D;AAC5D3D,kBAAI,mBAAJ;AACD;AACD,gBAAIiC,UAAU,OAAK6B,oBAAnB;AACA,gBAAIC,QAAQ,IAAZ;AACA,gBAAIC,UAAU,IAAd;AACA,mBAAKC,aAAL,CAAmBrE,IAAnB,EAAyB,GAAzB,EAA8B,GAA9B,EAAmC7B,IAAnC,CAAyC,UAACmG,GAAO;AAC/CH,sBAAQG,IAAR;AACD,aAAY;AACX,qBAAO,OAAKD,aAAL,CAAmBrE,IAAnB,EAAyB,GAAzB,EAA8B,GAA9B,CAAP;AACD,aAJD,EAIG7B,IAJH,CAIS,SAAQ;AACfiG,wBAAUE,IAAV;AACA,kBAAId,SAAS,OAAKe,kBAAL,CAAwBR,GAAxB,EAA6BE,GAA7B,EAAkCE,KAAlC,EAAyCC,CAAtD;AACAlF,kBAAI,CAACsE,MAAD,CAAJ;AACD,aARD;AAfE;AAwBH,SAxBD,CAwBE,OAAOnH,CAAP,EAAU;AACV+D,cAAI/D,CAAJ;AACD;AACF,OA5BM,CAAP;AA6BD;;;iBAGkB;AAAA;;AACjB,KAA8B;AAC5B;AACA,YAAIN,UAAU,CAAd;AACAA,gBAAQ7D,IAAR,GAAea,KAAKb,IAApB;AACA6D,gBAAQ3D,QAAR;AACqC;AACnC2D,kBAAQ5D,YAAR,CAAqBM,IAArB,CAA0B,MAA1B;AACD,SAFD;AAGAM,aAAKO,QAAL,CAAcjB,OAAd,CAAuB,UAACmK,CAAD,EAAM;AAC3B,cAAIxJ,UAAU4B,EAAEsI,OAAhB;AACAlK,kBAAQqF,SAAR,CAAmB,UAACX,KAAD,EAAU;;AAE3B;AACA,WAAuC;AACtCA,oBAAM1E,OAAN,CAAcK,UAAd,CAAyBkD,KAAzB;AACA;AACD,gBAAI,CAAEmB,MAAM1E,OAAN,CAAcK,UAAd,CAAyBoD,IAAY;AACzC;AACD;AACD,gBAAI,CAAEiB,MAAM1E,OAAN,CAAcK,UAAd,CAAyBmL,OAA/B,CAAyC;AACvC9G,oBAAM1E,OAAN,CAAcK,UAAd,CAAyBmL,OAAzB,GAAmC,GAAnC;AACD;;AAED,iBAAK,IAAIpL,GAAT,IAAgBsE,MAAM1E,OAAN,CAAcK,UAA9B,EAA0C;AACxCqE,cAAA;AACD;AACD,gBAAI;AACF,cAAa;AAAA;AAEXnB,uBAAOmB,MAAM1E,OAAN,CAAcK,UAAd,CAAyBkD,KAFrB;AAGXiI,yBAAS9G,MAAM1E,OAAN,CAAcK,UAAd,CAAyBmL;AAHvB,aAAb;AAKA9G,iBAAA;AACD,aAPD,CAOE,OAAOrB,CAAP,EAAU;AACV;AACA;AACAS,sBAAQC,GAAR,CAAYV,CAAZ;AACD;;AAED,gBAAIoI,SAAJ;AACA,gBAAK/G,iBAAiB9C,EAAE8J,MAApB,IAAgChH,MAAM1E,OAAN,CAAcK,UAAd,CAAyBsL,SAA7D,EAAyE;AACvE,kBAAIC,SAASlH,MAAMmH,SAAN,EAAb;AACAnH,sBAAQ,OAAR;AACA;AACA;AACD;AACD3B,oBAAQ5D,YAAR,CAAqBsM,iBAArB,EAAwC7M,aAAxC,CAAsDoE,QAAtD,CAA+D0B,KAA/D;AACAA,kBAAMzF,OAAN,CAAcP,KAAd,GAAsB8K,EAAEnJ,SAAxB;AACD,WAtCD;AAwCD,SA1CD;AA2CA6F,YAAInD,OAAJ;AACD,OApDM,CAAP;AAqDD;;;WAE2B;;AAE1B;AACA,KAA8B;AAC5B+I,MAAA;;AAEAA,eAAOpD,MAAP,GAAgB,UAACrF,CAAD,EAAO;AACrB;AACD,SAFD;AAGD,OANM,CAAP;AAOD;;AAED;;;;;;;yCAIsB2D,I,EAAM;AAAA;;AACI;AAC5B;AACA,YAAI8E,SAAS,IAAIC,OAAjB;AACA;AACA,YAAKnC,QAAQ,IAA8C;AACzD;AACD,SAFD,MAEO;AACLkC,iBAAOE,UAAP,CAAkBhF,IAAlB;AACD;AACD8E,UAAuB;AACrB,cAAIvG,IAAI,IAAR;AACA,kBAAQqE,GAAR;AACE;AACErE,kBAAK,OAAK0E,SAAV;AACA;AACF,iBAAK,MAAL;AACE1E,kBAAI,KAAJ;AACA;AACF,iBAAK,MAAL;AACEA,kBAAI,OAAK0G,UAAL,CAAgBC,KAAKC,KAAL,CAAW9I,EAAE4D,GAAjC;AACA;AACF,iBAAK,KAAL;AACE1B,kBAAI,OAAK6G,SAAL,CAAe/I,EAAE4D,MAAF,CAAS/B,MAAxB,CAAJ;AACA;AACF,iBAAK,KAAL;AACEK,kBAAI,OAAK8G,SAAL,CAAehJ,EAAE4D,MAAF,CAAS/B,MAAxB,CAAJ;AACA;AACF,iBAAK,MAAL;AACEK,kBAAI,OAAK+G,WAAL,CAAiBjJ,EAAE4D,MAAF,CAAS/B,MAA9B;AACA;AACF,iBAAK,KAAL;AACEK,kBAAI,OAAK+G,WAAL,CAAiBjJ,EAAE4D,MAAF,CAAS/B,MAA9B;AACA;AACF,iBAAK,OAAL;AACEK,kBAAI,OAAKgH,WAAL,CAAiBL,KAAKC,KAAL,CAAW9I,EAAE4D,IAAlC;AACA;AACF;AACE1B,kBAAI,OAAK0G,UAAL,CAAgBC,KAAKC,KAAL,CAAW9I,EAAE4D,KAAjC;AA1BJ;AA4BA,iBAAOf,IAAIX,CAAJ,CAAP;AACD,SA/BD;AAgCD,OAzCM,CAAP;AA0CD;;AAED;AACA;AACA;;;;MACwB;AAAA;;AACtB;AACA;AACA,UAAKqE,QAAQ,KAAT,IAAoBA,KAAoC;AAC1D4C,uBAAe,aAAf;AACD;AACkF;AACjF,YAAIjH,IAAI,IAAR;AACA,gBAAQqE,GAAR;AACE,eAAK,KAAL;AACErE,gBAAK,OAAK0E,SAAL,CAAeqB,IAApB;AACA;AACF,eAAK,MAAL;AACE/F,gBAAI,KAAJ;AACA;AACF,eAAK,MAAL;AACEA,gBAAI,OAAK0G,UAAL,CAAgBX,KAAKhF,IAArB,CAAJ;AACA;AACF,eAAK,KAAL;AACEf,gBAAI,OAAK6G,SAAL,CAAed,KAAKhF,IAApB,CAAJ;AACA;AACF,eAAK,KAAL;AACEf,gBAAI,OAAK8G,SAAL,CAAef,KAAKhF,IAApB,CAAJ;AACA;AACF,eAAK,MAAL;AACEf,gBAAI,OAAK+G,WAAL,CAAiBhB,KAAKhF,IAA1B;AACA;AACF,eAAK,KAAL;AACEf,gBAAI,OAAK+G,WAAL,CAAiBhB,KAAKhF,IAA1B;AACA;AACF,eAAK,OAAL;AACEf,gBAAI,OAAKgH,WAAL,CAAiBjB,KAAKhF,IAAtB,CAAJ;AACA;AACF;AACEf,gBAAI,OAAK0G,UAAL,CAAgBX,KAAKhF,IAArB,CAAJ;AA1BJ;AA4BA,eAAOf,CAAP;AACD,OA/BM,CAAP;AAgCD;;;SAEsB;AACrB,UAAIkH,OAAJ;AACA;AACA;;AAEA,UAAIC,IAAIzE,SAASC,CAAjB;AACAD,eAAS0E,IAAT,CAAcC,WAAd,CAA0BF,CAA1B;AACAA,QAAEG,QAAF,GAAgB9J,QAAQ7D,IAAR,GAAe,EAA/B;AACAwN,QAAEI,IAAF,GAAgBrG,GAAhB;AACAiG,QAAEK,WAAF,GAAgB,UAAhB;AACA;AACA9E,eAAS0E,IAAT,CAAcK,WAAd,CAA0BN,CAA1B;AACD;;;iCAE6B;AAC5B,UAAIO,MAAJ;AACA,UAAIR,QAAQ1J,QAAQmK,OAAR,EAAZ;AACA;AACA;AACA,UAAIC,WAAWC,IAAf;AACAD,iBAAWA,WAAWE,KAAKC,MAA3B;AACA,UAAItG,OAAO,IAAX;AACA,UAAIqG,KAAKE,IAAL,KAAc,KAAlB,EAAyB;AACvBJ,mBAAWA,WAAWE,KAAKA,IAA3B;AACArG,eAAO,IAAIwG,IAAJ,CAAS,CAAC1F,IAAD,CAAT,EAAiBuF,KAAKnO,IAAtB,CAAP;AACA+N,aAAKQ,MAAL,CAAY,cAAZ;AACD,OAJD,MAIO;AACL;AACAN,mBAAWA,WAAWE,EAAtB;AACArG,eAAO,IAAIwG,IAAJ,CAAS,CAAC1F,IAAD,CAAT,EAAiBuF,KAAKnO,GAA7B;AACA+N,aAAKQ,MAAL,CAAY,cAAZ,EAA4BzG,IAA5B,EAAkCqG,CAAlC;AACD;AACD,aAAO,CAAP;AACD;;;;;;;;;;;;;;;;;;;;;IC1XkB5L,kB,GAEnB,qBAAe;AAAA;;AACG;AACdiM,wBAAoB,SADN;AAAA;AAGdC,0BAAsB,GAHR;AAIdC,uBAAmB;AAJrB;AAMD,C;;kBATkBnM,kB;;;;;;;;;;;;;;;ICAAoM,e,GAEnB,wBAA6B;AAAA;;AAC3B;AACA,OAAKC,MAAL,GAAcA,MAAd;AACA;AACD,C;;;;;;;;ACNH,mB;;;;;;;;;;;;;ACAA;;AACA;;AACA;;AAEA;AACAlN,IAAImN,QAAJ,CAAatO,IAAb,CAAkB,WAAlB,EAA+B,UAA/B;;AAEA,SAASuO,MAAT,CAAgBC,cAAhB,EAAgCC,UAA0E;AACxG;;AAE6B;AAC3BC,aAAS;AADkB,GAA7B;;AAIAC,oBAAkBC,SAAlB,CAA4B;AAC1BC,aAAS;AADX;;AAIAL,iBAA4B;AAC1BxH,KAD0B;AAAA;AAG1BxB,iBAAa,cAHa;AAI1BsJ,aAAS;AACPC,KAAkB;AAChB;AACD;AAHM;AAJiB,GAA5B,EASGC,KATH,CASS,QAAW;AAClBhI,SAAK,YADa;AAElBxB,cAFkB;AAGlBpE,gBAAY;AAHM,GATpB,EAaG4N,KAbH,CAaS,SAAY;AACnBhI,OADmB;AAEnBxB,KAFmB;AAGnBpE,gBAAY;AAHO,GAbrB;AAkBA6N,qBAAmBC,IAAnB,CAAwB,GAAxB,EAA6B,YAA7B;;AAEA;AACAT,iBAAA;AAED;;AAEDtN,IAAIoN,CAAJ;;;;;;;;;;;;;;;;;;;IC3CqBY,S;AAEnB,GAAwC;AACtC;;AADsC;;AAEtC;AACA,SAAKvQ,iBAAL,GAAyBA,YAAzB;AACA,SAAKiI,IAAL,GAAY;AACVU,YAAM,IADI;AAEVP,WAAK,IAFK;AAAA;AAIVoI,eAAS,IAJC;AAAA;AAMVC,eAAS;AANC,KAAZ;AAQD;;;;oBAEK;AACJ,WAAKzQ,SAAL;AACD;;;AAES;AACR,WAAKA,iBAAL,CAAuBI,OAAvB,CAA+B,GAA/B;AACD;;;;;;kBAtBkBmQ,Y","file":"./designsafe/apps/geo/static/designsafe/apps/geo/build/bundle.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 16);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap c626da950b02f60c64e9","export default class DBModalCtrl {\n\n constructor ($scope, $uibModalInstance, filename) {\n 'ngInject';\n this.$scope = $scope;\n this.$uibModalInstance = $uibModalInstance;\n this.selected = null;\n this.saveas = {filename: filename};\n }\n\n ok () {\n this.$uibModalInstance.close({selected:this.selected, saveas:this.saveas.filename});\n };\n\n cancel () {\n this.$uibModalInstance.dismiss('cancel');\n };\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/db-modal.js","export default class LayerGroup {\n\n constructor (label, fg) {\n this.label = label;\n this.feature_group = fg;\n this.show = true;\n this.show_contents = true;\n }\n\n num_features () {\n return this.feature_group.getLayers().length;\n }\n\n get_feature_type (f) {\n // debugger\n if (f.options.image_src) {\n return 'Image';\n } else if (f instanceof L.Marker) {\n return 'Point';\n } else if (f instanceof L.Polygon) {\n return 'Polygon';\n } else {\n return 'Path';\n }\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/models/layer_group.js","import L from 'L';\n\nexport default class MapProject {\n\n constructor (name) {\n this.name = name;\n this.layer_groups = [];\n this.description = null;\n }\n\n clear() {\n this.layer_groups.forEach( (lg) => {\n lg.feature_group.clearLayers();\n });\n }\n\n get_bounds() {\n let bounds = [];\n this.layer_groups.forEach( (lg) => {\n bounds.push(lg.feature_group.getBounds());\n });\n return bounds;\n }\n\n num_features () {\n total = 0;\n this.layer_groups.forEach( (lg) => {\n total += lg.num_features();\n });\n }\n\n to_json() {\n let out = {\n \"type\": \"FeatureCollection\",\n \"features\": [],\n \"ds_map\": true,\n \"name\": this.name,\n \"description\": this.description,\n \"num_layers\": this.layer_groups.length,\n \"layer_groups\": []\n };\n this.layer_groups.forEach( (lg, lg_idx) => {\n out.layer_groups.push(lg.label);\n let tmp = {\n \"type\": \"FeatureCollection\",\n \"features\": [],\n \"label\": lg.label\n };\n lg.feature_group.getLayers().forEach( (feature) => {\n let json = feature.toGeoJSON();\n // These are all the keys in the options object that we need to\n // re-create the layers in the application after loading.\n let opt_keys = [\n 'label',\n 'color',\n 'fillColor',\n 'fillOpacity',\n 'description',\n 'image_src',\n 'thumb_src',\n 'original_src',\n ];\n\n // //add in any options\n // if (feature.options.image_src) {\n // json.properties.image_src = feature.options.image_src;\n // }\n // if (feature.options.thumb_src) {\n // json.properties.thumb_src = feature.options.thumb_src;\n // }\n for (let key in feature.options) {\n if (opt_keys.indexOf(key) !== -1) {\n json.properties[key] = feature.options[key];\n }\n };\n json.layer_group_index = lg_idx;\n out.features.push(json);\n });\n });\n return out;\n }\n\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/models/map-project.js","export function get_file_extension (fname) {\n return fname.split('.').pop().toLowerCase();\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/utils/geo-utils.js","import MapSidebarCtrl from './map-sidebar';\nimport DBModalCtrl from './db-modal';\nimport ImageOverlayModalCtrl from './image-overlay-modal.js';\nimport HelpCtrl from './help';\nimport SettingsModalCtrl from './settings-modal';\nimport ConfirmClearModalCtrl from './confirm-clear-modal';\n\nlet mod = angular.module('ds.geo.controllers', []);\n\nmod.controller('MapSidebarCtrl', MapSidebarCtrl);\nmod.controller('DBModalCtrl', DBModalCtrl);\nmod.controller('HelpCtrl', HelpCtrl);\nmod.controller('SettingsModalCtrl', SettingsModalCtrl);\nmod.controller('ConfirmClearModalCtrl', ConfirmClearModalCtrl);\nmod.controller('ImageOverlayModalCtrl', ImageOverlayModalCtrl);\n\nexport default mod;\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/index.js","import customOnChange from './custom-on-change';\n\nlet mod = angular.module('ds.geo.directives', []);\n\n\nmod.directive('customOnChange', customOnChange);\nexport default mod;\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/directives/index.js","// import customOnChange from './custom-on-change';\nimport GeoStateService from './geo-state-service';\nimport GeoDataService from './geo-data-service';\nimport GeoSettingsService from './geo-settings-service';\n\nlet mod = angular.module('ds.geo.services', []);\nmod.service('GeoStateService', GeoStateService);\nmod.service('GeoDataService', GeoDataService);\nmod.service('GeoSettingsService', GeoSettingsService);\n\n// mod.directive('customOnChange', customOnChange);\n\nexport default mod;\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/index.js","export default class ConfirmClearModalCtrl {\n\n constructor ($scope, $uibModalInstance, GeoSettingsService) {\n 'ngInject';\n this.$scope = $scope;\n this.$uibModalInstance = $uibModalInstance;\n }\n\n ok () {\n this.$uibModalInstance.close('ok');\n };\n\n cancel () {\n this.$uibModalInstance.dismiss('cancel');\n };\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/confirm-clear-modal.js","export default class HelpCtrl {\n\n constructor ($scope) {\n 'ngInject';\n this.$scope = $scope;\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/help.js","import LayerGroup from '../models/layer_group';\nimport MapProject from '../models/map-project';\nimport DBModalCtrl from './db-modal';\nimport * as GeoUtils from '../utils/geo-utils';\n\nexport default class MapSidebarCtrl {\n\n constructor ($scope, $window, $timeout, $interval, $q, $uibModal, toastr, DataService, $http, GeoDataService, GeoSettingsService) {\n 'ngInject';\n this.$scope = $scope;\n this.LGeo = $window.LGeo;\n this.$timeout = $timeout;\n this.$interval = $interval;\n this.$window = $window;\n this.$q = $q;\n this.$uibModal = $uibModal;\n this.DataService = DataService;\n this.$http = $http;\n this.GeoDataService = GeoDataService;\n this.GeoSettingsService = GeoSettingsService;\n this.toastr = toastr;\n\n this.settings = this.GeoSettingsService.settings;\n\n //method binding for callback, sigh...\n this.local_file_selected = this.local_file_selected.bind(this);\n // this.open_db_modal = this.open_db_modal.bind(this);\n\n let streets = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {\n attribution: '© OpenStreetMap contributors'\n });\n\n let satellite = L.tileLayer(\n 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {\n attribution: '©',\n maxZoom: 20,\n });\n\n let basemaps = {\n 'Street': streets,\n 'Satellite': satellite\n };\n this.map = L.map('geo_map', {\n layers: [streets, satellite],\n preferCanvas: false\n }).setView([0, 0], 3);\n this.mc = new L.Control.Measure({primaryLengthUnit:'meters', primaryAreaUnit: 'meters'});\n this.mc.addTo(this.map);\n L.control.layers(basemaps).addTo(this.map);\n this.map.zoomControl.setPosition('bottomleft');\n\n // Load in a map project from the data service if one does exist, if not\n // create a new one from scratch\n if (this.GeoDataService.current_project()) {\n this.project = this.GeoDataService.current_project();\n\n this._init_map_layers();\n this.fit_map_to_project();\n\n } else {\n this.project = new MapProject('New Map');\n this.project.layer_groups = [new LayerGroup('New Group', new L.FeatureGroup())];\n this.map.addLayer(this.project.layer_groups[0].feature_group);\n }\n\n // this._add_click_handlers();\n\n // trick to fix the tiles that sometimes don't load for some reason...\n $timeout( () => {this.map.invalidateSize();}, 10);\n\n // init an active layer group\n this.active_layer_group = this.project.layer_groups[0];\n\n // Auto keep track of current project in the GeoDataService\n // so that if they switch states they will not lose work...\n $interval( () => {\n this.GeoDataService.current_project(this.project);\n }, 1000);\n\n this.add_draw_controls(this.active_layer_group.feature_group);\n\n // This handles making sure that the features that get created with the draw tool\n // are styled with the default colors etc.\n this.map.on('draw:created', (e) => {\n let object = e.layer;\n object.options.color = this.settings.default_stroke_color;\n object.options.fillColor = this.settings.default_fill_color;\n object.options.fillOpacity = this.settings.default_fill_opacity;\n this.active_layer_group.feature_group.addLayer(object);\n this.$scope.$apply();\n });\n\n this.map.on('mousemove', (e) => {\n this.current_mouse_coordinates = e.latlng;\n this.$scope.$apply();\n });\n\n\n } // end constructor\n\n fit_map_to_project() {\n try {\n this.map.fitBounds(this.project.get_bounds(), {maxZoom: 16});\n } catch (e) {\n console.log('get_bounds fail', e);\n }\n }\n\n add_draw_controls (fg) {\n let dc = new L.Control.Draw({\n position: 'topright',\n draw: {\n circle: false,\n },\n edit: {\n featureGroup: fg,\n remove: true\n }\n });\n this.map.addControl(dc);\n this.drawControl = dc;\n }\n\n feature_click (layer) {\n console.log(layer);\n }\n\n _init_map_layers() {\n // For some reason, need to readd the feature groups for markers to be displayed correctly???\n this.project.layer_groups.forEach( (lg)=>{\n // lg.feature_group.getLayers().forEach( (layer)=>{\n // this.map.addLayer(layer);\n // });\n // this.map.addLayer(lg.feature_group);\n this.map.removeLayer(lg.feature_group);\n this.map.addLayer(lg.feature_group);\n });\n this.active_layer_group = this.project.layer_groups[0];\n }\n\n // Adds click handlers to map elements. This does NOT feel\n // right to me...\n _add_click_handlers () {\n this.project.layer_groups.forEach( (lg) => {\n lg.feature_group.on('click', (ev)=> {\n this.project.layer_groups.forEach( (lg) => {\n lg.feature_group.getLayers().forEach( (layer)=>{\n layer.active = false;\n if (layer == ev.layer) {\n layer.active = true;\n }\n });\n });\n });\n });\n }\n\n create_layer_group () {\n let lg = new LayerGroup(\"New Group\", new L.FeatureGroup());\n this.project.layer_groups.push(lg);\n this.active_layer_group = this.project.layer_groups[this.project.layer_groups.length -1];\n this.map.addLayer(lg.feature_group);\n this.select_active_layer_group(this.active_layer_group);\n }\n\n delete_layer_group (lg, i) {\n this.map.removeLayer(lg.feature_group);\n this.project.layer_groups.splice(i, 1);\n if (this.project.layer_groups.length === 0) {\n this.create_layer_group();\n }\n this.active_layer_group = this.project.layer_groups[0];\n }\n\n delete_feature (lg, f) {\n this.map.removeLayer(f);\n lg.feature_group.removeLayer(f);\n }\n\n show_hide_layer_group (lg) {\n lg.show ? this.map.addLayer(lg.feature_group) : this.map.removeLayer(lg.feature_group);\n }\n\n select_active_layer_group(lg) {\n this.map.removeControl(this.drawControl);\n this.add_draw_controls(lg.feature_group);\n this.active_layer_group = lg;\n lg.active = true;\n lg.show = true;\n }\n\n select_feature(lg, feature) {\n this.active_layer_group = lg;\n this.current_layer == feature ? this.current_layer = null : this.current_layer = feature;\n }\n\n create_new_project () {\n let modal = this.$uibModal.open({\n templateUrl: \"/static/designsafe/apps/geo/html/confirm-new-modal.html\",\n controller: \"ConfirmClearModalCtrl as vm\",\n });\n modal.result.then( (s) => {\n this.map.eachLayer(function (layer) {\n console.log(layer)\n // this.map.removeLayer(layer);\n });\n this.project.clear();\n let p = new MapProject('New Map');\n p.layer_groups = [new LayerGroup('New Group', new L.FeatureGroup())];\n this.project = p;\n this.active_layer_group = this.project.layer_groups[0];\n this.map.addLayer(this.active_layer_group.feature_group);\n });\n }\n\n zoom_to(feature) {\n if (feature instanceof L.Marker) {\n let latLngs = [ feature.getLatLng() ];\n let markerBounds = L.latLngBounds(latLngs);\n try {\n this.map.fitBounds(markerBounds, {maxZoom: 16});\n } catch (e) {\n console.log(e);\n }\n } else {\n try {\n this.map.fitBounds(feature.getBounds(), {maxZoom: 16});\n } catch (e) {\n console.log(e);\n }\n };\n }\n\n on_drop (ev, data, lg) {\n let src_lg = this.project.layer_groups[data.pidx];\n let feature = src_lg.feature_group.getLayers()[data.idx];\n src_lg.feature_group.removeLayer(feature);\n lg.feature_group.addLayer(feature);\n }\n\n update_layer_style (prop) {\n let tmp = this.current_layer;\n // debugger;\n // this.current_layer.setStyle({prop: this.current_layer.options[prop]});\n let styles = {};\n styles[prop] = this.current_layer.options[prop];\n this.current_layer.setStyle(styles);\n }\n\n drop_feature_success (ev, data, lg) {\n console.log(\"drag_feature_success\", ev, data, lg);\n }\n\n _load_data_success (retval) {\n if (this.open_existing) {\n if (retval instanceof MapProject) {\n //clear off all the layers from the map\n this.project.layer_groups.forEach( (lg) => {\n this.map.removeLayer(lg.feature_group);\n });\n\n // set the project to be the return value\n this.project = retval;\n this._init_map_layers();\n this.fit_map_to_project();\n } else {\n this.toastr.error('Load failed! File was not compatible');\n }\n this.open_existing = false;\n } else if (retval instanceof MapProject) {\n retval.layer_groups.forEach( (lg) => {\n this.project.layer_groups.push(lg);\n this.map.addLayer(lg.feature_group);\n });\n this.active_layer_group = this.project.layer_groups[0];\n this.fit_map_to_project();\n } else {\n\n if (this.project.layer_groups.length == 0) {\n this.project.layer_groups = [new LayerGroup('New Group', new L.FeatureGroup())];\n this.active_layer_group = this.project.layer_groups[0];\n this.map.addLayer(this.project.layer_groups[0].feature_group);\n }\n //it will be an array of features...\n retval.forEach( (f) => {\n this.active_layer_group.feature_group.addLayer(f);\n });\n this.fit_map_to_project();\n }\n }\n\n open_existing_locally () {\n this.open_existing = true;\n this.open_file_dialog();\n }\n\n open_existing_from_depot() {\n this.open_existing = true;\n this.open_db_modal();\n }\n\n open_db_modal () {\n let modal = this.$uibModal.open({\n templateUrl: \"/static/designsafe/apps/geo/html/db-modal.html\",\n controller: \"DBModalCtrl as vm\",\n resolve: {\n filename: ()=> {return null;}\n }\n });\n modal.result.then( (f, saveas) => {this.load_from_data_depot(f);});\n }\n\n open_settings_modal () {\n let modal = this.$uibModal.open({\n templateUrl: \"/static/designsafe/apps/geo/html/settings-modal.html\",\n controller: \"SettingsModalCtrl as vm\",\n });\n modal.result.then( (s) => {\n this.settings = this.GeoSettingsService.settings;\n\n });\n }\n\n open_image_overlay_modal () {\n let modal = this.$uibModal.open({\n templateUrl: \"/static/designsafe/apps/geo/html/image-overlay-modal.html\",\n controller: \"ImageOverlayModalCtrl as vm\",\n });\n modal.result.then( (res) => {\n let bounds;\n bounds = [\n [res.min_lat, res.min_lon],\n [res.max_lat, res.max_lon]\n ];\n if (res.file) {\n this.GeoDataService.read_file_as_data_url(res.file).then( (data) => {\n console.log(data);\n });\n }\n let overlay = L.imageOverlay(res.url, bounds).addTo(this.map);\n console.log(overlay);\n });\n }\n\n open_file_dialog () {\n this.$timeout(() => {\n $('#file_picker').click();\n }, 0);\n }\n\n load_from_data_depot(f) {\n this.loading = true;\n this.GeoDataService.load_from_data_depot(f.selected)\n .then(\n (retval) =>{\n this._load_data_success(retval);\n this.loading = false;\n },\n (err)=> {\n this.toastr.error('Load failed!');\n this.loading = false;\n this.open_existing = false;\n });\n }\n\n local_file_selected (ev) {\n this.loading = true;\n let reqs = [];\n for (let i=0; i{return this._load_data_success(retval);});\n reqs.push(prom);\n // this.loading = false;\n };\n this.$q.all(reqs).then( ()=>{\n this.loading = false;\n //reset the picker\n $('#file_picker').val('');\n this.toastr.success('Imported file');\n }, (rej)=>{\n this.loading = false;\n this.toastr.error('Load failed!');\n }).then( () => {\n\n });\n }\n\n save_locally () {\n this.GeoDataService.save_locally(this.project);\n }\n\n save_to_depot () {\n let modal = this.$uibModal.open({\n templateUrl: \"/static/designsafe/apps/geo/html/db-modal.html\",\n controller: \"DBModalCtrl as vm\",\n resolve: {\n filename: ()=> {return this.project.name + '.geojson';}\n }\n });\n modal.result.then( (res) => {\n let newname = res.saveas;\n this.project.name = newname.split('.')[0];\n res.selected.name = res.saveas;\n this.loading = true;\n this.GeoDataService.save_to_depot(this.project, res.selected).then( (resp) => {\n this.loading = false;\n this.toastr.success('Saved to data depot');\n }, (err) => {\n this.toastr.error('Save failed!');\n this.loading = false;\n });\n }, (rej)=> {\n this.loading = false;\n });\n\n }\n\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/map-sidebar.js","export default class SettingsModalCtrl {\n\n constructor ($scope, $uibModalInstance, GeoSettingsService) {\n 'ngInject';\n this.$scope = $scope;\n this.$uibModalInstance = $uibModalInstance;\n this.GeoSettingsService = GeoSettingsService;\n this.settings = GeoSettingsService.settings;\n }\n\n ok () {\n this.GeoSettingsService.settings = this.settings;\n this.$uibModalInstance.close(this.settings);\n };\n\n // cancel () {\n // this.$uibModalInstance.dismiss('cancel');\n // };\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/settings-modal.js","export default function customOnChange() {\n return {\n restrict: 'A',\n scope: {\n handler: '&'\n },\n link: function (scope, element, attrs) {\n element.on('change', function (ev) {\n scope.$apply(function(){\n scope.handler({ev:ev});\n });\n });\n }\n };\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/directives/custom-on-change.js","import * as GeoUtils from '../utils/geo-utils';\nimport LayerGroup from '../models/layer_group';\nimport MapProject from '../models/map-project';\n\n\nexport default class GeoDataService {\n\n constructor ($http, $q, UserService, GeoSettingsService) {\n 'ngInject';\n this.$http = $http;\n this.$q = $q;\n this.UserService = UserService;\n this.GeoSettingsService = GeoSettingsService;\n this.active_project = null;\n this.previous_project_state = null;\n }\n\n current_project(project) {\n if (!(project)) {\n return this.active_project;\n }\n this.active_project = project;\n }\n\n _resize_image (blob, max_width=400, max_height=400) {\n return this.$q( (res, rej) => {\n let base64 = this._arrayBufferToBase64(blob);\n // Create and initialize two canvas\n let canvas = document.createElement(\"canvas\");\n let ctx = canvas.getContext(\"2d\");\n let canvasCopy = document.createElement(\"canvas\");\n let copyContext = canvasCopy.getContext(\"2d\");\n\n // Create original image\n let img = new Image();\n img.src = base64;\n img.onload = ()=>{\n // Determine new ratio based on max size\n let ratio = 1;\n if(img.width > max_width) {\n ratio = max_width / img.width;\n } else if(img.height > max_height) {\n ratio = max_height / img.height;\n }\n // Draw original image in second canvas\n canvasCopy.width = img.width;\n canvasCopy.height = img.height;\n copyContext.drawImage(img, 0, 0);\n\n // Copy and resize second canvas to first canvas\n canvas.width = img.width * ratio;\n canvas.height = img.height * ratio;\n ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);\n res(canvas.toDataURL());\n };\n });\n }\n\n _arrayBufferToBase64( buffer ) {\n let binary = '';\n let bytes = new Uint8Array( buffer );\n let len = bytes.byteLength;\n for (let i = 0; i < len; i++) {\n binary += String.fromCharCode( bytes[ i ] );\n }\n let encoded = btoa( binary );\n return 'data:image/jpg;base64,' + encoded;\n }\n\n _from_kml(text_blob) {\n return this.$q( (res, rej) => {\n let features = [];\n let l = omnivore.kml.parse(text_blob);\n l.getLayers().forEach((d) => {\n // d.feature.properties = {};\n d.options.label = d.feature.properties.name;\n features.push(d);\n });\n res(features);\n });\n }\n\n _from_kmz (blob) {\n return this.$q( (res, rej) => {\n let zipper = new JSZip();\n zipper.loadAsync(blob).then( (zip) => {\n //loop over all the files in the archive\n let proms = [];\n for (let key in zip.files) {\n let ext = key.split('.').pop();\n if (ext === 'kml') {\n return zip.files[key].async('text');\n }\n }\n }).then( (txt) => {\n let features = this._from_kml(txt);\n res(features);\n });\n });\n }\n\n _from_json (blob) {\n return this.$q( (res, rej) => {\n if (blob.ds_map) return res(this._from_dsmap(blob));\n\n try {\n let features = [];\n L.geoJSON(blob).getLayers().forEach( (layer) => {\n for (let key in layer.feature.properties) {\n layer.options[key] = layer.feature.properties[key];\n }\n features.push(layer);\n });\n res(features);\n } catch (e) {\n rej('Bad geoJSON');\n }\n });\n }\n\n _from_gpx (blob) {\n return this.$q( (res, rej) => {\n // console.log(text_blob)\n let features = [];\n let l = omnivore.gpx.parse(blob);\n l.getLayers().forEach((d) => {\n features.push(d);\n });\n res(features);\n });\n }\n\n _make_image_marker (lat, lon, thumb, preview, original) {\n let icon = L.divIcon({\n iconSize: [40, 40],\n html: \"
        \",\n className: 'leaflet-marker-photo'\n });\n\n let marker = L.marker([lat, lon], {icon: icon})\n .bindPopup(\"full res\",\n {\n className: 'leaflet-popup-photo',\n maxWidth: \"auto\",\n // maxHeight: 400\n });\n marker.options.image_src = preview;\n marker.options.thumb_src = thumb;\n marker.options.original_src = original;\n return marker;\n }\n\n _from_image (file) {\n return this.$q( (res, rej) => {\n try {\n let exif = EXIF.readFromBinaryFile(file);\n let lat = exif.GPSLatitude;\n let lon = exif.GPSLongitude;\n //Convert coordinates to WGS84 decimal\n let latRef = exif.GPSLatitudeRef || \"N\";\n let lonRef = exif.GPSLongitudeRef || \"W\";\n lat = (lat[0] + lat[1]/60 + lat[2]/3600) * (latRef == \"N\" ? 1 : -1);\n lon = (lon[0] + lon[1]/60 + lon[2]/3600) * (lonRef == \"W\" ? -1 : 1);\n if ((lat > 90) || (lat < -90) || (lon > 360) || (lon < -360)) {\n rej('Bad EXIF GPS data');\n }\n let encoded = this._arrayBufferToBase64(file);\n let thumb = null;\n let preview = null;\n this._resize_image(file, 100, 100).then( (resp)=>{\n thumb = resp;\n }).then( ()=>{\n return this._resize_image(file, 400, 400);\n }).then( (resp)=>{\n preview = resp;\n let marker = this._make_image_marker(lat, lon, thumb, preview, encoded);\n res([marker]);\n });\n } catch (e) {\n rej(e);\n }\n });\n }\n\n\n _from_dsmap (json) {\n return this.$q( (res, rej) => {\n // if (json instanceof String) {\n let project = new MapProject();\n project.name = json.name;\n project.description = json.description;\n json.layer_groups.forEach( (name) => {\n project.layer_groups.push(new LayerGroup(name, new L.FeatureGroup()));\n });\n json.features.forEach( (d)=> {\n let feature = L.geoJSON(d);\n feature.eachLayer( (layer)=> {\n\n // If there were no styles applied, it might be transparent???\n if (!(layer.feature.properties.color)) {\n layer.feature.properties.color = '#ff0000';\n }\n if (!(layer.feature.properties.fillColor)) {\n layer.feature.properties.fillColor = '#ff0000';\n };\n if (!(layer.feature.properties.opacity)) {\n layer.feature.properties.opacity = 1.0;\n };\n\n for (let key in layer.feature.properties) {\n layer.options[key] = layer.feature.properties[key];\n }\n try {\n let styles = {\n fillColor: layer.feature.properties.fillColor,\n color: layer.feature.properties.color,\n opacity: layer.feature.properties.opacity\n };\n layer.setStyle(styles);\n } catch (e) {\n // this can get caught for marker type objects, which for some reason\n // do not have a setStyle() method\n console.log(e);\n }\n\n let layer_group_index = d.layer_group_index;\n if ((layer instanceof L.Marker) && (layer.feature.properties.image_src)) {\n let latlng = layer.getLatLng();\n layer = this._make_image_marker(latlng.lat, latlng.lng, layer.feature.properties.thumb_src, layer.feature.properties.image_src, layer.feature.properties.original_src);\n // feat.options.image_src = feat.feature.properties.image_src;\n // feat.options.thumb_src = feat.feature.properties.thumb_src;\n }\n project.layer_groups[layer_group_index].feature_group.addLayer(layer);\n layer.options.label = d.properties.label;\n });\n\n });\n res(project);\n });\n }\n\n read_file_as_data_url(file) {\n\n let reader = new FileReader();\n return this.$q( (res, rej) => {\n reader.readAsDataURL(file);\n\n reader.onload = (e) => {\n return res(reader.result);\n };\n });\n }\n\n /*\n This will return a promise that resolves to an array of features\n that can be added to a LayerGroup\n */\n load_from_local_file (file) {\n return this.$q( (res, rej) => {\n let ext = GeoUtils.get_file_extension(file.name);\n let reader = new FileReader();\n //\n if ((ext === 'kmz') || (ext === 'jpeg') || (ext === 'jpg')){\n reader.readAsArrayBuffer(file);\n } else {\n reader.readAsText(file);\n }\n reader.onload = (e) => {\n let p = null;\n switch (ext) {\n case 'kml':\n p = this._from_kml(e.target.result);\n break;\n case 'json':\n p = this._from_json(JSON.parse(e.target.result));\n break;\n case 'geojson':\n p = this._from_json(JSON.parse(e.target.result));\n break;\n case 'kmz':\n p = this._from_kmz(e.target.result);\n break;\n case 'gpx':\n p = this._from_gpx(e.target.result);\n break;\n case 'jpeg':\n p = this._from_image(e.target.result);\n break;\n case 'jpg':\n p = this._from_image(e.target.result);\n break;\n case 'dsmap':\n p = this._from_dsmap(JSON.parse(e.target.result));\n break;\n default:\n p = this._from_json(JSON.parse(e.target.result));\n }\n return res(p);\n };\n });\n }\n\n //\n // @param f: a file from DataService\n // returns a promise with the LayerGroup\n load_from_data_depot(f) {\n let ext = GeoUtils.get_file_extension(f.name);\n let responseType = 'text';\n if ((ext === 'kmz') || (ext === 'jpg') || (ext === 'jpeg')) {\n responseType = 'arraybuffer';\n }\n return this.$http.get(f.agaveUrl(), {'responseType': responseType}).then((resp) => {\n let p = null;\n switch (ext) {\n case 'kml':\n p = this._from_kml(resp.data);\n break;\n case 'json':\n p = this._from_json(resp.data);\n break;\n case 'geojson':\n p = this._from_json(resp.data);\n break;\n case 'kmz':\n p = this._from_kmz(resp.data);\n break;\n case 'gpx':\n p = this._from_gpx(resp.data);\n break;\n case 'jpeg':\n p = this._from_image(resp.data);\n break;\n case 'jpg':\n p = this._from_image(resp.data);\n break;\n case 'dsmap':\n p = this._from_dsmap(resp.data);\n break;\n default:\n p = this._from_json(resp.data);\n }\n return p;\n });\n }\n\n save_locally (project) {\n let gjson = project.to_json();\n let blob = new Blob([JSON.stringify(gjson)], {type: \"application/json\"});\n let url = URL.createObjectURL(blob);\n\n let a = document.createElement('a');\n document.body.appendChild(a);\n a.download = project.name + \".geojson\";\n a.href = url;\n a.textContent = \"Download\";\n a.click();\n document.body.removeChild(a);\n }\n\n save_to_depot (project, path) {\n let form = new FormData();\n let gjson = project.to_json();\n let blob = new Blob([JSON.stringify(gjson)], {type: \"application/json\"});\n let base_file_url = 'https://agave.designsafe-ci.org/files/v2/media/system/';\n let post_url = base_file_url;\n post_url = post_url + path.system;\n let file = null;\n if (path.type === 'dir') {\n post_url = post_url + path.path;\n file = new File([blob], path.name);\n form.append('fileToUpload', file, path.name);\n } else {\n // A file was picked, so this WILL replace it\n post_url = post_url + path.trail[path.trail.length-2].path;\n file = new File([blob], path.name);\n form.append('fileToUpload', file, path.name);\n }\n return this.$http.post(post_url, form, {headers: {'Content-Type': undefined}});\n }\n\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-data-service.js","export default class GeoSettingsService {\n\n constructor () {\n this.settings = {\n default_fill_color: '#ff0000',\n default_stroke_color: '#ff0000',\n default_fill_opacity: 0.5,\n measurement_units: 'si'\n };\n }\n\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-settings-service.js","export default class GeoStateService {\n\n constructor ($scope, $state) {\n this.$scope = $scope;\n this.$state = $state;\n this.last_db_path = null;\n }\n\n \n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-state-service.js","module.exports = L;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"L\"\n// module id = 15\n// module chunks = 0","import {mod as geo_directives} from './directives';\nimport {mod as geo_controllers} from './controllers';\nimport {mod as geo_services} from './services';\n\nlet mod = angular.module('designsafe');\nmod.requires.push('ui.router', 'ang-drag-drop', 'ds.geo.directives', 'ds.geo.controllers', 'ds.geo.services', 'toastr');\n\nfunction config($stateProvider, $uibTooltipProvider, $urlRouterProvider, $locationProvider, toastrConfig) {\n 'ngInject';\n\n angular.extend(toastrConfig, {\n timeOut: 1000\n });\n\n $locationProvider.html5Mode({\n enabled: true\n });\n\n $stateProvider.state('geo', {\n url: '',\n abstract: true,\n templateUrl: '/static/designsafe/apps/geo/html/index.html',\n resolve: {\n auth: function () {\n return true;\n }\n }\n }).state('geo.map', {\n url: '/hazmapper',\n templateUrl: '/static/designsafe/apps/geo/html/map.html',\n controller: 'MapSidebarCtrl as vm'\n }).state('geo.help', {\n url: '/help',\n templateUrl: '/static/designsafe/apps/geo/html/help.html',\n controller: 'HelpCtrl as vm'\n });\n $urlRouterProvider.when('/', '/hazmapper');\n\n //config popups etc\n $uibTooltipProvider.options({popupDelay:1000});\n\n}\n\nmod.config(config);\n\n\n\nexport default mod;\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/index.js","export default class ImageOverlayModalCtrl {\n\n constructor ($scope, $uibModalInstance) {\n 'ngInject';\n this.$scope = $scope;\n this.$uibModalInstance = $uibModalInstance;\n this.data = {\n file: null,\n url: null,\n min_lat: null,\n max_lat: null,\n min_lon: null,\n max_lon: null\n };\n }\n\n ok () {\n this.$uibModalInstance.close(this.data);\n };\n\n cancel () {\n this.$uibModalInstance.dismiss('cancel');\n };\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/image-overlay-modal.js"],"sourceRoot":""} \ No newline at end of file diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/confirm-new-modal.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/confirm-new-modal.html new file mode 100644 index 0000000000..16f465a31d --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/confirm-new-modal.html @@ -0,0 +1,10 @@ + + + diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/db-modal.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/db-modal.html new file mode 100644 index 0000000000..ee4363c141 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/db-modal.html @@ -0,0 +1,15 @@ + + diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/help.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/help.html new file mode 100644 index 0000000000..cc06eb3d8c --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/help.html @@ -0,0 +1,36 @@ +
        +

        Designsafe Hazmapper Help

        +
        +
        +
        +

        Layers

        +

        Maps can be organized using multiple layers. Click on the "New Layer Group" button to create an empty Layer Group. + The active layer will be highlighted. Drawn objects will automatically be added to the active layer. Clicking on the + checkbox for that layer group will show/hide all features in the group. +

        + +

        - Edit project name / details

        +

        - Show/hide the layer group tools.

        +

        - Zoom to feature.

        +

        - Show/hide the individual features in the group.

        +
        +
        +
        +

        Tools

        + +
        +
        +

        Description of tools, from top to bottom:

        +
          +
        • Measurement tool
        • +
        • Base layer picker
        • +
        • Line drawing tool
        • +
        • Polygon drawing tool
        • +
        • Marker tool
        • +
        • Edit drawn shape
        • +
        • Delete drawn shape
        • +
        +
        +
        +
        +
        diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/image-overlay-modal.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/image-overlay-modal.html new file mode 100644 index 0000000000..796dfb92b2 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/image-overlay-modal.html @@ -0,0 +1,52 @@ + + + diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/index.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/index.html new file mode 100644 index 0000000000..bbcbda0579 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/index.html @@ -0,0 +1,22 @@ +
        + +
        +
        diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/map.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/map.html new file mode 100644 index 0000000000..e42d140636 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/map.html @@ -0,0 +1,220 @@ +
        + + +
      • + + + + +
        + + + +
        + + + + + + +
      + +
      +
      + +
      +
    • + + + + + +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    + + + + +
    + Lat: {{vm.current_mouse_coordinates.lat | number:4}} Lon: {{vm.current_mouse_coordinates.lng| number:4 }} +
    +
    +
    + +
    + +
    diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/settings-modal.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/settings-modal.html new file mode 100644 index 0000000000..1d5ed557e3 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/settings-modal.html @@ -0,0 +1,27 @@ + + + diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/images/help-draw-tools.jpg b/designsafe/apps/geo/static/designsafe/apps/geo/images/help-draw-tools.jpg new file mode 100644 index 0000000000..e0ce379e6e Binary files /dev/null and b/designsafe/apps/geo/static/designsafe/apps/geo/images/help-draw-tools.jpg differ diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/images/help-full-window.jpg b/designsafe/apps/geo/static/designsafe/apps/geo/images/help-full-window.jpg new file mode 100644 index 0000000000..b94ffa223c Binary files /dev/null and b/designsafe/apps/geo/static/designsafe/apps/geo/images/help-full-window.jpg differ diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/images/help-layers.jpg b/designsafe/apps/geo/static/designsafe/apps/geo/images/help-layers.jpg new file mode 100644 index 0000000000..265e652ef8 Binary files /dev/null and b/designsafe/apps/geo/static/designsafe/apps/geo/images/help-layers.jpg differ diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/images/logo.png b/designsafe/apps/geo/static/designsafe/apps/geo/images/logo.png new file mode 100644 index 0000000000..03e46e7ada Binary files /dev/null and b/designsafe/apps/geo/static/designsafe/apps/geo/images/logo.png differ diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/confirm-clear-modal.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/confirm-clear-modal.js new file mode 100644 index 0000000000..39cb09431a --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/confirm-clear-modal.js @@ -0,0 +1,16 @@ +export default class ConfirmClearModalCtrl { + + constructor ($scope, $uibModalInstance, GeoSettingsService) { + 'ngInject'; + this.$scope = $scope; + this.$uibModalInstance = $uibModalInstance; + } + + ok () { + this.$uibModalInstance.close('ok'); + }; + + cancel () { + this.$uibModalInstance.dismiss('cancel'); + }; +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/db-modal.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/db-modal.js new file mode 100644 index 0000000000..2fc2688423 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/db-modal.js @@ -0,0 +1,18 @@ +export default class DBModalCtrl { + + constructor ($scope, $uibModalInstance, filename) { + 'ngInject'; + this.$scope = $scope; + this.$uibModalInstance = $uibModalInstance; + this.selected = null; + this.saveas = {filename: filename}; + } + + ok () { + this.$uibModalInstance.close({selected:this.selected, saveas:this.saveas.filename}); + }; + + cancel () { + this.$uibModalInstance.dismiss('cancel'); + }; +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/help.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/help.js new file mode 100644 index 0000000000..ce97a9d8f0 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/help.js @@ -0,0 +1,7 @@ +export default class HelpCtrl { + + constructor ($scope) { + 'ngInject'; + this.$scope = $scope; + } +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/image-overlay-modal.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/image-overlay-modal.js new file mode 100644 index 0000000000..b1ea4f3efe --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/image-overlay-modal.js @@ -0,0 +1,24 @@ +export default class ImageOverlayModalCtrl { + + constructor ($scope, $uibModalInstance) { + 'ngInject'; + this.$scope = $scope; + this.$uibModalInstance = $uibModalInstance; + this.data = { + file: null, + url: null, + min_lat: null, + max_lat: null, + min_lon: null, + max_lon: null + }; + } + + ok () { + this.$uibModalInstance.close(this.data); + }; + + cancel () { + this.$uibModalInstance.dismiss('cancel'); + }; +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/index.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/index.js new file mode 100644 index 0000000000..ab85c645f5 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/index.js @@ -0,0 +1,17 @@ +import MapSidebarCtrl from './map-sidebar'; +import DBModalCtrl from './db-modal'; +import ImageOverlayModalCtrl from './image-overlay-modal.js'; +import HelpCtrl from './help'; +import SettingsModalCtrl from './settings-modal'; +import ConfirmClearModalCtrl from './confirm-clear-modal'; + +let mod = angular.module('ds.geo.controllers', []); + +mod.controller('MapSidebarCtrl', MapSidebarCtrl); +mod.controller('DBModalCtrl', DBModalCtrl); +mod.controller('HelpCtrl', HelpCtrl); +mod.controller('SettingsModalCtrl', SettingsModalCtrl); +mod.controller('ConfirmClearModalCtrl', ConfirmClearModalCtrl); +mod.controller('ImageOverlayModalCtrl', ImageOverlayModalCtrl); + +export default mod; diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/map-sidebar.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/map-sidebar.js new file mode 100644 index 0000000000..04446b49cb --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/map-sidebar.js @@ -0,0 +1,419 @@ +import LayerGroup from '../models/layer_group'; +import MapProject from '../models/map-project'; +import DBModalCtrl from './db-modal'; +import * as GeoUtils from '../utils/geo-utils'; + +export default class MapSidebarCtrl { + + constructor ($scope, $window, $timeout, $interval, $q, $uibModal, toastr, DataService, $http, GeoDataService, GeoSettingsService) { + 'ngInject'; + this.$scope = $scope; + this.LGeo = $window.LGeo; + this.$timeout = $timeout; + this.$interval = $interval; + this.$window = $window; + this.$q = $q; + this.$uibModal = $uibModal; + this.DataService = DataService; + this.$http = $http; + this.GeoDataService = GeoDataService; + this.GeoSettingsService = GeoSettingsService; + this.toastr = toastr; + + this.settings = this.GeoSettingsService.settings; + + //method binding for callback, sigh... + this.local_file_selected = this.local_file_selected.bind(this); + // this.open_db_modal = this.open_db_modal.bind(this); + + let streets = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }); + + let satellite = L.tileLayer( + 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: '©', + maxZoom: 20, + }); + + let basemaps = { + 'Street': streets, + 'Satellite': satellite + }; + this.map = L.map('geo_map', { + layers: [streets, satellite], + preferCanvas: false + }).setView([0, 0], 3); + this.mc = new L.Control.Measure({primaryLengthUnit:'meters', primaryAreaUnit: 'meters'}); + this.mc.addTo(this.map); + L.control.layers(basemaps).addTo(this.map); + this.map.zoomControl.setPosition('bottomleft'); + + // Load in a map project from the data service if one does exist, if not + // create a new one from scratch + if (this.GeoDataService.current_project()) { + this.project = this.GeoDataService.current_project(); + + this._init_map_layers(); + this.fit_map_to_project(); + + } else { + this.project = new MapProject('New Map'); + this.project.layer_groups = [new LayerGroup('New Group', new L.FeatureGroup())]; + this.map.addLayer(this.project.layer_groups[0].feature_group); + } + + // this._add_click_handlers(); + + // trick to fix the tiles that sometimes don't load for some reason... + $timeout( () => {this.map.invalidateSize();}, 10); + + // init an active layer group + this.active_layer_group = this.project.layer_groups[0]; + + // Auto keep track of current project in the GeoDataService + // so that if they switch states they will not lose work... + $interval( () => { + this.GeoDataService.current_project(this.project); + }, 1000); + + this.add_draw_controls(this.active_layer_group.feature_group); + + // This handles making sure that the features that get created with the draw tool + // are styled with the default colors etc. + this.map.on('draw:created', (e) => { + let object = e.layer; + object.options.color = this.settings.default_stroke_color; + object.options.fillColor = this.settings.default_fill_color; + object.options.fillOpacity = this.settings.default_fill_opacity; + this.active_layer_group.feature_group.addLayer(object); + this.$scope.$apply(); + }); + + this.map.on('mousemove', (e) => { + this.current_mouse_coordinates = e.latlng; + this.$scope.$apply(); + }); + + + } // end constructor + + fit_map_to_project() { + try { + this.map.fitBounds(this.project.get_bounds(), {maxZoom: 16}); + } catch (e) { + console.log('get_bounds fail', e); + } + } + + add_draw_controls (fg) { + let dc = new L.Control.Draw({ + position: 'topright', + draw: { + circle: false, + }, + edit: { + featureGroup: fg, + remove: true + } + }); + this.map.addControl(dc); + this.drawControl = dc; + } + + feature_click (layer) { + console.log(layer); + } + + _init_map_layers() { + // For some reason, need to readd the feature groups for markers to be displayed correctly??? + this.project.layer_groups.forEach( (lg)=>{ + // lg.feature_group.getLayers().forEach( (layer)=>{ + // this.map.addLayer(layer); + // }); + // this.map.addLayer(lg.feature_group); + this.map.removeLayer(lg.feature_group); + this.map.addLayer(lg.feature_group); + }); + this.active_layer_group = this.project.layer_groups[0]; + } + + // Adds click handlers to map elements. This does NOT feel + // right to me... + _add_click_handlers () { + this.project.layer_groups.forEach( (lg) => { + lg.feature_group.on('click', (ev)=> { + this.project.layer_groups.forEach( (lg) => { + lg.feature_group.getLayers().forEach( (layer)=>{ + layer.active = false; + if (layer == ev.layer) { + layer.active = true; + } + }); + }); + }); + }); + } + + create_layer_group () { + let lg = new LayerGroup("New Group", new L.FeatureGroup()); + this.project.layer_groups.push(lg); + this.active_layer_group = this.project.layer_groups[this.project.layer_groups.length -1]; + this.map.addLayer(lg.feature_group); + this.select_active_layer_group(this.active_layer_group); + } + + delete_layer_group (lg, i) { + this.map.removeLayer(lg.feature_group); + this.project.layer_groups.splice(i, 1); + if (this.project.layer_groups.length === 0) { + this.create_layer_group(); + } + this.active_layer_group = this.project.layer_groups[0]; + } + + delete_feature (lg, f) { + this.map.removeLayer(f); + lg.feature_group.removeLayer(f); + } + + show_hide_layer_group (lg) { + lg.show ? this.map.addLayer(lg.feature_group) : this.map.removeLayer(lg.feature_group); + } + + select_active_layer_group(lg) { + this.map.removeControl(this.drawControl); + this.add_draw_controls(lg.feature_group); + this.active_layer_group = lg; + lg.active = true; + lg.show = true; + } + + select_feature(lg, feature) { + this.active_layer_group = lg; + this.current_layer == feature ? this.current_layer = null : this.current_layer = feature; + } + + create_new_project () { + let modal = this.$uibModal.open({ + templateUrl: "/static/designsafe/apps/geo/html/confirm-new-modal.html", + controller: "ConfirmClearModalCtrl as vm", + }); + modal.result.then( (s) => { + this.map.eachLayer(function (layer) { + console.log(layer) + // this.map.removeLayer(layer); + }); + this.project.clear(); + let p = new MapProject('New Map'); + p.layer_groups = [new LayerGroup('New Group', new L.FeatureGroup())]; + this.project = p; + this.active_layer_group = this.project.layer_groups[0]; + this.map.addLayer(this.active_layer_group.feature_group); + }); + } + + zoom_to(feature) { + if (feature instanceof L.Marker) { + let latLngs = [ feature.getLatLng() ]; + let markerBounds = L.latLngBounds(latLngs); + try { + this.map.fitBounds(markerBounds, {maxZoom: 16}); + } catch (e) { + console.log(e); + } + } else { + try { + this.map.fitBounds(feature.getBounds(), {maxZoom: 16}); + } catch (e) { + console.log(e); + } + }; + } + + on_drop (ev, data, lg) { + let src_lg = this.project.layer_groups[data.pidx]; + let feature = src_lg.feature_group.getLayers()[data.idx]; + src_lg.feature_group.removeLayer(feature); + lg.feature_group.addLayer(feature); + } + + update_layer_style (prop) { + let tmp = this.current_layer; + // debugger; + // this.current_layer.setStyle({prop: this.current_layer.options[prop]}); + let styles = {}; + styles[prop] = this.current_layer.options[prop]; + this.current_layer.setStyle(styles); + } + + drop_feature_success (ev, data, lg) { + console.log("drag_feature_success", ev, data, lg); + } + + _load_data_success (retval) { + if (this.open_existing) { + if (retval instanceof MapProject) { + //clear off all the layers from the map + this.project.layer_groups.forEach( (lg) => { + this.map.removeLayer(lg.feature_group); + }); + + // set the project to be the return value + this.project = retval; + this._init_map_layers(); + this.fit_map_to_project(); + } else { + this.toastr.error('Load failed! File was not compatible'); + } + this.open_existing = false; + } else if (retval instanceof MapProject) { + retval.layer_groups.forEach( (lg) => { + this.project.layer_groups.push(lg); + this.map.addLayer(lg.feature_group); + }); + this.active_layer_group = this.project.layer_groups[0]; + this.fit_map_to_project(); + } else { + + if (this.project.layer_groups.length == 0) { + this.project.layer_groups = [new LayerGroup('New Group', new L.FeatureGroup())]; + this.active_layer_group = this.project.layer_groups[0]; + this.map.addLayer(this.project.layer_groups[0].feature_group); + } + //it will be an array of features... + retval.forEach( (f) => { + this.active_layer_group.feature_group.addLayer(f); + }); + this.fit_map_to_project(); + } + } + + open_existing_locally () { + this.open_existing = true; + this.open_file_dialog(); + } + + open_existing_from_depot() { + this.open_existing = true; + this.open_db_modal(); + } + + open_db_modal () { + let modal = this.$uibModal.open({ + templateUrl: "/static/designsafe/apps/geo/html/db-modal.html", + controller: "DBModalCtrl as vm", + resolve: { + filename: ()=> {return null;} + } + }); + modal.result.then( (f, saveas) => {this.load_from_data_depot(f);}); + } + + open_settings_modal () { + let modal = this.$uibModal.open({ + templateUrl: "/static/designsafe/apps/geo/html/settings-modal.html", + controller: "SettingsModalCtrl as vm", + }); + modal.result.then( (s) => { + this.settings = this.GeoSettingsService.settings; + + }); + } + + open_image_overlay_modal () { + let modal = this.$uibModal.open({ + templateUrl: "/static/designsafe/apps/geo/html/image-overlay-modal.html", + controller: "ImageOverlayModalCtrl as vm", + }); + modal.result.then( (res) => { + let bounds; + bounds = [ + [res.min_lat, res.min_lon], + [res.max_lat, res.max_lon] + ]; + if (res.file) { + this.GeoDataService.read_file_as_data_url(res.file).then( (data) => { + console.log(data); + }); + } + let overlay = L.imageOverlay(res.url, bounds).addTo(this.map); + console.log(overlay); + }); + } + + open_file_dialog () { + this.$timeout(() => { + $('#file_picker').click(); + }, 0); + } + + load_from_data_depot(f) { + this.loading = true; + this.GeoDataService.load_from_data_depot(f.selected) + .then( + (retval) =>{ + this._load_data_success(retval); + this.loading = false; + }, + (err)=> { + this.toastr.error('Load failed!'); + this.loading = false; + this.open_existing = false; + }); + } + + local_file_selected (ev) { + this.loading = true; + let reqs = []; + for (let i=0; i{return this._load_data_success(retval);}); + reqs.push(prom); + // this.loading = false; + }; + this.$q.all(reqs).then( ()=>{ + this.loading = false; + //reset the picker + $('#file_picker').val(''); + this.toastr.success('Imported file'); + }, (rej)=>{ + this.loading = false; + this.toastr.error('Load failed!'); + }).then( () => { + + }); + } + + save_locally () { + this.GeoDataService.save_locally(this.project); + } + + save_to_depot () { + let modal = this.$uibModal.open({ + templateUrl: "/static/designsafe/apps/geo/html/db-modal.html", + controller: "DBModalCtrl as vm", + resolve: { + filename: ()=> {return this.project.name + '.geojson';} + } + }); + modal.result.then( (res) => { + let newname = res.saveas; + this.project.name = newname.split('.')[0]; + res.selected.name = res.saveas; + this.loading = true; + this.GeoDataService.save_to_depot(this.project, res.selected).then( (resp) => { + this.loading = false; + this.toastr.success('Saved to data depot'); + }, (err) => { + this.toastr.error('Save failed!'); + this.loading = false; + }); + }, (rej)=> { + this.loading = false; + }); + + } + +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/settings-modal.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/settings-modal.js new file mode 100644 index 0000000000..79ed40692b --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/controllers/settings-modal.js @@ -0,0 +1,19 @@ +export default class SettingsModalCtrl { + + constructor ($scope, $uibModalInstance, GeoSettingsService) { + 'ngInject'; + this.$scope = $scope; + this.$uibModalInstance = $uibModalInstance; + this.GeoSettingsService = GeoSettingsService; + this.settings = GeoSettingsService.settings; + } + + ok () { + this.GeoSettingsService.settings = this.settings; + this.$uibModalInstance.close(this.settings); + }; + + // cancel () { + // this.$uibModalInstance.dismiss('cancel'); + // }; +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/directives/custom-on-change.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/directives/custom-on-change.js new file mode 100644 index 0000000000..16df7b3ef7 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/directives/custom-on-change.js @@ -0,0 +1,15 @@ +export default function customOnChange() { + return { + restrict: 'A', + scope: { + handler: '&' + }, + link: function (scope, element, attrs) { + element.on('change', function (ev) { + scope.$apply(function(){ + scope.handler({ev:ev}); + }); + }); + } + }; +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/directives/index.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/directives/index.js new file mode 100644 index 0000000000..045aefd8f2 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/directives/index.js @@ -0,0 +1,7 @@ +import customOnChange from './custom-on-change'; + +let mod = angular.module('ds.geo.directives', []); + + +mod.directive('customOnChange', customOnChange); +export default mod; diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/index.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/index.js new file mode 100644 index 0000000000..4c6b59ff79 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/index.js @@ -0,0 +1,48 @@ +import {mod as geo_directives} from './directives'; +import {mod as geo_controllers} from './controllers'; +import {mod as geo_services} from './services'; + +let mod = angular.module('designsafe'); +mod.requires.push('ui.router', 'ang-drag-drop', 'ds.geo.directives', 'ds.geo.controllers', 'ds.geo.services', 'toastr'); + +function config($stateProvider, $uibTooltipProvider, $urlRouterProvider, $locationProvider, toastrConfig) { + 'ngInject'; + + angular.extend(toastrConfig, { + timeOut: 1000 + }); + + $locationProvider.html5Mode({ + enabled: true + }); + + $stateProvider.state('geo', { + url: '', + abstract: true, + templateUrl: '/static/designsafe/apps/geo/html/index.html', + resolve: { + auth: function () { + return true; + } + } + }).state('geo.map', { + url: '/hazmapper', + templateUrl: '/static/designsafe/apps/geo/html/map.html', + controller: 'MapSidebarCtrl as vm' + }).state('geo.help', { + url: '/help', + templateUrl: '/static/designsafe/apps/geo/html/help.html', + controller: 'HelpCtrl as vm' + }); + $urlRouterProvider.when('/', '/hazmapper'); + + //config popups etc + $uibTooltipProvider.options({popupDelay:1000}); + +} + +mod.config(config); + + + +export default mod; diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/models/layer_group.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/models/layer_group.js new file mode 100644 index 0000000000..d28b333a67 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/models/layer_group.js @@ -0,0 +1,26 @@ +export default class LayerGroup { + + constructor (label, fg) { + this.label = label; + this.feature_group = fg; + this.show = true; + this.show_contents = true; + } + + num_features () { + return this.feature_group.getLayers().length; + } + + get_feature_type (f) { + // debugger + if (f.options.image_src) { + return 'Image'; + } else if (f instanceof L.Marker) { + return 'Point'; + } else if (f instanceof L.Polygon) { + return 'Polygon'; + } else { + return 'Path'; + } + } +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/models/map-project.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/models/map-project.js new file mode 100644 index 0000000000..73dfc164be --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/models/map-project.js @@ -0,0 +1,83 @@ +import L from 'L'; + +export default class MapProject { + + constructor (name) { + this.name = name; + this.layer_groups = []; + this.description = null; + } + + clear() { + this.layer_groups.forEach( (lg) => { + lg.feature_group.clearLayers(); + }); + } + + get_bounds() { + let bounds = []; + this.layer_groups.forEach( (lg) => { + bounds.push(lg.feature_group.getBounds()); + }); + return bounds; + } + + num_features () { + total = 0; + this.layer_groups.forEach( (lg) => { + total += lg.num_features(); + }); + } + + to_json() { + let out = { + "type": "FeatureCollection", + "features": [], + "ds_map": true, + "name": this.name, + "description": this.description, + "num_layers": this.layer_groups.length, + "layer_groups": [] + }; + this.layer_groups.forEach( (lg, lg_idx) => { + out.layer_groups.push(lg.label); + let tmp = { + "type": "FeatureCollection", + "features": [], + "label": lg.label + }; + lg.feature_group.getLayers().forEach( (feature) => { + let json = feature.toGeoJSON(); + // These are all the keys in the options object that we need to + // re-create the layers in the application after loading. + let opt_keys = [ + 'label', + 'color', + 'fillColor', + 'fillOpacity', + 'description', + 'image_src', + 'thumb_src', + 'original_src', + ]; + + // //add in any options + // if (feature.options.image_src) { + // json.properties.image_src = feature.options.image_src; + // } + // if (feature.options.thumb_src) { + // json.properties.thumb_src = feature.options.thumb_src; + // } + for (let key in feature.options) { + if (opt_keys.indexOf(key) !== -1) { + json.properties[key] = feature.options[key]; + } + }; + json.layer_group_index = lg_idx; + out.features.push(json); + }); + }); + return out; + } + +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-data-service.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-data-service.js new file mode 100644 index 0000000000..f34fcbe6ec --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-data-service.js @@ -0,0 +1,381 @@ +import * as GeoUtils from '../utils/geo-utils'; +import LayerGroup from '../models/layer_group'; +import MapProject from '../models/map-project'; + + +export default class GeoDataService { + + constructor ($http, $q, UserService, GeoSettingsService) { + 'ngInject'; + this.$http = $http; + this.$q = $q; + this.UserService = UserService; + this.GeoSettingsService = GeoSettingsService; + this.active_project = null; + this.previous_project_state = null; + } + + current_project(project) { + if (!(project)) { + return this.active_project; + } + this.active_project = project; + } + + _resize_image (blob, max_width=400, max_height=400) { + return this.$q( (res, rej) => { + let base64 = this._arrayBufferToBase64(blob); + // Create and initialize two canvas + let canvas = document.createElement("canvas"); + let ctx = canvas.getContext("2d"); + let canvasCopy = document.createElement("canvas"); + let copyContext = canvasCopy.getContext("2d"); + + // Create original image + let img = new Image(); + img.src = base64; + img.onload = ()=>{ + // Determine new ratio based on max size + let ratio = 1; + if(img.width > max_width) { + ratio = max_width / img.width; + } else if(img.height > max_height) { + ratio = max_height / img.height; + } + // Draw original image in second canvas + canvasCopy.width = img.width; + canvasCopy.height = img.height; + copyContext.drawImage(img, 0, 0); + + // Copy and resize second canvas to first canvas + canvas.width = img.width * ratio; + canvas.height = img.height * ratio; + ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height); + res(canvas.toDataURL()); + }; + }); + } + + _arrayBufferToBase64( buffer ) { + let binary = ''; + let bytes = new Uint8Array( buffer ); + let len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode( bytes[ i ] ); + } + let encoded = btoa( binary ); + return 'data:image/jpg;base64,' + encoded; + } + + _from_kml(text_blob) { + return this.$q( (res, rej) => { + let features = []; + let l = omnivore.kml.parse(text_blob); + l.getLayers().forEach((d) => { + // d.feature.properties = {}; + d.options.label = d.feature.properties.name; + features.push(d); + }); + res(features); + }); + } + + _from_kmz (blob) { + return this.$q( (res, rej) => { + let zipper = new JSZip(); + zipper.loadAsync(blob).then( (zip) => { + //loop over all the files in the archive + let proms = []; + for (let key in zip.files) { + let ext = key.split('.').pop(); + if (ext === 'kml') { + return zip.files[key].async('text'); + } + } + }).then( (txt) => { + let features = this._from_kml(txt); + res(features); + }); + }); + } + + _from_json (blob) { + return this.$q( (res, rej) => { + if (blob.ds_map) return res(this._from_dsmap(blob)); + + try { + let features = []; + L.geoJSON(blob).getLayers().forEach( (layer) => { + for (let key in layer.feature.properties) { + layer.options[key] = layer.feature.properties[key]; + } + features.push(layer); + }); + res(features); + } catch (e) { + rej('Bad geoJSON'); + } + }); + } + + _from_gpx (blob) { + return this.$q( (res, rej) => { + // console.log(text_blob) + let features = []; + let l = omnivore.gpx.parse(blob); + l.getLayers().forEach((d) => { + features.push(d); + }); + res(features); + }); + } + + _make_image_marker (lat, lon, thumb, preview, original) { + let icon = L.divIcon({ + iconSize: [40, 40], + html: "
    ", + className: 'leaflet-marker-photo' + }); + + let marker = L.marker([lat, lon], {icon: icon}) + .bindPopup("full res", + { + className: 'leaflet-popup-photo', + maxWidth: "auto", + // maxHeight: 400 + }); + marker.options.image_src = preview; + marker.options.thumb_src = thumb; + marker.options.original_src = original; + return marker; + } + + _from_image (file) { + return this.$q( (res, rej) => { + try { + let exif = EXIF.readFromBinaryFile(file); + let lat = exif.GPSLatitude; + let lon = exif.GPSLongitude; + //Convert coordinates to WGS84 decimal + let latRef = exif.GPSLatitudeRef || "N"; + let lonRef = exif.GPSLongitudeRef || "W"; + lat = (lat[0] + lat[1]/60 + lat[2]/3600) * (latRef == "N" ? 1 : -1); + lon = (lon[0] + lon[1]/60 + lon[2]/3600) * (lonRef == "W" ? -1 : 1); + if ((lat > 90) || (lat < -90) || (lon > 360) || (lon < -360)) { + rej('Bad EXIF GPS data'); + } + let encoded = this._arrayBufferToBase64(file); + let thumb = null; + let preview = null; + this._resize_image(file, 100, 100).then( (resp)=>{ + thumb = resp; + }).then( ()=>{ + return this._resize_image(file, 400, 400); + }).then( (resp)=>{ + preview = resp; + let marker = this._make_image_marker(lat, lon, thumb, preview, encoded); + res([marker]); + }); + } catch (e) { + rej(e); + } + }); + } + + + _from_dsmap (json) { + return this.$q( (res, rej) => { + // if (json instanceof String) { + let project = new MapProject(); + project.name = json.name; + project.description = json.description; + json.layer_groups.forEach( (name) => { + project.layer_groups.push(new LayerGroup(name, new L.FeatureGroup())); + }); + json.features.forEach( (d)=> { + let feature = L.geoJSON(d); + feature.eachLayer( (layer)=> { + + // If there were no styles applied, it might be transparent??? + if (!(layer.feature.properties.color)) { + layer.feature.properties.color = '#ff0000'; + } + if (!(layer.feature.properties.fillColor)) { + layer.feature.properties.fillColor = '#ff0000'; + }; + if (!(layer.feature.properties.opacity)) { + layer.feature.properties.opacity = 1.0; + }; + + for (let key in layer.feature.properties) { + layer.options[key] = layer.feature.properties[key]; + } + try { + let styles = { + fillColor: layer.feature.properties.fillColor, + color: layer.feature.properties.color, + opacity: layer.feature.properties.opacity + }; + layer.setStyle(styles); + } catch (e) { + // this can get caught for marker type objects, which for some reason + // do not have a setStyle() method + console.log(e); + } + + let layer_group_index = d.layer_group_index; + if ((layer instanceof L.Marker) && (layer.feature.properties.image_src)) { + let latlng = layer.getLatLng(); + layer = this._make_image_marker(latlng.lat, latlng.lng, layer.feature.properties.thumb_src, layer.feature.properties.image_src, layer.feature.properties.original_src); + // feat.options.image_src = feat.feature.properties.image_src; + // feat.options.thumb_src = feat.feature.properties.thumb_src; + } + project.layer_groups[layer_group_index].feature_group.addLayer(layer); + layer.options.label = d.properties.label; + }); + + }); + res(project); + }); + } + + read_file_as_data_url(file) { + + let reader = new FileReader(); + return this.$q( (res, rej) => { + reader.readAsDataURL(file); + + reader.onload = (e) => { + return res(reader.result); + }; + }); + } + + /* + This will return a promise that resolves to an array of features + that can be added to a LayerGroup + */ + load_from_local_file (file) { + return this.$q( (res, rej) => { + let ext = GeoUtils.get_file_extension(file.name); + let reader = new FileReader(); + // + if ((ext === 'kmz') || (ext === 'jpeg') || (ext === 'jpg')){ + reader.readAsArrayBuffer(file); + } else { + reader.readAsText(file); + } + reader.onload = (e) => { + let p = null; + switch (ext) { + case 'kml': + p = this._from_kml(e.target.result); + break; + case 'json': + p = this._from_json(JSON.parse(e.target.result)); + break; + case 'geojson': + p = this._from_json(JSON.parse(e.target.result)); + break; + case 'kmz': + p = this._from_kmz(e.target.result); + break; + case 'gpx': + p = this._from_gpx(e.target.result); + break; + case 'jpeg': + p = this._from_image(e.target.result); + break; + case 'jpg': + p = this._from_image(e.target.result); + break; + case 'dsmap': + p = this._from_dsmap(JSON.parse(e.target.result)); + break; + default: + p = this._from_json(JSON.parse(e.target.result)); + } + return res(p); + }; + }); + } + + // + // @param f: a file from DataService + // returns a promise with the LayerGroup + load_from_data_depot(f) { + let ext = GeoUtils.get_file_extension(f.name); + let responseType = 'text'; + if ((ext === 'kmz') || (ext === 'jpg') || (ext === 'jpeg')) { + responseType = 'arraybuffer'; + } + return this.$http.get(f.agaveUrl(), {'responseType': responseType}).then((resp) => { + let p = null; + switch (ext) { + case 'kml': + p = this._from_kml(resp.data); + break; + case 'json': + p = this._from_json(resp.data); + break; + case 'geojson': + p = this._from_json(resp.data); + break; + case 'kmz': + p = this._from_kmz(resp.data); + break; + case 'gpx': + p = this._from_gpx(resp.data); + break; + case 'jpeg': + p = this._from_image(resp.data); + break; + case 'jpg': + p = this._from_image(resp.data); + break; + case 'dsmap': + p = this._from_dsmap(resp.data); + break; + default: + p = this._from_json(resp.data); + } + return p; + }); + } + + save_locally (project) { + let gjson = project.to_json(); + let blob = new Blob([JSON.stringify(gjson)], {type: "application/json"}); + let url = URL.createObjectURL(blob); + + let a = document.createElement('a'); + document.body.appendChild(a); + a.download = project.name + ".geojson"; + a.href = url; + a.textContent = "Download"; + a.click(); + document.body.removeChild(a); + } + + save_to_depot (project, path) { + let form = new FormData(); + let gjson = project.to_json(); + let blob = new Blob([JSON.stringify(gjson)], {type: "application/json"}); + let base_file_url = 'https://agave.designsafe-ci.org/files/v2/media/system/'; + let post_url = base_file_url; + post_url = post_url + path.system; + let file = null; + if (path.type === 'dir') { + post_url = post_url + path.path; + file = new File([blob], path.name); + form.append('fileToUpload', file, path.name); + } else { + // A file was picked, so this WILL replace it + post_url = post_url + path.trail[path.trail.length-2].path; + file = new File([blob], path.name); + form.append('fileToUpload', file, path.name); + } + return this.$http.post(post_url, form, {headers: {'Content-Type': undefined}}); + } + +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-settings-service.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-settings-service.js new file mode 100644 index 0000000000..e1c7dbd0f0 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-settings-service.js @@ -0,0 +1,12 @@ +export default class GeoSettingsService { + + constructor () { + this.settings = { + default_fill_color: '#ff0000', + default_stroke_color: '#ff0000', + default_fill_opacity: 0.5, + measurement_units: 'si' + }; + } + +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-state-service.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-state-service.js new file mode 100644 index 0000000000..06ef4aeb58 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/geo-state-service.js @@ -0,0 +1,10 @@ +export default class GeoStateService { + + constructor ($scope, $state) { + this.$scope = $scope; + this.$state = $state; + this.last_db_path = null; + } + + +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/index.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/index.js new file mode 100644 index 0000000000..74fcb82e8d --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/services/index.js @@ -0,0 +1,13 @@ +// import customOnChange from './custom-on-change'; +import GeoStateService from './geo-state-service'; +import GeoDataService from './geo-data-service'; +import GeoSettingsService from './geo-settings-service'; + +let mod = angular.module('ds.geo.services', []); +mod.service('GeoStateService', GeoStateService); +mod.service('GeoDataService', GeoDataService); +mod.service('GeoSettingsService', GeoSettingsService); + +// mod.directive('customOnChange', customOnChange); + +export default mod; diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/scripts/utils/geo-utils.js b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/utils/geo-utils.js new file mode 100644 index 0000000000..20fa7c3753 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/scripts/utils/geo-utils.js @@ -0,0 +1,3 @@ +export function get_file_extension (fname) { + return fname.split('.').pop().toLowerCase(); +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/styles/geo.css b/designsafe/apps/geo/static/designsafe/apps/geo/styles/geo.css new file mode 100644 index 0000000000..5e65d6f561 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/styles/geo.css @@ -0,0 +1,277 @@ +/*#geo_container .modal-dialog { + color: #000000 +}*/ + + +#geo_container { + display: flex; + min-height: 100vh; + color: #fff; + font-family: 'Open Sans', sans-serif; +} + +#geo_container h1, #geo_container h2 { + color: #fff; +} + +#geo_container #navbar { + background-color: #272727; + flex: 0 0 40; + flex-direction: column; + border-right: 1px solid #6e6e6e; + color: #d2d2d2; + padding-top: 10px; +} + +#geo_container #navbar .nav-icon { + margin-left: 5px; + padding: 5px 10px 10px 10px; + cursor: pointer; +} +#geo_container #navbar .nav-icon.active{ + margin-left: 0px; + border-left: 5px solid #bd4cd9; + background-color: #404040; +} +#geo_container #navbar a { + color: #d2d2d2; +} + +#geo_container .blue { + color: #0d63ff; +} + +#geo_sidebar, #current_layer_details, #map_project_details { + /*padding-top: 10px;*/ + width: 25%; + font-size: 13px; + letter-spacing: 0px; + float: left; + flex: 0 0 20%; + height: 100vh; + background: #313131; + border-right: 1px solid #6e6e6e; + flex-direction: column; + overflow-y: auto; +} + +#map_project_details { + padding: 5px; +} + +#geo_container input, +#geo_container textarea { + color: #fff; + background-color: #545454; +} + + +.strong { + font-family: 'Open Sans Bold',sans-serif; + font-weight: 700; +} + +.map-title { + font-size: 1.35em; +} + + + +#geo_sidebar .list-group-item > div { + float: left; +} + +#geo_sidebar .list-group-item input[type=text] { + color: #FFF; + background-color: #545454; + width: 70%; +} + +#geo_sidebar .list-group-item.menu, #current_layer_details .list-group-item.menu{ + background-color: #313131; + border-bottom: 1px solid #707070; + border-left: 5px; + border-right: 0px; + border-top: 0px; + margin-top: 1px; + cursor: pointer; + padding: 3px; + padding-bottom: 3px; + padding-right: 3px; + line-height: 1.25em; +} + + +#geo_sidebar .list-group-item.menu.active { + border-left: 3px solid #de5b0f; + background-color: #444444; +} +#geo_sidebar .list-group-item.menu.lg-tools { + background-color: #29948a; +} + +#geo_sidebar .list-group-item.menu.action { + background-color: #4575b0; +} + +#geo_sidebar .list-group-item.subgroup { + margin-left: 0px; + padding-left: 10px; +} +#geo_sidebar .list-group-item.subgroup.active { + margin-left: 0px; + border-left: 0px; + padding-left: 10px; + +} + +#geo_sidebar .list-group-item.subgroup.active.highlight { + border: 1px solid #50dfbe; +} + + +#geo_sidebar .list-group-item.menu.title{ + background-color: #404040; + padding-top: 15px; + padding-bottom: 15px; + padding-left:10px; + padding-right:10px; +} + +#geo_sidebar .lg-count { + font-size: 0.9em; + color: #fff; + margin-left: 5px; + padding: 3px; + border-radius: 3px; + background-color: #524e90; +} + +#right_panel { + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + background-color: #fff; +} + +#top_toolbar { + /*height: 2em;*/ + /*border-bottom: 1px solid #d2d2d2;*/ + background-color: #d2d2d2; +} + +#geo_container .dropdown-menu { + padding: 0px; + background-color: #313131; + min-width: 200px; +} + +#geo_container .dropdown-menu li { + border-bottom: 1px solid #aaa; + cursor: pointer; + padding-top: 2px; + padding-bottom: 2px; +} + +#geo_container .dropdown-menu li a { + color: #74a7f3; + padding: 0; +} + +#geo_map { + /*width: 100%;*/ + flex: 1; + background: grey; +} + +#current_layer_details .tools{ + padding-bottom: 5px; + border-bottom: 1px solid #707070; +} + +#current_layer_details .list-group-item .left { + float: left; + width: 50%; +} +#current_layer_details .list-group-item .right { + float: right; + width: 50%; +} + +#current_layer_details .list-group-item input[type=color] { + /*height: inherit;*/ + padding: 0px; + line-height: 1.2em; + height:1.2em; +} + +#mouse_coordinates { + font-family: monospace; + font-size: 0.9em; + color: #505050; + margin-top: 0px; + background-color: #f2f5dc; + /*padding: 5px;*/ +} + +.dropdown-menu { + background-color: #646464; +} +/* for drag and drop effects */ + +.on-drag-hover { + border: 2px solid red; +} + +/* photo markers*/ +.leaflet-marker-photo { + border: 2px solid #fff; + box-shadow: 3px 3px 10px #888; +} + +.leaflet-marker-photo .image { + width: 100%; + height: 100%; + background-size: 100% 100%; + background-position: center center; + background-repeat: no-repeat; +} + + +.leaflet-popup-photo img { + width: 400px; +} + +.leaflet-control-measure { + color: #000; +} + +#geo_sidebar::-webkit-scrollbar-track +{ + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + background-color: #F5F5F5; +} + +#geo_sidebar::-webkit-scrollbar +{ + width: 6px; + background-color: #F5F5F5; +} + +#geo_sidebar::-webkit-scrollbar-thumb +{ + background-color: #838383; + border-radius: 4px; +} + +#geo_container .modal-title h3 { + color: #000; +} + +#geo_help { + background-color: #3e3e3e; + width: 100%; + padding: 10px; + color: #fff; +} diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.eot b/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.eot new file mode 100644 index 0000000000..45b49c3d51 Binary files /dev/null and b/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.eot differ diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.svg b/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.svg new file mode 100644 index 0000000000..17bb1c845c --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.svg @@ -0,0 +1,12 @@ + + + +Generated by IcoMoon + + + + + + + + \ No newline at end of file diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.ttf b/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.ttf new file mode 100644 index 0000000000..5f8415ccd4 Binary files /dev/null and b/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.ttf differ diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.woff b/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.woff new file mode 100644 index 0000000000..f08458e710 Binary files /dev/null and b/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/fonts/icomoon.woff differ diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/style.css b/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/style.css new file mode 100644 index 0000000000..828f3910f5 --- /dev/null +++ b/designsafe/apps/geo/static/designsafe/apps/geo/styles/icons/style.css @@ -0,0 +1,32 @@ +@font-face { + font-family: 'icomoon'; + src: url('fonts/icomoon.eot?vnswf1'); + src: url('fonts/icomoon.eot?vnswf1#iefix') format('embedded-opentype'), + url('fonts/icomoon.ttf?vnswf1') format('truetype'), + url('fonts/icomoon.woff?vnswf1') format('woff'), + url('fonts/icomoon.svg?vnswf1#icomoon') format('svg'); + font-weight: normal; + font-style: normal; +} + +[class^="icon-"], [class*=" icon-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'icomoon' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-point-to-point:before { + content: "\e901"; +} +.icon-polygon:before { + content: "\e900"; +} diff --git a/designsafe/apps/geo/templates/designsafe/apps/geo/index.html b/designsafe/apps/geo/templates/designsafe/apps/geo/index.html new file mode 100644 index 0000000000..0c6b0e1fe5 --- /dev/null +++ b/designsafe/apps/geo/templates/designsafe/apps/geo/index.html @@ -0,0 +1,51 @@ +{% extends "uber_base.html" %} +{% load sekizai_tags %} +{% load static %} + +{% block head_extra %} + + +{% endblock %} + + + + +{% block content %} +
    +
    +
    +{% addtoblock "js" %} + + + + + + + + + + + + + + + + + + + + + +{% endaddtoblock %} + +{% addtoblock "css" %} + + + + + + + +{% endaddtoblock %} + +{% endblock %} diff --git a/designsafe/apps/geo/templates/designsafe/apps/geo/test.html b/designsafe/apps/geo/templates/designsafe/apps/geo/test.html new file mode 100644 index 0000000000..3cbee39d28 --- /dev/null +++ b/designsafe/apps/geo/templates/designsafe/apps/geo/test.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% load sekizai_tags %} +{% load static %} + +{% block head_extra %} + + +{% endblock %} + + + + +{% block content %} +

    Test

    +
    +
    +
    + +
    +
    + {#
    #} +
    + + + + +{% addtoblock "js" %} + + + + + + + + + + + + + + + + + + + + +{% endaddtoblock %} + +{% addtoblock "css" %} + + + + +{% endaddtoblock %} + +{% endblock %} diff --git a/designsafe/apps/geo/tests.py b/designsafe/apps/geo/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/designsafe/apps/geo/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/designsafe/apps/geo/urls.py b/designsafe/apps/geo/urls.py new file mode 100644 index 0000000000..e33540ea37 --- /dev/null +++ b/designsafe/apps/geo/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import patterns, include, url +from django.core.urlresolvers import reverse + +urlpatterns = patterns( + 'designsafe.apps.geo.views', + url(r'^$', 'index', name='index'), + url(r'^test/$', 'test', name='test'), + url(r'^.*$', 'index', name="index"), + +) diff --git a/designsafe/apps/geo/views.py b/designsafe/apps/geo/views.py new file mode 100644 index 0000000000..36af58d20b --- /dev/null +++ b/designsafe/apps/geo/views.py @@ -0,0 +1,21 @@ +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +import logging + + +logger = logging.getLogger(__name__) +metrics_logger = logging.getLogger('metrics') + +@login_required +def index(request): + metrics_logger.info('HazMapper Index', + extra = { + 'user': request.user.username, + 'sessionId': getattr(request.session, 'session_key', ''), + 'operation': 'hazmapper_index_view' + }) + return render(request, 'designsafe/apps/geo/index.html') + +@login_required +def test(request): + return render(request, 'designsafe/apps/geo/test.html') diff --git a/designsafe/apps/notifications/static/designsafe/apps/notifications/html/notification_list.html b/designsafe/apps/notifications/static/designsafe/apps/notifications/html/notification_list.html index f22bf98a31..0c180086cd 100644 --- a/designsafe/apps/notifications/static/designsafe/apps/notifications/html/notification_list.html +++ b/designsafe/apps/notifications/static/designsafe/apps/notifications/html/notification_list.html @@ -80,6 +80,12 @@

    Coming soon!

    + +
    + Displaying {{data.pagination.item_start}}-{{data.pagination.item_end}} of {{data.pagination.total}} items +
    diff --git a/designsafe/apps/notifications/static/designsafe/apps/notifications/scripts/app.js b/designsafe/apps/notifications/static/designsafe/apps/notifications/scripts/app.js index 57d9d872bd..2f80c1c9d6 100644 --- a/designsafe/apps/notifications/static/designsafe/apps/notifications/scripts/app.js +++ b/designsafe/apps/notifications/static/designsafe/apps/notifications/scripts/app.js @@ -11,7 +11,7 @@ } var app = angular.module('designsafe'); - app.requires.push('djng.urls','ds.wsBus', 'ds.notifications', 'logging', 'toastr'); + app.requires.push('djng.urls','ds.wsBus', 'ds.notifications', 'logging', 'toastr', 'ui.bootstrap'); app.config(['WSBusServiceProvider', 'NotificationServiceProvider', '$interpolateProvider', '$httpProvider', config]); @@ -19,9 +19,15 @@ $scope.data = {}; $scope.showRawMessage = false; - $scope.list = function(){ - NotificationService.list({limit:10}).then(function(resp) { - $scope.data.notifications = resp; + $scope.list = function(page=0){ + $scope.data.pagination = {'limit': 10} + var params = {'limit': $scope.data.pagination.limit, page: page} + + NotificationService.list(params).then(function(resp) { + $scope.data.pagination.show = false; + $scope.data.pagination.page = resp.page; + $scope.data.pagination.total = resp.total; + $scope.data.notifications = resp.notifs; for (var i=0; i < $scope.data.notifications.length; i++){ // $scope.data.notifications[i] = angular.fromJson($scope.data.notifications[i]); @@ -30,10 +36,26 @@ if ($scope.data.notifications[i]['event_type'] == 'job') { $scope.data.notifications[i]['action_link']=djangoUrl.reverse('designsafe_workspace:process_notification', {'pk': $scope.data.notifications[i]['pk']}); - } else if ($scope.data.notifications[i]['event_type'] == 'data') { + } else if ($scope.data.notifications[i]['event_type'] == 'data_depot') { $scope.data.notifications[i]['action_link']=djangoUrl.reverse('designsafe_api:process_notification', {'pk': $scope.data.notifications[i]['pk']}); } } + + if ($scope.data.pagination.total > 0) { + var offset = $scope.data.pagination.page * $scope.data.pagination.limit; + $scope.data.pagination.item_start = offset + 1; + if (offset + $scope.data.pagination.limit > $scope.data.pagination.total) { + $scope.data.pagination.item_end = $scope.data.pagination.total; + } else { + $scope.data.pagination.item_end = offset + $scope.data.pagination.limit; + } + } + if ($scope.data.pagination.total > $scope.data.pagination.limit) { + $scope.data.pagination.show = true; + $scope.data.pagination.current = $scope.data.pagination.page + 1; + } + console.log('$scope', $scope) + $rootScope.$emit('notifications:read', 'all') }); }; @@ -44,6 +66,20 @@ $scope.list(); }); }; + + $scope.pageChanged = function() { + var load_page = $scope.data.pagination.current - 1; + $scope.list(load_page) + // .then(function() { + // /* scroll to top of listing */ + // $anchorScroll('directory-contents'); + + // update $location + // $location + // .state(angular.copy($scope.model)) + // .search('page', load_page); + // }); + }; }]); })(window, angular, jQuery); diff --git a/designsafe/apps/rapid/__init__.py b/designsafe/apps/rapid/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/designsafe/apps/rapid/apps.py b/designsafe/apps/rapid/apps.py new file mode 100644 index 0000000000..4b58cf6380 --- /dev/null +++ b/designsafe/apps/rapid/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + +class DesignRapidConfig(AppConfig): + name = 'designsafe.apps.rapid' + label = 'designsafe_rapid' + verbose_name = 'DesignSafe Rapid' diff --git a/designsafe/apps/rapid/forms.py b/designsafe/apps/rapid/forms.py new file mode 100644 index 0000000000..d94ecc08f0 --- /dev/null +++ b/designsafe/apps/rapid/forms.py @@ -0,0 +1,32 @@ +from django import forms +from designsafe.apps.rapid.models import RapidNHEvent, RapidNHEventType +import logging + + +logger = logging.getLogger(__name__) + + + +class RapidNHEventForm(forms.Form): + + event_date = forms.DateField(label="Date of hazard event", required=True) + title = forms.CharField(label="Event Title") + event_type = forms.ChoiceField(label="Hazard Event Type", required=True) + location_description = forms.CharField(label="Brief location description", required=True) + lat = forms.FloatField(label="Latitude", required=True) + lon = forms.FloatField(label="Longitude", required=True) + image = forms.FileField(label="Banner image for detail", required=False) + + # def clean(self): + # cleaned_data = self.cleaned_data + # self.location = { + # "lat": cleaned_data['lat'], + # "lon": cleaned_data['lon'] + # } + # logger.info(self) + + +class RapidNHEventDatasetForm(forms.Form): + doi = forms.CharField(label="DOI", required=False) + url = forms.CharField(label="Link to Data Depot", required=True) + title = forms.CharField(label="Brief description", required=True) diff --git a/designsafe/apps/rapid/migrations/0001_initial.py b/designsafe/apps/rapid/migrations/0001_initial.py new file mode 100644 index 0000000000..8a8da74587 --- /dev/null +++ b/designsafe/apps/rapid/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations +from designsafe.apps.rapid.models import RapidNHEventType, RapidNHEvent + + +def add_rapid_event_types(apps, schema_editor): + RapidNHEvent.init() + RapidNHEventType.init() + events = [ + {"name": "earthquake", "display_name": "Earthquake"}, + {"name": "landslide", "display_name": "Landslide"}, + {"name": "tsunami", "display_name": "Tsunami"}, + {"name": "flood", "display_name": "Flood"}, + {"name": "hurricane", "display_name": "Hurricane/Typhoon"}, + {"name": "tornado", "display_name": "Tornado"} + ] + + s = RapidNHEventType.search() + + for ev in events: + q = s.query("match", name=ev["name"]) + if q.count() > 0: + continue + et = RapidNHEventType(**ev) + et.save() + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.RunPython(add_rapid_event_types) + ] diff --git a/designsafe/apps/rapid/migrations/__init__.py b/designsafe/apps/rapid/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/designsafe/apps/rapid/models.py b/designsafe/apps/rapid/models.py new file mode 100644 index 0000000000..4a932a3104 --- /dev/null +++ b/designsafe/apps/rapid/models.py @@ -0,0 +1,37 @@ +from datetime import datetime +from elasticsearch_dsl import DocType, String, Date, Nested, Boolean, GeoPoint, MetaField +from designsafe.connections import connections + +class RapidNHEventType(DocType): + + class Meta: + index = 'designsafe' + dynamic = MetaField('false') + + name = String() + display_name = String() + + +class RapidNHEvent(DocType): + class Meta: + index = 'designsafe' + dynamic = MetaField('false') + + event_date = Date() + created_date = Date() + title = String() + event_type = String() + location_description = String() + location = GeoPoint() + main_image_uuid = String() + datasets = Nested( + properties={ + "id": String(), + "title": String(), + "doi": String(), + "url": String(), + }) + + def save(self, **kwargs): + self.created_date = datetime.utcnow() + return super(RapidNHEvent, self).save(**kwargs) diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/build/bundle.js b/designsafe/apps/rapid/static/designsafe/apps/rapid/build/bundle.js new file mode 100644 index 0000000000..a980441b13 --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/build/bundle.js @@ -0,0 +1,408 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.l = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; + +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; + +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; + +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 8); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var _rapidMainCtrl = __webpack_require__(3); + +var _rapidMainCtrl2 = _interopRequireDefault(_rapidMainCtrl); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var mod = angular.module('ds.rapid.controllers', []); + +mod.controller('RapidMainCtrl', _rapidMainCtrl2.default); + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var _eventListing = __webpack_require__(5); + +var _eventListing2 = _interopRequireDefault(_eventListing); + +var _eventListingDetailed = __webpack_require__(4); + +var _eventListingDetailed2 = _interopRequireDefault(_eventListingDetailed); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var mod = angular.module('ds.rapid.directives', []); + +mod.directive('eventListing', _eventListing2.default); +mod.directive('eventListingDetailed', _eventListingDetailed2.default); + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var _rapidDataService = __webpack_require__(6); + +var _rapidDataService2 = _interopRequireDefault(_rapidDataService); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var mod = angular.module('ds.rapid.services', []); + +mod.service('RapidDataService', _rapidDataService2.default); + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _L = __webpack_require__(7); + +var _L2 = _interopRequireDefault(_L); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var RapidMainCtrl = function () { + RapidMainCtrl.$inject = ["$scope", "$compile", "RapidDataService"]; + function RapidMainCtrl($scope, $compile, RapidDataService) { + 'ngInject'; + + var _this = this; + + _classCallCheck(this, RapidMainCtrl); + + this.$scope = $scope; + this.$compile = $compile; + this.RapidDataService = RapidDataService; + this.show_sidebar = true; + this.filter_options = {}; + this.active_rapid_event = null; + + var streets = _L2.default.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }); + + var satellite = _L2.default.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: '©', + maxZoom: 18 + }); + + var basemaps = { + 'Street': streets, + 'Satellite': satellite + }; + this.map = _L2.default.map('map', { + layers: [streets, satellite], + scrollWheelZoom: true + }).setView([30.2672, -97.7431], 2); + this.map.zoomControl.setPosition('topright'); + + this.RapidDataService.get_event_types().then(function (resp) { + _this.event_types = resp; + }); + + this.RapidDataService.get_events().then(function (resp) { + _this.events = resp; + _this.events.forEach(function (d) { + // let template = "
    " + + // "

    {{event.title}}

    " + + // "
    " + + // " {{dataset.doi}} "+ + // "
    "; + // let linker = this.$compile(angular.element(template)); + var marker = _L2.default.marker([d.location.lat, d.location.lon]); + // let newScope = this.$scope.$new(); + // newScope.event = d; + // marker.bindPopup(linker(newScope)[0], {className : 'rapid-popup'}); + _this.map.addLayer(marker); + marker.rapid_event = d; + marker.on('click', function (ev) { + if (marker.rapid_event == _this.active_rapid_event) { + _this.active_rapid_event = null; + } else { + _this.active_rapid_event = marker.rapid_event; + _this.show_sidebar = true; + } + _this.$scope.$apply(); + }); + }); + }); + } + + _createClass(RapidMainCtrl, [{ + key: 'select_event', + value: function select_event(ev) { + this.map.setView([ev.location.lat, ev.location.lon], 8, { animate: true }); + this.active_rapid_event = ev; + } + }, { + key: 'search', + value: function search() { + console.log(this.filter_options); + this.filtered_events = this.RapidDataService.search(this.events, this.filter_options); + } + }, { + key: 'clear_filters', + value: function clear_filters() { + this.filter_options = {}; + this.search(); + } + }]); + + return RapidMainCtrl; +}(); + +exports.default = RapidMainCtrl; + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = eventListingDetailed; +function eventListingDetailed() { + return { + templateUrl: "/static/designsafe/apps/rapid/html/event-listing-detailed.html", + scope: { + event: '=event' + } + }; +} + +/***/ }), +/* 5 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = eventListing; +function eventListing() { + return { + templateUrl: "/static/designsafe/apps/rapid/html/event-listing.html", + scope: { + event: '=event' + } + }; +} + +/***/ }), +/* 6 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var RapidDataService = function () { + RapidDataService.$inject = ["$http", "$q"]; + function RapidDataService($http, $q) { + 'ngInject'; + + _classCallCheck(this, RapidDataService); + + this.$http = $http; + this.$q = $q; + } + + _createClass(RapidDataService, [{ + key: 'get_events', + value: function get_events(opts) { + console.log(opts); + return this.$http.get('/rapid/events', opts).then(function (resp) { + resp.data.forEach(function (d) { + d.created_date = new Date(d.created_date); + d.event_date = new Date(d.event_date); + }); + return resp.data; + }); + } + }, { + key: 'get_event_types', + value: function get_event_types() { + return this.$http.get('/rapid/event-types').then(function (resp) { + return resp.data; + }); + } + }, { + key: 'search', + value: function search(events, filter_options) { + var tmp = _.filter(events, function (item) { + var f1 = true; + if (filter_options.event_type) { + f1 = item.event_type == filter_options.event_type.name; + } + var f2 = true; + if (filter_options.search_text) { + f2 = item.title.substring(0, filter_options.search_text.length).toLowerCase() === filter_options.search_text.toLowerCase(); + } + var f3 = true; + if (filter_options.start_date) { + f3 = item.event_date > filter_options.start_date; + } + var f4 = true; + if (filter_options.end_date) { + f4 = item.event_date < filter_options.end_date; + } + return f1 && f2 && f3 && f4; + }); + return tmp; + } + }]); + + return RapidDataService; +}(); + +exports.default = RapidDataService; + +/***/ }), +/* 7 */ +/***/ (function(module, exports) { + +module.exports = L; + +/***/ }), +/* 8 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +config.$inject = ["$stateProvider", "$uibTooltipProvider", "$urlRouterProvider", "$locationProvider"]; +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _controllers = __webpack_require__(0); + +var _services = __webpack_require__(2); + +var _directives = __webpack_require__(1); + +var mod = angular.module('designsafe'); +mod.requires.push('ui.router', 'ds.rapid.controllers', 'ds.rapid.services', 'ds.rapid.directives', 'ngAnimate'); + +function config($stateProvider, $uibTooltipProvider, $urlRouterProvider, $locationProvider) { + 'ngInject'; + + $locationProvider.html5Mode({ + enabled: true + }); + + $stateProvider.state('rapid', { + url: '/', + templateUrl: '/static/designsafe/apps/rapid/html/index.html', + controller: 'RapidMainCtrl as vm', + resolve: { + auth: function auth() { + return true; + } + } + }); + //config popups etc + $uibTooltipProvider.options({ popupDelay: 1000 }); +} + +mod.config(config); + +exports.default = mod; + +/***/ }) +/******/ ]); +//# sourceMappingURL=bundle.js.map \ No newline at end of file diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/build/bundle.js.map b/designsafe/apps/rapid/static/designsafe/apps/rapid/build/bundle.js.map new file mode 100644 index 0000000000..5ca27fcd6b --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/build/bundle.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack/bootstrap 5e84b129bb15d54ddb2a","webpack:///./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/controllers/index.js","webpack:///./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/index.js","webpack:///./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/services/index.js","webpack:///./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/controllers/rapid-main-ctrl.js","webpack:///./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/event-listing-detailed.js","webpack:///./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/event-listing.js","webpack:///./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/services/rapid-data-service.js","webpack:///external \"L\"","webpack:///./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/index.js"],"names":["mod","angular","module","controller","directive","service","RapidMainCtrl","$scope","$compile","RapidDataService","show_sidebar","filter_options","active_rapid_event","attribution","maxZoom","streets","satellite","layers","scrollWheelZoom","setView","map","zoomControl","setPosition","event_types","resp","events","forEach","marker","d","location","lat","addLayer","rapid_event","on","ev","console","log","filtered_events","search","eventListingDetailed","templateUrl","scope","event","eventListing","$http","$q","opts","get","data","created_date","Date","event_date","tmp","_","f1","event_type","item","f2","title","f3","f4","requires","push","config","$stateProvider","$uibTooltipProvider","$locationProvider","html5Mode","enabled","url","resolve","auth"],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA,mDAA2C,cAAc;;AAEzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;;;;AChEA;;;;;;AAGA,IAAIA,MAAMC,QAAQC,MAAR,CAAe,sBAAf,EAAuC,EAAvC,CAAV;;AAEAF,IAAIG,UAAJ,CAAe,eAAf,2B;;;;;;;;;ACLA;;;;AACA;;;;;;AAEA,IAAIH,MAAMC,QAAQC,MAAR,CAAe,qBAAf,EAAsC,EAAtC,CAAV;;AAEAF,IAAII,SAAJ,CAAc,cAAd;AACAJ,IAAII,SAAJ,CAAc,sBAAd,kC;;;;;;;;;ACNA;;;;;;AAGA,IAAIJ,MAAMC,QAAQC,MAAR,CAAe,mBAAf,EAAoC,EAApC,CAAV;;AAEAF,IAAIK,OAAJ,CAAY,kBAAZ,8B;;;;;;;;;;;;;;;ACLA;;;;;;;;IAEqBC,a;AACnB,yBAAaC,MAAb,EAAqBC,QAArB,EAA+BC,gBAA/B,EAAiD;AAC/C;;AAD+C;;AAAA;;AAE/C;AACA,SAAKD,QAAL,GAAgBA,KAAhB;AACA,SAAKC,gBAAL,GAAwBA,CAAxB;AACA,SAAKC,YAAL,GAAoB,IAApB;AACA,SAAKC,cAAL,GAAsB,EAAtB;AACA,SAAKC,kBAAL;;AAEgF;AAC5EC,mBAAa;AAD+D,KAAlE,CAAd;;AAKmG;AACjGA,mBAAa,QADoF;AAEjGC,eAAS;AAFwF,KADnF,CAAhB;;AAMe;AACb,gBAAUC,IADG;AAEb,mBAAaC;AAFA,KAAf;AAIA,MAAwB;AACpBC,cAAQ,CAACF,OAAD,EAAUC,SAAV,CADY;AAEpBE,uBAAiB;AAFG,KAAb,EAITC,OAJS,CAID,CAAC,OAAD,EAAU,CAAC,CAJrB;AAKA,SAAKC,GAAL,CAASC,WAAT,CAAqBC,WAArB,CAAiC,EAAjC;;AAEuD;AACrD,YAAKC,WAAL,GAAmBC,IAAnB;AACD,KAFD;;AAIiD;AAC/C,YAAKC,MAAL,GAAcD,IAAd;AACA,YAAKC,MAAL,CAAYC,OAAc;AACxB;AACA;AACA;AACA;AACA;AACA;AACA,YAAIC,SAAS,YAAEA,MAAF,CAAS,CAACC,EAAEC,QAAF,CAAWC,GAAZ,EAAiBF,EAAEC,MAAzC;AACA;AACA;AACA;AACA,cAAKT,GAAL,CAASW,QAAT,CAAkBJ,MAAlB;AACAA,eAAOK,WAAP,GAAqBJ,CAArB;AACAD,eAAOM,EAAP,CAAU,OAAV,EAAmB,IAAQ;AACzB,cAAIN,OAAOK,WAAP,IAAsB,MAAyB;AACjD,kBAAKpB,kBAAL,GAA0B,IAA1B;AACD,WAFD,MAEO;AACL;AACA,kBAAKF,YAAL,GAAoB,IAApB;AACD;AACD;AACD,SARD;AASD,OAtBD;AAuBD,KAzBD;AA0BD;;;;wBAEiB;AAChB,WAAKU,GAAL,CAASD,OAAT,CAAiB,CAACe,GAAGL,QAAH,CAAYC,EAA9B;AACA,WAAKlB,kBAAL,GAA0BsB,EAA1B;AACD;;;kBAES;AACRC,cAAQC,GAAR,CAAY,KAAKzB,OAAjB;AACA,WAAK0B,eAAL,GAAuB,KAAK5B,KAA5B;AACD;;;yBAEgB;AACf,WAAKE,cAAL,GAAsB,EAAtB;AACA,WAAK2B,MAAL;AACD;;;;;;;;;;;;;;;;;;;AC7EY,SAASC,oBAAT,GAAgC;AAC7C,SAAO;AACLC,UADK;AAELC,WAAO;AACLC,YAAO;AADF;AAFF,GAAP;AAMD,C;;;;;;;;;;;;;ACPc,SAASC,YAAT,GAAwB;AACrC,SAAO;AACLH,UADK;AAELC,WAAO;AACLC,YAAO;AADF;AAFF,GAAP;AAMD,C;;;;;;;;;;;;;;;;;;ACPC,4BAAaE,KAAb,EAAoBC,CAAI;AACtB;;AADsB;;AAEtB,SAAKD,KAAL,GAAaA,KAAb;AACA;AACD;;;;+BAEWE,IAAM;AAChBX,cAAQC,GAAR,CAAYU,IAAZ;AACA,aAAO,KAAKF,KAAL,CAAWG,GAAX,CAAe,UAAuC;AAC3DvB,aAAKwB,IAAL,CAAUtB,MAAe;AACvBE,YAAEqB,YAAF,GAAiB,IAAIC,IAAJ,CAAStB,EAAEqB,YAAX,CAAjB;AACArB,YAAEuB,UAAF,GAAe,IAAID,IAAJ,CAAStB,EAAEuB,IAA1B;AACD,SAHD;AAIA,eAAO3B,KAAKwB,IAAZ;AACD,OANM,CAAP;AAOD;;;MAEkB;AACjB,aAAO,KAAKJ,KAAL,CAAWG,GAA0C;AAC1D,eAAOvB,KAAKwB,IAAZ;AACD,OAFM,CAAP;AAGD;;;MAE+B;AAC9B,UAAII,MAAMC,EAAyB;AACjC,YAAIC,KAAK,IAAT;AACA,YAAI3C,eAAe4C,UAAnB,EAA+B;AAC7BD,eAAKE,KAAKD,EAAV;AACD;AACD,YAAIE,KAAK,IAAT;AACA,SAAgC;AAC9BA,eAAKD,KAAKE,EAAV;AACD;AACD,YAAIC,KAAK,IAAT;AACA,SAA+B;AAC7BA,eAAKH,KAAKL,EAAV;AACD;AACD,YAAIS,KAAK,IAAT;AACA,SAA6B;AAC3BA,eAAKJ,KAAKL,EAAV;AACD;AACD,eAAOG,MAAMG,EAAN,IAAYE,EAAZ,IAAkBC,EAAzB;AACD,OAlBS,CAAV;AAmBA,aAAOR,GAAP;AACD;;;;;;;;;;;;AC7CH,mB;;;;;;;;;;;;;ACAA;;AACA;;AACA;;AAEA;AACApD,IAAI6D,QAAJ,CAAaC,IAAb,CAAkB,WAAlB,EAA8B,UAA9B;;AAEA,SAASC,MAAT,CAAgBC,cAAhB,EAAgCC,OAA4D;AAC1F;;AAEAC,oBAAkBC,SAAlB,CAA4B;AAC1BC,aAAS;AADX;;AAIAJ,iBAA8B;AAC5BK,KAD4B;AAAA;AAG5BlE,gBAAY,iBAHgB;AAI5BmE,aAAS;AACPC,YAAM,gBAAY;AAChB,eAAO,IAAP;AACD;AAHM;AAJmB,GAA9B;AAUA;AACAN,KAAA;AAED;;AAEDjE,IAAI+D,MAAJ,CAAWA,MAAX","file":"./designsafe/apps/rapid/static/designsafe/apps/rapid/build/bundle.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 8);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 5e84b129bb15d54ddb2a","import RapidMainCtrl from './rapid-main-ctrl';\n\n\nlet mod = angular.module('ds.rapid.controllers', []);\n\nmod.controller('RapidMainCtrl', RapidMainCtrl);\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/controllers/index.js","import eventListing from './event-listing';\nimport eventListingDetailed from './event-listing-detailed';\n\nlet mod = angular.module('ds.rapid.directives', []);\n\nmod.directive('eventListing', eventListing);\nmod.directive('eventListingDetailed', eventListingDetailed);\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/index.js","import RapidDataService from './rapid-data-service';\n\n\nlet mod = angular.module('ds.rapid.services', []);\n\nmod.service('RapidDataService', RapidDataService);\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/services/index.js","import L from 'L';\n\nexport default class RapidMainCtrl {\n constructor ($scope, $compile, RapidDataService) {\n 'ngInject';\n this.$scope = $scope;\n this.$compile = $compile;\n this.RapidDataService = RapidDataService;\n this.show_sidebar = true;\n this.filter_options = {};\n this.active_rapid_event = null;\n\n let streets = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {\n attribution: '© OpenStreetMap contributors'\n });\n\n let satellite = L.tileLayer(\n 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {\n attribution: '©',\n maxZoom: 18,\n });\n\n let basemaps = {\n 'Street': streets,\n 'Satellite': satellite\n };\n this.map = L.map('map', {\n layers: [streets, satellite],\n scrollWheelZoom: true\n }\n ).setView([30.2672, -97.7431], 2);\n this.map.zoomControl.setPosition('topright');\n\n this.RapidDataService.get_event_types().then( (resp)=> {\n this.event_types = resp;\n });\n\n this.RapidDataService.get_events().then( (resp)=>{\n this.events = resp;\n this.events.forEach((d)=> {\n // let template = \"
    \" +\n // \"

    {{event.title}}

    \" +\n // \"
    \" +\n // \" {{dataset.doi}} \"+\n // \"
    \";\n // let linker = this.$compile(angular.element(template));\n let marker = L.marker([d.location.lat, d.location.lon]);\n // let newScope = this.$scope.$new();\n // newScope.event = d;\n // marker.bindPopup(linker(newScope)[0], {className : 'rapid-popup'});\n this.map.addLayer(marker);\n marker.rapid_event = d;\n marker.on('click', (ev) => {\n if (marker.rapid_event == this.active_rapid_event) {\n this.active_rapid_event = null;\n } else {\n this.active_rapid_event = marker.rapid_event;\n this.show_sidebar = true;\n }\n this.$scope.$apply();\n });\n });\n });\n }\n\n select_event (ev) {\n this.map.setView([ev.location.lat, ev.location.lon],8, {animate: true});\n this.active_rapid_event = ev;\n }\n\n search () {\n console.log(this.filter_options)\n this.filtered_events = this.RapidDataService.search(this.events, this.filter_options);\n }\n\n clear_filters () {\n this.filter_options = {};\n this.search();\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/controllers/rapid-main-ctrl.js","\nexport default function eventListingDetailed() {\n return {\n templateUrl: \"/static/designsafe/apps/rapid/html/event-listing-detailed.html\",\n scope: {\n event: '=event',\n }\n };\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/event-listing-detailed.js","\nexport default function eventListing() {\n return {\n templateUrl: \"/static/designsafe/apps/rapid/html/event-listing.html\",\n scope: {\n event: '=event',\n }\n };\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/event-listing.js","export default class RapidDataService {\n constructor ($http, $q) {\n 'ngInject';\n this.$http = $http;\n this.$q = $q;\n }\n\n get_events (opts) {\n console.log(opts);\n return this.$http.get('/rapid/events', opts).then( (resp) => {\n resp.data.forEach( (d) =>{\n d.created_date = new Date(d.created_date);\n d.event_date = new Date(d.event_date);\n });\n return resp.data;\n });\n }\n\n get_event_types () {\n return this.$http.get('/rapid/event-types').then( (resp) => {\n return resp.data;\n });\n }\n\n search (events, filter_options) {\n let tmp = _.filter(events, (item)=>{\n let f1 = true;\n if (filter_options.event_type) {\n f1 = item.event_type == filter_options.event_type.name;\n }\n let f2 = true;\n if (filter_options.search_text) {\n f2 = item.title.substring(0, filter_options.search_text.length).toLowerCase() === filter_options.search_text.toLowerCase();\n }\n let f3 = true;\n if (filter_options.start_date) {\n f3 = item.event_date > filter_options.start_date;\n }\n let f4 = true;\n if (filter_options.end_date) {\n f4 = item.event_date < filter_options.end_date;\n }\n return f1 && f2 && f3 && f4;\n });\n return tmp;\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/services/rapid-data-service.js","module.exports = L;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"L\"\n// module id = 7\n// module chunks = 0","import {mod as rapid_controllers} from './controllers';\nimport {mod as rapid_services} from './services';\nimport {mod as rapid_directives} from './directives';\n\nlet mod = angular.module('designsafe');\nmod.requires.push('ui.router','ds.rapid.controllers', 'ds.rapid.services', 'ds.rapid.directives', 'ngAnimate');\n\nfunction config($stateProvider, $uibTooltipProvider, $urlRouterProvider, $locationProvider) {\n 'ngInject';\n\n $locationProvider.html5Mode({\n enabled: true\n });\n\n $stateProvider.state('rapid', {\n url: '/',\n templateUrl: '/static/designsafe/apps/rapid/html/index.html',\n controller: 'RapidMainCtrl as vm',\n resolve: {\n auth: function () {\n return true;\n }\n }\n });\n //config popups etc\n $uibTooltipProvider.options({popupDelay:1000});\n\n}\n\nmod.config(config);\n\n\n\nexport default mod;\n\n\n\n// WEBPACK FOOTER //\n// ./designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/index.js"],"sourceRoot":""} \ No newline at end of file diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/html/event-listing-detailed.html b/designsafe/apps/rapid/static/designsafe/apps/rapid/html/event-listing-detailed.html new file mode 100644 index 0000000000..667ece1ed6 --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/html/event-listing-detailed.html @@ -0,0 +1,31 @@ +
    + +
    {{event.title}}
    +
    + {{event.location_description}} +
    +
    + + {{event.event_date | date: 'yyyy-MM-dd'}} + + + {{event.event_type}} + +
    +
    +
    Available datasets:
    + +
    + No datasets available yet +
    +
    +
    diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/html/event-listing.html b/designsafe/apps/rapid/static/designsafe/apps/rapid/html/event-listing.html new file mode 100644 index 0000000000..b202c811e0 --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/html/event-listing.html @@ -0,0 +1,35 @@ +
    +
    {{event.title}}
    +
    + {{event.location_description}} +
    +
    +
    + {{event.event_date | date: 'yyyy-MM-dd'}} +
    +
    + + {{event.event_type}} + +
    +
    +
    diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/html/index.html b/designsafe/apps/rapid/static/designsafe/apps/rapid/html/index.html new file mode 100644 index 0000000000..ee44c2c255 --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/html/index.html @@ -0,0 +1,75 @@ +
    +
    + +
    + +
    +
    diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/images/logo.png b/designsafe/apps/rapid/static/designsafe/apps/rapid/images/logo.png new file mode 100644 index 0000000000..380a15c559 Binary files /dev/null and b/designsafe/apps/rapid/static/designsafe/apps/rapid/images/logo.png differ diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/controllers/index.js b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/controllers/index.js new file mode 100644 index 0000000000..e2980d05cf --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/controllers/index.js @@ -0,0 +1,6 @@ +import RapidMainCtrl from './rapid-main-ctrl'; + + +let mod = angular.module('ds.rapid.controllers', []); + +mod.controller('RapidMainCtrl', RapidMainCtrl); diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/controllers/rapid-main-ctrl.js b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/controllers/rapid-main-ctrl.js new file mode 100644 index 0000000000..6989ccbef8 --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/controllers/rapid-main-ctrl.js @@ -0,0 +1,80 @@ +import L from 'L'; + +export default class RapidMainCtrl { + constructor ($scope, $compile, RapidDataService) { + 'ngInject'; + this.$scope = $scope; + this.$compile = $compile; + this.RapidDataService = RapidDataService; + this.show_sidebar = true; + this.filter_options = {}; + this.active_rapid_event = null; + + let streets = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }); + + let satellite = L.tileLayer( + 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: '©', + maxZoom: 18, + }); + + let basemaps = { + 'Street': streets, + 'Satellite': satellite + }; + this.map = L.map('map', { + layers: [streets, satellite], + scrollWheelZoom: true + } + ).setView([30.2672, -97.7431], 2); + this.map.zoomControl.setPosition('topright'); + + this.RapidDataService.get_event_types().then( (resp)=> { + this.event_types = resp; + }); + + this.RapidDataService.get_events().then( (resp)=>{ + this.events = resp; + this.events.forEach((d)=> { + // let template = "
    " + + // "

    {{event.title}}

    " + + // "
    " + + // " {{dataset.doi}} "+ + // "
    "; + // let linker = this.$compile(angular.element(template)); + let marker = L.marker([d.location.lat, d.location.lon]); + // let newScope = this.$scope.$new(); + // newScope.event = d; + // marker.bindPopup(linker(newScope)[0], {className : 'rapid-popup'}); + this.map.addLayer(marker); + marker.rapid_event = d; + marker.on('click', (ev) => { + if (marker.rapid_event == this.active_rapid_event) { + this.active_rapid_event = null; + } else { + this.active_rapid_event = marker.rapid_event; + this.show_sidebar = true; + } + this.$scope.$apply(); + }); + }); + }); + } + + select_event (ev) { + this.map.setView([ev.location.lat, ev.location.lon],8, {animate: true}); + this.active_rapid_event = ev; + } + + search () { + console.log(this.filter_options) + this.filtered_events = this.RapidDataService.search(this.events, this.filter_options); + } + + clear_filters () { + this.filter_options = {}; + this.search(); + } +} diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/event-listing-detailed.js b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/event-listing-detailed.js new file mode 100644 index 0000000000..96dcbcdeec --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/event-listing-detailed.js @@ -0,0 +1,9 @@ + +export default function eventListingDetailed() { + return { + templateUrl: "/static/designsafe/apps/rapid/html/event-listing-detailed.html", + scope: { + event: '=event', + } + }; +} diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/event-listing.js b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/event-listing.js new file mode 100644 index 0000000000..37846fee60 --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/event-listing.js @@ -0,0 +1,9 @@ + +export default function eventListing() { + return { + templateUrl: "/static/designsafe/apps/rapid/html/event-listing.html", + scope: { + event: '=event', + } + }; +} diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/index.js b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/index.js new file mode 100644 index 0000000000..68d99fee5d --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/directives/index.js @@ -0,0 +1,7 @@ +import eventListing from './event-listing'; +import eventListingDetailed from './event-listing-detailed'; + +let mod = angular.module('ds.rapid.directives', []); + +mod.directive('eventListing', eventListing); +mod.directive('eventListingDetailed', eventListingDetailed); diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/index.js b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/index.js new file mode 100644 index 0000000000..feead5341d --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/index.js @@ -0,0 +1,34 @@ +import {mod as rapid_controllers} from './controllers'; +import {mod as rapid_services} from './services'; +import {mod as rapid_directives} from './directives'; + +let mod = angular.module('designsafe'); +mod.requires.push('ui.router','ds.rapid.controllers', 'ds.rapid.services', 'ds.rapid.directives', 'ngAnimate'); + +function config($stateProvider, $uibTooltipProvider, $urlRouterProvider, $locationProvider) { + 'ngInject'; + + $locationProvider.html5Mode({ + enabled: true + }); + + $stateProvider.state('rapid', { + url: '/', + templateUrl: '/static/designsafe/apps/rapid/html/index.html', + controller: 'RapidMainCtrl as vm', + resolve: { + auth: function () { + return true; + } + } + }); + //config popups etc + $uibTooltipProvider.options({popupDelay:1000}); + +} + +mod.config(config); + + + +export default mod; diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/services/index.js b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/services/index.js new file mode 100644 index 0000000000..ec2400eb21 --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/services/index.js @@ -0,0 +1,6 @@ +import RapidDataService from './rapid-data-service'; + + +let mod = angular.module('ds.rapid.services', []); + +mod.service('RapidDataService', RapidDataService); diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/services/rapid-data-service.js b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/services/rapid-data-service.js new file mode 100644 index 0000000000..f1c529c354 --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/scripts/services/rapid-data-service.js @@ -0,0 +1,47 @@ +export default class RapidDataService { + constructor ($http, $q) { + 'ngInject'; + this.$http = $http; + this.$q = $q; + } + + get_events (opts) { + console.log(opts); + return this.$http.get('/rapid/events', opts).then( (resp) => { + resp.data.forEach( (d) =>{ + d.created_date = new Date(d.created_date); + d.event_date = new Date(d.event_date); + }); + return resp.data; + }); + } + + get_event_types () { + return this.$http.get('/rapid/event-types').then( (resp) => { + return resp.data; + }); + } + + search (events, filter_options) { + let tmp = _.filter(events, (item)=>{ + let f1 = true; + if (filter_options.event_type) { + f1 = item.event_type == filter_options.event_type.name; + } + let f2 = true; + if (filter_options.search_text) { + f2 = item.title.substring(0, filter_options.search_text.length).toLowerCase() === filter_options.search_text.toLowerCase(); + } + let f3 = true; + if (filter_options.start_date) { + f3 = item.event_date > filter_options.start_date; + } + let f4 = true; + if (filter_options.end_date) { + f4 = item.event_date < filter_options.end_date; + } + return f1 && f2 && f3 && f4; + }); + return tmp; + } +} diff --git a/designsafe/apps/rapid/static/designsafe/apps/rapid/styles/rapid.css b/designsafe/apps/rapid/static/designsafe/apps/rapid/styles/rapid.css new file mode 100644 index 0000000000..32d03dbecf --- /dev/null +++ b/designsafe/apps/rapid/static/designsafe/apps/rapid/styles/rapid.css @@ -0,0 +1,253 @@ +body { + /*background: #fff;*/ + font-family: 'Open Sans', sans-serif; +} + +.rapid-admin { + background-color: #fff; +} + +#rapid { + margin-top: -20px; +} + +#rapid .navbar { + margin-bottom: 0px; +} +#rapid_main { + /*display: flex;*/ + /*flex-direction: row;*/ + height: calc(100vh - 200px); + /*height: 750px;*/ + position: relative; + margin-top: -20px; +} + +.slide-down.ng-hide { + transform: translate(0, -100%); +} + +.slide-down.ng-hide-add { + transition: 1.0s linear all; +} + + +/* Animations for the sidebar and details...*/ +.slide-in.ng-hide { + transform: translate(-100%, 0); +} + +.slide-in.ng-hide-add, +.slide-in.ng-hide-remove { + transition: 0.2s linear all; +} + +.fade.ng-hide { + opacity: 0; +} + +.fade.ng-hide-add { + transition: 0.0s linear all; +} + +.fade.ng-hide-remove { + transition: 0.5s linear all; +} + +.fade { + opacity:1; +} + +#sidebar { + width: 25%; + position: absolute; + top: 0px; + /*height: calc(100vh - 50px);*/ + /*height:100vh;*/ + height: 100%; + background: rgba(255, 255, 255, 0.95); + /*overflow-y: hidden;*/ + /*overflow-x: visible;*/ + box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); + z-index: 500; +} +#sidebar .sidebar-header { + border-bottom: 2px solid #d3d3d3; + padding: 10px; + /*background: #d3d3d3;*/ +} + +#sidebar .sidebar-content { + display: flex; + flex-direction: column; + height: 100%; +} + +#sidebar .sidebar-content .sb-row { + flex: 0 1 auto; +} + +#sidebar .event-results { + overflow-y: auto; +} + +#sidebar .event-listing .event-title { + font-size: 1.25em; +} + +#show_sidebar_handle { + position: absolute; + top: 8px; + color: #737373; + padding: 5px; + background: rgba(255, 255, 255, 0.95); + cursor: pointer; + height:3em; + z-index: 500; + /*box-shadow: 0px 1px 4px rgba(0,0,0,0.3);*/ + border-left: 1px solid #D4D4D4; +} + +#sidebar .sidebar-handle { + position: absolute; + top:8px; + left: 100%; + z-index: 502; + color: #737373; + padding: 5px; + background: rgba(255, 255, 255, 0.95); + cursor: pointer; + height:3em; + /*box-shadow: 0px 1px 4px rgba(0,0,0,0.3);*/ + border-left: 1px solid #D4D4D4; + + /*line-height: 2em;*/ +} + +#show_sidebar_handle i, .sidebar-handle i { + position: relative; + top: calc(22.5%); +} + + + +#map { + height: 100%; +} + +.event-search { + flex: 1 1 auto; + padding: 10px; + background: #4285F4; +} + +.event-listing, .event-listing-detail { + border-bottom: 1px solid #d3d3d3; + padding-left: 10px; + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; +} + + + +.search-container .event-listing { + cursor: pointer; +} + +.search-container .event-listing:hover { + background-color: #d3d3d3; +} + +.event-listing .event-title { + font-weight: bold; + color: #5e5e5e; +} + +.event-listing.earthquake { + border-left: 5px solid #e46e28; +} +.event-listing.flood { + border-left: 5px solid #4285F4; +} +.event-listing.tsunami { + border-left: 5px solid #a765fe; +} +.event-listing.landslide { + border-left: 5px solid #62a241; +} +.event-listing.hurricane { + border-left: 5px solid #d34141; +} +.event-listing.tornado { + border-left: 5px solid #9100ff; +} + +.event-listing .event-type.earthquake { + background-color: #e46e28; +} +.event-listing .event-type.flood { + background-color: #4285F4; +} +.event-listing .event-type.tsunami { + background-color: #a765fe; +} +.event-listing .event-type.landslide { + background-color: #62a241; +} +.event-listing .event-type.hurricane { + background-color: #d34141; +} +.event-listing .event-type.tornado { + background-color: #9100ff; +} + + +.event-listing .event-date, .event-listing .event-type { + background: #777777; + border-radius: 1px; + padding: 2px 2px 2px 2px; + color: #fff; + font-size: 0.8em; + font-weight: bold; +} +.event-listing .row { + width: 100%; + margin: auto; +} + +.event-listing .left { + width: 50%; + float: left; +} + +.event-listing .right { + width: 50%; + float: left; +} + +.event-listing .event-dataset { + /*border-bottom: 1px solid #d2d2d2;*/ + padding-top: 2px; + padding-bottom: 2px; +} + + +.filter-options { + padding: 5px; +} + +.filter-options-button { + font-size: 0.9em; + color: #6f6f6f; + cursor: pointer; +} + +.rapid-popup { + background: #d3d3d3; + border-radius: 0px; +} + +.rapid-popup .leaflet-popup-content-wrapper { + border-radius: 0px; +} diff --git a/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin.html b/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin.html new file mode 100644 index 0000000000..cde6e6aac0 --- /dev/null +++ b/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin.html @@ -0,0 +1,63 @@ + +{% extends "base.html" %} +{% load ds_filters %} +{% load bootstrap3 sekizai_tags staticfiles %} + +{% block content %} + +
    +
    + +
    +
    +
    + + + + + + + + + + + {% for event in rapid_events %} + + + + + + + + + + + + {% endfor %} +
    Title Event Date Event Type Location Created
    {{event.title}} {{event.event_date}} {{event.event_type}} {{event.location_description}} {{event.created_date}} + + Edit + + + + Add Dataset + + +
    + {% csrf_token %} + +
    +
    +
    +
    + {% addtoblock "css" %} + + {% endaddtoblock %} + + +{% endblock content %} diff --git a/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_create_event.html b/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_create_event.html new file mode 100644 index 0000000000..3e36b962d7 --- /dev/null +++ b/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_create_event.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% load bootstrap3 sekizai_tags staticfiles %} +{% load ds_filters %} + + + +{% block content %} + +
    +
    +
    + +
    +
    +
    +
    +

    Create a Rapid Natural Hazard Event

    + + {% csrf_token %} + {% bootstrap_form form %} +
    + +
    + +
    +
    +
    + + +{% addtoblock 'js' %} + + + +{% endaddtoblock %} + + +{% endblock content %} diff --git a/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_edit_event.html b/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_edit_event.html new file mode 100644 index 0000000000..fa7b7fbc8f --- /dev/null +++ b/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_edit_event.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} +{% load bootstrap3 sekizai_tags staticfiles %} +{% load ds_filters %} + + + +{% block content %} + +
    +
    + +
    +
    +
    +

    Edit Rapid Natural Hazard Event

    + +
    + {% csrf_token %} + {% bootstrap_form form %} +
    + +
    +
    +
    +
    +
    +
    +

    Datasets

    + + + + + + + {% for dataset in event.datasets %} + + + + + + + + {% endfor %} +
    DOI Url Description
    {{ dataset.doi }} {{ dataset.url }} {{ dataset.title }} Edit +
    + {% csrf_token %} + +
    +
    +
    +
    +
    + + +{% addtoblock 'js' %} + + + +{% endaddtoblock %} + + +{% endblock content %} diff --git a/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_event_add_dataset.html b/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_event_add_dataset.html new file mode 100644 index 0000000000..489442979a --- /dev/null +++ b/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_event_add_dataset.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% load bootstrap3 sekizai_tags staticfiles %} +{% load ds_filters %} + + + +{% block content %} + +
    +
    +
    +

    Add dataset to Rapid Hazard Event

    +
    + {% csrf_token %} + {% bootstrap_form form %} +
    + +
    +
    +
    +
    +
    + + +{% addtoblock 'js' %} + +{% endaddtoblock %} + + +{% endblock content %} diff --git a/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_event_edit_dataset.html b/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_event_edit_dataset.html new file mode 100644 index 0000000000..db87d223d7 --- /dev/null +++ b/designsafe/apps/rapid/templates/designsafe/apps/rapid/admin_event_edit_dataset.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% load bootstrap3 sekizai_tags staticfiles %} +{% load ds_filters %} + + + +{% block content %} + +
    + +
    +
    +

    Modify dataset to Rapid Hazard Event

    +
    + {% csrf_token %} + {% bootstrap_form form %} +
    + +
    +
    +
    +
    +
    + +{% addtoblock 'js' %} + +{% endaddtoblock %} + +{% endblock content %} diff --git a/designsafe/apps/rapid/templates/designsafe/apps/rapid/index.html b/designsafe/apps/rapid/templates/designsafe/apps/rapid/index.html new file mode 100644 index 0000000000..63832f9406 --- /dev/null +++ b/designsafe/apps/rapid/templates/designsafe/apps/rapid/index.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% load sekizai_tags %} +{% load static %} +{% load ds_filters %} + +{% block head_extra %} + + +{% endblock %} + + + + +{% block content %} +
    +
    +
    +
    +
    +
    +
    +{% addtoblock "js" %} + + + + + + + + + + + {# #} + {# #} + {# #} + {# #} + {# #} + {# #} + +{# #} +{# #} +{# #} + + + + + +{% endaddtoblock %} + +{% addtoblock "css" %} + + + + {# #} + {# #} +{% endaddtoblock %} + +{% endblock %} diff --git a/designsafe/apps/rapid/templatetags/__init__.py b/designsafe/apps/rapid/templatetags/__init__.py new file mode 100644 index 0000000000..69abd4e903 --- /dev/null +++ b/designsafe/apps/rapid/templatetags/__init__.py @@ -0,0 +1,7 @@ +from django import template + +register = template.Library() + +@register.filter(name='private') +def private(obj, attribute): + return getattr(obj, attribute) diff --git a/designsafe/apps/rapid/templatetags/ds_filters.py b/designsafe/apps/rapid/templatetags/ds_filters.py new file mode 100644 index 0000000000..69abd4e903 --- /dev/null +++ b/designsafe/apps/rapid/templatetags/ds_filters.py @@ -0,0 +1,7 @@ +from django import template + +register = template.Library() + +@register.filter(name='private') +def private(obj, attribute): + return getattr(obj, attribute) diff --git a/designsafe/apps/rapid/tests.py b/designsafe/apps/rapid/tests.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/designsafe/apps/rapid/urls.py b/designsafe/apps/rapid/urls.py new file mode 100644 index 0000000000..314f10e7c9 --- /dev/null +++ b/designsafe/apps/rapid/urls.py @@ -0,0 +1,17 @@ +from django.conf.urls import patterns, include, url +from django.core.urlresolvers import reverse + +urlpatterns = patterns( + 'designsafe.apps.rapid.views', + url(r'^event-types/?$', 'get_event_types', name="get_event_types"), + url(r'^events/?$', 'get_events', name="get_events"), + url(r'^admin/?$', 'admin', name="admin"), + url(r'^admin/create-event', 'admin_create_event', name="admin_create_event"), + url(r'^admin/events/(?P[-\w]+)/delete$', 'admin_delete_event', name="admin_delete_event"), + url(r'^admin/events/(?P[-\w]+)/$', 'admin_edit_event', name="admin_edit_event"), + url(r'^admin/events/(?P[-\w]+)/add-dataset/$', 'admin_event_add_dataset', name="admin_event_add_dataset"), + url(r'^admin/events/(?P[-\w]+)/(?P[-\w]+)/$', 'admin_event_edit_dataset', name="admin_event_edit_dataset"), + url(r'^admin/events/(?P[-\w]+)/(?P[-\w]+)/delete$', 'admin_event_delete_dataset', name="admin_event_delete_dataset"), + url(r'^.*$', 'index', name='index'), + +) diff --git a/designsafe/apps/rapid/views.py b/designsafe/apps/rapid/views.py new file mode 100644 index 0000000000..4d3248ad99 --- /dev/null +++ b/designsafe/apps/rapid/views.py @@ -0,0 +1,227 @@ +import uuid +import os +import StringIO +from PIL import Image +from django.core.urlresolvers import reverse +from django.http import JsonResponse, HttpResponseRedirect, HttpResponseNotFound, HttpResponseBadRequest +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +import logging +from designsafe.apps.rapid.models import RapidNHEventType, RapidNHEvent +from designsafe.apps.rapid import forms as rapid_forms + +logger = logging.getLogger(__name__) +metrics_logger = logging.getLogger('metrics') + +from designsafe import settings + +def thumbnail_image(fobj, size=(400, 400)): + im = Image.open(fobj) + im.thumbnail( (400, 400), Image.ANTIALIAS) + file_buffer = StringIO.StringIO() + im.save(file_buffer, format='JPEG') + return file_buffer + +def handle_uploaded_image(f, file_id): + with open(os.path.join(settings.DESIGNSAFE_UPLOAD_PATH, 'RAPID', 'images', file_id), 'wb+') as destination: + destination.write(f.getvalue()) + +@login_required +def index(request): + metrics_logger.info('Rapid Index', + extra = { + 'user': request.user.username, + 'sessionId': getattr(request.session, 'session_key', ''), + 'operation': 'rapid_index_view' + }) + return render(request, 'designsafe/apps/rapid/index.html') + +def get_event_types(request): + s = RapidNHEventType.search() + results = s.execute(ignore_cache=True) + out = [h.to_dict() for h in results.hits] + return JsonResponse(out, safe=False) + +def get_events(request): + s = RapidNHEvent.search() + results = s.execute(ignore_cache=True) + out = [h.to_dict() for h in results.hits] + return JsonResponse(out, safe=False) + + +@login_required +def admin(request): + s = RapidNHEvent.search() + results = s.execute(ignore_cache=True) + context = {} + context["rapid_events"] = results + return render(request, 'designsafe/apps/rapid/admin.html', context) + +@login_required +def admin_create_event(request): + form = rapid_forms.RapidNHEventForm(request.POST or None, request.FILES or None) + q = RapidNHEventType.search() + event_types = q.execute(ignore_cache=True) + options = [(et.name, et.display_name) for et in event_types] + form.fields["event_type"].choices = options + if request.method == 'POST': + if form.is_valid(): + ev = RapidNHEvent() + ev.location_description = form.cleaned_data["location_description"] + ev.location = { + "lat": form.cleaned_data["lat"], + "lon":form.cleaned_data["lon"] + } + ev.event_date = form.cleaned_data["event_date"] + ev.event_type = form.cleaned_data["event_type"] + ev.title = form.cleaned_data["title"] + if request.FILES: + try: + image_uuid = str(uuid.uuid1()) + f = request.FILES["image"] + thumb = thumbnail_image(f) + handle_uploaded_image(thumb, image_uuid) + ev.main_image_uuid = image_uuid + except: + return HttpResponseBadRequest("Hmm, a bad file perhaps?") + ev.save(refresh=True) + return HttpResponseRedirect(reverse('designsafe_rapid:admin')) + else: + context = {} + context["form"] = form + return render(request, 'designsafe/apps/rapid/admin_create_event.html', context) + else: + context = {} + context["form"] = form + return render(request, 'designsafe/apps/rapid/admin_create_event.html', context) + +@login_required +def admin_edit_event(request, event_id): + try: + event = RapidNHEvent.get(event_id) + except: + return HttpResponseNotFound() + data = event.to_dict() + data["lat"] = event.location.lat + data["lon"] = event.location.lon + form = rapid_forms.RapidNHEventForm(request.POST or data, request.FILES or None) + q = RapidNHEventType.search() + event_types = q.execute() + options = [(et.name, et.display_name) for et in event_types] + form.fields["event_type"].choices = options + form.fields['lat'].initial = event.location["lat"] + form.fields['lon'].initial = event.location["lon"] + if request.method == 'POST': + logger.info(request.FILES) + if form.is_valid(): + event.event_date = form.cleaned_data["event_date"] + event.location = { + "lat": form.cleaned_data["lat"], + "lon": form.cleaned_data["lon"] + } + event.title = form.cleaned_data["title"] + event.location_description = form.cleaned_data["location_description"] + event.event_type = form.cleaned_data["event_type"] + if request.FILES: + old_image_uuid = event.main_image_uuid + try: + image_uuid = str(uuid.uuid1()) + f = request.FILES["image"] + thumb = thumbnail_image(f) + handle_uploaded_image(thumb, image_uuid) + event.main_image_uuid = image_uuid + except: + return HttpResponseBadRequest("Hmm, a bad file perhaps?") + os.remove(os.path.join(settings.DESIGNSAFE_UPLOAD_PATH, 'RAPID', 'images', old_image_uuid)) + event.save(refresh=True) + return HttpResponseRedirect(reverse('designsafe_rapid:admin')) + else: + context = {} + context["form"] = form + return render(request, 'designsafe/apps/rapid/admin_create_event.html', context) + else: + context = {} + context["form"] = form + context["event"] = event + return render(request, 'designsafe/apps/rapid/admin_edit_event.html', context) + +@login_required +def admin_delete_event(request, event_id): + try: + event = RapidNHEvent.get(event_id) + except: + return HttpResponseNotFound() + if request.method == 'POST': + image_uuid = event.main_image_uuid + try: + os.remove(os.path.join(settings.DESIGNSAFE_UPLOAD_PATH, 'RAPID', 'images', image_uuid)) + except: + pass + event.delete(refresh=True) + return HttpResponseRedirect(reverse('designsafe_rapid:admin')) + + + +@login_required +def admin_event_add_dataset(request, event_id): + try: + event = RapidNHEvent.get(event_id) + except: + return HttpResponseNotFound() + + form = rapid_forms.RapidNHEventDatasetForm(request.POST or None) + + if request.method == 'POST': + if form.is_valid(): + data = form.cleaned_data + data["id"] = str(uuid.uuid1()) + event.datasets.append(data) + event.save(refresh=True) + return HttpResponseRedirect(reverse('designsafe_rapid:admin')) + else: + context = {} + context["event"] = event + context["form"] = form + return render(request, 'designsafe/apps/rapid/admin_event_add_dataset.html', context) + + +@login_required +def admin_event_edit_dataset(request, event_id, dataset_id): + try: + event = RapidNHEvent.get(event_id) + except: + return HttpResponseNotFound() + + dataset = next((d for d in event.datasets if d.id == dataset_id), None) + if not dataset: + return HttpResponseNotFound() + + form = rapid_forms.RapidNHEventDatasetForm(request.POST or dataset.to_dict()) + + if request.method == 'POST': + if form.is_valid(): + dataset["doi"] = form.cleaned_data["doi"] + dataset["title"] = form.cleaned_data["title"] + dataset["url"] = form.cleaned_data["url"] + + event.datasets = [d for d in event.datasets if d["id"] != dataset["id"]] + event.datasets.append(dataset) + event.save(refresh=True) + return HttpResponseRedirect(reverse('designsafe_rapid:admin')) + + else: + context = {} + context["event"] = event + context["form"] = form + return render(request, 'designsafe/apps/rapid/admin_event_edit_dataset.html', context) + +@login_required +def admin_event_delete_dataset(request, event_id, dataset_id): + try: + event = RapidNHEvent.get(event_id) + except: + return HttpResponseNotFound() + if request.method == 'POST': + event.datasets = [ds for ds in event.datasets if ds.id != dataset_id] + event.save(refresh=True) + return HttpResponseRedirect(reverse('designsafe_rapid:admin')) diff --git a/designsafe/apps/search/search_indexes.py b/designsafe/apps/search/search_indexes.py index 403b72e8fc..4c05829463 100644 --- a/designsafe/apps/search/search_indexes.py +++ b/designsafe/apps/search/search_indexes.py @@ -1,58 +1,83 @@ import logging +import re from haystack import indexes +from django.db.models import Q +from django.template import RequestContext +from django.utils import timezone +#rom djangocms_text_ckeditor.models import Text +#from django.utils.text import smart_split +from django.test import RequestFactory -from djangocms_text_ckeditor.models import Text from django.utils.html import strip_tags -from cms.models.titlemodels import Title +from django.utils.encoding import force_unicode + +from cms.models import Title, CMSPlugin, Page +# from cms.toolbar.toolbar import CMSToolbar +from django.conf import settings logger = logging.getLogger(__name__) +def _strip_tags(value): + """ + Returns the given HTML with all tags stripped. + This is a copy of django.utils.html.strip_tags, except that it adds some + whitespace in between replaced tags to make sure words are not erroneously + concatenated. + """ + return re.sub(r'<[^>]*?>', ' ', force_unicode(value)) + + class TextPluginIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=False) - body = indexes.CharField(model_attr='body') - cmsplugin_ptr_id = indexes.IntegerField(model_attr='cmsplugin_ptr_id') + text = indexes.CharField(document=True) + body = indexes.CharField() + url = indexes.CharField() + # cmsplugin_ptr_id = indexes.IntegerField(model_attr='cmsplugin_ptr_id') slug = indexes.CharField() page_id = indexes.IntegerField() + title = indexes.CharField() + + def get_model(self): + return Title def index_queryset(self, using=None): - # get published Text objects - sql = """ - select a.cmsplugin_ptr_id from djangocms_text_ckeditor_text as a - JOIN cms_cmsplugin as b on a.cmsplugin_ptr_id = b.id - JOIN cms_page_placeholders as c on b.placeholder_id = c.placeholder_id - JOIN cms_title as d on c.page_id = d.page_id - where d.publisher_state = 1 - and d.publisher_is_draft = false - """ - texts = Text.objects.raw(sql) - ids = [] - for text in texts: - ids.append(text.cmsplugin_ptr_id) - queryset = Text.objects.filter(id__in=ids) - return queryset + queryset = Title.objects.public().filter( + Q(page__publication_date__lt=timezone.now()) | Q(page__publication_date__isnull=True), + Q(page__publication_end_date__gte=timezone.now()) | Q(page__publication_end_date__isnull=True), + Q(redirect__exact='') | Q(redirect__isnull=True) + ).select_related('page').distinct() - def prepare_body(self, obj): - obj.body = strip_tags(obj.body) - return obj.body + # queryset = Title.objects.public().all() + # queryset = Page.objects.published().filter(publisher_is_draft=False).distinct() + return queryset def prepare(self, obj): - # append slug, page_id and path - self.prepared_data = super(TextPluginIndex, self).prepare(obj) - sql = """ - select d.id, d.slug, d.page_id, d.path from djangocms_text_ckeditor_text as a - JOIN cms_cmsplugin as b on a.cmsplugin_ptr_id = b.id - JOIN cms_page_placeholders as c on b.placeholder_id = c.placeholder_id - JOIN cms_title as d on c.page_id = d.page_id - where d.publisher_state = 1 and a.cmsplugin_ptr_id = %s - """ - titles = Title.objects.raw(sql, [obj.cmsplugin_ptr_id]) - for title in titles: - logger.info(title) - self.prepared_data['slug'] = title.slug - self.prepared_data['page_id'] = title.page_id - self.prepared_data['url'] = title.path - return self.prepared_data - + #logger.info(self.prepared_data) + page = obj.page + rf = RequestFactory() + request = rf.get("/") + request.session = {} + request.LANGUAGE_CODE = settings.LANGUAGE_CODE + self.prepared_data = super(TextPluginIndex, self).prepare(page) + plugins = CMSPlugin.objects.filter(placeholder__in=obj.page.placeholders.all()) + text = u'' + for base_plugin in plugins: + instance, plugin_type = base_plugin.get_plugin_instance() + if instance is None: + # this is an empty plugin + continue + if hasattr(instance, 'search_fields'): + text += u' '.join(force_unicode(strip_tags(getattr(instance, field, ''))) for field in instance.search_fields) + if getattr(instance, 'search_fulltext', False) or getattr(plugin_type, 'search_fulltext', False): + text += _strip_tags(instance.render_plugin(context=RequestContext(request))) + u' ' + text += page.get_meta_description() or u'' + text += u' ' + # text += obj.get_meta_keywords() or u'' + self.prepared_data['text'] = text + self.prepared_data["body"] = text + self.prepared_data["slug"] = obj.slug + self.prepared_data["url"] = obj.path + self.prepared_data["title"] = obj.title - def get_model(self): - return Text + # self.prepared_data['language'] = self._language + return self.prepared_data + diff --git a/designsafe/apps/search/static/designsafe/apps/search/html/searchListing.html b/designsafe/apps/search/static/designsafe/apps/search/html/searchListing.html index 76b19fe918..bbf9ce2e71 100644 --- a/designsafe/apps/search/static/designsafe/apps/search/html/searchListing.html +++ b/designsafe/apps/search/static/designsafe/apps/search/html/searchListing.html @@ -29,7 +29,7 @@

    Web Content

    -

    {{data.slug}}

    +

    {{data.title}}

    diff --git a/designsafe/apps/search/static/designsafe/apps/search/scripts/app.js b/designsafe/apps/search/static/designsafe/apps/search/scripts/app.js index b75a31e408..f5052326fa 100644 --- a/designsafe/apps/search/static/designsafe/apps/search/scripts/app.js +++ b/designsafe/apps/search/static/designsafe/apps/search/scripts/app.js @@ -26,7 +26,7 @@ $scope.data.type_filter = null; $scope.filetype_filter = 'all'; $scope.searching = false; - $scope.inital_q = $location.search().q + $scope.inital_q = $location.search().q; $scope.search = function(reset){ arguments.length ? reset = true : reset= false; @@ -63,19 +63,19 @@ $scope.next = function () { $scope.page_num = $scope.page_num + 1; $scope.search(); - } + }; $scope.prev = function () { $scope.page_num--; if ($scope.page_num < 0) $scope.page_num = 0; $scope.search(); - } + }; $scope.select_page = function (page_num) { $scope.page_num = page_num; $scope.offset = page_num * $scope.results_per_page; $scope.search(); - } + }; if ($scope.inital_q) { $scope.data.search_text = $scope.inital_q; diff --git a/designsafe/apps/workspace/static/designsafe/apps/workspace/html/application-form.html b/designsafe/apps/workspace/static/designsafe/apps/workspace/html/application-form.html index a529abe7e8..1a970b27b5 100644 --- a/designsafe/apps/workspace/static/designsafe/apps/workspace/html/application-form.html +++ b/designsafe/apps/workspace/static/designsafe/apps/workspace/html/application-form.html @@ -6,10 +6,7 @@

    Select an app

    This initial version of the Discovery Workspace allows users to perform simulations and analyze data using popular open source simulation codes OpenSees, ADCIRC, and OpenFOAM, as well as commercial tools such as - MATLAB (software license verification required). The selection of codes and - tools will continue to be expanded as seen at the - Workbench Roadmap. -

    + MATLAB (software license verification required).

    diff --git a/designsafe/apps/workspace/static/designsafe/apps/workspace/html/data-browser.html b/designsafe/apps/workspace/static/designsafe/apps/workspace/html/data-browser.html index 7ed586eaa8..32fd18ddd3 100644 --- a/designsafe/apps/workspace/static/designsafe/apps/workspace/html/data-browser.html +++ b/designsafe/apps/workspace/static/designsafe/apps/workspace/html/data-browser.html @@ -29,7 +29,7 @@

    Data Depot Browser


    - + + + + + + + + + + + + + + + + + + + diff --git a/designsafe/static/images/ds-icons/icon-greek.png b/designsafe/static/images/ds-icons/icon-greek.png new file mode 100755 index 0000000000..6a497bf896 Binary files /dev/null and b/designsafe/static/images/ds-icons/icon-greek.png differ diff --git a/designsafe/static/scripts/data-depot/app.js b/designsafe/static/scripts/data-depot/app.js index 70fee0a5aa..01c53a5537 100644 --- a/designsafe/static/scripts/data-depot/app.js +++ b/designsafe/static/scripts/data-depot/app.js @@ -8,7 +8,8 @@ 'ds.notifications', 'ds.wsBus', 'toastr', - 'logging' + 'logging', + 'ui.customSelect' ); function config($httpProvider, $locationProvider, $stateProvider, $urlRouterProvider, $urlMatcherFactoryProvider, Django, toastrConfig, UserService) { @@ -340,6 +341,35 @@ }] } }) + .state('publishedData', { + url: '/public/designsafe.storage.published/{filePath:any}', + controller: 'PublishedDataCtrl', + templateUrl: '/static/scripts/data-depot/templates/published-data-listing.html', + params: { + systemId: 'designsafe.storage.published', + filePath: '', + }, + resolve: { + 'listing': ['$stateParams', 'DataBrowserService', function($stateParams, DataBrowserService){ + var systemId = $stateParams.systemId || 'designsafe.storage.published'; + var filePath = $stateParams.filePath; + DataBrowserService.apiParams.fileMgr = 'published'; + DataBrowserService.apiParams.baseUrl = '/api/public/files'; + DataBrowserService.apiParams.searchState = undefined; + return DataBrowserService.browse({system: systemId, path: filePath}); + }], + 'auth': function($q){ + return true; + }, + userAuth: ['UserService', function (UserService) { + return UserService.authenticate().then(function (resp) { + return true; + }, function (err) { + return false; + }); + }] + } + }) .state('trainingMaterials', { url: '/training/', template: '
    local/trainingMaterials.html
    ' diff --git a/designsafe/static/scripts/data-depot/controllers/data-depot-nav.js b/designsafe/static/scripts/data-depot/controllers/data-depot-nav.js index 8922b5c445..e689fddb12 100644 --- a/designsafe/static/scripts/data-depot/controllers/data-depot-nav.js +++ b/designsafe/static/scripts/data-depot/controllers/data-depot-nav.js @@ -13,12 +13,14 @@ { name: 'Published', collapsible: false, - state: 'publicData' + state: 'publicData', + description: "Curated data/projects with DOI's" }, { name: 'Community Data', collapsible: false, - state: 'communityData' + state: 'communityData', + description: 'Non-curated user-contributed data' }/*, { name: 'Training Materials', @@ -32,12 +34,14 @@ { name: 'My Data', collapsible: false, - state: 'myData' + state: 'myData', + description: 'Private directory for your data' }, { name: 'My Projects', collapsible: false, - state: 'projects' + state: 'projects', + description: 'Group access to shared directories' },/* { name: 'My Publications', @@ -47,17 +51,20 @@ { name: 'Shared with Me', collapsible: false, - state: 'sharedData' + state: 'sharedData', + description: 'Data other users shared with me' }, { name: 'Box.com', collapsible: false, - state: 'boxData' + state: 'boxData', + description: 'Access to my Box files for copying' }, { name: 'Dropbox.com', collapsible: false, - state: 'dropboxData' + state: 'dropboxData', + description: 'Access to my Dropbox for copying' } ); diff --git a/designsafe/static/scripts/data-depot/controllers/data-depot-toolbar.js b/designsafe/static/scripts/data-depot/controllers/data-depot-toolbar.js index 741a2366ad..53f33b66b4 100644 --- a/designsafe/static/scripts/data-depot/controllers/data-depot-toolbar.js +++ b/designsafe/static/scripts/data-depot/controllers/data-depot-toolbar.js @@ -45,7 +45,10 @@ DataBrowserService.previewImages($scope.browser.listing); }, viewMetadata: function () { - DataBrowserService.viewMetadata($scope.browser.selected[0], $scope.browser.listing); + DataBrowserService.viewMetadata($scope.browser.selected, $scope.browser.listing); + }, + viewCategories: function() { + DataBrowserService.viewCategories($scope.browser.selected, $scope.browser.listing); }, share: function () { DataBrowserService.share($scope.browser.selected[0]); diff --git a/designsafe/static/scripts/data-depot/controllers/projects.js b/designsafe/static/scripts/data-depot/controllers/projects.js index b34af61748..cf1079d2db 100644 --- a/designsafe/static/scripts/data-depot/controllers/projects.js +++ b/designsafe/static/scripts/data-depot/controllers/projects.js @@ -68,16 +68,80 @@ }]); - app.controller('ProjectViewCtrl', ['$scope', '$state', 'Django', 'ProjectService', 'DataBrowserService', 'projectId', function ($scope, $state, Django, ProjectService, DataBrowserService, projectId) { + app.controller('ProjectViewCtrl', ['$scope', '$state', 'Django', 'ProjectService', 'ProjectEntitiesService', 'DataBrowserService', 'projectId', 'FileListing', '$uibModal', '$q', '$http', function ($scope, $state, Django, ProjectService, ProjectEntitiesService, DataBrowserService, projectId, FileListing, $uibModal, $q, $http) { $scope.data = {}; + $scope.state = DataBrowserService.state(); + + function setEntitiesRel(resp){ + $scope.data.project.setEntitiesRel(resp); + return resp; + } ProjectService.get({uuid: projectId}).then(function (project) { $scope.data.project = project; + DataBrowserService.state().project = project; + DataBrowserService.state().loadingEntities = true; + $scope.data.loadingEntities = true; + var _related = project._related; + var tasks = []; + for (var attrname in _related){ + var name = _related[attrname]; + if (name != 'designsafe.file'){ + tasks.push(ProjectEntitiesService.listEntities( + {uuid: projectId, name: name}) + .then(setEntitiesRel) + ); + } + } + $q.all(tasks).then( + function(resp){ + //$scope.data.project.setupAllRels(); + return resp; + }).then( + function(resp){ + DataBrowserService.state().loadingEntities = false; + $scope.data.loadingEntities = false; + }, function(err){ + DataBrowserService.state().loadingEntities = false; + $scope.data.loadingEntities = false; + }).then(function(){ + ProjectService.getCollaborators({uuid:DataBrowserService.state().project.uuid}).then(function(resp){ + DataBrowserService.state().project.value.teamMembers = _.without(resp.data.teamMembers, 'ds_admin'); + }); + } + ); }); + $scope.showText = function(text){ + $uibModal.open({ + template: '' + + '' + + '', + controller: ['$scope', '$uibModalInstance', function($scope, $uibModalInstance){ + $scope.text = text; + $scope.close = function(){ + $uibModalInstance.dismiss('Close'); + }; + }] + }); + }; + $scope.editProject = function($event) { - $event.preventDefault(); + if ($event){ + $event.preventDefault(); + } ProjectService.editProject($scope.data.project) .then(function (project) { $scope.data.project = project; @@ -85,25 +149,261 @@ }; $scope.manageCollabs = function($event) { - $event.preventDefault(); - ProjectService.manageCollaborators({uuid: projectId}).then(function (project) { - $scope.data.project = project; + if ($event){ + $event.preventDefault(); + } + ProjectService.manageCollaborators($scope.data.project).then(function (res) { + + // $scope.data.project.pi = res.data.pi; + // $scope.data.project.coPis = res.data.coPis; + // $scope.data.project.teamMembers = res.data.teamMembers; + }); + }; + + $scope.manageExperiments = function($event) { + if ($event){ + $event.preventDefault(); + } + var experimentsAttr = $scope.data.project.getRelatedAttrName('designsafe.project.experiment'); + var experiments = $scope.data.project[experimentsAttr]; + if (typeof experiments === 'undefined'){ + $scope.data.project[experimentsAttr] = []; + experiments = $scope.data.project[experimentsAttr]; + } + ProjectService.manageExperiments({'experiments': experiments, + 'project': $scope.data.project}).then(function (experiments) { + $scope.data.experiments = experiments; + }); + }; + + $scope.dateString = function(s){ + var d = Date(s); + return d; + }; + + $scope.showListing = function(){ + //DataBrowserService.state().showMainListing = true; + //DataBrowserService.state().showPreviewListing = false; + DataBrowserService.showListing(); + }; + + $scope.showPreview = function(){ + //DataBrowserService.state().showMainListing = false; + //DataBrowserService.state().showPreviewListing = true; + $scope.previewHref = undefined; + DataBrowserService.showPreview(); + FileListing.get({'system': $scope.browser.listing.system, + 'name': 'projectimage.jpg', + 'path': '/projectimage.jpg'}).then(function(list){ + list.preview().then(function(data){ + $scope.previewHref = data.postit; + }); + }); + }; + + $scope.publishPipeline_start = function(){ + $scope.state.publishPipeline = 'select'; + }; + + $scope.publishPipeline_review = function(){ + $scope.state.publishPipeline = 'review'; + }; + + $scope.publishPipeline_meta = function(){ + $scope.state.publishPipeline = 'meta'; + }; + + $scope.publishPipeline_exit = function(){ + $scope.state.publishPipeline = undefined; + }; + + $scope.publishPipeline_prev = function(st){ + if (st == 'agreement'){ + $scope.state.publishPipeline = 'meta'; + } else if (st == 'meta'){ + $scope.state.publishPipeline = 'review'; + } + else if (st == 'review'){ + $scope.state.publishPipeline = 'select'; + } + else { + $scope.state.publishPipeline = 'select'; + } + }; + + $scope.publishPipeline_next = function(st){ + if (st == 'select'){ + $scope.state.publishPipeline = 'review'; + } + else if (st == 'review'){ + var institutions = []; + _.each($scope.state.publication.experimentsList, function(exp){ + var o = { + label: exp.getEF($scope.state.project.value.projectType, + exp.value.experimentalFacility).institution, + name: exp.value.experimentalFacility + }; + institutions.push(o); + }); + _.each($scope.state.publication.users, function(user){ + institutions.push({ label: user.profile.institution, + name: user.username}); + }); + $scope.state.publication.institutions = _.uniq(institutions, function(inst){ return inst.label;}); + $scope.state.publishPipeline = 'meta'; + } + else if (st == 'meta'){ + $scope.state.publishPipeline = 'agreement'; + }else { + $scope.state.publishPipeline = 'agreement'; + } + }; + + $scope.publishPipeline_publish = function(){ + var publication = angular.copy($scope.state.publication); + var experimentsList = []; + var eventsList = []; + var analysisList = []; + var reportsList = []; + var modelConfigs = []; + var sensorLists = []; + if (publication.experimentsList){ + experimentsList = _.map(publication.experimentsList, function(exp){ + exp.value.equipmentType = exp.getET(exp.value.experimentalFacility, + exp.value.equipmentType).label; + exp.value.experimentalFacility = exp.getEF($scope.state.project + .value.projectType, + exp.value.experimentalFacility).label; + exp.events = $scope.state.publication; + delete exp._ui; + delete exp.events; + return exp; + }); + delete publication.experimentsList; + } + if (publication.eventsList){ + var _eventsList = angular.copy(publication.eventsList); + delete publication.eventsList; + var expsUuids = _.map(experimentsList, function(exp){ + return exp.uuid; }); + eventsList = _.filter(_eventsList, + function(evt){ + return _.intersection(evt.associationIds, expsUuids); + }); + var mcfsUuids = []; + var slsUuids = []; + _.each(eventsList, function(evt){ + mcfsUuids = mcfsUuids.concat(evt.value.modelConfigs); + slsUuids = slsUuids.concat(evt.value.sensorLists); + delete evt.tagsAsOptions; + evt.fileObjs = _.map($scope.state.listings[evt.uuid], function(f){ + return { + 'path': f.path, + 'type': f.type, + 'length': f.length, + 'name': f.name + }; + }); + }); + _.each(mcfsUuids, function(mcf){ + var _mcf = angular.copy($scope.state.project.getRelatedByUuid(mcf)); + delete _mcf.tagsAsOptions; + _mcf.fileObjs = _.map($scope.state.listings[_mcf.uuid], function(f){ + return { + 'path': f.path, + 'type': f.type, + 'length': f.length, + 'name': f.name + }; + }); + modelConfigs.push(_mcf); + }); + _.each(slsUuids, function(slt){ + var _slt = angular.copy($scope.state.project.getRelatedByUuid(slt)); + delete _slt.tagsAsOptions; + _slt.fileObjs = _.map($scope.state.listings[_slt.uuid], function(f){ + return { + 'path': f.path, + 'type': f.type, + 'length': f.length, + 'name': f.name + }; + }); + sensorLists.push(_slt); + }); + } + if (publication.analysisList){ + analysisList = _.map(publication.analysisList, function(ana){ + delete ana.tagsAsOptions; + ana.fileObjs = _.map($scope.state.listings[ana.uuid], function(f){ + return { + 'path': f.path, + 'type': f.type, + 'length': f.length, + 'name': f.name + }; + }); + return ana; + }); + delete publication.analysisList; + } + if (publication.reportsList) { + reportsList = _.map(publication.reportsList, function(rep){ + rep.fileObjs = _.map($scope.state.listings[rep.uuid], function(f){ + return { + 'path': f.path, + 'type': f.type, + 'length': f.length, + 'name': f.name + }; + }); + return rep; + }); + delete publication.reportsList; + } + var project = angular.copy($scope.state.project); + delete project._allRelatedObjects; + _.each(project._related, function(val, key){ + delete project[key]; }); + delete publication.filesSelected; + publication.project = project; + publication.eventsList = eventsList; + publication.modelConfigs = modelConfigs; + publication.sensorLists = sensorLists; + publication.analysisList = analysisList; + publication.reportsList = reportsList; + publication.experimentsList = experimentsList; + $http.post('/api/projects/publication/', {publication: publication}) + .then(function(resp){ + $scope.state.publicationMsg = resp.data.message; + $scope.state.project.publicationStatus = 'publishing'; + DataBrowserService.state().publicationMsg = resp.data.message; + DataBrowserService.state().project.publicationStatus = 'publishing'; + }); }; }]); - app.controller('ProjectDataCtrl', ['$scope', '$state', 'Django', 'ProjectService', 'DataBrowserService', 'projectId', 'filePath', 'projectTitle', function ($scope, $state, Django, ProjectService, DataBrowserService, projectId, filePath, projectTitle) { + app.controller('ProjectDataCtrl', ['$scope', '$state', 'Django', 'ProjectService', 'DataBrowserService', 'projectId', 'filePath', 'projectTitle', 'FileListing', 'UserService', '$uibModal', '$http', '$q', function ($scope, $state, Django, ProjectService, DataBrowserService, projectId, filePath, projectTitle, FileListing, UserService, $uibModal, $http, $q) { DataBrowserService.apiParams.fileMgr = 'agave'; DataBrowserService.apiParams.baseUrl = '/api/agave/files'; DataBrowserService.apiParams.searchState = undefined; $scope.browser = DataBrowserService.state(); + $scope.browser.listings = {}; + $scope.browser.ui = {}; + $scope.browser.publication = {experimentsList: [], eventsList: [], + users: [], analysisList: [], + filesSelected: []}; if (typeof $scope.browser !== 'undefined'){ $scope.browser.busy = true; } + DataBrowserService.browse({system: 'project-' + projectId, path: filePath}) .then(function () { $scope.browser = DataBrowserService.state(); + $scope.browser.busy = true; + $scope.browser.busyListing = true; $scope.browser.listing.href = $state.href('projects.view.data', { projectId: projectId, filePath: $scope.browser.listing.path, @@ -116,11 +416,152 @@ projectTitle: projectTitle }); }); + if (typeof $scope.browser.loadingEntities !== 'undefined' && + !$scope.browser.loadingEntities){ + var entities = $scope.browser.project.getAllRelatedObjects(); + _.each($scope.browser.listing.children, function(child){ + child.setEntities(DataBrowserService.state().project.uuid, entities); + }); + } else { + $scope.$watch('browser.loadingEntities', function(newVal, oldVal){ + if (!newVal){ + var entities = $scope.browser.project.getAllRelatedObjects(); + _.each($scope.browser.listing.children, function(child){ + child.setEntities($scope.browser.project.uuid, entities); + }); + //var _state = DataBrowserService.state(); + //_state.project.setupAllRels(); + //$scope.browser = _state; + } + }); + } + }).then(function(){ + $http.get('/api/projects/publication/' + $scope.browser.project.value.projectId) + .then(function(resp){ + if (resp.data.project && resp.data.project.doi){ + $scope.browser.project.doi = resp.data.project.doi; + DataBrowserService.state().project.doi = resp.data.project.doi; + } + if (resp.data.project && resp.data.status){ + $scope.browser.project.publicationStatus = resp.data.status; + DataBrowserService.state().project.publicationStatus = resp.data.status; + } + $scope.browser.busy = false; + $scope.browser.busyListing = false; + }, function(){ + $scope.browser.busy = false; + $scope.browser.busyListing = false; + }); }); - $scope.onBrowseData = function onBrowseData($event, file) { + var setFilesDetails = function(filePaths){ + filePaths = _.uniq(filePaths); + var p = $q(function(resolve, reject){ + var results = []; + var index = 0; + var size = 5; + var fileCalls = _.map(filePaths, function(filePath){ + return FileListing.get({system: 'project-' + projectId, + path: filePath}, + DataBrowserService.apiParams) + .then(function(resp){ + var allEntities = $scope.browser.project.getAllRelatedObjects(); + var entities = _.filter(allEntities, function(entity){ + return _.contains(entity._filePaths, resp.path); + }); + _.each(entities, function(entity){ + $scope.browser.listings[entity.uuid].push(resp); + }); + return resp; + }); + }); + + function step(){ + var calls = fileCalls.slice(index, index += size); + if(calls.length){ + $q.all(calls).then(function(res){ + results.concat(res); + step(); + return res; + }).catch(reject); + } else { + resolve(results); + } + } + step(); + }); + return p.then(function(results){ + return results; + }, function(err){ + $scope.browser.ui.error = err; + }); + }; + + var setUserDetails = function(usernames){ + $scope.browser.publication.users = []; + filePaths = _.uniq(usernames); + var p = $q(function(resolve, reject){ + var results = []; + var index = 0; + var size = 5; + var userIndex = 0; + var calls = _.map(usernames, function(username){ + return UserService.get(username) + .then(function(resp){ + resp._ui = {order:userIndex}; + $scope.browser.publication.users.push(resp); + userIndex += 1; + }); + }); + + function step(){ + var _calls = calls.slice(index, index += size); + if(_calls.length){ + $q.all(_calls).then(function(res){ + results.concat(res); + step(); + return res; + }).catch(reject); + } else { + resolve(results); + } + } + step(); + }); + $scope.browser.ui.loadingPreview = true; + return p.then(function(results){ + $scope.browser.publication.users = _.uniq($scope.browser.publication.users, function(obj){return obj.username;}); + return results; + }, function(err){ + $scope.browser.ui.error = err; + }); + }; + + $scope.$watch('browser.showPreviewListing', function(newVal, oldVal){ + if (newVal){ + $scope.browser.ui.loadingListings = true; + $scope.browser.listings = {}; + var entities = $scope.browser.project.getAllRelatedObjects(); + var allFilePaths = []; + _.each(entities, function(entity){ + $scope.browser.listings[entity.uuid] = []; + allFilePaths = allFilePaths.concat(entity._filePaths); + }); + $scope.data.rootPaths = allFilePaths; + setFilesDetails(allFilePaths) + .then(function(){ + users = [$scope.browser.project.value.pi] + .concat($scope.browser.project.value.coPis) + .concat($scope.browser.project.value.teamMembers); + return setUserDetails(users); + }).then(function(){ + }); + } + }); + $scope.onBrowseData = function onBrowseData($event, file) { $event.preventDefault(); + DataBrowserService.showListing(); if (file.type === 'file') { DataBrowserService.preview(file, $scope.browser.listing); } else { @@ -168,5 +609,341 @@ DataBrowserService.preview(file, $scope.browser.listing); }; + $scope.openPreviewTree = function($event, entityUuid){ + $event.preventDefault(); + $event.stopPropagation(); + DataBrowserService.openPreviewTree(entityUuid); + }; + + function _addToLists(ent, evt){ + if (ent.name === 'designsafe.project.experiment'){ + $scope.browser.publication.experimentsList.push(ent); + $scope.browser.publication.experimentsList = _.uniq($scope.browser.publication.experimentsList, function(e){return e.uuid;}); + $scope.browser.publication.eventsList.push(evt); + $scope.browser.publication.eventsList = _.uniq($scope.browser.publication.eventsList, function(e){return e.uuid;}); + } else if (ent.name === 'designsafe.project.analysis'){ + $scope.browser.publication.analysisList.push(ent); + $scope.browser.publication.analysisList = _.uniq($scope.browser.publication.analysisList, function(e){return e.uuid;}); + } else if (ent.name === 'designsafe.project.report'){ + $scope.browser.publication.reportsList.push(ent); + $scope.browser.publication.reportsList = _.uniq($scope.browser.publication.reportsList, function(e){return e.uuid;}); + } + } + + function _removeFromLists(ent, evt){ + if (ent && ent.name == 'designsafe.project.experiment'){ + $scope.browser.publication.experimentsList = _.filter($scope.browser.publication.experimentsList, function(e){ return e.uuid !== ent.uuid;}); + }else if (ent && ent.name == 'designsafe.project.analysis'){ + $scope.browser.publication.analysisList = _.filter($scope.browser.publication.analysisList, function(e){ return e.uuid !== ent.uuid;}); + }else if (ent && ent.name == 'designsafe.project.report'){ + $scope.browser.publication.reportsList = _.filter($scope.browser.publication.reportsList, function(e){ return e.uuid !== ent.uuid;}); + } + + if (evt){ + $scope.browser.publication.eventsList = _.filter($scope.browser.publication.eventsList, function(e){ return evt.uuid !== e.uuid;}); + } + } + + $scope.editProject = function() { + ProjectService.editProject($scope.browser.project) + .then(function (project) { + $scope.browser.project = project; + }); + }; + + $scope.manageCollabs = function() { + ProjectService.manageCollaborators($scope.browser.project).then(function (project) { + $scope.browserproject = project; + }); + }; + + $scope.manageExperiments = function() { + var experimentsAttr = $scope.browser.project.getRelatedAttrName('designsafe.project.experiment'); + var experiments = $scope.browser.project[experimentsAttr]; + if (typeof experiments === 'undefined'){ + $scope.browser.project[experimentsAttr] = []; + experiments = $scope.browser.project[experimentsAttr]; + } + ProjectService.manageExperiments({'experiments': experiments, + 'project': $scope.browser.project}).then(function (experiments) { + $scope.browser.experiments = experiments; + }); + }; + + var _editFieldModal = function(objArr, title, fields, classes){ + modal = $uibModal.open({ + template : '' + + '' + + '', + controller: ['$scope', '$uibModalInstance', function($scope, $uibModalInstance){ + $scope.ui = {fields: fields, + classes: classes, + title: title, + form: {}}; + $scope.data = {objArr: angular.copy(objArr)}; + + $scope.close = function(){ + $uibModalInstance.dismiss('Cancel'); + }; + + $scope.save = function(){ + $uibModalInstance.close($scope.data.objArr); + }; + }] + }); + return modal; + }; + + var _publicationCtrl = { + + filterUsers: function(usernames, users){ + return _.filter(users, function(usr){ + return _.contains(usernames, usr.username); + }); + }, + + showText : function(text){ + $uibModal.open({ + template: '' + + '' + + '', + controller: ['$scope', '$uibModalInstance', function($scope, $uibModalInstance){ + $scope.text = text; + $scope.close = function(){ + $uibModalInstance.dismiss('Close'); + }; + }] + }); + }, + + moveOrderUp: function($index, ent, list){ + if (typeof ent._ui.order === 'undefined'){ + ent._ui.order = 0; + } else if (ent._ui.order > 0){ + var order = ent._ui.order; + var _ent = _.find(list, function(e){ + return e._ui.order === order - 1; }); + ent._ui.order -= 1; + _ent._ui.order += 1; + } + }, + + moveOrderDown: function($index, ent, list){ + if (typeof ent._ui.order === 'undefined'){ + ent._ui.order = 0; + } else if (ent._ui.order < list.length - 1){ + var _ent = _.find(list, function(e){ + return e._ui.order === ent._ui.order + 1; }); + ent._ui.order += 1; + _ent._ui.order -= 1; + } + }, + + openEditProject : function(){ + $scope.editProject(); + }, + + openEditExperiments: function(){ + $scope.manageExperiments(); + }, + + openEditTeamMembers: function(){ + $scope.manageCollabs(); + }, + + openEditCategories: function(){ + DataBrowserService.viewCategories(); + }, + + selectAllFiles : function(ent, evt){ + var listing = []; + if (ent.name === 'designsafe.project.experiment'){ + listing = $scope.browser.listings[evt.uuid]; + } else { + listing = $scope.browser.listings[ent.uuid]; + } + if (typeof $scope.browser.publication.filesSelected[ent.uuid] === 'undefined'){ + $scope.browser.publication.filesSelected[ent.uuid] = {}; + } + if (ent.name === 'designsafe.project.experiment'){ + var files = listing; + $scope.browser.publication.filesSelected[ent.uuid][evt.uuid] = files; + _addToLists(ent, evt); + } else { + $scope.browser.publication.filesSelected[ent.uuid] = listing; + _addToLists(ent); + } + }, + + deselectAllFiles : function(ent, evt){ + if (ent.name === 'designsafe.project.experiment'){ + $scope.browser.publication.filesSelected[ent.uuid][evt.uuid] = []; + delete $scope.browser.publication.filesSelected[ent.uuid][evt.uuid]; + if (_.isEmpty($scope.browser.publication.filesSelected[ent.uuid])){ + delete $scope.browser.publication.filesSelected[ent.uuid]; + _removeFromLists(ent, evt); + } else { + _removeFromLists(ent, evt); + } + } else { + $scope.browser.publication.filesSelected[ent.uuid] = []; + delete $scope.browser.publication.filesSelected[ent.uuid]; + _removeFromLists(ent); + } + }, + + isFileSelectedForPublication : function(ent, evt, file){ + if (typeof $scope.browser.publication.filesSelected[ent.uuid] === 'undefined'){ + $scope.browser.publication.filesSelected[ent.uuid] = {}; + } + var files = []; + if(ent.name === 'designsafe.project.experiment'){ + files = $scope.browser.publication.filesSelected[ent.uuid][evt.uuid] || []; + return _.find(files, function(f){ return f.uuid() === file.uuid(); }); + } else { + files = $scope.browser.publication.filesSelected[ent.uuid] || []; + return _.find(files, function(f){ return f.uuid() === file.uuid(); }); + } + }, + + deselectFileForPublication : function(ent, evt, file){ + var files = []; + if (ent.name === 'designsafe.project.experiment'){ + files = $scope.browser.publication.filesSelected[ent.uuid][evt.uuid]; + } else { + files = $scope.browser.publication.filesSelected[ent.uuid]; + } + files = _.reject(files, function(f){ return f.uuid() === file.uuid(); }); + if (ent.name === 'designsafe.project.experiment'){ + $scope.browser.publication.filesSelected[ent.uuid][evt.uuid] = files; + if (!$scope.browser.publication.filesSelected[ent.uuid][evt.uuid].length){ + delete $scope.browser.publication.filesSelected[ent.uuid][evt.uuid]; + if (_.isEmpty($scope.browser.publication.filesSelected[ent.uuid])){ + _removeFromLists(ent, evt); + }else { + _removeFromLists(ent, evt); + } + } + } else { + $scope.browser.publication.filesSelected[ent.uuid] = files; + if (!$scope.browser.publication.filesSelected[ent.uuid].length){ + delete $scope.browser.publication.filesSelected[ent.uuid]; + if (_.isEmpty($scope.browser.publication.filesSelected[ent.uuid])){ + _removeFromLists(ent); + } + } + } + }, + + selectFileForPublication : function(ent, evt, file){ + if (typeof $scope.browser.publication[ent.uuid] === 'undefined'){ + $scope.browser.publication[ent.uuid] = {}; + } + var files = []; + if (ent.name === 'designsafe.project.experiment'){ + files = $scope.browser.publication[ent.uuid][evt.uuid]; + } else { + files = $scope.browser.publication[ent.uuid]; + } + if (typeof files == 'undefined'){ + files = []; + } + files.push(file); + if (ent.name === 'designsafe.project.experiment'){ + $scope.browser.publication[ent.uuid][evt.uuid] = files; + } else { + $scope.browser.publication[ent.uuid] = files; + } + _addToLists(ent, evt); + }, + + filterExperiments : function(experiments){ + if(!$scope.browser.publishPipeline || $scope.browser.publishPipeline === 'select'){ + return experiments; + } else { + return $scope.browser.publication.experimentsList; + } + }, + + filterEvents : function(events, exp){ + if(!$scope.browser.publishPipeline || $scope.browser.publishPipeline === 'select'){ + return events; + } else { + return _.filter($scope.browser.publication.eventsList, + function(evt){ + return _.contains(evt.associationIds, exp.uuid); + }); + } + }, + + filterFiles : function(ent, evt, listing){ + if(!$scope.browser.publishPipeline || $scope.browser.publishPipeline === 'select'){ + return listing; + } else if (ent.name === 'designsafe.project.experiment') { + return $scope.browser.publication.filesSelected[ent.uuid][evt.uuid]; + } else { + return $scope.browser.publication.filesSelected[ent.uuid]; + } + }, + + editUsers : function(){ + fields = [ + {id: 'last_name', name: 'last_name', label: 'Last Name', uniq: 'username', type: 'text'}, + {id: 'first_name', name: 'first_name', label: 'First Name', uniq: 'username', type: 'text'} + ]; + classes = { + 'first_name': 'col-md-6', + 'last_name': 'col-md-6' + }; + modal = _editFieldModal($scope.browser.publication.users, 'Edit Users', fields, classes); + + modal.result.then(function(respArr){ + $scope.browser.publication.users = respArr; + }); + }, + + editInsts : function(){ + fields = [{id:'label', name:'label', label:'Institution', uniq: 'name', type:'text'}]; + modal = _editFieldModal($scope.browser.publication.institutions, 'Edit Institutions', fields); + + modal.result.then(function(respArr){ + $scope.browser.publication.institutions = respArr; + }); + }, + + togglePubAgreement : function() { + $scope.state.publishAgreement = !$scope.state.publishAgreement; + } + }; + + $scope.publicationCtrl = _publicationCtrl; + }]); })(window, angular); diff --git a/designsafe/static/scripts/data-depot/controllers/publications.js b/designsafe/static/scripts/data-depot/controllers/publications.js index eaa85edf2d..eb8cfbfd1b 100644 --- a/designsafe/static/scripts/data-depot/controllers/publications.js +++ b/designsafe/static/scripts/data-depot/controllers/publications.js @@ -56,7 +56,11 @@ if (file.type === 'file'){ DataBrowserService.preview(file, $scope.browser.listing); } else { - $state.go('publicData', {systemId: file.system, filePath: file.path}); + if (file.system === 'nees.public'){ + $state.go('publicData', {systemId: file.system, filePath: file.path}); + } else { + $state.go('publishedData', {systemId: file.system, filePath: file.path}); + } } }; @@ -105,7 +109,11 @@ if (typeof file.metadata === 'undefined' || file.metadata === null || _.isEmpty(file.metadata)){ - return file.name; + if(file.meta && file.meta.title){ + return file.meta.title; + } else { + return file.name; + } } var pathComps = file.path.split('/'); var experiment_re = /^experiment/; diff --git a/designsafe/static/scripts/data-depot/controllers/published.js b/designsafe/static/scripts/data-depot/controllers/published.js new file mode 100644 index 0000000000..52ca1abcde --- /dev/null +++ b/designsafe/static/scripts/data-depot/controllers/published.js @@ -0,0 +1,435 @@ +(function(window, angular) { + var app = angular.module('designsafe'); + app.requires.push('django.context'); + + app.controller('PublishedDataCtrl', ['$scope', '$state', 'Django', + 'DataBrowserService', 'FileListing', + '$uibModal','$http', + function ($scope, $state, Django, DataBrowserService, + FileListing, $uibModal, $http) { + + $scope.browser = DataBrowserService.state(); + $scope.state = { + loadingMore : false, + reachedEnd : false, + page : 0 + }; + var projId = ''; + if ($scope.browser.listing.projectId){ + projId = $scope.browser.listing.projectId; + $scope.project = $scope.browser.listing.project; + } else { + projId = $scope.browser.listing.path.split('/')[1]; + if (projId){ + $http.get('/api/projects/publication/' + projId) + .then(function(resp){ + $scope.browser.publication = resp.data; + $scope.project = resp.data.project; + }); + } + } + FileListing.get({'system': 'designsafe.storage.published', + 'name': 'projectimage.jpg', + 'path': '/' + projId + '/projectimage.jpg'}).then(function(list){ + list.preview().then(function(data){ + $scope.imageHref = data.postit; + }); + }); + $scope.browser.listing.permissions = 'READ'; + if ($scope.browser.listing.projectId){ + _.each($scope.browser.listing.eventsList, function(evt){ + evt.files = _.map(evt.fileObjs, function(f){ + f.system = 'designsafe.storage.published'; + f.path = $scope.browser.listing.projectId + f.path; + f.permissions = 'READ'; + return FileListing.init(f); + }); + }); + _.each($scope.browser.listing.modelConfigs, function(mcf){ + mcf.files = _.map(mcf.fileObjs, function(f){ + f.system = 'designsafe.storage.published'; + f.path = $scope.browser.listing.projectId + f.path; + f.permissions = 'READ'; + return FileListing.init(f); + }); + }); + _.each($scope.browser.listing.sensorLists, function(slt){ + slt.files = _.map(slt.fileObjs, function(f){ + f.system = 'designsafe.storage.published'; + f.path = $scope.browser.listing.projectId + f.path; + f.permissions = 'READ'; + return FileListing.init(f); + }); + }); + _.each($scope.browser.listing.analysisList, function(anl){ + anl.files = _.map(anl.fileObjs, function(f){ + f.system = 'designsafe.storage.published'; + f.path = $scope.browser.listing.projectId + f.path; + f.permissions = 'READ'; + return FileListing.init(f); + }); + }); + _.each($scope.browser.listing.reportsList, function(rep){ + rep.files = _.map(rep.fileObjs, function(f){ + f.system = 'designsafe.storage.published'; + f.path = $scope.browser.listing.projectId + f.path; + f.permissions = 'READ'; + return FileListing.init(f); + }); + }); + } + if (! $scope.browser.error){ + $scope.browser.listing.href = $state.href('publishedData', { + system: $scope.browser.listing.system, + filePath: $scope.browser.listing.path + }); + _.each($scope.browser.listing.children, function (child) { + child.href = $state.href('publishedData', {system: child.system, filePath: child.path}); + }); + } + + $scope.data = { + customRoot: { + name: 'Published', + href: $state.href('publicData', {systemId: 'nees.public', + filePath: 'public/'}) + } + }; + + $scope.resolveBreadcrumbHref = function(trailItem) { + return $state.href('publicData', {systemId: $scope.browser.listing.system, filePath: trailItem.path}); + }; + + $scope.scrollToTop = function(){ + return; + }; + $scope.scrollToBottom = function(){ + DataBrowserService.scrollToBottom(); + }; + + $scope.onBrowse = function($event, file) { + $event.preventDefault(); + $event.stopPropagation(); + + var systemId = file.system || file.systemId; + var filePath; + if (file.path == '/'){ + filePath = file.path + file.name; + } else { + filePath = file.path; + } + if (file.type === 'file'){ + DataBrowserService.preview(file, $scope.browser.listing); + } else { + $state.go('publishedData', {systemId: file.system, filePath: file.path}); + } + }; + + $scope.onSelect = function($event, file) { + $event.preventDefault(); + $event.stopPropagation(); + + if ($event.ctrlKey || $event.metaKey) { + var selectedIndex = $scope.browser.selected.indexOf(file); + if (selectedIndex > -1) { + DataBrowserService.deselect([file]); + } else { + DataBrowserService.select([file]); + } + } else if ($event.shiftKey && $scope.browser.selected.length > 0) { + var lastFile = $scope.browser.selected[$scope.browser.selected.length - 1]; + var lastIndex = $scope.browser.listing.children.indexOf(lastFile); + var fileIndex = $scope.browser.listing.children.indexOf(file); + var min = Math.min(lastIndex, fileIndex); + var max = Math.max(lastIndex, fileIndex); + DataBrowserService.select($scope.browser.listing.children.slice(min, max + 1)); + } else if (typeof file._ui !== 'undefined' && + file._ui.selected){ + DataBrowserService.deselect([file]); + } else { + DataBrowserService.select([file], true); + } + }; + + $scope.showFullPath = function(item){ + if ($scope.browser.listing.path != '$PUBLIC' && + item.parentPath() != $scope.browser.listing.path && + item.parentPath() != '/'){ + return true; + } else { + return false; + } + }; + + $scope.onDetail = function($event, file) { + $event.stopPropagation(); + DataBrowserService.preview(file, $scope.browser.listing); + }; + + $scope.renderName = function(file){ + if (typeof file.metadata === 'undefined' || + file.metadata === null || + _.isEmpty(file.metadata)){ + return file.name; + } + var pathComps = file.path.split('/'); + var experiment_re = /^experiment/; + if (file.path[0] === '/' && pathComps.length === 2) { + return file.metadata.project.title; + } + else if (file.path[0] !== '/' && + pathComps.length === 2 && + experiment_re.test(file.name.toLowerCase())){ + return file.metadata.experiments[0].title; + } + return file.name; + }; + + $scope.getRelated = function(attrib, uuids){ + if (_.isString(uuids)){ + uuids = [uuids]; + } + var ents = []; + if ($scope.browser.listing.projectId){ + ents = $scope.browser.listing[attrib]; + } else { + ents = $scope.browser.publication[attrib]; + } + var res = _.filter(ents, function(ent){ + if (_.intersection(uuids, ent.associationIds).length === uuids.length){ + return ent; + } + }); + return res; + }; + + $scope.getUserDets = function(username, noEmail){ + var users; + if ($scope.browser.listing.projectId){ + users = $scope.browser.listing.users; + } else { + useres = $scope.browser.publication.users; + } + var user = _.find(users, function(usr){ + return usr.username === username; + }); + if (user){ + if (!noEmail){ + return user.last_name + ', ' + user.first_name + ' <' + user.email + '>'; + } else { + return user.last_name + ', ' + user.first_name; + } + } + }; + + $scope.filterUsers = function(usernames, users){ + return _.filter(users, function(usr){ + return _.contains(usernames, usr.username); + }); + }; + + $scope.viewCollabs = function(){ + $uibModal.open({ + templateUrl: '/static/scripts/data-depot/templates/view-collabs.html', + controller: ['$uibModalInstance', 'browser', function($uibModalInstance, browser){ + var $ctrl = this; + $ctrl.data = {}; + if (browser.listing.project){ + $ctrl.data.project = browser.listing.project; + } else { + $ctrl.data.project = browser.publication.project; + } + $ctrl.close = function(){ + $uibModalInstance.dismiss('close'); + }; + }], + controllerAs: '$ctrl', + resolve: { + browser: $scope.browser + }, + scope: $scope + }); + }; + + $scope.viewProject = function(){ + $uibModal.open({ + templateUrl: '/static/scripts/data-depot/templates/view-project.html', + controller: ['$uibModalInstance', 'browser', function($uibModalInstance, browser){ + var $ctrl = this; + $ctrl.data = {}; + if (browser.listing.project){ + $ctrl.data.publication = browser.listing; + } else { + $ctrl.data.publication = browser.publication; + } + $ctrl.close = function(){ + $uibModalInstance.dismiss('close'); + }; + }], + controllerAs: '$ctrl', + resolve: { + browser: $scope.browser + }, + size: 'lg' + }); + }; + + $scope.viewExperiments = function(){ + $uibModal.open({ + templateUrl: '/static/scripts/data-depot/templates/view-experiments.html', + controller: ['$uibModalInstance', 'browser', function($uibModalInstance, browser){ + var $ctrl = this; + $ctrl.data = {}; + if (browser.listing.project){ + $ctrl.data.publication = browser.listing; + } else { + $ctrl.data.publication = browser.publication; + } + $ctrl.close = function(){ + $uibModalInstance.dismiss('close'); + }; + }], + controllerAs: '$ctrl', + resolve: { + browser: $scope.browser + }, + scope: $scope, + size: 'lg' + }); + }; + + $scope.viewRelations = function(uuid){ + $uibModal.open({ + templateUrl: '/static/scripts/data-depot/templates/view-relations.html', + controller: ['$uibModalInstance', 'browser', function($uibModalInstance, browser){ + var $ctrl = this; + $ctrl.data = {}; + if (browser.listing.project){ + $ctrl.data.publication = browser.listing; + } else { + $ctrl.data.publication = browser.publication; + } + $ctrl.data.selectedUuid = uuid; + $ctrl.isSelected = function(uuid){ + if (uuid ===$ctrl.data.selectedUuid){ + return true; + } else { + return false; + } + }; + $ctrl.close = function(){ + $uibModalInstance.dismiss('close'); + }; + }], + controllerAs: '$ctrl', + resolve: { + browser: $scope.browser + }, + scope: $scope, + size: 'lg' + }); + }; + + $scope.showText = function(text){ + $uibModal.open({ + template: '' + + '' + + '', + controller: ['$scope', '$uibModalInstance', function($scope, $uibModalInstance){ + $scope.text = text; + $scope.close = function(){ + $uibModalInstance.dismiss('Close'); + }; + }] + }); + }; + + $scope.showCitation = function(ent){ + $uibModal.open({ + templateUrl: '/static/scripts/data-depot/templates/view-citations.html', + controller: ['$sce', '$uibModalInstance', 'browser', function($sce, $uibModalInstance, browser){ + var $ctrl = this; + $ctrl.data = {}; + $ctrl.ui = {}; + $ctrl.data.ent = ent; + $ctrl.ui.style = 'BibTeX'; + $ctrl.ui.styles = ['BibTeX', 'Endnote']; + var authors = ''; + var ieeeAuthors = ''; + if (browser.listing.project){ + $ctrl.data.publication = browser.listing; + } else { + $ctrl.data.publication = browser.publication; + } + var publishers = _.filter($ctrl.data.publication.users, function(usr){ + if (ent.name === 'designsafe.project' || ent.name === 'designsafe.project.analysis'){ + return _.contains($ctrl.data.publication.project.value.coPis, usr.username) || + usr.username === $ctrl.data.publication.project.value.pi; + } else { + return _.contains(ent.value.authors, usr.username); + } + }); + _.each(publishers, function(usr, index, list){ + var str = usr.last_name + ', ' + usr.first_name; + if (index < list.length - 1){ + authors += str + ' and '; + ieeeAuthors += str + '; '; + } else { + authors += str; + ieeeAuthors += str; + } + }); + $ctrl.getCitation = function(){ + if ($ctrl.ui.style === 'BibTeX'){ + $ctrl.data.citation = + '@misc{dataset, \n' + + ' author = {' + authors + '} \n' + + ' title = {' + ent.value.title + '} \n' + + ' publisher = {DesignSafe-CI} \n' + + ' year = {2017} \n' + + ' note = {' + ent.value.description + '} \n' + + '}'; + } else if ($ctrl.ui.style === 'Endnote'){ + $ctrl.data.citation = + '%0 Generic \n' + + '%A ' + authors + '\n' + + '%T ' + ent.value.title + '\n' + + '%I DesignSafe-CI\n' + + '%D 2017\n'; + } + }; + $ctrl.close = function(){ + $uibModalInstance.dismiss('Close'); + }; + $ctrl.downloadCitation = function(){ + var blob = new Blob([$ctrl.data.citation]); + var downloadLink = $('
    '); + downloadLink.attr('href', window.URL.createObjectURL(blob)); + downloadLink.attr('download', 'citation.' + $ctrl.ui.style); + downloadLink[0].click(); + }; + $ctrl.ui.ieeeCitation = $sce.trustAsHtml(ieeeAuthors + ', (2017), "' + ent.value.title + '" , DesignSafe-CI [publisher], Dataset, ' + ent.doi); + $ctrl.getCitation(); + }], + size: 'md', + controllerAs: '$ctrl', + resolve: { + browser: $scope.browser + }, + scope: $scope + }); +}; + +}]); +})(window, angular); diff --git a/designsafe/static/scripts/data-depot/templates/data-depot-nav.html b/designsafe/static/scripts/data-depot/templates/data-depot-nav.html index 2c4096dc1c..97d3c7445e 100644 --- a/designsafe/static/scripts/data-depot/templates/data-depot-nav.html +++ b/designsafe/static/scripts/data-depot/templates/data-depot-nav.html @@ -4,7 +4,7 @@ ng-class="{'in': !routerItem.collapse}" ng-repeat="childItem in routerItems"> - {{ childItem.name }} diff --git a/designsafe/static/scripts/data-depot/templates/data-depot-toolbar.html b/designsafe/static/scripts/data-depot/templates/data-depot-toolbar.html index 5a57f334f4..d78d1a6483 100644 --- a/designsafe/static/scripts/data-depot/templates/data-depot-toolbar.html +++ b/designsafe/static/scripts/data-depot/templates/data-depot-toolbar.html @@ -10,45 +10,77 @@ diff --git a/designsafe/static/scripts/data-depot/templates/project-data.html b/designsafe/static/scripts/data-depot/templates/project-data.html index 0ebb1310ea..ff2758c161 100644 --- a/designsafe/static/scripts/data-depot/templates/project-data.html +++ b/designsafe/static/scripts/data-depot/templates/project-data.html @@ -4,4 +4,6 @@ on-detail="onDetail" scroll-to-top="scrollToTop" scroll-to-bottom="scrollToBottom" + open-preview-tree="openPreviewTree" + publication-ctrl="publicationCtrl" > diff --git a/designsafe/static/scripts/data-depot/templates/project-view.html b/designsafe/static/scripts/data-depot/templates/project-view.html index 1e0c3a1397..9e852346b1 100644 --- a/designsafe/static/scripts/data-depot/templates/project-view.html +++ b/designsafe/static/scripts/data-depot/templates/project-view.html @@ -1,17 +1,131 @@

    {{ data.project.value.title }}

    - +
    +
    + + + + + + + +
    ID{{data.project.value.projectId}}
    PI {{ data.project.value.pi }}
    Created{{data.project.created}}
    +
    +
    + +
    +
    + +
    + Loading ... +
    +
    +
    +
    +

    Project Description

    +

    + {{data.project.value.description}} +

    + +
    + +
    + + +
    + +
    +
    + +
    +
    + +
    + + Selection + + -- + + + Review + + + -- + + + Metadata + + -- + + Agreement + +
    + +
    + +
    + +
    + +
    + +
    +
    +
    +

    This project is currently in a publication process. Please do not modify any data or metadata in this project.

    diff --git a/designsafe/static/scripts/data-depot/templates/published-data-listing.html b/designsafe/static/scripts/data-depot/templates/published-data-listing.html new file mode 100644 index 0000000000..5ec66982d9 --- /dev/null +++ b/designsafe/static/scripts/data-depot/templates/published-data-listing.html @@ -0,0 +1,468 @@ + + +
    +
    +

    {{ project.value.title }}

    +
    + + + + + + + + + + + + + + + + + + + + + +
    ID{{project.value.projectId}}
    PI + {{getUserDets(project.value.pi)}} + +
    Created{{project.created}}
    DOI{{project.doi}}
    + +
    +
    +
    +
    + + + +
    +
    + +
    + Loading ... +
    +
    +
    +
    +

    Description

    +

    + {{project.value.description}} +

    + +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + + +
    +
    + +
    + +
    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + +
    NameSizeLast modified
    + + + {{ item.name }} + + + + {{entityTag}} + +
    + + Loading + + {{ entity._displayName }} + {{entity.value.title}} +
    +
    {{ item.length|bytes }}{{ item.lastModified|date:"short" }}
    + + Loading... + +
    + +
    + + Loading... + +
    + +
    +

    + {{ browser.error.message || browser.error }} +

    +
    + +
    +

    + This folder is empty! +
    +

    +
    +
    +
    + +
    diff --git a/designsafe/static/scripts/data-depot/templates/view-citations.html b/designsafe/static/scripts/data-depot/templates/view-citations.html new file mode 100644 index 0000000000..208566362a --- /dev/null +++ b/designsafe/static/scripts/data-depot/templates/view-citations.html @@ -0,0 +1,28 @@ + + + diff --git a/designsafe/static/scripts/data-depot/templates/view-collabs.html b/designsafe/static/scripts/data-depot/templates/view-collabs.html new file mode 100644 index 0000000000..1657c05bea --- /dev/null +++ b/designsafe/static/scripts/data-depot/templates/view-collabs.html @@ -0,0 +1,30 @@ + + + diff --git a/designsafe/static/scripts/data-depot/templates/view-experiments.html b/designsafe/static/scripts/data-depot/templates/view-experiments.html new file mode 100644 index 0000000000..9aebb6fa95 --- /dev/null +++ b/designsafe/static/scripts/data-depot/templates/view-experiments.html @@ -0,0 +1,106 @@ + + + diff --git a/designsafe/static/scripts/data-depot/templates/view-project.html b/designsafe/static/scripts/data-depot/templates/view-project.html new file mode 100644 index 0000000000..ee36352302 --- /dev/null +++ b/designsafe/static/scripts/data-depot/templates/view-project.html @@ -0,0 +1,94 @@ + + + diff --git a/designsafe/static/scripts/data-depot/templates/view-relations.html b/designsafe/static/scripts/data-depot/templates/view-relations.html new file mode 100644 index 0000000000..7ff5cb3747 --- /dev/null +++ b/designsafe/static/scripts/data-depot/templates/view-relations.html @@ -0,0 +1,83 @@ + +
    -

    - {{ browser.error.message || browser.error }} -

    +

    diff --git a/designsafe/static/scripts/ng-designsafe/html/directives/dd-dropbox-listing.html b/designsafe/static/scripts/ng-designsafe/html/directives/dd-dropbox-listing.html index 604bed4314..5b229b088e 100644 --- a/designsafe/static/scripts/ng-designsafe/html/directives/dd-dropbox-listing.html +++ b/designsafe/static/scripts/ng-designsafe/html/directives/dd-dropbox-listing.html @@ -42,9 +42,7 @@
    -

    - {{ browser.error.message || browser.error }} -

    +

    diff --git a/designsafe/static/scripts/ng-designsafe/html/directives/dd-listing.html b/designsafe/static/scripts/ng-designsafe/html/directives/dd-listing.html index 692d373a0c..5dca4ab944 100644 --- a/designsafe/static/scripts/ng-designsafe/html/directives/dd-listing.html +++ b/designsafe/static/scripts/ng-designsafe/html/directives/dd-listing.html @@ -1,14 +1,17 @@ -
    +
    + data-bottom-height="0" + ng-if="browser.showMainListing"> + @@ -22,14 +25,30 @@ {{ item.name }} + + + {{entityTag}} + +
    + + Loading + + {{ entity._displayName }} + {{entity.value.title}} +
    +
    Name Size Last modified
    {{ item.length|bytes }} {{ item.lastModified|date:"short" }}
    @@ -64,4 +83,681 @@
    + +
    +
    + + + +
    +
    + +
    + +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + Project Information + Need Help? +
    +
    +
    +

    Project Information

    +
    + Date of Publication + {{browser.project.dateOfPublication()}} +
    +
    +
    + + Edit + + Project Title (*) +
    +
    + {{browser.project.value.title}} +
    +
    +
    +
    + + Edit + + Project Type (*) +
    +
    + {{browser.project.value.projectType}} +
    +
    +
    +
    + + Edit + + Participant Instiutions (*) +
    +
    + + {{inst.label}} + +
    +
    +
    +
    + + Edit + + Team Members (*) +
    + +
    +
    + + + + {{user.last_name}}, {{user.first_name}} + +
    +
    +
    +
    +
    + + Edit + + Project Description (*) +
    +
    + {{browser.project.value.description}} +
    +
    +
    +
    + + Edit + + Funding Agency & Award Number +
    +
    + {{browser.project.value.awardNumber}} +
    +
    +
    +
    + + Edit + + Associated Projects +
    +
    + + {{asoc}} + +
    +
    +
    +
    + Rights (*) +
    + +
    +
    +
    + + Edit + + Keywords (*)
    +
    +
    + {{keyword}}
    +
    +
    +
    +
    + Publisher (*) +
    +
    + Designsafe-CI +
    +
    +
    +
    +
    +
    +

    Experiment Information

    + +
    +
    + Date of Publication +
    +
    + {{browser.project.dateOfPublication()}} +
    +
    +
    +
    + + Edit + + Experiment Title(*) +
    +
    + {{exp.value.title}} +
    +
    +
    +
    + + Edit + + Experiment Type(*) +
    +
    + {{exp.value.experimentType}} +
    +
    +
    +
    + + Edit + + Experiment Facility(*) +
    +
    + {{exp.getEF(browser.project.value.projectType, exp.value.experimentalFacility).label}} +
    +
    +
    +
    + Authors(*) +
    +
    +
    + + {{user.last_name}}, {{user.first_name}} + +
    +
    +
    +
    +
    + + Edit + + Model Configurations(*) +
    +
    + + {{model_config.value.title}} +
    +
    +
    +
    +
    + + Edit + + Sensor Information(*) +
    +
    + + {{sensor_list.value.title}} +
    +
    +
    +
    +
    + + Edit + + Events(*) +
    +
    + + {{event.value.title}} +
    +
    +
    +
    +
    +

    Analysis Information

    +
    +
    + Date of Publication +
    +
    + {{browser.project.dateOfPublication()}} +
    +
    +
    + + Edit + + Analysis Title(*) +
    +
    + {{analysis.value.title}} +
    +
    +
    +
    + + Edit + + Description(*) +
    +
    + {{analysis.value.description}} +
    +
    +
    +
    + Referenced Data(*) +
    +
    + + [Event] {{event.value.title}} +
    +
    +
    +
    + +
    +
    +
    +
    +

    Agreement

    +

    + I agree and understand that when I publish this data and receive a DOI that this will result in a locked, read-only publication that cannot be changed. If I have changes or revisions, this will result in a new DOI being created for the revised publication. I understand that my dataset meets the minimum metadata requirements per the Data Curation and Publication Guidelines. +

    + I agree +
    + +
    diff --git a/designsafe/static/scripts/ng-designsafe/html/directives/my-data-browser.html b/designsafe/static/scripts/ng-designsafe/html/directives/my-data-browser.html new file mode 100644 index 0000000000..dd1f7f5583 --- /dev/null +++ b/designsafe/static/scripts/ng-designsafe/html/directives/my-data-browser.html @@ -0,0 +1,71 @@ +
    +
    + + +
    +
    +
    + +
    + {{data.selectedProject.value.title}} + + + {{dirElem}} + + / +
    +
    +
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + +
    File nameSize
    + + + {{ renderName(f) }} + + {{f.length|bytes:0}}
    {{project.value.title }}

    {{data.error}}

    +
    + +
    +
    +
    {{data.error}}
    +
    +
    +
    diff --git a/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-preview-tree.html b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-preview-tree.html new file mode 100644 index 0000000000..0f9a661d93 --- /dev/null +++ b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-preview-tree.html @@ -0,0 +1,51 @@ + + + diff --git a/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-categories.html b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-categories.html new file mode 100644 index 0000000000..1dea7c53a5 --- /dev/null +++ b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-categories.html @@ -0,0 +1,404 @@ + + diff --git a/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-custom-tags.html b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-custom-tags.html new file mode 100644 index 0000000000..30ec8909de --- /dev/null +++ b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-custom-tags.html @@ -0,0 +1,48 @@ + + diff --git a/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-metadata.html b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-metadata.html index 30ec8909de..ae8f327200 100644 --- a/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-metadata.html +++ b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-metadata.html @@ -1,18 +1,136 @@
    - +
    diff --git a/designsafe/static/scripts/ng-designsafe/html/modals/project-service-add-collaborator.html b/designsafe/static/scripts/ng-designsafe/html/modals/project-service-add-collaborator.html index e7f2e7c640..dbd0e01ed4 100644 --- a/designsafe/static/scripts/ng-designsafe/html/modals/project-service-add-collaborator.html +++ b/designsafe/static/scripts/ng-designsafe/html/modals/project-service-add-collaborator.html @@ -17,16 +17,128 @@
    + +
    +
    PI
    + Assign Authorship +
    +
    + +
    +
    + +
    +
    +
      +
    • + + +
    • +
    +
    +
    +
    +
    +
    + Remove + Current Co-Pi(s): + Assign Authorship +
    +
    + +
    +
    + +
    +
    +
      +
    • + + +
    • +
    +
    +
    + +
    +

    Add new Co-PI:

    +
    +
    +
    + +
    +
    + Searching for users... +
    + No users found. +
    +
    +
    +
    + +
    +
    +
    -

    Remove current collaborator(s):

    + Remove + Current Team Member(s): + Assign Authorship
    -
    +
    + +
    + +
    +
    +
      +
    • + + +
    • +
    +
    @@ -63,7 +175,7 @@
    + + + + + + + + + +
    +
    +

    + Drag and drop between the two lists. +

    +
    + +

    Beasts

    + +

    Left column of beasts is not draggable and accepts both beasts and priests

    + +
    + +
    +
    +
      +
    • + {{man}} +
    • +
    +
    +
    +
      +
    • + {{woman}} +
    • +
    +
    +
    + +
    + +

    Priests

    + +
    + +
    +
    +
      +
    • + {{man}} +
    • +
    +
    +
    +
      +
    • + {{woman}} +
    • +
    +
    +
    + +
    + +

    Terrorists

    + +

    Each terrorist list item accepts a new terrorist. Shows inserting into a particular + position in an array.

    + +
    + + +
    +
    +
      +
    • + {{man}} +
    • +
    +
    +
    +
      +
    • + {{woman}} +
    • +
    +
    +
    + +
    + +

    Custom Drag Image

    + +

    You may specify a drag-image-element-id to use as the drag image. You can use the + ghost image + technique for the custom drag image.

    + +
    + + +
    +
    +
      +
    • + {{man}} +
    • +
    +
    +
    +
      +
    • + {{woman}} +
    • +
    +
    +
    + +
    Custom drag image #1
    +
    Custom drag image #2
    +
    +
    + + + diff --git a/designsafe/static/vendor/angular-native-dragdrop/demo/js/app.js b/designsafe/static/vendor/angular-native-dragdrop/demo/js/app.js new file mode 100644 index 0000000000..e839e205c8 --- /dev/null +++ b/designsafe/static/vendor/angular-native-dragdrop/demo/js/app.js @@ -0,0 +1,54 @@ +angular.module('app', [ + 'hljs', + 'ang-drag-drop' +]).controller('MainCtrl', function($scope) { + $scope.men = [ + 'John', + 'Jack', + 'Mark', + 'Ernie', + 'Mike (Locked)' + ]; + + + $scope.women = [ + 'Jane', + 'Jill', + 'Betty', + 'Mary' + ]; + + $scope.addText = ''; + + $scope.dropValidateHandler = function($drop, $event, $data) { + if ($data === 'Mike (Locked)') { + return false; + } + if ($drop.element[0] === $event.srcElement.parentNode) { + // Don't allow moving to same container + return false; + } + return true; + }; + + $scope.dropSuccessHandler = function($event, index, array) { + array.splice(index, 1); + }; + + $scope.dropFailureHandler = function($event, index, array) { + alert(array[index] + ' could be dropped into left list!') + }; + + $scope.onDrop = function($event, $data, array, index) { + if (index !== undefined) { + array.splice(index, 0, $data); + } else { + array.push($data); + } + }; + + $scope.getCustomDragElementId = function (index) { + return 'customDrag' + (index % 2); + } + +}); diff --git a/designsafe/static/vendor/angular-native-dragdrop/docs/examples.md b/designsafe/static/vendor/angular-native-dragdrop/docs/examples.md new file mode 100644 index 0000000000..5530a7601e --- /dev/null +++ b/designsafe/static/vendor/angular-native-dragdrop/docs/examples.md @@ -0,0 +1,8 @@ +#Examples + +##Simple Usage + + + +##With drop validation + \ No newline at end of file diff --git a/designsafe/static/vendor/angular-native-dragdrop/docs/getting-started.md b/designsafe/static/vendor/angular-native-dragdrop/docs/getting-started.md new file mode 100644 index 0000000000..2fd3f5b353 --- /dev/null +++ b/designsafe/static/vendor/angular-native-dragdrop/docs/getting-started.md @@ -0,0 +1,26 @@ +#Installation + +##Download +Download the file [**draganddrop.min.js**](https://raw.githubusercontent.com/angular-dragdrop/angular-dragdrop/master/draganddrop.min.js) or [**draganddrop.js**](https://raw.githubusercontent.com/angular-dragdrop/angular-dragdrop/master/draganddrop.js). + +##Bower + +You can also install via Bower using + +`bower install angular-native-dragdrop --save` + +--- + +#Usage + +##Step - 1 **Add script** +``` + +``` + +##Step - 2 **Include in app** +``` + myApp = angular.module('myApp','ang-drag-drop'); +``` + +##Step - 3 ***Profit!!*** diff --git a/designsafe/static/vendor/angular-native-dragdrop/docs/index.md b/designsafe/static/vendor/angular-native-dragdrop/docs/index.md new file mode 100644 index 0000000000..b5370fc23b --- /dev/null +++ b/designsafe/static/vendor/angular-native-dragdrop/docs/index.md @@ -0,0 +1,120 @@ +# Angular Drag and Drop +**A Native ( without jquery ) Drag and Drop directive for AngularJS using HTML5 Drag and Drop** + +--- + +##ui-draggable(expression) + +Directive in module ang-drag-drop (since 1.0.5 - old module name ngDragDrop) + +The ui-draggable attribute tells Angular that the element is draggable. ui-draggable takes an expression as the attribute value. The expression should evaluate to either true or false. You can toggle the draggability of an element using this expression. + +###Additional Attributes + +####_**drag**_(variable) + +The class used to mark child elements of draggable object to be used as drag handle. Default class name is `drag-handle` + +**NOTE**: If attribute is not present drag handle feature is not active. + + + +####_**drag-handle-class**_(string) + +The `drag` property is used to assign the data that needs to be passed along with the dragging element. + + + + +####_**on-drop-success**_(function) + +The `on-drop-success` attribute takes a function. We can consider this to be an on-drop-success handler function. This can be useful if you need to do some post processing after the dragged element is dropped successfully on the drop site. + +**NOTE**: This callback function is only called when the drop succeeds. +You can request the `drag-end` event ( very similiar to requesting the click event in `ng-click` ) by passing `$event` in the event handler. + + + +####_**on-drop-failure**_(function) + +The `on-drop-failure` attribute takes a function. We can consider this to be an on-drop-failure handler function. This can be useful if you need to do some post processing after the dragged element is dropped unsuccessfully on any drop site. + +**NOTE**: This callback function is only called when the drop fails. +You can request the `drag-end` event ( very similiar to requesting the click event in `ng-click` ) by passing `$event` in the event handler. + + + +####_**drag-channel**_(string) + +The `on-drop-failure` attribute takes a function. We can consider this to be an on-drop-failure handler function. This can be useful if you need to do some post processing after the dragged element is dropped unsuccessfully on any drop site. + +**NOTE**: This callback function is only called when the drop fails. +You can request the `drag-end` event ( very similiar to requesting the click event in `ng-click` ) by passing `$event` in the event handler. + + + +###Usage + + + +###Events + +On start of dragging an Angular Event `ANGULAR_DRAG_START` is dispatched from the `$rootScope`. The event also carries carries the information about the channel in which the dragging has started. + +On end of dragging an Angular Event `ANGULAR_DRAG_END` is dispatched from the `$rootScope`. The event also carries carries the information about the channel in which the dragging has started. + +When hovering a draggable element on top of a drop area an Angular Event `ANGULAR_HOVER` is dispatched from the `$rootScope`. The event also carries the information about the channel in which the dragging has started. + +--- + +##ui-on-drop(expression) + +Directive in module ang-drag-drop (since 1.0.5 - old module name ngDragDrop) + +The `ui-on-drop` attribute tells Angular that the element is a drop site. `ui-on-drop` takes a function as the attribute value. The function will be called when a valid dragged element is dropped in that location. A valid dragged element is one which has the same channel as the drop location. + +**NOTE** : This callback function is only called when the drop succeeds. +The `ui-on-drop` callback can request additional parameters. The data that is dragged is available to the callback as $data and its channel as `$channel`. Apart from this the drop event is exposed as `$event`. + +###Additional Attributes + +####_**drop-channel**_(variable) + +The channel that the drop site accepts. The dragged element should have the same channel as this drop site for it to be droppable at this location. It is possible to provide comma separated list of channels. + +**NOTE**: Also special value of `drag-channel` attribute is available to accept dragged element with any channel value — * + + + +####_**drop-validate**_(function) + +Extra validation that makes sure that the drop site accepts the dragged element beyond having the same channel. If not defined, no extra validation is made. + +**NOTE**: This callback function is called only if the channel condition is met, when the element starts being dragged + + + + +####_**drag-enter-class**_(string) + +The class that will be added to the the droppable element when a dragged element ( which is droppable ) enters the drop location. The default value for this is `on-drag-enter` + + + +####_**drag-hover-class**_(string) + +The class that will be added to the drop area element when hovering with an element. The default value for this is `on-drag-hover` + + + +###Usage + + + +###Events + +On start of dragging an Angular Event `ANGULAR_DRAG_START` is dispatched from the `$rootScope`. The event also carries carries the information about the channel in which the dragging has started. + +On end of dragging an Angular Event `ANGULAR_DRAG_END` is dispatched from the `$rootScope`. The event also carries carries the information about the channel in which the dragging has started. + +When hovering a draggable element on top of a drop area an Angular Event `ANGULAR_HOVER` is dispatched from the `$rootScope`. The event also carries the information about the channel in which the dragging has started. \ No newline at end of file diff --git a/designsafe/static/vendor/angular-native-dragdrop/draganddrop.js b/designsafe/static/vendor/angular-native-dragdrop/draganddrop.js new file mode 100755 index 0000000000..af8acac934 --- /dev/null +++ b/designsafe/static/vendor/angular-native-dragdrop/draganddrop.js @@ -0,0 +1,459 @@ +(function(angular) { + 'use strict'; + + function isDnDsSupported() { + return 'ondrag' in document.createElement('a'); + } + + function determineEffectAllowed(e) { + if(e.originalEvent) { + e.dataTransfer = e.originalEvent.dataTransfer; + } + + // Chrome doesn't set dropEffect, so we have to work it out ourselves + if (typeof e.dataTransfer !== 'undefined' && e.dataTransfer.dropEffect === 'none') { + if (e.dataTransfer.effectAllowed === 'copy' || + e.dataTransfer.effectAllowed === 'move') { + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed; + } else if (e.dataTransfer.effectAllowed === 'copyMove' || e.dataTransfer.effectAllowed === 'copymove') { + e.dataTransfer.dropEffect = e.ctrlKey ? 'copy' : 'move'; + } + } + } + + if (!isDnDsSupported()) { + angular.module('ang-drag-drop', []); + return; + } + + var module = angular.module('ang-drag-drop', []); + + module.directive('uiDraggable', ['$parse', '$rootScope', '$dragImage', function($parse, $rootScope, $dragImage) { + return function(scope, element, attrs) { + var isDragHandleUsed = false, + dragHandleClass, + draggingClass = attrs.draggingClass || 'on-dragging', + dragTarget; + + element.attr('draggable', false); + + scope.$watch(attrs.uiDraggable, function(newValue) { + if (newValue) { + element.attr('draggable', newValue); + element.bind('dragend', dragendHandler); + element.bind('dragstart', dragstartHandler); + } + else { + element.removeAttr('draggable'); + element.unbind('dragend', dragendHandler); + element.unbind('dragstart', dragstartHandler); + } + + }); + + if (angular.isString(attrs.dragHandleClass)) { + isDragHandleUsed = true; + dragHandleClass = attrs.dragHandleClass.trim() || 'drag-handle'; + + element.bind('mousedown', function(e) { + dragTarget = e.target; + }); + } + + function dragendHandler(e) { + if(e.originalEvent) { + e.dataTransfer = e.originalEvent.dataTransfer; + } + + setTimeout(function() { + element.unbind('$destroy', dragendHandler); + }, 0); + var sendChannel = attrs.dragChannel || 'defaultchannel'; + $rootScope.$broadcast('ANGULAR_DRAG_END', e, sendChannel); + + determineEffectAllowed(e); + + if (e.dataTransfer && e.dataTransfer.dropEffect !== 'none') { + if (attrs.onDropSuccess) { + var onDropSuccessFn = $parse(attrs.onDropSuccess); + scope.$evalAsync(function() { + onDropSuccessFn(scope, {$event: e}); + }); + } + }else if (e.dataTransfer && e.dataTransfer.dropEffect === 'none'){ + if (attrs.onDropFailure) { + var onDropFailureFn = $parse(attrs.onDropFailure); + scope.$evalAsync(function() { + onDropFailureFn(scope, {$event: e}); + }); + } + } + element.removeClass(draggingClass); + } + + function setDragElement(e, dragImageElementId) { + var dragImageElementFn; + + if(e.originalEvent) { + e.dataTransfer = e.originalEvent.dataTransfer; + } + + dragImageElementFn = $parse(dragImageElementId); + + scope.$apply(function() { + var elementId = dragImageElementFn(scope, {$event: e}), + dragElement; + + if (!(elementId && angular.isString(elementId))) { + return; + } + + dragElement = document.getElementById(elementId); + + if (!dragElement) { + return; + } + + e.dataTransfer.setDragImage(dragElement, 0, 0); + }); + } + + function dragstartHandler(e) { + if(e.originalEvent) { + e.dataTransfer = e.originalEvent.dataTransfer; + } + + var isDragAllowed = !isDragHandleUsed || dragTarget.classList.contains(dragHandleClass); + + if (isDragAllowed) { + var sendChannel = attrs.dragChannel || 'defaultchannel'; + var dragData = ''; + if (attrs.drag) { + dragData = scope.$eval(attrs.drag); + } + + var dragImage = attrs.dragImage || null; + + element.addClass(draggingClass); + element.bind('$destroy', dragendHandler); + + //Code to make sure that the setDragImage is available. IE 10, 11, and Opera do not support setDragImage. + var hasNativeDraggable = !(document.uniqueID || window.opera); + + //If there is a draggable image passed in, then set the image to be dragged. + if (dragImage && hasNativeDraggable) { + var dragImageFn = $parse(attrs.dragImage); + scope.$apply(function() { + var dragImageParameters = dragImageFn(scope, {$event: e}); + if (dragImageParameters) { + if (angular.isString(dragImageParameters)) { + dragImageParameters = $dragImage.generate(dragImageParameters); + } + if (dragImageParameters.image) { + var xOffset = dragImageParameters.xOffset || 0, + yOffset = dragImageParameters.yOffset || 0; + e.dataTransfer.setDragImage(dragImageParameters.image, xOffset, yOffset); + } + } + }); + } else if (attrs.dragImageElementId) { + setDragElement(e, attrs.dragImageElementId); + } + + var offset = {x: e.offsetX, y: e.offsetY}; + var transferDataObject = {data: dragData, channel: sendChannel, offset: offset}; + var transferDataText = angular.toJson(transferDataObject); + + e.dataTransfer.setData('text', transferDataText); + e.dataTransfer.effectAllowed = 'copyMove'; + + $rootScope.$broadcast('ANGULAR_DRAG_START', e, sendChannel, transferDataObject); + } + else { + e.preventDefault(); + } + } + }; + } + ]); + + module.directive('uiOnDrop', ['$parse', '$rootScope', function($parse, $rootScope) { + return function(scope, element, attr) { + var dragging = 0; //Ref. http://stackoverflow.com/a/10906204 + var dropChannel = attr.dropChannel || 'defaultchannel'; + var dragChannel = ''; + var dragEnterClass = attr.dragEnterClass || 'on-drag-enter'; + var dragHoverClass = attr.dragHoverClass || 'on-drag-hover'; + var customDragEnterEvent = $parse(attr.onDragEnter); + var customDragLeaveEvent = $parse(attr.onDragLeave); + + function calculateDropOffset(e) { + var offset = { + x: e.offsetX, + y: e.offsetY + }; + var target = e.target; + + while (target !== element[0]) { + offset.x = offset.x + target.offsetLeft; + offset.y = offset.y + target.offsetTop; + + target = target.offsetParent; + if (!target) { + return null; + } + } + + return offset; + } + + function onDragOver(e) { + if (e.preventDefault) { + e.preventDefault(); // Necessary. Allows us to drop. + } + + if (e.stopPropagation) { + e.stopPropagation(); + } + + var uiOnDragOverFn = $parse(attr.uiOnDragOver); + scope.$evalAsync(function() { + uiOnDragOverFn(scope, {$event: e, $channel: dropChannel}); + }); + + return false; + } + + function onDragLeave(e) { + if (e.preventDefault) { + e.preventDefault(); + } + + if (e.stopPropagation) { + e.stopPropagation(); + } + dragging--; + + if (dragging === 0) { + scope.$evalAsync(function() { + customDragLeaveEvent(scope, {$event: e, $channel: dropChannel}); + }); + element.addClass(dragEnterClass); + element.removeClass(dragHoverClass); + } + + var uiOnDragLeaveFn = $parse(attr.uiOnDragLeave); + scope.$evalAsync(function() { + uiOnDragLeaveFn(scope, {$event: e, $channel: dropChannel}); + }); + } + + function onDragEnter(e) { + if (e.preventDefault) { + e.preventDefault(); + } + + if (e.stopPropagation) { + e.stopPropagation(); + } + + if (dragging === 0) { + scope.$evalAsync(function() { + customDragEnterEvent(scope, {$event: e, $channel: dropChannel}); + }); + element.removeClass(dragEnterClass); + element.addClass(dragHoverClass); + } + dragging++; + + var uiOnDragEnterFn = $parse(attr.uiOnDragEnter); + scope.$evalAsync(function() { + uiOnDragEnterFn(scope, {$event: e, $channel: dropChannel}); + }); + + $rootScope.$broadcast('ANGULAR_HOVER', dragChannel); + } + + function onDrop(e) { + if(e.originalEvent) { + e.dataTransfer = e.originalEvent.dataTransfer; + } + + if (e.preventDefault) { + e.preventDefault(); // Necessary. Allows us to drop. + } + if (e.stopPropagation) { + e.stopPropagation(); // Necessary. Allows us to drop. + } + + var sendData = e.dataTransfer.getData('text'); + sendData = angular.fromJson(sendData); + + var dropOffset = calculateDropOffset(e); + + var position = dropOffset ? { + x: dropOffset.x - sendData.offset.x, + y: dropOffset.y - sendData.offset.y + } : null; + + determineEffectAllowed(e); + + var uiOnDropFn = $parse(attr.uiOnDrop); + scope.$evalAsync(function() { + uiOnDropFn(scope, {$data: sendData.data, $event: e, $channel: sendData.channel, $position: position}); + }); + element.removeClass(dragEnterClass); + dragging = 0; + } + + function isDragChannelAccepted(dragChannel, dropChannel) { + if (dropChannel === '*') { + return true; + } + + var channelMatchPattern = new RegExp('(\\s|[,])+(' + dragChannel + ')(\\s|[,])+', 'i'); + + return channelMatchPattern.test(',' + dropChannel + ','); + } + + function preventNativeDnD(e) { + if(e.originalEvent) { + e.dataTransfer = e.originalEvent.dataTransfer; + } + + if (e.preventDefault) { + e.preventDefault(); + } + if (e.stopPropagation) { + e.stopPropagation(); + } + e.dataTransfer.dropEffect = 'none'; + return false; + } + + var deregisterDragStart = $rootScope.$on('ANGULAR_DRAG_START', function(_, e, channel, transferDataObject) { + dragChannel = channel; + + var valid = true; + + if (!isDragChannelAccepted(channel, dropChannel)) { + valid = false; + } + + if (valid && attr.dropValidate) { + var validateFn = $parse(attr.dropValidate); + valid = validateFn(scope, { + $drop: {scope: scope, element: element}, + $event: e, + $data: transferDataObject.data, + $channel: transferDataObject.channel + }); + } + + if (valid) { + element.bind('dragover', onDragOver); + element.bind('dragenter', onDragEnter); + element.bind('dragleave', onDragLeave); + element.bind('drop', onDrop); + + element.addClass(dragEnterClass); + } else { + element.bind('dragover', preventNativeDnD); + element.bind('dragenter', preventNativeDnD); + element.bind('dragleave', preventNativeDnD); + element.bind('drop', preventNativeDnD); + + element.removeClass(dragEnterClass); + } + + }); + + + var deregisterDragEnd = $rootScope.$on('ANGULAR_DRAG_END', function() { + element.unbind('dragover', onDragOver); + element.unbind('dragenter', onDragEnter); + element.unbind('dragleave', onDragLeave); + + element.unbind('drop', onDrop); + element.removeClass(dragHoverClass); + element.removeClass(dragEnterClass); + + element.unbind('dragover', preventNativeDnD); + element.unbind('dragenter', preventNativeDnD); + element.unbind('dragleave', preventNativeDnD); + element.unbind('drop', preventNativeDnD); + }); + + scope.$on('$destroy', function() { + deregisterDragStart(); + deregisterDragEnd(); + }); + + + attr.$observe('dropChannel', function(value) { + if (value) { + dropChannel = value; + } + }); + + + }; + } + ]); + + module.constant('$dragImageConfig', { + height: 20, + width: 200, + padding: 10, + font: 'bold 11px Arial', + fontColor: '#eee8d5', + backgroundColor: '#93a1a1', + xOffset: 0, + yOffset: 0 + }); + + module.service('$dragImage', ['$dragImageConfig', function(defaultConfig) { + var ELLIPSIS = '…'; + + function fitString(canvas, text, config) { + var width = canvas.measureText(text).width; + if (width < config.width) { + return text; + } + while (width + config.padding > config.width) { + text = text.substring(0, text.length - 1); + width = canvas.measureText(text + ELLIPSIS).width; + } + return text + ELLIPSIS; + } + + this.generate = function(text, options) { + var config = angular.extend({}, defaultConfig, options || {}); + var el = document.createElement('canvas'); + + el.height = config.height; + el.width = config.width; + + var canvas = el.getContext('2d'); + + canvas.fillStyle = config.backgroundColor; + canvas.fillRect(0, 0, config.width, config.height); + canvas.font = config.font; + canvas.fillStyle = config.fontColor; + + var title = fitString(canvas, text, config); + canvas.fillText(title, 4, config.padding + 4); + + var image = new Image(); + image.src = el.toDataURL(); + + return { + image: image, + xOffset: config.xOffset, + yOffset: config.yOffset + }; + }; + } + ]); + +}(angular)); diff --git a/designsafe/static/vendor/angular-native-dragdrop/draganddrop.min.js b/designsafe/static/vendor/angular-native-dragdrop/draganddrop.min.js new file mode 100755 index 0000000000..3ae7432907 --- /dev/null +++ b/designsafe/static/vendor/angular-native-dragdrop/draganddrop.min.js @@ -0,0 +1 @@ +!function(e){"use strict";function a(){return"ondrag"in document.createElement("a")}function n(e){e.originalEvent&&(e.dataTransfer=e.originalEvent.dataTransfer),"undefined"!=typeof e.dataTransfer&&"none"===e.dataTransfer.dropEffect&&("copy"===e.dataTransfer.effectAllowed||"move"===e.dataTransfer.effectAllowed?e.dataTransfer.dropEffect=e.dataTransfer.effectAllowed:"copyMove"!==e.dataTransfer.effectAllowed&&"copymove"!==e.dataTransfer.effectAllowed||(e.dataTransfer.dropEffect=e.ctrlKey?"copy":"move"))}if(!a())return void e.module("ang-drag-drop",[]);var r=e.module("ang-drag-drop",[]);r.directive("uiDraggable",["$parse","$rootScope","$dragImage",function(a,r,t){return function(o,d,f){function i(e){e.originalEvent&&(e.dataTransfer=e.originalEvent.dataTransfer),setTimeout(function(){d.unbind("$destroy",i)},0);var t=f.dragChannel||"defaultchannel";if(r.$broadcast("ANGULAR_DRAG_END",e,t),n(e),e.dataTransfer&&"none"!==e.dataTransfer.dropEffect){if(f.onDropSuccess){var s=a(f.onDropSuccess);o.$evalAsync(function(){s(o,{$event:e})})}}else if(e.dataTransfer&&"none"===e.dataTransfer.dropEffect&&f.onDropFailure){var l=a(f.onDropFailure);o.$evalAsync(function(){l(o,{$event:e})})}d.removeClass(c)}function s(n,r){var t;n.originalEvent&&(n.dataTransfer=n.originalEvent.dataTransfer),t=a(r),o.$apply(function(){var a,r=t(o,{$event:n});r&&e.isString(r)&&(a=document.getElementById(r),a&&n.dataTransfer.setDragImage(a,0,0))})}function l(n){n.originalEvent&&(n.dataTransfer=n.originalEvent.dataTransfer);var l=!v||u.classList.contains(g);if(l){var p=f.dragChannel||"defaultchannel",$="";f.drag&&($=o.$eval(f.drag));var m=f.dragImage||null;d.addClass(c),d.bind("$destroy",i);var T=!(document.uniqueID||window.opera);if(m&&T){var h=a(f.dragImage);o.$apply(function(){var a=h(o,{$event:n});if(a&&(e.isString(a)&&(a=t.generate(a)),a.image)){var r=a.xOffset||0,d=a.yOffset||0;n.dataTransfer.setDragImage(a.image,r,d)}})}else f.dragImageElementId&&s(n,f.dragImageElementId);var b={x:n.offsetX,y:n.offsetY},D={data:$,channel:p,offset:b},y=e.toJson(D);n.dataTransfer.setData("text",y),n.dataTransfer.effectAllowed="copyMove",r.$broadcast("ANGULAR_DRAG_START",n,p,D)}else n.preventDefault()}var g,u,v=!1,c=f.draggingClass||"on-dragging";d.attr("draggable",!1),o.$watch(f.uiDraggable,function(e){e?(d.attr("draggable",e),d.bind("dragend",i),d.bind("dragstart",l)):(d.removeAttr("draggable"),d.unbind("dragend",i),d.unbind("dragstart",l))}),e.isString(f.dragHandleClass)&&(v=!0,g=f.dragHandleClass.trim()||"drag-handle",d.bind("mousedown",function(e){u=e.target}))}}]),r.directive("uiOnDrop",["$parse","$rootScope",function(a,r){return function(t,o,d){function f(e){for(var a={x:e.offsetX,y:e.offsetY},n=e.target;n!==o[0];)if(a.x=a.x+n.offsetLeft,a.y=a.y+n.offsetTop,n=n.offsetParent,!n)return null;return a}function i(e){e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation();var n=a(d.uiOnDragOver);return t.$evalAsync(function(){n(t,{$event:e,$channel:p})}),!1}function s(e){e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation(),c--,0===c&&(t.$evalAsync(function(){b(t,{$event:e,$channel:p})}),o.addClass(m),o.removeClass(T));var n=a(d.uiOnDragLeave);t.$evalAsync(function(){n(t,{$event:e,$channel:p})})}function l(e){e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation(),0===c&&(t.$evalAsync(function(){h(t,{$event:e,$channel:p})}),o.removeClass(m),o.addClass(T)),c++;var n=a(d.uiOnDragEnter);t.$evalAsync(function(){n(t,{$event:e,$channel:p})}),r.$broadcast("ANGULAR_HOVER",$)}function g(r){r.originalEvent&&(r.dataTransfer=r.originalEvent.dataTransfer),r.preventDefault&&r.preventDefault(),r.stopPropagation&&r.stopPropagation();var i=r.dataTransfer.getData("text");i=e.fromJson(i);var s=f(r),l=s?{x:s.x-i.offset.x,y:s.y-i.offset.y}:null;n(r);var g=a(d.uiOnDrop);t.$evalAsync(function(){g(t,{$data:i.data,$event:r,$channel:i.channel,$position:l})}),o.removeClass(m),c=0}function u(e,a){if("*"===a)return!0;var n=new RegExp("(\\s|[,])+("+e+")(\\s|[,])+","i");return n.test(","+a+",")}function v(e){return e.originalEvent&&(e.dataTransfer=e.originalEvent.dataTransfer),e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation(),e.dataTransfer.dropEffect="none",!1}var c=0,p=d.dropChannel||"defaultchannel",$="",m=d.dragEnterClass||"on-drag-enter",T=d.dragHoverClass||"on-drag-hover",h=a(d.onDragEnter),b=a(d.onDragLeave),D=r.$on("ANGULAR_DRAG_START",function(e,n,r,f){$=r;var c=!0;if(u(r,p)||(c=!1),c&&d.dropValidate){var T=a(d.dropValidate);c=T(t,{$drop:{scope:t,element:o},$event:n,$data:f.data,$channel:f.channel})}c?(o.bind("dragover",i),o.bind("dragenter",l),o.bind("dragleave",s),o.bind("drop",g),o.addClass(m)):(o.bind("dragover",v),o.bind("dragenter",v),o.bind("dragleave",v),o.bind("drop",v),o.removeClass(m))}),y=r.$on("ANGULAR_DRAG_END",function(){o.unbind("dragover",i),o.unbind("dragenter",l),o.unbind("dragleave",s),o.unbind("drop",g),o.removeClass(T),o.removeClass(m),o.unbind("dragover",v),o.unbind("dragenter",v),o.unbind("dragleave",v),o.unbind("drop",v)});t.$on("$destroy",function(){D(),y()}),d.$observe("dropChannel",function(e){e&&(p=e)})}}]),r.constant("$dragImageConfig",{height:20,width:200,padding:10,font:"bold 11px Arial",fontColor:"#eee8d5",backgroundColor:"#93a1a1",xOffset:0,yOffset:0}),r.service("$dragImage",["$dragImageConfig",function(a){function n(e,a,n){var t=e.measureText(a).width;if(tn.width;)a=a.substring(0,a.length-1),t=e.measureText(a+r).width;return a+r}var r="…";this.generate=function(r,t){var o=e.extend({},a,t||{}),d=document.createElement("canvas");d.height=o.height,d.width=o.width;var f=d.getContext("2d");f.fillStyle=o.backgroundColor,f.fillRect(0,0,o.width,o.height),f.font=o.font,f.fillStyle=o.fontColor;var i=n(f,r,o);f.fillText(i,4,o.padding+4);var s=new Image;return s.src=d.toDataURL(),{image:s,xOffset:o.xOffset,yOffset:o.yOffset}}}])}(angular); \ No newline at end of file diff --git a/designsafe/static/vendor/angular-native-dragdrop/index.js b/designsafe/static/vendor/angular-native-dragdrop/index.js new file mode 100644 index 0000000000..548e0434b3 --- /dev/null +++ b/designsafe/static/vendor/angular-native-dragdrop/index.js @@ -0,0 +1,2 @@ +require('./draganddrop'); +module.exports = 'ang-drag-drop'; diff --git a/designsafe/static/vendor/angular-native-dragdrop/mkdocs.yml b/designsafe/static/vendor/angular-native-dragdrop/mkdocs.yml new file mode 100644 index 0000000000..4eca025bd0 --- /dev/null +++ b/designsafe/static/vendor/angular-native-dragdrop/mkdocs.yml @@ -0,0 +1,14 @@ +site_name: Angular Drag and Drop + +site_url: http://angular-dragdrop.github.io/angular-dragdrop/ +site_description: Project documentation for angular Drag and Drop. + +repo_url: https://github.com/angular-dragdrop/angular-dragdrop + + +theme : flatly + +pages: +- 'index.md' +- 'getting-started.md' +- 'examples.md' \ No newline at end of file diff --git a/designsafe/static/vendor/angular-native-dragdrop/package.json b/designsafe/static/vendor/angular-native-dragdrop/package.json new file mode 100644 index 0000000000..d74355e789 --- /dev/null +++ b/designsafe/static/vendor/angular-native-dragdrop/package.json @@ -0,0 +1,26 @@ +{ + "name": "angular-native-dragdrop", + "version": "1.2.2", + "description": "Angular HTML5 Drag and Drop directive written in pure with no dependency on JQuery.", + "main": "index.js", + "scripts": { + "test": "gulp" + }, + "repository": { + "type": "git", + "url": "https://github.com/angular-dragdrop/angular-dragdrop.git" + }, + "author": "ganarajpr", + "license": "MIT", + "bugs": { + "url": "https://github.com/angular-dragdrop/angular-dragdrop/issues" + }, + "homepage": "http://angular-dragdrop.github.io/angular-dragdrop", + "devDependencies": { + "gulp": "^3.8.11", + "gulp-jshint": "^1.9.2", + "gulp-rename": "^1.2.2", + "gulp-uglify": "^1.4.2", + "jshint-stylish": "^1.0.1" + } +} diff --git a/designsafe/static/vendor/exif-js/.bower.json b/designsafe/static/vendor/exif-js/.bower.json new file mode 100644 index 0000000000..4f503b0004 --- /dev/null +++ b/designsafe/static/vendor/exif-js/.bower.json @@ -0,0 +1,33 @@ +{ + "name": "exif-js", + "version": "2.1.1", + "homepage": "https://github.com/exif-js/exif-js", + "authors": [ + "Jacob Seidelin" + ], + "description": "JavaScript library for reading EXIF image metadata", + "main": "exif.js", + "keywords": [ + "exif" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests", + "spec", + "example" + ], + "_release": "2.1.1", + "_resolution": { + "type": "version", + "tag": "v2.1.1", + "commit": "ee4d6b161215020b3fd8a14a89de7f6456f5014b" + }, + "_source": "https://github.com/jseidelin/exif-js.git", + "_target": "^2.1.1", + "_originalSource": "exif-js", + "_direct": true +} \ No newline at end of file diff --git a/designsafe/static/vendor/exif-js/LICENSE.md b/designsafe/static/vendor/exif-js/LICENSE.md new file mode 100644 index 0000000000..455a8fc587 --- /dev/null +++ b/designsafe/static/vendor/exif-js/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2008 Jacob Seidelin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/designsafe/static/vendor/exif-js/README.md b/designsafe/static/vendor/exif-js/README.md new file mode 100644 index 0000000000..0071072617 --- /dev/null +++ b/designsafe/static/vendor/exif-js/README.md @@ -0,0 +1,3 @@ +#Exif.js + +A JavaScript library for reading EXIF meta data from JPEG image files. \ No newline at end of file diff --git a/designsafe/static/vendor/exif-js/bower.json b/designsafe/static/vendor/exif-js/bower.json new file mode 100644 index 0000000000..8058a05b41 --- /dev/null +++ b/designsafe/static/vendor/exif-js/bower.json @@ -0,0 +1,23 @@ +{ + "name": "exif-js", + "version": "2.1.1", + "homepage": "https://github.com/exif-js/exif-js", + "authors": [ + "Jacob Seidelin" + ], + "description": "JavaScript library for reading EXIF image metadata", + "main": "exif.js", + "keywords": [ + "exif" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests", + "spec", + "example" + ] +} diff --git a/designsafe/static/vendor/exif-js/exif.js b/designsafe/static/vendor/exif-js/exif.js new file mode 100644 index 0000000000..296a338bee --- /dev/null +++ b/designsafe/static/vendor/exif-js/exif.js @@ -0,0 +1,805 @@ +(function() { + + var debug = false; + + var root = this; + + var EXIF = function(obj) { + if (obj instanceof EXIF) return obj; + if (!(this instanceof EXIF)) return new EXIF(obj); + this.EXIFwrapped = obj; + }; + + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = EXIF; + } + exports.EXIF = EXIF; + } else { + root.EXIF = EXIF; + } + + var ExifTags = EXIF.Tags = { + + // version tags + 0x9000 : "ExifVersion", // EXIF version + 0xA000 : "FlashpixVersion", // Flashpix format version + + // colorspace tags + 0xA001 : "ColorSpace", // Color space information tag + + // image configuration + 0xA002 : "PixelXDimension", // Valid width of meaningful image + 0xA003 : "PixelYDimension", // Valid height of meaningful image + 0x9101 : "ComponentsConfiguration", // Information about channels + 0x9102 : "CompressedBitsPerPixel", // Compressed bits per pixel + + // user information + 0x927C : "MakerNote", // Any desired information written by the manufacturer + 0x9286 : "UserComment", // Comments by user + + // related file + 0xA004 : "RelatedSoundFile", // Name of related sound file + + // date and time + 0x9003 : "DateTimeOriginal", // Date and time when the original image was generated + 0x9004 : "DateTimeDigitized", // Date and time when the image was stored digitally + 0x9290 : "SubsecTime", // Fractions of seconds for DateTime + 0x9291 : "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal + 0x9292 : "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized + + // picture-taking conditions + 0x829A : "ExposureTime", // Exposure time (in seconds) + 0x829D : "FNumber", // F number + 0x8822 : "ExposureProgram", // Exposure program + 0x8824 : "SpectralSensitivity", // Spectral sensitivity + 0x8827 : "ISOSpeedRatings", // ISO speed rating + 0x8828 : "OECF", // Optoelectric conversion factor + 0x9201 : "ShutterSpeedValue", // Shutter speed + 0x9202 : "ApertureValue", // Lens aperture + 0x9203 : "BrightnessValue", // Value of brightness + 0x9204 : "ExposureBias", // Exposure bias + 0x9205 : "MaxApertureValue", // Smallest F number of lens + 0x9206 : "SubjectDistance", // Distance to subject in meters + 0x9207 : "MeteringMode", // Metering mode + 0x9208 : "LightSource", // Kind of light source + 0x9209 : "Flash", // Flash status + 0x9214 : "SubjectArea", // Location and area of main subject + 0x920A : "FocalLength", // Focal length of the lens in mm + 0xA20B : "FlashEnergy", // Strobe energy in BCPS + 0xA20C : "SpatialFrequencyResponse", // + 0xA20E : "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit + 0xA20F : "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit + 0xA210 : "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution + 0xA214 : "SubjectLocation", // Location of subject in image + 0xA215 : "ExposureIndex", // Exposure index selected on camera + 0xA217 : "SensingMethod", // Image sensor type + 0xA300 : "FileSource", // Image source (3 == DSC) + 0xA301 : "SceneType", // Scene type (1 == directly photographed) + 0xA302 : "CFAPattern", // Color filter array geometric pattern + 0xA401 : "CustomRendered", // Special processing + 0xA402 : "ExposureMode", // Exposure mode + 0xA403 : "WhiteBalance", // 1 = auto white balance, 2 = manual + 0xA404 : "DigitalZoomRation", // Digital zoom ratio + 0xA405 : "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm) + 0xA406 : "SceneCaptureType", // Type of scene + 0xA407 : "GainControl", // Degree of overall image gain adjustment + 0xA408 : "Contrast", // Direction of contrast processing applied by camera + 0xA409 : "Saturation", // Direction of saturation processing applied by camera + 0xA40A : "Sharpness", // Direction of sharpness processing applied by camera + 0xA40B : "DeviceSettingDescription", // + 0xA40C : "SubjectDistanceRange", // Distance to subject + + // other tags + 0xA005 : "InteroperabilityIFDPointer", + 0xA420 : "ImageUniqueID" // Identifier assigned uniquely to each image + }; + + var TiffTags = EXIF.TiffTags = { + 0x0100 : "ImageWidth", + 0x0101 : "ImageHeight", + 0x8769 : "ExifIFDPointer", + 0x8825 : "GPSInfoIFDPointer", + 0xA005 : "InteroperabilityIFDPointer", + 0x0102 : "BitsPerSample", + 0x0103 : "Compression", + 0x0106 : "PhotometricInterpretation", + 0x0112 : "Orientation", + 0x0115 : "SamplesPerPixel", + 0x011C : "PlanarConfiguration", + 0x0212 : "YCbCrSubSampling", + 0x0213 : "YCbCrPositioning", + 0x011A : "XResolution", + 0x011B : "YResolution", + 0x0128 : "ResolutionUnit", + 0x0111 : "StripOffsets", + 0x0116 : "RowsPerStrip", + 0x0117 : "StripByteCounts", + 0x0201 : "JPEGInterchangeFormat", + 0x0202 : "JPEGInterchangeFormatLength", + 0x012D : "TransferFunction", + 0x013E : "WhitePoint", + 0x013F : "PrimaryChromaticities", + 0x0211 : "YCbCrCoefficients", + 0x0214 : "ReferenceBlackWhite", + 0x0132 : "DateTime", + 0x010E : "ImageDescription", + 0x010F : "Make", + 0x0110 : "Model", + 0x0131 : "Software", + 0x013B : "Artist", + 0x8298 : "Copyright" + }; + + var GPSTags = EXIF.GPSTags = { + 0x0000 : "GPSVersionID", + 0x0001 : "GPSLatitudeRef", + 0x0002 : "GPSLatitude", + 0x0003 : "GPSLongitudeRef", + 0x0004 : "GPSLongitude", + 0x0005 : "GPSAltitudeRef", + 0x0006 : "GPSAltitude", + 0x0007 : "GPSTimeStamp", + 0x0008 : "GPSSatellites", + 0x0009 : "GPSStatus", + 0x000A : "GPSMeasureMode", + 0x000B : "GPSDOP", + 0x000C : "GPSSpeedRef", + 0x000D : "GPSSpeed", + 0x000E : "GPSTrackRef", + 0x000F : "GPSTrack", + 0x0010 : "GPSImgDirectionRef", + 0x0011 : "GPSImgDirection", + 0x0012 : "GPSMapDatum", + 0x0013 : "GPSDestLatitudeRef", + 0x0014 : "GPSDestLatitude", + 0x0015 : "GPSDestLongitudeRef", + 0x0016 : "GPSDestLongitude", + 0x0017 : "GPSDestBearingRef", + 0x0018 : "GPSDestBearing", + 0x0019 : "GPSDestDistanceRef", + 0x001A : "GPSDestDistance", + 0x001B : "GPSProcessingMethod", + 0x001C : "GPSAreaInformation", + 0x001D : "GPSDateStamp", + 0x001E : "GPSDifferential" + }; + + var StringValues = EXIF.StringValues = { + ExposureProgram : { + 0 : "Not defined", + 1 : "Manual", + 2 : "Normal program", + 3 : "Aperture priority", + 4 : "Shutter priority", + 5 : "Creative program", + 6 : "Action program", + 7 : "Portrait mode", + 8 : "Landscape mode" + }, + MeteringMode : { + 0 : "Unknown", + 1 : "Average", + 2 : "CenterWeightedAverage", + 3 : "Spot", + 4 : "MultiSpot", + 5 : "Pattern", + 6 : "Partial", + 255 : "Other" + }, + LightSource : { + 0 : "Unknown", + 1 : "Daylight", + 2 : "Fluorescent", + 3 : "Tungsten (incandescent light)", + 4 : "Flash", + 9 : "Fine weather", + 10 : "Cloudy weather", + 11 : "Shade", + 12 : "Daylight fluorescent (D 5700 - 7100K)", + 13 : "Day white fluorescent (N 4600 - 5400K)", + 14 : "Cool white fluorescent (W 3900 - 4500K)", + 15 : "White fluorescent (WW 3200 - 3700K)", + 17 : "Standard light A", + 18 : "Standard light B", + 19 : "Standard light C", + 20 : "D55", + 21 : "D65", + 22 : "D75", + 23 : "D50", + 24 : "ISO studio tungsten", + 255 : "Other" + }, + Flash : { + 0x0000 : "Flash did not fire", + 0x0001 : "Flash fired", + 0x0005 : "Strobe return light not detected", + 0x0007 : "Strobe return light detected", + 0x0009 : "Flash fired, compulsory flash mode", + 0x000D : "Flash fired, compulsory flash mode, return light not detected", + 0x000F : "Flash fired, compulsory flash mode, return light detected", + 0x0010 : "Flash did not fire, compulsory flash mode", + 0x0018 : "Flash did not fire, auto mode", + 0x0019 : "Flash fired, auto mode", + 0x001D : "Flash fired, auto mode, return light not detected", + 0x001F : "Flash fired, auto mode, return light detected", + 0x0020 : "No flash function", + 0x0041 : "Flash fired, red-eye reduction mode", + 0x0045 : "Flash fired, red-eye reduction mode, return light not detected", + 0x0047 : "Flash fired, red-eye reduction mode, return light detected", + 0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode", + 0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", + 0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", + 0x0059 : "Flash fired, auto mode, red-eye reduction mode", + 0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode", + 0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode" + }, + SensingMethod : { + 1 : "Not defined", + 2 : "One-chip color area sensor", + 3 : "Two-chip color area sensor", + 4 : "Three-chip color area sensor", + 5 : "Color sequential area sensor", + 7 : "Trilinear sensor", + 8 : "Color sequential linear sensor" + }, + SceneCaptureType : { + 0 : "Standard", + 1 : "Landscape", + 2 : "Portrait", + 3 : "Night scene" + }, + SceneType : { + 1 : "Directly photographed" + }, + CustomRendered : { + 0 : "Normal process", + 1 : "Custom process" + }, + WhiteBalance : { + 0 : "Auto white balance", + 1 : "Manual white balance" + }, + GainControl : { + 0 : "None", + 1 : "Low gain up", + 2 : "High gain up", + 3 : "Low gain down", + 4 : "High gain down" + }, + Contrast : { + 0 : "Normal", + 1 : "Soft", + 2 : "Hard" + }, + Saturation : { + 0 : "Normal", + 1 : "Low saturation", + 2 : "High saturation" + }, + Sharpness : { + 0 : "Normal", + 1 : "Soft", + 2 : "Hard" + }, + SubjectDistanceRange : { + 0 : "Unknown", + 1 : "Macro", + 2 : "Close view", + 3 : "Distant view" + }, + FileSource : { + 3 : "DSC" + }, + + Components : { + 0 : "", + 1 : "Y", + 2 : "Cb", + 3 : "Cr", + 4 : "R", + 5 : "G", + 6 : "B" + } + }; + + function addEvent(element, event, handler) { + if (element.addEventListener) { + element.addEventListener(event, handler, false); + } else if (element.attachEvent) { + element.attachEvent("on" + event, handler); + } + } + + function imageHasData(img) { + return !!(img.exifdata); + } + + + function base64ToArrayBuffer(base64, contentType) { + contentType = contentType || base64.match(/^data\:([^\;]+)\;base64,/mi)[1] || ''; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg' + base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, ''); + var binary = atob(base64); + var len = binary.length; + var buffer = new ArrayBuffer(len); + var view = new Uint8Array(buffer); + for (var i = 0; i < len; i++) { + view[i] = binary.charCodeAt(i); + } + return buffer; + } + + function objectURLToBlob(url, callback) { + var http = new XMLHttpRequest(); + http.open("GET", url, true); + http.responseType = "blob"; + http.onload = function(e) { + if (this.status == 200 || this.status === 0) { + callback(this.response); + } + }; + http.send(); + } + + function getImageData(img, callback) { + function handleBinaryFile(binFile) { + var data = findEXIFinJPEG(binFile); + var iptcdata = findIPTCinJPEG(binFile); + img.exifdata = data || {}; + img.iptcdata = iptcdata || {}; + if (callback) { + callback.call(img); + } + } + + if (img.src) { + if (/^data\:/i.test(img.src)) { // Data URI + var arrayBuffer = base64ToArrayBuffer(img.src); + handleBinaryFile(arrayBuffer); + + } else if (/^blob\:/i.test(img.src)) { // Object URL + var fileReader = new FileReader(); + fileReader.onload = function(e) { + handleBinaryFile(e.target.result); + }; + objectURLToBlob(img.src, function (blob) { + fileReader.readAsArrayBuffer(blob); + }); + } else { + var http = new XMLHttpRequest(); + http.onload = function() { + if (this.status == 200 || this.status === 0) { + handleBinaryFile(http.response); + } else { + throw "Could not load image"; + } + http = null; + }; + http.open("GET", img.src, true); + http.responseType = "arraybuffer"; + http.send(null); + } + } else if (window.FileReader && (img instanceof window.Blob || img instanceof window.File)) { + var fileReader = new FileReader(); + fileReader.onload = function(e) { + if (debug) console.log("Got file of length " + e.target.result.byteLength); + handleBinaryFile(e.target.result); + }; + + fileReader.readAsArrayBuffer(img); + } + } + + function findEXIFinJPEG(file) { + var dataView = new DataView(file); + + if (debug) console.log("Got file of length " + file.byteLength); + if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { + if (debug) console.log("Not a valid JPEG"); + return false; // not a valid jpeg + } + + var offset = 2, + length = file.byteLength, + marker; + + while (offset < length) { + if (dataView.getUint8(offset) != 0xFF) { + if (debug) console.log("Not a valid marker at offset " + offset + ", found: " + dataView.getUint8(offset)); + return false; // not a valid marker, something is wrong + } + + marker = dataView.getUint8(offset + 1); + if (debug) console.log(marker); + + // we could implement handling for other markers here, + // but we're only looking for 0xFFE1 for EXIF data + + if (marker == 225) { + if (debug) console.log("Found 0xFFE1 marker"); + + return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2); + + // offset += 2 + file.getShortAt(offset+2, true); + + } else { + offset += 2 + dataView.getUint16(offset+2); + } + + } + + } + + function findIPTCinJPEG(file) { + var dataView = new DataView(file); + + if (debug) console.log("Got file of length " + file.byteLength); + if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { + if (debug) console.log("Not a valid JPEG"); + return false; // not a valid jpeg + } + + var offset = 2, + length = file.byteLength; + + + var isFieldSegmentStart = function(dataView, offset){ + return ( + dataView.getUint8(offset) === 0x38 && + dataView.getUint8(offset+1) === 0x42 && + dataView.getUint8(offset+2) === 0x49 && + dataView.getUint8(offset+3) === 0x4D && + dataView.getUint8(offset+4) === 0x04 && + dataView.getUint8(offset+5) === 0x04 + ); + }; + + while (offset < length) { + + if ( isFieldSegmentStart(dataView, offset )){ + + // Get the length of the name header (which is padded to an even number of bytes) + var nameHeaderLength = dataView.getUint8(offset+7); + if(nameHeaderLength % 2 !== 0) nameHeaderLength += 1; + // Check for pre photoshop 6 format + if(nameHeaderLength === 0) { + // Always 4 + nameHeaderLength = 4; + } + + var startOffset = offset + 8 + nameHeaderLength; + var sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength); + + return readIPTCData(file, startOffset, sectionLength); + + break; + + } + + + // Not the marker, continue searching + offset++; + + } + + } + var IptcFieldMap = { + 0x78 : 'caption', + 0x6E : 'credit', + 0x19 : 'keywords', + 0x37 : 'dateCreated', + 0x50 : 'byline', + 0x55 : 'bylineTitle', + 0x7A : 'captionWriter', + 0x69 : 'headline', + 0x74 : 'copyright', + 0x0F : 'category' + }; + function readIPTCData(file, startOffset, sectionLength){ + var dataView = new DataView(file); + var data = {}; + var fieldValue, fieldName, dataSize, segmentType, segmentSize; + var segmentStartPos = startOffset; + while(segmentStartPos < startOffset+sectionLength) { + if(dataView.getUint8(segmentStartPos) === 0x1C && dataView.getUint8(segmentStartPos+1) === 0x02){ + segmentType = dataView.getUint8(segmentStartPos+2); + if(segmentType in IptcFieldMap) { + dataSize = dataView.getInt16(segmentStartPos+3); + segmentSize = dataSize + 5; + fieldName = IptcFieldMap[segmentType]; + fieldValue = getStringFromDB(dataView, segmentStartPos+5, dataSize); + // Check if we already stored a value with this name + if(data.hasOwnProperty(fieldName)) { + // Value already stored with this name, create multivalue field + if(data[fieldName] instanceof Array) { + data[fieldName].push(fieldValue); + } + else { + data[fieldName] = [data[fieldName], fieldValue]; + } + } + else { + data[fieldName] = fieldValue; + } + } + + } + segmentStartPos++; + } + return data; + } + + + + function readTags(file, tiffStart, dirStart, strings, bigEnd) { + var entries = file.getUint16(dirStart, !bigEnd), + tags = {}, + entryOffset, tag, + i; + + for (i=0;i 4 ? valueOffset : (entryOffset + 8); + vals = []; + for (n=0;n 4 ? valueOffset : (entryOffset + 8); + return getStringFromDB(file, offset, numValues-1); + + case 3: // short, 16 bit int + if (numValues == 1) { + return file.getUint16(entryOffset + 8, !bigEnd); + } else { + offset = numValues > 2 ? valueOffset : (entryOffset + 8); + vals = []; + for (n=0;n +
    +``` + +### Complex objects + +```HTML +
    +
    +``` + +### Custom filtering + +```HTML +
    +
    +``` + +```JS +$scope.findPeople = function (term) { + // Suppose we have a people array + var found = []; + for (var i = 0; i < people.length; i++) { + if (/* search all properties you like */) { + found.push(people[i]); + } + } + return found; +}; +``` + +### Enable adding items + +```HTML +
    +
    +``` + +```JS +$scope.growable = ['Item 1', 'Item 2', 'Item 3']; +$scope.growableOptions = { + addText: 'Add new item', + onAdd: function () { + var newItem = 'Item ' + ($scope.growable.length + 1); + $scope.growable.push(newItem); + return newItem; + } +}; +``` + +### Asynchronous (server-side) filtering + +You need to use a function just as you would for a custom filter, with the difference that, given the asynchronous nature of AJAX, such function must return a promise. As soon as the promise is resolved, the new items (if any) are displayed inside the list. + +```HTML +
    +
    +``` + +```JS +$scope.searchAsync = function (term) { + var url = 'http://mysite.com/search?q=' + encodeURIComponent(term); + return $http.get(url); // This server call must return an array of objects +}; +``` + +**Note**: It is important that you set the `async` option to `true` to disable eager searching (and reduce the number of server calls). + +### Custom item template + +Whatever markup you put inside the element decorated with the `custom-select` attribute, acts as an item template. You have access to any AngularJS directive or filter inside this template. + +```HTML +
    +
    + +
    +
    + {{ t.name }}
    + {{ t.phone }} +
    +
    +
    +``` + +```JS +$scope.people = [ + { name: 'John Doe', phone: '555-123-456', picture: 'http://www.saintsfc.co.uk/images/common/bg_player_profile_default_big.png' }, + { name: 'Axel Zarate', phone: '888-777-6666', picture: 'https://avatars0.githubusercontent.com/u/4431445?s=60' }, + { name: 'Walter White', phone: '303-111-2222', picture: 'http://upstreamideas.org/wp-content/uploads/2013/10/ww.jpg' } +]; +``` + +### Reacting to items being selected +```HTML +
    +``` +```JS +$scope.level1Options = { + onSelect: function (item) { + // We're simulation the population of the nested options + var items = []; + for (var i = 1; i <= 5; i++) { + items.push(item + ': ' + 'Nested ' + i); + } + $scope.nestedItemsLevel2 = items; + } +}; +``` + +## Options +Name | Type | Details +---- | ---- | ------- +displayText | String | Placeholder text to display in the select box when there is no item is selected. Default: `'Select...'`. +emptyListText | String | Message to display in the dropdown when there source array is empty. Default: `'There are no items to display'`. +emptySearchResultText | String | Message to display in the dropdown when the search filter yields zero results (the difference with `emptyListText` is that there may be items in the data source, but none of them match the search string). Default: `'No results match "$0"'`. +addText | String | Text to display on the add button; additionally, `onAdd` callback function must be supplied. Default: `'Add'`. +onAdd | Function | A callback function to execute when the Add button is pressed. Default: `undefined`. +searchDelay | Integer | Time in milliseconds to wait until the filtering is performed. Default: `300` (0.3 seconds). +onSelect | Function | Callback function invoked when the user selects an item from the dropdown. +async | Boolean | Indicates whether the search filter is asynchronous or not; setting this option to `true` will limit the number of times the search function is evaluated (it will only run when the user types something in the search box). + +## Additional attributes +Name | Details +---- | ------- +cs-depends-on | Used to specify a scope variable to listen for changes. When a change is detected, the selected value and matches in the directive (if any) are reset. Useful for cascading select elements (like country/state/city) or any other scenario where you need to force item and/or filter evaluation. + +### Changing options globally +The configuration options can be set per directive instance but also globally by means of `customSelectDefaults`. You can override the default options at some point in your application (usually the module's `run` callback or in a localization file): + +```JS +var app = angular.module('myApp'); +app.run(['customSelectDefaults', function(customSelectDefaults) { + customSelectDefaults.displayText = 'Seleccionar...'; + customSelectDefaults.emptyListText = 'No hay resultados'; + customSelectDefaults.emptySearchResultText = 'Ningún resultado para "$0"'; + customSelectDefaults.addText = 'Agregar'; + customSelectDefaults.searchDelay = 500; +}]); +``` +## Dependencies +* jQuery +* AngularJS +* Twitter Bootstrap (2.x or 3.x) diff --git a/designsafe/static/vendor/js-custom-select/bower.json b/designsafe/static/vendor/js-custom-select/bower.json new file mode 100644 index 0000000000..0eab2c30ca --- /dev/null +++ b/designsafe/static/vendor/js-custom-select/bower.json @@ -0,0 +1,27 @@ +{ + "name": "js-custom-select", + "version": "0.0.1", + "homepage": "https://github.com/axel-zarate/js-custom-select", + "authors": [ + "Tom Caflisch " + ], + "main": [ + "js/customSelect.js", + "css/style.css" + ], + "description": "A custom auto-complete select box for AngularJS", + "keywords": [ + "angular", + "angularjs", + "select", + "dropdown" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/designsafe/static/vendor/js-custom-select/css/style.css b/designsafe/static/vendor/js-custom-select/css/style.css new file mode 100644 index 0000000000..83ab459998 --- /dev/null +++ b/designsafe/static/vendor/js-custom-select/css/style.css @@ -0,0 +1,178 @@ +.custom-select { + position: relative; + display: inline-block; + vertical-align: middle; + font-size: 13px; + zoom: 1; + *display: inline; + width: 220px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.custom-select.small { + width: 104px; +} +.custom-select.medium { + width: 164px; +} +.custom-select.large { + width: 300px; +} +.custom-select.xlarge { + width: 380px; +} +.custom-select > select { + display: none !important; +} +.custom-select > a.dropdown-toggle { + border-radius: 0; + line-height: 28px; + box-shadow: none; + background: #ffffff; + position: relative; + display: block; + overflow: hidden; + padding: 0 0 0 8px; + border: 1px solid #aaa; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + color: #888; + width: 100%; + -moz-box-sizing: border-box; + box-sizing: border-box; + height: 30px; +} +.custom-select > a.dropdown-toggle.disabled, +.custom-select > a.dropdown-toggle.disabled:hover { + cursor: not-allowed; + color: #aaa; + background: #eee; +} +.custom-select > a.dropdown-toggle:hover, +.custom-select.open > a.dropdown-toggle { + color: #333; +} +.custom-select.open > a.dropdown-toggle { + border-bottom: 0; + line-height: 29px; +} +.control-group.error .custom-select > a.dropdown-toggle { + border-color: #f09784; + color: #d68273; +} +.custom-select > a.dropdown-toggle > span { + display: block; + overflow: hidden; + margin-right: 26px; + text-overflow: ellipsis; + white-space: nowrap; +} +.custom-select > a.dropdown-toggle > b { + position: absolute; + top: 0; + right: 0; + display: block; + width: 18px; + height: 100%; +} +.custom-select > a.dropdown-toggle > b:before { + content: "\f0d7"; + display: inline-block; + font-family: FontAwesome; + font-size: 12px; + position: relative; + top: -1px; + left: 1px; +} +.custom-select.open > a.dropdown-toggle > b:before { + content: "\f0d8"; +} +.custom-select > .dropdown-menu { + margin-top: 0; + border: 1px solid #aaa; + border-top: 0; + padding-bottom: 0; + width: auto; + min-width: 100%; + -moz-box-sizing: border-box; + box-sizing: border-box; + border-radius: 0; +} +.custom-select > .dropdown-menu > .custom-select-search { + position: relative; + z-index: 1010; + margin: 0; + padding: 0 4px; + white-space: nowrap; +} +.custom-select > .dropdown-menu > .custom-select-search > input { + width: 100%; + height: 30px; + margin: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 4px 20px 4px 5px; + border-radius: 0; +} +.custom-select > .dropdown-menu > .custom-select-search:after { + content: "\f002"; + display: inline-block; + color: #888; + font-family: FontAwesome; + font-size: 14px; + position: absolute; + top: 4px; + right: 10px; +} +.custom-select > .dropdown-menu > ul { + border-color: #aaa; + border-top: 0; + margin: 4px 0; + padding: 0; + list-style: none; + background-color: #fff; + overflow-x: hidden; + overflow-y: auto; + max-height: 240px; + padding-right: 14px; +} +.custom-select > .dropdown-menu > ul > li > a { + font-size: 13px; + margin-bottom: 1px; + margin-top: 1px; + display: block; + padding: 3px 8px; + clear: both; + font-weight: normal; + line-height: 20px; + color: #333; + cursor: pointer; + width: 100%; +} +.custom-select > .dropdown-menu > ul > li > a:hover, +.custom-select > .dropdown-menu > ul > li > a:focus { + color: #fff; + text-decoration: none; + background-repeat: repeat-x; +} +.custom-select > .dropdown-menu > ul > li > a:hover { + background: #4f99c6; +} +.custom-select > .dropdown-menu > ul > li > a:focus { + background: #2283c5; +} +.custom-select > .dropdown-menu > ul > li.empty-result > em { + text-align: center; + padding: 4px 8px; + display: block; +} +.custom-select > .dropdown-menu > .custom-select-action > button { + border-radius: 0; + background-image: none; +} +.custom-select > .dropdown-menu:before { + border: 0; +} \ No newline at end of file diff --git a/designsafe/static/vendor/js-custom-select/index.html b/designsafe/static/vendor/js-custom-select/index.html new file mode 100644 index 0000000000..9ac7a21432 --- /dev/null +++ b/designsafe/static/vendor/js-custom-select/index.html @@ -0,0 +1,233 @@ + + + + + Angular Custom Select + + + + + + + + + + + + + + + + +
    +

    Custom Select Example

    + +

    Simple objects

    +

    Selected fruit: {{ fruit }}

    +
    + +

    Complex objects

    +

    Selected state: {{ state }}

    +
    + + +

    Custom options

    +

    Change the appearance

    + +
    +
    +
    +
    +
    + + + +
    +
    + +

    Add new items

    +
    + +

    Custom filtering

    +
    + +

    Custom item template

    +

    Selected person: {{ person | json }}

    +
    +
    + +
    +
    + {{ t.name }}
    + {{ t.phone }} +
    +
    +
    + +

    Nested

    + +
    + +
    +
    + + + + + + + + + + + + + + + diff --git a/designsafe/static/vendor/js-custom-select/js/customSelect.js b/designsafe/static/vendor/js-custom-select/js/customSelect.js new file mode 100644 index 0000000000..702aaaead7 --- /dev/null +++ b/designsafe/static/vendor/js-custom-select/js/customSelect.js @@ -0,0 +1,426 @@ +(function (angular, undefined) { + 'use strict'; + + // TODO: Move to polyfill? + if (!String.prototype.trim) { + String.prototype.trim = function () { + return this.replace(/^\s+|\s+$/g, ''); + }; + } + + /** + * A replacement utility for internationalization very similar to sprintf. + * + * @param replace {mixed} The tokens to replace depends on type + * string: all instances of $0 will be replaced + * array: each instance of $0, $1, $2 etc. will be placed with each array item in corresponding order + * object: all attributes will be iterated through, with :key being replaced with its corresponding value + * @return string + * + * @example: 'Hello :name, how are you :day'.format({ name:'John', day:'Today' }) + * @example: 'Records $0 to $1 out of $2 total'.format(['10', '20', '3000']) + * @example: '$0 agrees to all mentions $0 makes in the event that $0 hits a tree while $0 is driving drunk'.format('Bob') + */ + function format(value, replace) { + if (!value) { + return value; + } + var target = value.toString(); + if (replace === undefined) { + return target; + } + if (!angular.isArray(replace) && !angular.isObject(replace)) { + return target.split('$0').join(replace); + } + var token = angular.isArray(replace) && '$' || ':'; + + angular.forEach(replace, function (value, key) { + target = target.split(token + key).join(value); + }); + return target; + } + + var module = angular.module('ui.customSelect', []); + + module.value('customSelectDefaults', { + displayText: 'Select...', + emptyListText: 'There are no items to display', + emptySearchResultText: 'No results match "$0"', + addText: 'Add', + searchDelay: 300 + }); + + module.directive('customSelect', ['$parse', '$compile', '$timeout', '$q', 'customSelectDefaults', function ($parse, $compile, $timeout, $q, baseOptions) { + var CS_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/; + var VALUES_REGEXP = /^.+?(?=\||$)/; + + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, elem, attrs, controller) { + var customSelect = attrs.customSelect; + if (!customSelect) { + throw new Error('Expected custom-select attribute value.'); + } + + var match = customSelect.match(CS_OPTIONS_REGEXP); + + if (!match) { + throw new Error("Expected expression in form of " + + "'_select_ (as _label_)? for _value_ in _collection_[ track by _id_]'" + + " but got '" + customSelect + "'."); + } + + elem.addClass('dropdown custom-select'); + + // Ng-Options break down + var displayFn = $parse(match[2] || match[1]), + valueName = match[3], + valueFn = $parse(match[2] ? match[1] : valueName), + values = match[4], + valuesFn = $parse(values), + track = match[5], + trackByExpr = track ? " track by " + track : "", + dependsOn = attrs.csDependsOn; + + var options = getOptions(), + timeoutHandle, + lastSearch = '', + focusedIndex = -1, + matchMap = {}; + + var itemTemplate = elem.html().trim() || '{{' + (match[2] || match[1]) + '}}', + + dropdownTemplate = + '' + + '{{displayText}}' + ' ' + + '' + + '' + + ''; + + // Clear element contents + elem.empty(); + + // Create dropdown element + var dropdownElement = angular.element(dropdownTemplate), + anchorElement = dropdownElement.eq(0).dropdown(), + inputElement = dropdownElement.eq(1).find(':text'), + ulElement = dropdownElement.eq(1).find('ul'); + + // Create child scope for input and dropdown + var childScope = scope.$new(true); + configChildScope(); + + // Click event handler to set initial values and focus when the dropdown is shown + anchorElement.on('click', function (event) { + if (childScope.disabled) { + return; + } + childScope.$apply(function () { + lastSearch = ''; + childScope.searchTerm = ''; + }); + + focusedIndex = -1; + inputElement.focus(); + + // If filter is not async, perform search in case model changed + //if (!options.async) { + // getMatches(); + //} + }); + + if (dependsOn) { + scope.$watch(dependsOn, function (newVal, oldVal) { + if (newVal !== oldVal) { + childScope.matches = []; + childScope.select(undefined); + } + }); + } + + // Event handler for key press (when the user types a character while focus is on the anchor element) + anchorElement.on('keypress', function (event) { + if (!(event.altKey || event.ctrlKey)) { + anchorElement.click(); + } + }); + + // Event handler for Esc, Enter, Tab and Down keys on input search + inputElement.on('keydown', function (event) { + if (!/(13|27|40|^9$)/.test(event.keyCode)) return; + event.preventDefault(); + event.stopPropagation(); + + switch (event.keyCode) { + case 27: // Esc + anchorElement.dropdown('toggle'); + break; + case 13: // Enter + selectFromInput(); + break; + case 40: // Down + focusFirst(); + break; + case 9:// Tab + anchorElement.dropdown('toggle'); + break; + } + }); + + // Event handler for Up and Down keys on dropdown menu + ulElement.on('keydown', function (event) { + if (!/(38|40)/.test(event.keyCode)) return; + event.preventDefault(); + event.stopPropagation(); + + var items = ulElement.find('li > a'); + + if (!items.length) return; + if (event.keyCode == 38) focusedIndex--; // up + if (event.keyCode == 40 && focusedIndex < items.length - 1) focusedIndex++; // down + //if (!~focusedIndex) focusedIndex = 0; + + if (focusedIndex >= 0) { + items.eq(focusedIndex) + .focus(); + } else { + focusedIndex = -1; + inputElement.focus(); + } + }); + + resetMatches(); + + // Compile template against child scope + $compile(dropdownElement)(childScope); + elem.append(dropdownElement); + + // When model changes outside of the control, update the display text + controller.$render = function () { + setDisplayText(); + }; + + // Watch for changes in the default display text + childScope.$watch(getDisplayText, setDisplayText); + + childScope.$watch(function () { return elem.attr('disabled'); }, function (value) { + childScope.disabled = value; + }); + + childScope.$watch('searchTerm', function (newValue) { + if (timeoutHandle) { + $timeout.cancel(timeoutHandle); + } + + var term = (newValue || '').trim(); + timeoutHandle = $timeout(function () { + getMatches(term); + }, + // If empty string, do not delay + (term && options.searchDelay) || 0); + }); + + if (!options.async) { + var m = values.match(VALUES_REGEXP); + if (m) { + var originalValues = m[0]; + scope.$watchCollection(originalValues, function (value) { + if (angular.isArray(value)) { + + getMatches(); + } + }); + } + } + + // Support for autofocus + if ('autofocus' in attrs) { + anchorElement.focus(); + } + + var needsDisplayText; + function setDisplayText() { + var locals = { }; + locals[valueName] = controller.$modelValue; + var text = displayFn(scope, locals); + + if (text === undefined) { + var map = matchMap[hashKey(controller.$modelValue)]; + if (map) { + text = map.label; + } + } + + needsDisplayText = !text; + childScope.displayText = text || options.displayText; + } + + function getOptions() { + return angular.extend({}, baseOptions, scope.$eval(attrs.customSelectOptions)); + } + + function getDisplayText() { + options = getOptions(); + return options.displayText; + } + + function focusFirst() { + var opts = ulElement.find('li > a'); + if (opts.length > 0) { + focusedIndex = 0; + opts.eq(0).focus(); + } + } + + // Selects the first element on the list when the user presses Enter inside the search input + function selectFromInput() { + var opts = ulElement.find('li > a'); + if (opts.length > 0) { + var ngRepeatItem = opts.eq(0).scope(); + var item = ngRepeatItem[valueName]; + childScope.$apply(function () { + childScope.select(item); + }); + anchorElement.dropdown('toggle'); + } + } + + function getMatches(searchTerm) { + if (searchTerm === undefined) { + searchTerm = (childScope.searchTerm || "").trim(); + } + var locals = { $searchTerm: searchTerm } + $q.when(valuesFn(scope, locals)).then(function (matches) { + if (!matches) return; + + if (searchTerm === inputElement.val().trim()/* && hasFocus*/) { + matchMap = {}; + childScope.matches.length = 0; + for (var i = 0; i < matches.length; i++) { + locals[valueName] = matches[i]; + var value = valueFn(scope, locals), + label = displayFn(scope, locals); + + matchMap[hashKey(value)] = { + value: value, + label: label/*, + model: matches[i]*/ + }; + + childScope.matches.push(matches[i]); + } + //childScope.matches = matches; + } + + if (needsDisplayText) setDisplayText(); + }, function() { + resetMatches(); + }); + } + + function resetMatches() { + childScope.matches = []; + focusedIndex = -1; + }; + + function configChildScope() { + childScope.addText = options.addText; + childScope.emptySearchResultText = options.emptySearchResultText; + childScope.emptyListText = options.emptyListText; + + childScope.select = function (item) { + var locals = {}; + locals[valueName] = item; + var value = valueFn(childScope, locals); + //setDisplayText(displayFn(scope, locals)); + childScope.displayText = displayFn(childScope, locals) || options.displayText; + controller.$setViewValue(value); + + anchorElement.focus(); + + typeof options.onSelect === "function" && options.onSelect(item); + }; + + childScope.add = function () { + $q.when(options.onAdd(childScope.searchTerm), function (item) { + if (!item) return; + + var locals = {}; + locals[valueName] = item; + var value = valueFn(scope, locals), + label = displayFn(scope, locals); + + matchMap[hashKey(value)] = { + value: value, + label: label/*, + model: matches[i]*/ + }; + + childScope.matches.push(item); + childScope.select(item); + }); + }; + + childScope.format = format; + + setDisplayText(); + } + + var current = 0; + function hashKey(obj) { + if (obj === undefined) return 'undefined'; + + var objType = typeof obj, + key; + + if (objType == 'object' && obj !== null) { + if (typeof (key = obj.$$hashKey) == 'function') { + // must invoke on object to keep the right this + key = obj.$$hashKey(); + } else if (key === undefined) { + key = obj.$$hashKey = 'cs-' + (current++); + } + } else { + key = obj; + } + + return objType + ':' + key; + } + } + }; + }]); + + module.directive('stopPropagation', function () { + return { + restrict: 'A', + link: function (scope, elem, attrs, ctrl) { + var events = attrs['stopPropagation']; + elem.bind(events, function (event) { + event.stopPropagation(); + }); + } + }; + }); +})(angular); diff --git a/designsafe/static/vendor/leaflet-measure/.bower.json b/designsafe/static/vendor/leaflet-measure/.bower.json new file mode 100644 index 0000000000..ad898a5702 --- /dev/null +++ b/designsafe/static/vendor/leaflet-measure/.bower.json @@ -0,0 +1,58 @@ +{ + "name": "leaflet-measure", + "authors": [ + "Brandon Copeland " + ], + "description": "Coordinate, linear, and area measure tool for Leaflet maps", + "main": [ + "dist/leaflet-measure.min.js", + "dist/leaflet-measure.css" + ], + "repository": { + "type": "git", + "url": "git://github.com/ljagis/leaflet-measure.git" + }, + "keywords": [ + "leaflet", + "measure", + "measurement", + "control", + "coordinate", + "line", + "length", + "area", + "polygon", + "path" + ], + "license": "MIT", + "ignore": [ + ".sass-cache", + ".DS_Store", + "node_modules", + "bower_components", + "examples", + "scss", + "src", + ".gitignore", + ".jscsrc", + ".jshintrc", + "Gruntfile.js", + "package.json", + "README.md" + ], + "devDependencies": { + "normalize.css": "~3.0.2" + }, + "homepage": "https://github.com/ljagis/leaflet-measure", + "version": "2.1.7", + "_release": "2.1.7", + "_resolution": { + "type": "version", + "tag": "2.1.7", + "commit": "ea5d37aba04961c2ab2d14abb55ce2edbc6b9e2e" + }, + "_source": "https://github.com/ljagis/leaflet-measure.git", + "_target": "^2.1.7", + "_originalSource": "leaflet-measure", + "_direct": true +} \ No newline at end of file diff --git a/designsafe/static/vendor/leaflet-measure/LICENSE b/designsafe/static/vendor/leaflet-measure/LICENSE new file mode 100644 index 0000000000..8b18758078 --- /dev/null +++ b/designsafe/static/vendor/leaflet-measure/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 LJA Engineering, Inc - GIS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/designsafe/static/vendor/leaflet-measure/bower.json b/designsafe/static/vendor/leaflet-measure/bower.json new file mode 100644 index 0000000000..810f99fb7c --- /dev/null +++ b/designsafe/static/vendor/leaflet-measure/bower.json @@ -0,0 +1,46 @@ +{ + "name": "leaflet-measure", + "authors": [ + "Brandon Copeland " + ], + "description": "Coordinate, linear, and area measure tool for Leaflet maps", + "main": [ + "dist/leaflet-measure.min.js", + "dist/leaflet-measure.css" + ], + "repository": { + "type": "git", + "url": "git://github.com/ljagis/leaflet-measure.git" + }, + "keywords": [ + "leaflet", + "measure", + "measurement", + "control", + "coordinate", + "line", + "length", + "area", + "polygon", + "path" + ], + "license": "MIT", + "ignore": [ + ".sass-cache", + ".DS_Store", + "node_modules", + "bower_components", + "examples", + "scss", + "src", + ".gitignore", + ".jscsrc", + ".jshintrc", + "Gruntfile.js", + "package.json", + "README.md" + ], + "devDependencies": { + "normalize.css": "~3.0.2" + } +} diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/cancel.png b/designsafe/static/vendor/leaflet-measure/dist/images/cancel.png new file mode 100644 index 0000000000..a4e7c492ef Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/cancel.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/cancel_@2X.png b/designsafe/static/vendor/leaflet-measure/dist/images/cancel_@2X.png new file mode 100644 index 0000000000..dcc72f0c19 Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/cancel_@2X.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/check.png b/designsafe/static/vendor/leaflet-measure/dist/images/check.png new file mode 100644 index 0000000000..55f274b1d8 Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/check.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/check_@2X.png b/designsafe/static/vendor/leaflet-measure/dist/images/check_@2X.png new file mode 100644 index 0000000000..df8032e496 Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/check_@2X.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/focus.png b/designsafe/static/vendor/leaflet-measure/dist/images/focus.png new file mode 100644 index 0000000000..5a87d2433d Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/focus.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/focus_@2X.png b/designsafe/static/vendor/leaflet-measure/dist/images/focus_@2X.png new file mode 100644 index 0000000000..1eb7dd4cef Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/focus_@2X.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/rulers.png b/designsafe/static/vendor/leaflet-measure/dist/images/rulers.png new file mode 100644 index 0000000000..5d6339d9d6 Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/rulers.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/rulers_@2X.png b/designsafe/static/vendor/leaflet-measure/dist/images/rulers_@2X.png new file mode 100644 index 0000000000..7247a0c9d1 Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/rulers_@2X.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/start.png b/designsafe/static/vendor/leaflet-measure/dist/images/start.png new file mode 100644 index 0000000000..b8ca942b5d Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/start.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/start_@2X.png b/designsafe/static/vendor/leaflet-measure/dist/images/start_@2X.png new file mode 100644 index 0000000000..01da494c80 Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/start_@2X.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/trash.png b/designsafe/static/vendor/leaflet-measure/dist/images/trash.png new file mode 100644 index 0000000000..7ff478a456 Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/trash.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/images/trash_@2X.png b/designsafe/static/vendor/leaflet-measure/dist/images/trash_@2X.png new file mode 100644 index 0000000000..fea11a8c45 Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/dist/images/trash_@2X.png differ diff --git a/designsafe/static/vendor/leaflet-measure/dist/leaflet-measure.css b/designsafe/static/vendor/leaflet-measure/dist/leaflet-measure.css new file mode 100644 index 0000000000..10e16b0861 --- /dev/null +++ b/designsafe/static/vendor/leaflet-measure/dist/leaflet-measure.css @@ -0,0 +1 @@ +.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/designsafe/static/vendor/leaflet-measure/dist/leaflet-measure.js b/designsafe/static/vendor/leaflet-measure/dist/leaflet-measure.js new file mode 100644 index 0000000000..90c482d21a --- /dev/null +++ b/designsafe/static/vendor/leaflet-measure/dist/leaflet-measure.js @@ -0,0 +1,7648 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92); + g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92); + b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92); + + var x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805); + var y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722); + var z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505); + + return [x * 100, y *100, z * 100]; +} + +function rgb2lab(rgb) { + var xyz = rgb2xyz(rgb), + x = xyz[0], + y = xyz[1], + z = xyz[2], + l, a, b; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116); + + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); + + return [l, a, b]; +} + +function rgb2lch(args) { + return lab2lch(rgb2lab(args)); +} + +function hsl2rgb(hsl) { + var h = hsl[0] / 360, + s = hsl[1] / 100, + l = hsl[2] / 100, + t1, t2, t3, rgb, val; + + if (s == 0) { + val = l * 255; + return [val, val, val]; + } + + if (l < 0.5) + t2 = l * (1 + s); + else + t2 = l + s - l * s; + t1 = 2 * l - t2; + + rgb = [0, 0, 0]; + for (var i = 0; i < 3; i++) { + t3 = h + 1 / 3 * - (i - 1); + t3 < 0 && t3++; + t3 > 1 && t3--; + + if (6 * t3 < 1) + val = t1 + (t2 - t1) * 6 * t3; + else if (2 * t3 < 1) + val = t2; + else if (3 * t3 < 2) + val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; + else + val = t1; + + rgb[i] = val * 255; + } + + return rgb; +} + +function hsl2hsv(hsl) { + var h = hsl[0], + s = hsl[1] / 100, + l = hsl[2] / 100, + sv, v; + + if(l === 0) { + // no need to do calc on black + // also avoids divide by 0 error + return [0, 0, 0]; + } + + l *= 2; + s *= (l <= 1) ? l : 2 - l; + v = (l + s) / 2; + sv = (2 * s) / (l + s); + return [h, sv * 100, v * 100]; +} + +function hsl2hwb(args) { + return rgb2hwb(hsl2rgb(args)); +} + +function hsl2cmyk(args) { + return rgb2cmyk(hsl2rgb(args)); +} + +function hsl2keyword(args) { + return rgb2keyword(hsl2rgb(args)); +} + + +function hsv2rgb(hsv) { + var h = hsv[0] / 60, + s = hsv[1] / 100, + v = hsv[2] / 100, + hi = Math.floor(h) % 6; + + var f = h - Math.floor(h), + p = 255 * v * (1 - s), + q = 255 * v * (1 - (s * f)), + t = 255 * v * (1 - (s * (1 - f))), + v = 255 * v; + + switch(hi) { + case 0: + return [v, t, p]; + case 1: + return [q, v, p]; + case 2: + return [p, v, t]; + case 3: + return [p, q, v]; + case 4: + return [t, p, v]; + case 5: + return [v, p, q]; + } +} + +function hsv2hsl(hsv) { + var h = hsv[0], + s = hsv[1] / 100, + v = hsv[2] / 100, + sl, l; + + l = (2 - s) * v; + sl = s * v; + sl /= (l <= 1) ? l : 2 - l; + sl = sl || 0; + l /= 2; + return [h, sl * 100, l * 100]; +} + +function hsv2hwb(args) { + return rgb2hwb(hsv2rgb(args)) +} + +function hsv2cmyk(args) { + return rgb2cmyk(hsv2rgb(args)); +} + +function hsv2keyword(args) { + return rgb2keyword(hsv2rgb(args)); +} + +// http://dev.w3.org/csswg/css-color/#hwb-to-rgb +function hwb2rgb(hwb) { + var h = hwb[0] / 360, + wh = hwb[1] / 100, + bl = hwb[2] / 100, + ratio = wh + bl, + i, v, f, n; + + // wh + bl cant be > 1 + if (ratio > 1) { + wh /= ratio; + bl /= ratio; + } + + i = Math.floor(6 * h); + v = 1 - bl; + f = 6 * h - i; + if ((i & 0x01) != 0) { + f = 1 - f; + } + n = wh + f * (v - wh); // linear interpolation + + switch (i) { + default: + case 6: + case 0: r = v; g = n; b = wh; break; + case 1: r = n; g = v; b = wh; break; + case 2: r = wh; g = v; b = n; break; + case 3: r = wh; g = n; b = v; break; + case 4: r = n; g = wh; b = v; break; + case 5: r = v; g = wh; b = n; break; + } + + return [r * 255, g * 255, b * 255]; +} + +function hwb2hsl(args) { + return rgb2hsl(hwb2rgb(args)); +} + +function hwb2hsv(args) { + return rgb2hsv(hwb2rgb(args)); +} + +function hwb2cmyk(args) { + return rgb2cmyk(hwb2rgb(args)); +} + +function hwb2keyword(args) { + return rgb2keyword(hwb2rgb(args)); +} + +function cmyk2rgb(cmyk) { + var c = cmyk[0] / 100, + m = cmyk[1] / 100, + y = cmyk[2] / 100, + k = cmyk[3] / 100, + r, g, b; + + r = 1 - Math.min(1, c * (1 - k) + k); + g = 1 - Math.min(1, m * (1 - k) + k); + b = 1 - Math.min(1, y * (1 - k) + k); + return [r * 255, g * 255, b * 255]; +} + +function cmyk2hsl(args) { + return rgb2hsl(cmyk2rgb(args)); +} + +function cmyk2hsv(args) { + return rgb2hsv(cmyk2rgb(args)); +} + +function cmyk2hwb(args) { + return rgb2hwb(cmyk2rgb(args)); +} + +function cmyk2keyword(args) { + return rgb2keyword(cmyk2rgb(args)); +} + + +function xyz2rgb(xyz) { + var x = xyz[0] / 100, + y = xyz[1] / 100, + z = xyz[2] / 100, + r, g, b; + + r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986); + g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415); + b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570); + + // assume sRGB + r = r > 0.0031308 ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055) + : r = (r * 12.92); + + g = g > 0.0031308 ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055) + : g = (g * 12.92); + + b = b > 0.0031308 ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055) + : b = (b * 12.92); + + r = Math.min(Math.max(0, r), 1); + g = Math.min(Math.max(0, g), 1); + b = Math.min(Math.max(0, b), 1); + + return [r * 255, g * 255, b * 255]; +} + +function xyz2lab(xyz) { + var x = xyz[0], + y = xyz[1], + z = xyz[2], + l, a, b; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116); + + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); + + return [l, a, b]; +} + +function xyz2lch(args) { + return lab2lch(xyz2lab(args)); +} + +function lab2xyz(lab) { + var l = lab[0], + a = lab[1], + b = lab[2], + x, y, z, y2; + + if (l <= 8) { + y = (l * 100) / 903.3; + y2 = (7.787 * (y / 100)) + (16 / 116); + } else { + y = 100 * Math.pow((l + 16) / 116, 3); + y2 = Math.pow(y / 100, 1/3); + } + + x = x / 95.047 <= 0.008856 ? x = (95.047 * ((a / 500) + y2 - (16 / 116))) / 7.787 : 95.047 * Math.pow((a / 500) + y2, 3); + + z = z / 108.883 <= 0.008859 ? z = (108.883 * (y2 - (b / 200) - (16 / 116))) / 7.787 : 108.883 * Math.pow(y2 - (b / 200), 3); + + return [x, y, z]; +} + +function lab2lch(lab) { + var l = lab[0], + a = lab[1], + b = lab[2], + hr, h, c; + + hr = Math.atan2(b, a); + h = hr * 360 / 2 / Math.PI; + if (h < 0) { + h += 360; + } + c = Math.sqrt(a * a + b * b); + return [l, c, h]; +} + +function lab2rgb(args) { + return xyz2rgb(lab2xyz(args)); +} + +function lch2lab(lch) { + var l = lch[0], + c = lch[1], + h = lch[2], + a, b, hr; + + hr = h / 360 * 2 * Math.PI; + a = c * Math.cos(hr); + b = c * Math.sin(hr); + return [l, a, b]; +} + +function lch2xyz(args) { + return lab2xyz(lch2lab(args)); +} + +function lch2rgb(args) { + return lab2rgb(lch2lab(args)); +} + +function keyword2rgb(keyword) { + return cssKeywords[keyword]; +} + +function keyword2hsl(args) { + return rgb2hsl(keyword2rgb(args)); +} + +function keyword2hsv(args) { + return rgb2hsv(keyword2rgb(args)); +} + +function keyword2hwb(args) { + return rgb2hwb(keyword2rgb(args)); +} + +function keyword2cmyk(args) { + return rgb2cmyk(keyword2rgb(args)); +} + +function keyword2lab(args) { + return rgb2lab(keyword2rgb(args)); +} + +function keyword2xyz(args) { + return rgb2xyz(keyword2rgb(args)); +} + +var cssKeywords = { + 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] +}; + +var reverseKeywords = {}; +for (var key in cssKeywords) { + reverseKeywords[JSON.stringify(cssKeywords[key])] = key; +} + +},{}],3:[function(require,module,exports){ +var conversions = require("./conversions"); + +var convert = function() { + return new Converter(); +} + +for (var func in conversions) { + // export Raw versions + convert[func + "Raw"] = (function(func) { + // accept array or plain args + return function(arg) { + if (typeof arg == "number") + arg = Array.prototype.slice.call(arguments); + return conversions[func](arg); + } + })(func); + + var pair = /(\w+)2(\w+)/.exec(func), + from = pair[1], + to = pair[2]; + + // export rgb2hsl and ["rgb"]["hsl"] + convert[from] = convert[from] || {}; + + convert[from][to] = convert[func] = (function(func) { + return function(arg) { + if (typeof arg == "number") + arg = Array.prototype.slice.call(arguments); + + var val = conversions[func](arg); + if (typeof val == "string" || val === undefined) + return val; // keyword + + for (var i = 0; i < val.length; i++) + val[i] = Math.round(val[i]); + return val; + } + })(func); +} + + +/* Converter does lazy conversion and caching */ +var Converter = function() { + this.convs = {}; +}; + +/* Either get the values for a space or + set the values for a space, depending on args */ +Converter.prototype.routeSpace = function(space, args) { + var values = args[0]; + if (values === undefined) { + // color.rgb() + return this.getValues(space); + } + // color.rgb(10, 10, 10) + if (typeof values == "number") { + values = Array.prototype.slice.call(args); + } + + return this.setValues(space, values); +}; + +/* Set the values for a space, invalidating cache */ +Converter.prototype.setValues = function(space, values) { + this.space = space; + this.convs = {}; + this.convs[space] = values; + return this; +}; + +/* Get the values for a space. If there's already + a conversion for the space, fetch it, otherwise + compute it */ +Converter.prototype.getValues = function(space) { + var vals = this.convs[space]; + if (!vals) { + var fspace = this.space, + from = this.convs[fspace]; + vals = convert[fspace][space](from); + + this.convs[space] = vals; + } + return vals; +}; + +["rgb", "hsl", "hsv", "cmyk", "keyword"].forEach(function(space) { + Converter.prototype[space] = function(vals) { + return this.routeSpace(space, arguments); + } +}); + +module.exports = convert; +},{"./conversions":2}],4:[function(require,module,exports){ +module.exports = { + "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] +}; +},{}],5:[function(require,module,exports){ +/* MIT license */ +var colorNames = require('color-name'); + +module.exports = { + getRgba: getRgba, + getHsla: getHsla, + getRgb: getRgb, + getHsl: getHsl, + getHwb: getHwb, + getAlpha: getAlpha, + + hexString: hexString, + rgbString: rgbString, + rgbaString: rgbaString, + percentString: percentString, + percentaString: percentaString, + hslString: hslString, + hslaString: hslaString, + hwbString: hwbString, + keyword: keyword +} + +function getRgba(string) { + if (!string) { + return; + } + var abbr = /^#([a-fA-F0-9]{3})$/, + hex = /^#([a-fA-F0-9]{6})$/, + rgba = /^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/, + per = /^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/, + keyword = /(\D+)/; + + var rgb = [0, 0, 0], + a = 1, + match = string.match(abbr); + if (match) { + match = match[1]; + for (var i = 0; i < rgb.length; i++) { + rgb[i] = parseInt(match[i] + match[i], 16); + } + } + else if (match = string.match(hex)) { + match = match[1]; + for (var i = 0; i < rgb.length; i++) { + rgb[i] = parseInt(match.slice(i * 2, i * 2 + 2), 16); + } + } + else if (match = string.match(rgba)) { + for (var i = 0; i < rgb.length; i++) { + rgb[i] = parseInt(match[i + 1]); + } + a = parseFloat(match[4]); + } + else if (match = string.match(per)) { + for (var i = 0; i < rgb.length; i++) { + rgb[i] = Math.round(parseFloat(match[i + 1]) * 2.55); + } + a = parseFloat(match[4]); + } + else if (match = string.match(keyword)) { + if (match[1] == "transparent") { + return [0, 0, 0, 0]; + } + rgb = colorNames[match[1]]; + if (!rgb) { + return; + } + } + + for (var i = 0; i < rgb.length; i++) { + rgb[i] = scale(rgb[i], 0, 255); + } + if (!a && a != 0) { + a = 1; + } + else { + a = scale(a, 0, 1); + } + rgb[3] = a; + return rgb; +} + +function getHsla(string) { + if (!string) { + return; + } + var hsl = /^hsla?\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/; + var match = string.match(hsl); + if (match) { + var alpha = parseFloat(match[4]); + var h = scale(parseInt(match[1]), 0, 360), + s = scale(parseFloat(match[2]), 0, 100), + l = scale(parseFloat(match[3]), 0, 100), + a = scale(isNaN(alpha) ? 1 : alpha, 0, 1); + return [h, s, l, a]; + } +} + +function getHwb(string) { + if (!string) { + return; + } + var hwb = /^hwb\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/; + var match = string.match(hwb); + if (match) { + var alpha = parseFloat(match[4]); + var h = scale(parseInt(match[1]), 0, 360), + w = scale(parseFloat(match[2]), 0, 100), + b = scale(parseFloat(match[3]), 0, 100), + a = scale(isNaN(alpha) ? 1 : alpha, 0, 1); + return [h, w, b, a]; + } +} + +function getRgb(string) { + var rgba = getRgba(string); + return rgba && rgba.slice(0, 3); +} + +function getHsl(string) { + var hsla = getHsla(string); + return hsla && hsla.slice(0, 3); +} + +function getAlpha(string) { + var vals = getRgba(string); + if (vals) { + return vals[3]; + } + else if (vals = getHsla(string)) { + return vals[3]; + } + else if (vals = getHwb(string)) { + return vals[3]; + } +} + +// generators +function hexString(rgb) { + return "#" + hexDouble(rgb[0]) + hexDouble(rgb[1]) + + hexDouble(rgb[2]); +} + +function rgbString(rgba, alpha) { + if (alpha < 1 || (rgba[3] && rgba[3] < 1)) { + return rgbaString(rgba, alpha); + } + return "rgb(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ")"; +} + +function rgbaString(rgba, alpha) { + if (alpha === undefined) { + alpha = (rgba[3] !== undefined ? rgba[3] : 1); + } + return "rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + + ", " + alpha + ")"; +} + +function percentString(rgba, alpha) { + if (alpha < 1 || (rgba[3] && rgba[3] < 1)) { + return percentaString(rgba, alpha); + } + var r = Math.round(rgba[0]/255 * 100), + g = Math.round(rgba[1]/255 * 100), + b = Math.round(rgba[2]/255 * 100); + + return "rgb(" + r + "%, " + g + "%, " + b + "%)"; +} + +function percentaString(rgba, alpha) { + var r = Math.round(rgba[0]/255 * 100), + g = Math.round(rgba[1]/255 * 100), + b = Math.round(rgba[2]/255 * 100); + return "rgba(" + r + "%, " + g + "%, " + b + "%, " + (alpha || rgba[3] || 1) + ")"; +} + +function hslString(hsla, alpha) { + if (alpha < 1 || (hsla[3] && hsla[3] < 1)) { + return hslaString(hsla, alpha); + } + return "hsl(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%)"; +} + +function hslaString(hsla, alpha) { + if (alpha === undefined) { + alpha = (hsla[3] !== undefined ? hsla[3] : 1); + } + return "hsla(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%, " + + alpha + ")"; +} + +// hwb is a bit different than rgb(a) & hsl(a) since there is no alpha specific syntax +// (hwb have alpha optional & 1 is default value) +function hwbString(hwb, alpha) { + if (alpha === undefined) { + alpha = (hwb[3] !== undefined ? hwb[3] : 1); + } + return "hwb(" + hwb[0] + ", " + hwb[1] + "%, " + hwb[2] + "%" + + (alpha !== undefined && alpha !== 1 ? ", " + alpha : "") + ")"; +} + +function keyword(rgb) { + return reverseNames[rgb.slice(0, 3)]; +} + +// helpers +function scale(num, min, max) { + return Math.min(Math.max(min, num), max); +} + +function hexDouble(num) { + var str = num.toString(16).toUpperCase(); + return (str.length < 2) ? "0" + str : str; +} + + +//create a list of reverse color names +var reverseNames = {}; +for (var name in colorNames) { + reverseNames[colorNames[name]] = name; +} + +},{"color-name":4}],6:[function(require,module,exports){ +/* MIT license */ +var convert = require("color-convert"), + string = require("color-string"); + +var Color = function(obj) { + if (obj instanceof Color) return obj; + if (! (this instanceof Color)) return new Color(obj); + + this.values = { + rgb: [0, 0, 0], + hsl: [0, 0, 0], + hsv: [0, 0, 0], + hwb: [0, 0, 0], + cmyk: [0, 0, 0, 0], + alpha: 1 + } + + // parse Color() argument + if (typeof obj == "string") { + var vals = string.getRgba(obj); + if (vals) { + this.setValues("rgb", vals); + } + else if(vals = string.getHsla(obj)) { + this.setValues("hsl", vals); + } + else if(vals = string.getHwb(obj)) { + this.setValues("hwb", vals); + } + else { + throw new Error("Unable to parse color from string \"" + obj + "\""); + } + } + else if (typeof obj == "object") { + var vals = obj; + if(vals["r"] !== undefined || vals["red"] !== undefined) { + this.setValues("rgb", vals) + } + else if(vals["l"] !== undefined || vals["lightness"] !== undefined) { + this.setValues("hsl", vals) + } + else if(vals["v"] !== undefined || vals["value"] !== undefined) { + this.setValues("hsv", vals) + } + else if(vals["w"] !== undefined || vals["whiteness"] !== undefined) { + this.setValues("hwb", vals) + } + else if(vals["c"] !== undefined || vals["cyan"] !== undefined) { + this.setValues("cmyk", vals) + } + else { + throw new Error("Unable to parse color from object " + JSON.stringify(obj)); + } + } +} + +Color.prototype = { + rgb: function (vals) { + return this.setSpace("rgb", arguments); + }, + hsl: function(vals) { + return this.setSpace("hsl", arguments); + }, + hsv: function(vals) { + return this.setSpace("hsv", arguments); + }, + hwb: function(vals) { + return this.setSpace("hwb", arguments); + }, + cmyk: function(vals) { + return this.setSpace("cmyk", arguments); + }, + + rgbArray: function() { + return this.values.rgb; + }, + hslArray: function() { + return this.values.hsl; + }, + hsvArray: function() { + return this.values.hsv; + }, + hwbArray: function() { + if (this.values.alpha !== 1) { + return this.values.hwb.concat([this.values.alpha]) + } + return this.values.hwb; + }, + cmykArray: function() { + return this.values.cmyk; + }, + rgbaArray: function() { + var rgb = this.values.rgb; + return rgb.concat([this.values.alpha]); + }, + hslaArray: function() { + var hsl = this.values.hsl; + return hsl.concat([this.values.alpha]); + }, + alpha: function(val) { + if (val === undefined) { + return this.values.alpha; + } + this.setValues("alpha", val); + return this; + }, + + red: function(val) { + return this.setChannel("rgb", 0, val); + }, + green: function(val) { + return this.setChannel("rgb", 1, val); + }, + blue: function(val) { + return this.setChannel("rgb", 2, val); + }, + hue: function(val) { + return this.setChannel("hsl", 0, val); + }, + saturation: function(val) { + return this.setChannel("hsl", 1, val); + }, + lightness: function(val) { + return this.setChannel("hsl", 2, val); + }, + saturationv: function(val) { + return this.setChannel("hsv", 1, val); + }, + whiteness: function(val) { + return this.setChannel("hwb", 1, val); + }, + blackness: function(val) { + return this.setChannel("hwb", 2, val); + }, + value: function(val) { + return this.setChannel("hsv", 2, val); + }, + cyan: function(val) { + return this.setChannel("cmyk", 0, val); + }, + magenta: function(val) { + return this.setChannel("cmyk", 1, val); + }, + yellow: function(val) { + return this.setChannel("cmyk", 2, val); + }, + black: function(val) { + return this.setChannel("cmyk", 3, val); + }, + + hexString: function() { + return string.hexString(this.values.rgb); + }, + rgbString: function() { + return string.rgbString(this.values.rgb, this.values.alpha); + }, + rgbaString: function() { + return string.rgbaString(this.values.rgb, this.values.alpha); + }, + percentString: function() { + return string.percentString(this.values.rgb, this.values.alpha); + }, + hslString: function() { + return string.hslString(this.values.hsl, this.values.alpha); + }, + hslaString: function() { + return string.hslaString(this.values.hsl, this.values.alpha); + }, + hwbString: function() { + return string.hwbString(this.values.hwb, this.values.alpha); + }, + keyword: function() { + return string.keyword(this.values.rgb, this.values.alpha); + }, + + rgbNumber: function() { + return (this.values.rgb[0] << 16) | (this.values.rgb[1] << 8) | this.values.rgb[2]; + }, + + luminosity: function() { + // http://www.w3.org/TR/WCAG20/#relativeluminancedef + var rgb = this.values.rgb; + var lum = []; + for (var i = 0; i < rgb.length; i++) { + var chan = rgb[i] / 255; + lum[i] = (chan <= 0.03928) ? chan / 12.92 + : Math.pow(((chan + 0.055) / 1.055), 2.4) + } + return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2]; + }, + + contrast: function(color2) { + // http://www.w3.org/TR/WCAG20/#contrast-ratiodef + var lum1 = this.luminosity(); + var lum2 = color2.luminosity(); + if (lum1 > lum2) { + return (lum1 + 0.05) / (lum2 + 0.05) + }; + return (lum2 + 0.05) / (lum1 + 0.05); + }, + + level: function(color2) { + var contrastRatio = this.contrast(color2); + return (contrastRatio >= 7.1) + ? 'AAA' + : (contrastRatio >= 4.5) + ? 'AA' + : ''; + }, + + dark: function() { + // YIQ equation from http://24ways.org/2010/calculating-color-contrast + var rgb = this.values.rgb, + yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; + return yiq < 128; + }, + + light: function() { + return !this.dark(); + }, + + negate: function() { + var rgb = [] + for (var i = 0; i < 3; i++) { + rgb[i] = 255 - this.values.rgb[i]; + } + this.setValues("rgb", rgb); + return this; + }, + + lighten: function(ratio) { + this.values.hsl[2] += this.values.hsl[2] * ratio; + this.setValues("hsl", this.values.hsl); + return this; + }, + + darken: function(ratio) { + this.values.hsl[2] -= this.values.hsl[2] * ratio; + this.setValues("hsl", this.values.hsl); + return this; + }, + + saturate: function(ratio) { + this.values.hsl[1] += this.values.hsl[1] * ratio; + this.setValues("hsl", this.values.hsl); + return this; + }, + + desaturate: function(ratio) { + this.values.hsl[1] -= this.values.hsl[1] * ratio; + this.setValues("hsl", this.values.hsl); + return this; + }, + + whiten: function(ratio) { + this.values.hwb[1] += this.values.hwb[1] * ratio; + this.setValues("hwb", this.values.hwb); + return this; + }, + + blacken: function(ratio) { + this.values.hwb[2] += this.values.hwb[2] * ratio; + this.setValues("hwb", this.values.hwb); + return this; + }, + + greyscale: function() { + var rgb = this.values.rgb; + // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale + var val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11; + this.setValues("rgb", [val, val, val]); + return this; + }, + + clearer: function(ratio) { + this.setValues("alpha", this.values.alpha - (this.values.alpha * ratio)); + return this; + }, + + opaquer: function(ratio) { + this.setValues("alpha", this.values.alpha + (this.values.alpha * ratio)); + return this; + }, + + rotate: function(degrees) { + var hue = this.values.hsl[0]; + hue = (hue + degrees) % 360; + hue = hue < 0 ? 360 + hue : hue; + this.values.hsl[0] = hue; + this.setValues("hsl", this.values.hsl); + return this; + }, + + mix: function(color2, weight) { + weight = 1 - (weight == null ? 0.5 : weight); + + // algorithm from Sass's mix(). Ratio of first color in mix is + // determined by the alphas of both colors and the weight + var t1 = weight * 2 - 1, + d = this.alpha() - color2.alpha(); + + var weight1 = (((t1 * d == -1) ? t1 : (t1 + d) / (1 + t1 * d)) + 1) / 2; + var weight2 = 1 - weight1; + + var rgb = this.rgbArray(); + var rgb2 = color2.rgbArray(); + + for (var i = 0; i < rgb.length; i++) { + rgb[i] = rgb[i] * weight1 + rgb2[i] * weight2; + } + this.setValues("rgb", rgb); + + var alpha = this.alpha() * weight + color2.alpha() * (1 - weight); + this.setValues("alpha", alpha); + + return this; + }, + + toJSON: function() { + return this.rgb(); + }, + + clone: function() { + return new Color(this.rgb()); + } +} + + +Color.prototype.getValues = function(space) { + var vals = {}; + for (var i = 0; i < space.length; i++) { + vals[space.charAt(i)] = this.values[space][i]; + } + if (this.values.alpha != 1) { + vals["a"] = this.values.alpha; + } + // {r: 255, g: 255, b: 255, a: 0.4} + return vals; +} + +Color.prototype.setValues = function(space, vals) { + var spaces = { + "rgb": ["red", "green", "blue"], + "hsl": ["hue", "saturation", "lightness"], + "hsv": ["hue", "saturation", "value"], + "hwb": ["hue", "whiteness", "blackness"], + "cmyk": ["cyan", "magenta", "yellow", "black"] + }; + + var maxes = { + "rgb": [255, 255, 255], + "hsl": [360, 100, 100], + "hsv": [360, 100, 100], + "hwb": [360, 100, 100], + "cmyk": [100, 100, 100, 100] + }; + + var alpha = 1; + if (space == "alpha") { + alpha = vals; + } + else if (vals.length) { + // [10, 10, 10] + this.values[space] = vals.slice(0, space.length); + alpha = vals[space.length]; + } + else if (vals[space.charAt(0)] !== undefined) { + // {r: 10, g: 10, b: 10} + for (var i = 0; i < space.length; i++) { + this.values[space][i] = vals[space.charAt(i)]; + } + alpha = vals.a; + } + else if (vals[spaces[space][0]] !== undefined) { + // {red: 10, green: 10, blue: 10} + var chans = spaces[space]; + for (var i = 0; i < space.length; i++) { + this.values[space][i] = vals[chans[i]]; + } + alpha = vals.alpha; + } + this.values.alpha = Math.max(0, Math.min(1, (alpha !== undefined ? alpha : this.values.alpha) )); + if (space == "alpha") { + return; + } + + // cap values of the space prior converting all values + for (var i = 0; i < space.length; i++) { + var capped = Math.max(0, Math.min(maxes[space][i], this.values[space][i])); + this.values[space][i] = Math.round(capped); + } + + // convert to all the other color spaces + for (var sname in spaces) { + if (sname != space) { + this.values[sname] = convert[space][sname](this.values[space]) + } + + // cap values + for (var i = 0; i < sname.length; i++) { + var capped = Math.max(0, Math.min(maxes[sname][i], this.values[sname][i])); + this.values[sname][i] = Math.round(capped); + } + } + return true; +} + +Color.prototype.setSpace = function(space, args) { + var vals = args[0]; + if (vals === undefined) { + // color.rgb() + return this.getValues(space); + } + // color.rgb(10, 10, 10) + if (typeof vals == "number") { + vals = Array.prototype.slice.call(args); + } + this.setValues(space, vals); + return this; +} + +Color.prototype.setChannel = function(space, index, val) { + if (val === undefined) { + // color.red() + return this.values[space][index]; + } + // color.red(100) + this.values[space][index] = val; + this.setValues(space, this.values[space]); + return this; +} + +module.exports = Color; + +},{"color-convert":3,"color-string":5}],7:[function(require,module,exports){ +module.exports = require('./lib/geocrunch'); +},{"./lib/geocrunch":12}],8:[function(require,module,exports){ +// distance.js - Distance mixins for Paths + +var _ = require('underscore'); + +var R = require('./constants').EARTHRADIUS; +var units = require('./units'); +var flipCoords = require('./flipcoords'); + +// Area conversions (from sqmeters) +var convertFuncs = { + sqmeters: function (a) { + return a; + }, + sqmiles: function (a) { + return units.sqMeters.toSqMiles(a); + }, + acres: function (a) { + return units.sqMeters.toAcres(a); + } +}; + +// Calculates area in square meters +// Method taken from OpenLayers API, https://github.com/openlayers/openlayers/blob/master/lib/OpenLayers/Geometry/LinearRing.js#L270 +var calcArea = function (coordArray) { + var area = 0, i, l, c1, c2; + for (i = 0, l = coordArray.length; i < l; i += 1) { + c1 = coordArray[i]; + c2 = coordArray[(i + 1) % coordArray.length]; // Access next item in array until last item is i, then accesses first (0) + area = area + units.degrees.toRadians(c2[0] - c1[0]) * (2 + Math.sin(units.degrees.toRadians(c1[1])) + Math.sin(units.degrees.toRadians(c2[1]))); + } + return Math.abs(area * R * R / 2); +}; + +var calcCenter = function (coordArray) { + var offset = coordArray[0], twiceArea = 0, x = 0, y = 0, i, l, c1, c2, f; + if (coordArray.length === 1) { + return coordArray[0]; + } + for (i = 0, l = coordArray.length; i < l; i += 1) { + c1 = coordArray[i]; + c2 = coordArray[(i + 1) % coordArray.length]; // Access next item in array until last item is i, then accesses first (0) + f = (c1[1] - offset[1]) * (c2[0] - offset[0]) - (c2[1] - offset[1]) * (c1[0] - offset[0]); + twiceArea = twiceArea + f; + x = x + ((c1[0] + c2[0] - 2 * offset[0]) * f); + y = y + ((c1[1] + c2[1] - 2 * offset[1]) * f); + } + f = twiceArea * 3; + return [x / f + offset[0], y / f + offset[1]]; +}; + +module.exports = { + _internalAreaCalc: function () { + // If not set, set this._calcedArea to total area in UNITS + // Checks for cache to prevent additional unnecessary calcs + if (!this._calcedArea) { + if (this._coords.length < 3) { + this._calcedArea = 0; + } else { + this._calcedArea = calcArea(this._coords); + } + } + }, + _internalCenterCalc: function () { + if (!this._calcedCenter && this._coords.length) { + this._calcedCenter = calcCenter(this._coords); + } + }, + area: function (options) { + var opts = _.extend({ + units: 'sqmeters' + }, options); + this._internalAreaCalc(); + if (_.isFunction(convertFuncs[opts.units])) { + return convertFuncs[opts.units](this._calcedArea); + } + // TODO. Handle non-matching units + }, + center: function () { + this._internalCenterCalc(); + return this._options.imBackwards === true ? flipCoords(this._calcedCenter) : this._calcedCenter; + } +}; +},{"./constants":9,"./flipcoords":11,"./units":14,"underscore":15}],9:[function(require,module,exports){ +// utils/constants.js + +module.exports = { + EARTHRADIUS: 6371000 // R in meters +}; +},{}],10:[function(require,module,exports){ +// distance.js - Distance mixins for Paths + +var _ = require('underscore'); + +var R = require('./constants').EARTHRADIUS; +var units = require('./units'); + +// Distance conversions (from meters) +var convertFuncs = { + meters: function (d) { + return d; + }, + kilometers: function (d) { + return units.meters.toKilometers(d); + }, + feet: function (d) { + return units.meters.toFeet(d); + }, + miles: function (d) { + return units.meters.toMiles(d); + } +}; + +// Distance in meters +// Always positive regardless of direction +// Calculation based on Haversine Formula http://en.wikipedia.org/wiki/Haversine_formula +// Another method is @ http://www.movable-type.co.uk/scripts/latlong-vincenty.html but seems way overcomplicated +var calcDistance = function (coord1, coord2) { + var deltaLng = units.degrees.toRadians(coord1[0] - coord2[0]), + deltaLat = units.degrees.toRadians(coord1[1] - coord2[1]), + lat1 = units.degrees.toRadians(coord1[1]), + lat2 = units.degrees.toRadians(coord2[1]), + hvsLng = Math.sin(deltaLng / 2), + hvsLat = Math.sin(deltaLat / 2); + + var a = hvsLat * hvsLat + hvsLng * hvsLng * Math.cos(lat1) * Math.cos(lat2); + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +}; + +module.exports = { + _internalDistanceCalc: function () { + // If not set, set this._calcedDistance to total distance in meters + // Checks for cache to prevent additional unnecessary calcs + var distance = 0, i, l; + if (!this._calcedDistance) { + for (i = 0, l = this._coords.length; i < l; i += 1) { + if (i > 0) { + distance = distance + calcDistance(this._coords[i - 1], this._coords[i]); + } + } + this._calcedDistance = distance; + } + }, + distance: function (options) { + var opts = _.extend({ + units: 'meters' + }, options); + this._internalDistanceCalc(); + if (_.isFunction(convertFuncs[opts.units])) { + return convertFuncs[opts.units](this._calcedDistance); + } + // TODO. Handle non-matching units + } +}; +},{"./constants":9,"./units":14,"underscore":15}],11:[function(require,module,exports){ +// utils/flipcoords.js - Util functions for working with backwards coordinates [lat, lng] + +var _ = require('underscore'); + +module.exports = function (backwardsCoordArray) { + return _.map(backwardsCoordArray, function (backwardsCoord) { + return [backwardsCoord[1], backwardsCoord[0]]; + }); +}; +},{"underscore":15}],12:[function(require,module,exports){ +// geocrunch.js + +var _ = require('underscore'); + +var Path = require('./path'); +var distanceMixins = require('./distance'), + areaMixins = require('./area'); + +_.extend(Path.prototype, distanceMixins, areaMixins); + +exports.path = function (coords, options) { + return new Path(coords, options); +}; +},{"./area":8,"./distance":10,"./path":13,"underscore":15}],13:[function(require,module,exports){ +// path.js - Object for working with a linear path of coordinates + +var flipCoords = require('./flipcoords'); + +var Path = function (coords, options) { + this._options = options || {}; + + // Set this._coords... Think about flipping at time of calcs for less iterations/better perf. May risk code clarity and mixin ease. + coords = coords || []; + this._coords = this._options.imBackwards === true ? flipCoords(coords) : coords; +}; + +module.exports = Path; + +},{"./flipcoords":11}],14:[function(require,module,exports){ +// units.js - Standard unit conversions + +exports.meters = { + toFeet: function (m) { + return m * 3.28084; + }, + toKilometers: function (m) { + return m * 0.001; + }, + toMiles: function (m) { + return m * 0.000621371; + } +}; + +exports.sqMeters = { + toSqMiles: function (m) { + return m * 0.000000386102; + }, + toAcres: function (m) { + return m * 0.000247105; + } +}; + +exports.degrees = { + toRadians: function (d) { + return d * Math.PI / 180; + } +}; +},{}],15:[function(require,module,exports){ +// Underscore.js 1.5.2 +// http://underscorejs.org +// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.5.2'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, length = obj.length; i < length; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; + } + } + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results.push(iterator.call(context, value, index, list)); + }); + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { + var result; + any(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); + each(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, iterator, context) { + return _.filter(obj, function(value, index, list) { + return !iterator.call(context, value, index, list); + }, context); + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); + each(obj, function(value, index, list) { + if (!(result = result && iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); + each(obj, function(value, index, list) { + if (result || (result = iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + return any(obj, function(value) { + return value === target; + }); + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, function(value){ return value[key]; }); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs, first) { + if (_.isEmpty(attrs)) return first ? void 0 : []; + return _[first ? 'find' : 'filter'](obj, function(value) { + for (var key in attrs) { + if (attrs[key] !== value[key]) return false; + } + return true; + }); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.where(obj, attrs, true); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return -Infinity; + var result = {computed : -Infinity, value: -Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed > result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return Infinity; + var result = {computed : Infinity, value: Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed < result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Shuffle an array, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // Sample **n** random values from an array. + // If **n** is not specified, returns a single random element from the array. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (arguments.length < 2 || guard) { + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + return _.isFunction(value) ? value : function(obj){ return obj[value]; }; + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, value, context) { + var iterator = lookupIterator(value); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, value, context) { + var result = {}; + var iterator = value == null ? _.identity : lookupIterator(value); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, key, value) { + (_.has(result, key) ? result[key] : (result[key] = [])).push(value); + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, key, value) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, key) { + _.has(result, key) ? result[key]++ : result[key] = 1; + }); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = iterator == null ? _.identity : lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + return (n == null) || guard ? array[0] : slice.call(array, 0, n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if ((n == null) || guard) { + return array[array.length - 1]; + } else { + return slice.call(array, Math.max(array.length - n, 0)); + } + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } + each(input, function(value) { + if (_.isArray(value) || _.isArguments(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(_.flatten(arguments, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.indexOf(other, item) >= 0; + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var length = _.max(_.pluck(arguments, "length").concat(0)); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(arguments, '' + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, length = list.length; i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, length = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < length; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(length); + + while(idx < length) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. + _.partial = function(func) { + var args = slice.call(arguments, 1); + return function() { + return func.apply(this, args.concat(slice.call(arguments))); + }; + }; + + // Bind all of an object's methods to that object. Useful for ensuring that + // all callbacks defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length === 0) throw new Error("bindAll must be passed function names"); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + options || (options = {}); + var later = function() { + previous = options.leading === false ? 0 : new Date; + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date; + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + return function() { + context = this; + args = arguments; + timestamp = new Date(); + var later = function() { + var last = (new Date()) - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) result = func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) result = func.apply(context, args); + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return function() { + var args = [func]; + push.apply(args, arguments); + return wrapper.apply(this, args); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = nativeKeys || function(obj) { + if (obj !== Object(obj)) throw new TypeError('Invalid object'); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = new Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = new Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor))) { + return false; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + var accum = Array(Math.max(0, n)); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property) { + if (object == null) return void 0; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + +}).call(this); + +},{}],16:[function(require,module,exports){ + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var root = this; + + // Save the previous value of the `humanize` variable. + var previousHumanize = root.humanize; + + var humanize = {}; + + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = humanize; + } + exports.humanize = humanize; + } else { + if (typeof define === 'function' && define.amd) { + define('humanize', function() { + return humanize; + }); + } + root.humanize = humanize; + } + + humanize.noConflict = function() { + root.humanize = previousHumanize; + return this; + }; + + humanize.pad = function(str, count, padChar, type) { + str += ''; + if (!padChar) { + padChar = ' '; + } else if (padChar.length > 1) { + padChar = padChar.charAt(0); + } + type = (type === undefined) ? 'left' : 'right'; + + if (type === 'right') { + while (str.length < count) { + str = str + padChar; + } + } else { + // default to left + while (str.length < count) { + str = padChar + str; + } + } + + return str; + }; + + // gets current unix time + humanize.time = function() { + return new Date().getTime() / 1000; + }; + + /** + * PHP-inspired date + */ + + /* jan feb mar apr may jun jul aug sep oct nov dec */ + var dayTableCommon = [ 0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ]; + var dayTableLeap = [ 0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 ]; + // var mtable_common[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + // static int ml_table_leap[13] = { 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + + + humanize.date = function(format, timestamp) { + var jsdate = ((timestamp === undefined) ? new Date() : // Not provided + (timestamp instanceof Date) ? new Date(timestamp) : // JS Date() + new Date(timestamp * 1000) // UNIX timestamp (auto-convert to int) + ); + + var formatChr = /\\?([a-z])/gi; + var formatChrCb = function (t, s) { + return f[t] ? f[t]() : s; + }; + + var shortDayTxt = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + var monthTxt = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + + var f = { + /* Day */ + // Day of month w/leading 0; 01..31 + d: function () { return humanize.pad(f.j(), 2, '0'); }, + + // Shorthand day name; Mon..Sun + D: function () { return f.l().slice(0, 3); }, + + // Day of month; 1..31 + j: function () { return jsdate.getDate(); }, + + // Full day name; Monday..Sunday + l: function () { return shortDayTxt[f.w()]; }, + + // ISO-8601 day of week; 1[Mon]..7[Sun] + N: function () { return f.w() || 7; }, + + // Ordinal suffix for day of month; st, nd, rd, th + S: function () { + var j = f.j(); + return j > 4 && j < 21 ? 'th' : {1: 'st', 2: 'nd', 3: 'rd'}[j % 10] || 'th'; + }, + + // Day of week; 0[Sun]..6[Sat] + w: function () { return jsdate.getDay(); }, + + // Day of year; 0..365 + z: function () { + return (f.L() ? dayTableLeap[f.n()] : dayTableCommon[f.n()]) + f.j() - 1; + }, + + /* Week */ + // ISO-8601 week number + W: function () { + // days between midweek of this week and jan 4 + // (f.z() - f.N() + 1 + 3.5) - 3 + var midWeekDaysFromJan4 = f.z() - f.N() + 1.5; + // 1 + number of weeks + rounded week + return humanize.pad(1 + Math.floor(Math.abs(midWeekDaysFromJan4) / 7) + (midWeekDaysFromJan4 % 7 > 3.5 ? 1 : 0), 2, '0'); + }, + + /* Month */ + // Full month name; January..December + F: function () { return monthTxt[jsdate.getMonth()]; }, + + // Month w/leading 0; 01..12 + m: function () { return humanize.pad(f.n(), 2, '0'); }, + + // Shorthand month name; Jan..Dec + M: function () { return f.F().slice(0, 3); }, + + // Month; 1..12 + n: function () { return jsdate.getMonth() + 1; }, + + // Days in month; 28..31 + t: function () { return (new Date(f.Y(), f.n(), 0)).getDate(); }, + + /* Year */ + // Is leap year?; 0 or 1 + L: function () { return new Date(f.Y(), 1, 29).getMonth() === 1 ? 1 : 0; }, + + // ISO-8601 year + o: function () { + var n = f.n(); + var W = f.W(); + return f.Y() + (n === 12 && W < 9 ? -1 : n === 1 && W > 9); + }, + + // Full year; e.g. 1980..2010 + Y: function () { return jsdate.getFullYear(); }, + + // Last two digits of year; 00..99 + y: function () { return (String(f.Y())).slice(-2); }, + + /* Time */ + // am or pm + a: function () { return jsdate.getHours() > 11 ? 'pm' : 'am'; }, + + // AM or PM + A: function () { return f.a().toUpperCase(); }, + + // Swatch Internet time; 000..999 + B: function () { + var unixTime = jsdate.getTime() / 1000; + var secondsPassedToday = unixTime % 86400 + 3600; // since it's based off of UTC+1 + if (secondsPassedToday < 0) { secondsPassedToday += 86400; } + var beats = ((secondsPassedToday) / 86.4) % 1000; + if (unixTime < 0) { + return Math.ceil(beats); + } + return Math.floor(beats); + }, + + // 12-Hours; 1..12 + g: function () { return f.G() % 12 || 12; }, + + // 24-Hours; 0..23 + G: function () { return jsdate.getHours(); }, + + // 12-Hours w/leading 0; 01..12 + h: function () { return humanize.pad(f.g(), 2, '0'); }, + + // 24-Hours w/leading 0; 00..23 + H: function () { return humanize.pad(f.G(), 2, '0'); }, + + // Minutes w/leading 0; 00..59 + i: function () { return humanize.pad(jsdate.getMinutes(), 2, '0'); }, + + // Seconds w/leading 0; 00..59 + s: function () { return humanize.pad(jsdate.getSeconds(), 2, '0'); }, + + // Microseconds; 000000-999000 + u: function () { return humanize.pad(jsdate.getMilliseconds() * 1000, 6, '0'); }, + + // Whether or not the date is in daylight savings time + /* + I: function () { + // Compares Jan 1 minus Jan 1 UTC to Jul 1 minus Jul 1 UTC. + // If they are not equal, then DST is observed. + var Y = f.Y(); + return 0 + ((new Date(Y, 0) - Date.UTC(Y, 0)) !== (new Date(Y, 6) - Date.UTC(Y, 6))); + }, + */ + + // Difference to GMT in hour format; e.g. +0200 + O: function () { + var tzo = jsdate.getTimezoneOffset(); + var tzoNum = Math.abs(tzo); + return (tzo > 0 ? '-' : '+') + humanize.pad(Math.floor(tzoNum / 60) * 100 + tzoNum % 60, 4, '0'); + }, + + // Difference to GMT w/colon; e.g. +02:00 + P: function () { + var O = f.O(); + return (O.substr(0, 3) + ':' + O.substr(3, 2)); + }, + + // Timezone offset in seconds (-43200..50400) + Z: function () { return -jsdate.getTimezoneOffset() * 60; }, + + // Full Date/Time, ISO-8601 date + c: function () { return 'Y-m-d\\TH:i:sP'.replace(formatChr, formatChrCb); }, + + // RFC 2822 + r: function () { return 'D, d M Y H:i:s O'.replace(formatChr, formatChrCb); }, + + // Seconds since UNIX epoch + U: function () { return jsdate.getTime() / 1000 || 0; } + }; + + return format.replace(formatChr, formatChrCb); + }; + + + /** + * format number by adding thousands separaters and significant digits while rounding + */ + humanize.numberFormat = function(number, decimals, decPoint, thousandsSep) { + decimals = isNaN(decimals) ? 2 : Math.abs(decimals); + decPoint = (decPoint === undefined) ? '.' : decPoint; + thousandsSep = (thousandsSep === undefined) ? ',' : thousandsSep; + + var sign = number < 0 ? '-' : ''; + number = Math.abs(+number || 0); + + var intPart = parseInt(number.toFixed(decimals), 10) + ''; + var j = intPart.length > 3 ? intPart.length % 3 : 0; + + return sign + (j ? intPart.substr(0, j) + thousandsSep : '') + intPart.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep) + (decimals ? decPoint + Math.abs(number - intPart).toFixed(decimals).slice(2) : ''); + }; + + + /** + * For dates that are the current day or within one day, return 'today', 'tomorrow' or 'yesterday', as appropriate. + * Otherwise, format the date using the passed in format string. + * + * Examples (when 'today' is 17 Feb 2007): + * 16 Feb 2007 becomes yesterday. + * 17 Feb 2007 becomes today. + * 18 Feb 2007 becomes tomorrow. + * Any other day is formatted according to given argument or the DATE_FORMAT setting if no argument is given. + */ + humanize.naturalDay = function(timestamp, format) { + timestamp = (timestamp === undefined) ? humanize.time() : timestamp; + format = (format === undefined) ? 'Y-m-d' : format; + + var oneDay = 86400; + var d = new Date(); + var today = (new Date(d.getFullYear(), d.getMonth(), d.getDate())).getTime() / 1000; + + if (timestamp < today && timestamp >= today - oneDay) { + return 'yesterday'; + } else if (timestamp >= today && timestamp < today + oneDay) { + return 'today'; + } else if (timestamp >= today + oneDay && timestamp < today + 2 * oneDay) { + return 'tomorrow'; + } + + return humanize.date(format, timestamp); + }; + + /** + * returns a string representing how many seconds, minutes or hours ago it was or will be in the future + * Will always return a relative time, most granular of seconds to least granular of years. See unit tests for more details + */ + humanize.relativeTime = function(timestamp) { + timestamp = (timestamp === undefined) ? humanize.time() : timestamp; + + var currTime = humanize.time(); + var timeDiff = currTime - timestamp; + + // within 2 seconds + if (timeDiff < 2 && timeDiff > -2) { + return (timeDiff >= 0 ? 'just ' : '') + 'now'; + } + + // within a minute + if (timeDiff < 60 && timeDiff > -60) { + return (timeDiff >= 0 ? Math.floor(timeDiff) + ' seconds ago' : 'in ' + Math.floor(-timeDiff) + ' seconds'); + } + + // within 2 minutes + if (timeDiff < 120 && timeDiff > -120) { + return (timeDiff >= 0 ? 'about a minute ago' : 'in about a minute'); + } + + // within an hour + if (timeDiff < 3600 && timeDiff > -3600) { + return (timeDiff >= 0 ? Math.floor(timeDiff / 60) + ' minutes ago' : 'in ' + Math.floor(-timeDiff / 60) + ' minutes'); + } + + // within 2 hours + if (timeDiff < 7200 && timeDiff > -7200) { + return (timeDiff >= 0 ? 'about an hour ago' : 'in about an hour'); + } + + // within 24 hours + if (timeDiff < 86400 && timeDiff > -86400) { + return (timeDiff >= 0 ? Math.floor(timeDiff / 3600) + ' hours ago' : 'in ' + Math.floor(-timeDiff / 3600) + ' hours'); + } + + // within 2 days + var days2 = 2 * 86400; + if (timeDiff < days2 && timeDiff > -days2) { + return (timeDiff >= 0 ? '1 day ago' : 'in 1 day'); + } + + // within 29 days + var days29 = 29 * 86400; + if (timeDiff < days29 && timeDiff > -days29) { + return (timeDiff >= 0 ? Math.floor(timeDiff / 86400) + ' days ago' : 'in ' + Math.floor(-timeDiff / 86400) + ' days'); + } + + // within 60 days + var days60 = 60 * 86400; + if (timeDiff < days60 && timeDiff > -days60) { + return (timeDiff >= 0 ? 'about a month ago' : 'in about a month'); + } + + var currTimeYears = parseInt(humanize.date('Y', currTime), 10); + var timestampYears = parseInt(humanize.date('Y', timestamp), 10); + var currTimeMonths = currTimeYears * 12 + parseInt(humanize.date('n', currTime), 10); + var timestampMonths = timestampYears * 12 + parseInt(humanize.date('n', timestamp), 10); + + // within a year + var monthDiff = currTimeMonths - timestampMonths; + if (monthDiff < 12 && monthDiff > -12) { + return (monthDiff >= 0 ? monthDiff + ' months ago' : 'in ' + (-monthDiff) + ' months'); + } + + var yearDiff = currTimeYears - timestampYears; + if (yearDiff < 2 && yearDiff > -2) { + return (yearDiff >= 0 ? 'a year ago' : 'in a year'); + } + + return (yearDiff >= 0 ? yearDiff + ' years ago' : 'in ' + (-yearDiff) + ' years'); + }; + + /** + * Converts an integer to its ordinal as a string. + * + * 1 becomes 1st + * 2 becomes 2nd + * 3 becomes 3rd etc + */ + humanize.ordinal = function(number) { + number = parseInt(number, 10); + number = isNaN(number) ? 0 : number; + var sign = number < 0 ? '-' : ''; + number = Math.abs(number); + var tens = number % 100; + + return sign + number + (tens > 4 && tens < 21 ? 'th' : {1: 'st', 2: 'nd', 3: 'rd'}[number % 10] || 'th'); + }; + + /** + * Formats the value like a 'human-readable' file size (i.e. '13 KB', '4.1 MB', '102 bytes', etc). + * + * For example: + * If value is 123456789, the output would be 117.7 MB. + */ + humanize.filesize = function(filesize, kilo, decimals, decPoint, thousandsSep, suffixSep) { + kilo = (kilo === undefined) ? 1024 : kilo; + if (filesize <= 0) { return '0 bytes'; } + if (filesize < kilo && decimals === undefined) { decimals = 0; } + if (suffixSep === undefined) { suffixSep = ' '; } + return humanize.intword(filesize, ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'], kilo, decimals, decPoint, thousandsSep, suffixSep); + }; + + /** + * Formats the value like a 'human-readable' number (i.e. '13 K', '4.1 M', '102', etc). + * + * For example: + * If value is 123456789, the output would be 117.7 M. + */ + humanize.intword = function(number, units, kilo, decimals, decPoint, thousandsSep, suffixSep) { + var humanized, unit; + + units = units || ['', 'K', 'M', 'B', 'T'], + unit = units.length - 1, + kilo = kilo || 1000, + decimals = isNaN(decimals) ? 2 : Math.abs(decimals), + decPoint = decPoint || '.', + thousandsSep = thousandsSep || ',', + suffixSep = suffixSep || ''; + + for (var i=0; i < units.length; i++) { + if (number < Math.pow(kilo, i+1)) { + unit = i; + break; + } + } + humanized = number / Math.pow(kilo, unit); + + var suffix = units[unit] ? suffixSep + units[unit] : ''; + return humanize.numberFormat(humanized, decimals, decPoint, thousandsSep) + suffix; + }; + + /** + * Replaces line breaks in plain text with appropriate HTML + * A single newline becomes an HTML line break (
    ) and a new line followed by a blank line becomes a paragraph break (

    ). + * + * For example: + * If value is Joel\nis a\n\nslug, the output will be

    Joel
    is a

    slug

    + */ + humanize.linebreaks = function(str) { + // remove beginning and ending newlines + str = str.replace(/^([\n|\r]*)/, ''); + str = str.replace(/([\n|\r]*)$/, ''); + + // normalize all to \n + str = str.replace(/(\r\n|\n|\r)/g, "\n"); + + // any consecutive new lines more than 2 gets turned into p tags + str = str.replace(/(\n{2,})/g, '

    '); + + // any that are singletons get turned into br + str = str.replace(/\n/g, '
    '); + return '

    ' + str + '

    '; + }; + + /** + * Converts all newlines in a piece of plain text to HTML line breaks (
    ). + */ + humanize.nl2br = function(str) { + return str.replace(/(\r\n|\n|\r)/g, '
    '); + }; + + /** + * Truncates a string if it is longer than the specified number of characters. + * Truncated strings will end with a translatable ellipsis sequence ('…'). + */ + humanize.truncatechars = function(string, length) { + if (string.length <= length) { return string; } + return string.substr(0, length) + '…'; + }; + + /** + * Truncates a string after a certain number of words. + * Newlines within the string will be removed. + */ + humanize.truncatewords = function(string, numWords) { + var words = string.split(' '); + if (words.length < numWords) { return string; } + return words.slice(0, numWords).join(' ') + '…'; + }; + +}).call(this); + +},{}],17:[function(require,module,exports){ +(function (process){ +/** + * @author John Resig + * @author Originally by Marcus Spiegel + * @link https://github.com/jeresig/i18n-node + * @license http://opensource.org/licenses/MIT + * + * @version 0.4.7 + */ + +// dependencies +var vsprintf = require("sprintf").vsprintf, + fs = require("fs"), + path = require("path"); + + +function dotNotation (obj, is, value) { + if (obj.hasOwnProperty(is)) { + return obj[is]; + } + + if (typeof is === 'string') { + return dotNotation(obj, is.split('.'), value); + } else if (is.length === 1 && value !== undefined) { + return obj[is[0]] = value; + } else if (is.length === 0) { + return obj; + } else { + if (obj.hasOwnProperty(is[0])) { + return dotNotation(obj[is[0]], is.slice(1), value); + } else { + return obj[is.join('.')] = is.join('.'); + } + } +} + +var i18n = module.exports = function (opt) { + var self = this; + + // Put into dev or production mode + this.devMode = process.env.NODE_ENV !== "production"; + + // Copy over options + for (var prop in opt) { + this[prop] = opt[prop]; + } + + // you may register helpers in global scope, up to you + if (typeof this.register === "object") { + i18n.resMethods.forEach(function (method) { + self.register[method] = self[method].bind(self); + }); + } + + // implicitly read all locales + // if it's an array of locale names, read in the data + if (opt.locales && opt.locales.forEach) { + this.locales = {}; + + opt.locales.forEach(function (locale) { + self.readFile(locale); + }); + + this.defaultLocale = opt.locales[0]; + } + + // Set the locale to the default locale + this.setLocale(this.defaultLocale); + + // Check the defaultLocale + if (!this.locales[this.defaultLocale]) { + console.error("Not a valid default locale."); + } + + if (this.request) { + if (this.subdomain) { + this.setLocaleFromSubdomain(this.request); + } + + if (this.query !== false) { + this.setLocaleFromQuery(this.request); + } + + if (this.session !== false) { + this.setLocaleFromSessionVar(this.request); + } + + this.prefLocale = this.preferredLocale(); + + if (this.prefLocale !== false && this.prefLocale !== this.locale) { + this.setLocale(this.prefLocale); + } + } +}; + +i18n.version = "0.4.7"; + +i18n.localeCache = {}; +i18n.resMethods = ["__", "__n", "getLocale", "isPreferredLocale"]; + +i18n.expressBind = function (app, opt) { + if (!app) { + return; + } + + app.use(function (req, res, next) { + opt.request = req; + req.i18n = new i18n(opt); + + // Express 3 + if (res.locals) { + i18n.registerMethods(res.locals, req) + } + + next(); + }); + + // Express 2 + if (app.dynamicHelpers) { + app.dynamicHelpers(i18n.registerMethods({})); + } +}; + +i18n.registerMethods = function (helpers, req) { + i18n.resMethods.forEach(function (method) { + if (req) { + helpers[method] = req.i18n[method].bind(req.i18n); + } else { + helpers[method] = function (req) { + return req.i18n[method].bind(req.i18n); + }; + } + + }); + + return helpers; +}; + +i18n.prototype = { + defaultLocale: "en", + extension: ".js", + directory: "./locales", + cookieName: "lang", + sessionVarName: "locale", + indent: "\t", + + parse: JSON.parse, + + dump: function (data, indent) { + return JSON.stringify(data, null, indent); + }, + + __: function () { + var msg = this.translate(this.locale, arguments[0]); + + if (arguments.length > 1) { + msg = vsprintf(msg, Array.prototype.slice.call(arguments, 1)); + } + + return msg; + }, + + __n: function (pathOrSingular, countOrPlural, additionalOrCount) { + var msg; + if (typeof countOrPlural === 'number') { + var path = pathOrSingular; + var count = countOrPlural; + msg = this.translate(this.locale, path); + + msg = vsprintf(parseInt(count, 10) > 1 ? msg.other : msg.one, Array.prototype.slice.call(arguments, 1)); + } else { + var singular = pathOrSingular; + var plural = countOrPlural; + var count = additionalOrCount; + msg = this.translate(this.locale, singular, plural); + + msg = vsprintf(parseInt(count, 10) > 1 ? msg.other : msg.one, [count]); + + if (arguments.length > 3) { + msg = vsprintf(msg, Array.prototype.slice.call(arguments, 3)); + } + } + + return msg; + }, + + setLocale: function (locale) { + + if (!locale) return; + + if (!this.locales[locale]) { + if (this.devMode) { + console.warn("Locale (" + locale + ") not found."); + } + + locale = this.defaultLocale; + } + + return (this.locale = locale); + }, + + getLocale: function () { + return this.locale; + }, + + isPreferredLocale: function () { + return !this.prefLocale || + this.prefLocale === this.getLocale(); + }, + + setLocaleFromSessionVar: function (req) { + req = req || this.request; + + if (!req || !req.session || !req.session[this.sessionVarName]) { + return; + } + + var locale = req.session[this.sessionVarName]; + + if (this.locales[locale]) { + if (this.devMode) { + console.log("Overriding locale from query: " + locale); + } + this.setLocale(locale); + } + + }, + + setLocaleFromQuery: function (req) { + req = req || this.request; + + if (!req || !req.query || !req.query.lang) { + return; + } + + var locale = (req.query.lang+'').toLowerCase(); + + if (this.locales[locale]) { + if (this.devMode) { + console.log("Overriding locale from query: " + locale); + } + + this.setLocale(locale); + } + }, + + setLocaleFromSubdomain: function (req) { + req = req || this.request; + + if (!req || !req.headers || !req.headers.host) { + return; + } + + if (/^([^.]+)/.test(req.headers.host) && this.locales[RegExp.$1]) { + if (this.devMode) { + console.log("Overriding locale from host: " + RegExp.$1); + } + + this.setLocale(RegExp.$1); + } + }, + + setLocaleFromCookie: function (req) { + req = req || this.request; + + if (!req || !req.cookies || !this.cookieName || !req.cookies[this.cookieName]) { + return; + } + + var locale = req.cookies[this.cookieName].toLowerCase(); + + if (this.locales[locale]) { + if (this.devMode) { + console.log("Overriding locale from cookie: " + locale); + } + + this.setLocale(locale); + } + }, + + setLocaleFromEnvironmentVariable: function () { + if (!process.env.LANG) { + return; + } + var locale = process.env.LANG.split("_")[0]; + if (this.locales[locale]) { + if (this.devMode) { + console.log("Overriding locale from environment variable: " + locale); + } + + this.setLocale(locale); + } + }, + + preferredLocale: function (req) { + req = req || this.request; + + if (!req || !req.headers) { + return; + } + + var accept = req.headers["accept-language"] || "", + regExp = /(^|,\s*)([a-z0-9-]+)/gi, + self = this, + prefLocale; + + while (!prefLocale && (match = regExp.exec(accept))) { + var locale = match[2].toLowerCase(); + var parts = locale.split("-"); + + if (self.locales[locale]) { + prefLocale = locale; + } else if (parts.length > 1 && self.locales[parts[0]]) { + prefLocale = parts[0]; + } + } + + return prefLocale || this.defaultLocale; + }, + + // read locale file, translate a msg and write to fs if new + translate: function (locale, singular, plural) { + if (!locale || !this.locales[locale]) { + if (this.devMode) { + console.warn("WARN: No locale found. Using the default (" + + this.defaultLocale + ") as current locale"); + } + + locale = this.defaultLocale; + + this.initLocale(locale, {}); + } + + if (!this.locales[locale][singular]) { + if (this.devMode) { + dotNotation(this.locales[locale], singular, plural ? { one: singular, other: plural } : undefined); + this.writeFile(locale); + } + } + + return dotNotation(this.locales[locale], singular, plural ? { one: singular, other: plural } : undefined); + }, + + // try reading a file + readFile: function (locale) { + var file = this.locateFile(locale); + + if (!this.devMode && i18n.localeCache[file]) { + this.initLocale(locale, i18n.localeCache[file]); + return; + } + + try { + var localeFile = fs.readFileSync(file); + var base; + + // reading base file if 'base' provided + if (typeof this.base === "function") { + var baseFilename; + + try { + baseFilename = this.base(locale); + } catch (e) { + console.error('base function threw exception for locale %s', locale, e); + } + + if (typeof baseFilename === "string") { + try { + base = this.parse(fs.readFileSync(this.locateFile(baseFilename))); + } catch (e) { + console.error('unable to read or parse base file %s for locale %s', baseFilename, locale, e); + } + } + } + + try { + // parsing file content + var content = this.parse(localeFile); + + if (base) { + // writing content to the base and swapping + for (var prop in content) { + base[prop] = content[prop]; + } + content = base; + } + + // putting content to locales[locale] + this.initLocale(locale, content); + } catch (e) { + console.error('unable to parse locales from file (maybe ' + file + + ' is empty or invalid ' + this.extension + '?): ', e); + } + + } catch (e) { + // unable to read, so intialize that file + // locales[locale] are already set in memory, so no extra read required + // or locales[locale] are empty, which initializes an empty locale.json file + if (!fs.existsSync(file)) { + this.writeFile(locale); + } + } + }, + + // try writing a file in a created directory + writeFile: function (locale) { + // don't write new locale information to disk if we're not in dev mode + if (!this.devMode) { + // Initialize the locale if didn't exist already + this.initLocale(locale, {}); + return; + } + + // creating directory if necessary + try { + fs.lstatSync(this.directory); + + } catch (e) { + if (this.devMode) { + console.log('creating locales dir in: ' + this.directory); + } + + fs.mkdirSync(this.directory, 0755); + } + + // Initialize the locale if didn't exist already + this.initLocale(locale, {}); + + // writing to tmp and rename on success + try { + var target = this.locateFile(locale), + tmp = target + ".tmp"; + + fs.writeFileSync(tmp, + this.dump(this.locales[locale], this.indent), + "utf8"); + + if (fs.statSync(tmp).isFile()) { + fs.renameSync(tmp, target); + + } else { + console.error('unable to write locales to file (either ' + tmp + + ' or ' + target + ' are not writeable?): '); + } + + } catch (e) { + console.error('unexpected error writing files (either ' + tmp + + ' or ' + target + ' are not writeable?): ', e); + } + }, + + // basic normalization of filepath + locateFile: function (locale) { + return path.normalize(this.directory + '/' + locale + this.extension); + }, + + initLocale: function (locale, data) { + if (!this.locales[locale]) { + this.locales[locale] = data; + + // Only cache the files when we're not in dev mode + if (!this.devMode) { + var file = this.locateFile(locale); + if (!i18n.localeCache[file]) { + i18n.localeCache[file] = data; + } + } + } + } +}; + +}).call(this,require('_process')) +},{"_process":20,"fs":1,"path":19,"sprintf":21}],18:[function(require,module,exports){ +module.exports = require('./i18n'); + +},{"./i18n":17}],19:[function(require,module,exports){ +(function (process){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// resolves . and .. elements in a path array with directory names there +// must be no slashes, empty elements, or device names (c:\) in the array +// (so also no leading and trailing slashes - it does not distinguish +// relative and absolute paths) +function normalizeArray(parts, allowAboveRoot) { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up--; up) { + parts.unshift('..'); + } + } + + return parts; +} + +// Split a filename into [root, dir, basename, ext], unix version +// 'root' is just a slash, or nothing. +var splitPathRe = + /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; +var splitPath = function(filename) { + return splitPathRe.exec(filename).slice(1); +}; + +// path.resolve([from ...], to) +// posix version +exports.resolve = function() { + var resolvedPath = '', + resolvedAbsolute = false; + + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? arguments[i] : process.cwd(); + + // Skip empty and invalid entries + if (typeof path !== 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { + return !!p; + }), !resolvedAbsolute).join('/'); + + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; +}; + +// path.normalize(path) +// posix version +exports.normalize = function(path) { + var isAbsolute = exports.isAbsolute(path), + trailingSlash = substr(path, -1) === '/'; + + // Normalize the path + path = normalizeArray(filter(path.split('/'), function(p) { + return !!p; + }), !isAbsolute).join('/'); + + if (!path && !isAbsolute) { + path = '.'; + } + if (path && trailingSlash) { + path += '/'; + } + + return (isAbsolute ? '/' : '') + path; +}; + +// posix version +exports.isAbsolute = function(path) { + return path.charAt(0) === '/'; +}; + +// posix version +exports.join = function() { + var paths = Array.prototype.slice.call(arguments, 0); + return exports.normalize(filter(paths, function(p, index) { + if (typeof p !== 'string') { + throw new TypeError('Arguments to path.join must be strings'); + } + return p; + }).join('/')); +}; + + +// path.relative(from, to) +// posix version +exports.relative = function(from, to) { + from = exports.resolve(from).substr(1); + to = exports.resolve(to).substr(1); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('/'); +}; + +exports.sep = '/'; +exports.delimiter = ':'; + +exports.dirname = function(path) { + var result = splitPath(path), + root = result[0], + dir = result[1]; + + if (!root && !dir) { + // No dirname whatsoever + return '.'; + } + + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.substr(0, dir.length - 1); + } + + return root + dir; +}; + + +exports.basename = function(path, ext) { + var f = splitPath(path)[2]; + // TODO: make this comparison case-insensitive on windows? + if (ext && f.substr(-1 * ext.length) === ext) { + f = f.substr(0, f.length - ext.length); + } + return f; +}; + + +exports.extname = function(path) { + return splitPath(path)[3]; +}; + +function filter (xs, f) { + if (xs.filter) return xs.filter(f); + var res = []; + for (var i = 0; i < xs.length; i++) { + if (f(xs[i], i, xs)) res.push(xs[i]); + } + return res; +} + +// String.prototype.substr - negative index don't work in IE8 +var substr = 'ab'.substr(-1) === 'b' + ? function (str, start, len) { return str.substr(start, len) } + : function (str, start, len) { + if (start < 0) start = str.length + start; + return str.substr(start, len); + } +; + +}).call(this,require('_process')) +},{"_process":20}],20:[function(require,module,exports){ +// shim for using process in browser +var process = module.exports = {}; + +// cached from whatever global is present so that test runners that stub it +// don't break things. But we need to wrap it in a try catch in case it is +// wrapped in strict mode code which doesn't define any globals. It's inside a +// function because try/catches deoptimize in certain engines. + +var cachedSetTimeout; +var cachedClearTimeout; + +function defaultSetTimout() { + throw new Error('setTimeout has not been defined'); +} +function defaultClearTimeout () { + throw new Error('clearTimeout has not been defined'); +} +(function () { + try { + if (typeof setTimeout === 'function') { + cachedSetTimeout = setTimeout; + } else { + cachedSetTimeout = defaultSetTimout; + } + } catch (e) { + cachedSetTimeout = defaultSetTimout; + } + try { + if (typeof clearTimeout === 'function') { + cachedClearTimeout = clearTimeout; + } else { + cachedClearTimeout = defaultClearTimeout; + } + } catch (e) { + cachedClearTimeout = defaultClearTimeout; + } +} ()) +function runTimeout(fun) { + if (cachedSetTimeout === setTimeout) { + //normal enviroments in sane situations + return setTimeout(fun, 0); + } + // if setTimeout wasn't available but was latter defined + if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { + cachedSetTimeout = setTimeout; + return setTimeout(fun, 0); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedSetTimeout(fun, 0); + } catch(e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedSetTimeout.call(null, fun, 0); + } catch(e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error + return cachedSetTimeout.call(this, fun, 0); + } + } + + +} +function runClearTimeout(marker) { + if (cachedClearTimeout === clearTimeout) { + //normal enviroments in sane situations + return clearTimeout(marker); + } + // if clearTimeout wasn't available but was latter defined + if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { + cachedClearTimeout = clearTimeout; + return clearTimeout(marker); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedClearTimeout(marker); + } catch (e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedClearTimeout.call(null, marker); + } catch (e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. + // Some versions of I.E. have different rules for clearTimeout vs setTimeout + return cachedClearTimeout.call(this, marker); + } + } + + + +} +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + if (!draining || !currentQueue) { + return; + } + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = runTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + runClearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}],21:[function(require,module,exports){ +/** +sprintf() for JavaScript 0.7-beta1 +http://www.diveintojavascript.com/projects/javascript-sprintf + +Copyright (c) Alexandru Marasteanu +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of sprintf() for JavaScript nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL Alexandru Marasteanu BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Changelog: +2010.11.07 - 0.7-beta1-node + - converted it to a node.js compatible module + +2010.09.06 - 0.7-beta1 + - features: vsprintf, support for named placeholders + - enhancements: format cache, reduced global namespace pollution + +2010.05.22 - 0.6: + - reverted to 0.4 and fixed the bug regarding the sign of the number 0 + Note: + Thanks to Raphael Pigulla (http://www.n3rd.org/) + who warned me about a bug in 0.5, I discovered that the last update was + a regress. I appologize for that. + +2010.05.09 - 0.5: + - bug fix: 0 is now preceeded with a + sign + - bug fix: the sign was not at the right position on padded results (Kamal Abdali) + - switched from GPL to BSD license + +2007.10.21 - 0.4: + - unit test and patch (David Baird) + +2007.09.17 - 0.3: + - bug fix: no longer throws exception on empty paramenters (Hans Pufal) + +2007.09.11 - 0.2: + - feature: added argument swapping + +2007.04.03 - 0.1: + - initial release +**/ + +var sprintf = (function() { + function get_type(variable) { + return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase(); + } + function str_repeat(input, multiplier) { + for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */} + return output.join(''); + } + + var str_format = function() { + if (!str_format.cache.hasOwnProperty(arguments[0])) { + str_format.cache[arguments[0]] = str_format.parse(arguments[0]); + } + return str_format.format.call(null, str_format.cache[arguments[0]], arguments); + }; + + // convert object to simple one line string without indentation or + // newlines. Note that this implementation does not print array + // values to their actual place for sparse arrays. + // + // For example sparse array like this + // l = [] + // l[4] = 1 + // Would be printed as "[1]" instead of "[, , , , 1]" + // + // If argument 'seen' is not null and array the function will check for + // circular object references from argument. + str_format.object_stringify = function(obj, depth, maxdepth, seen) { + var str = ''; + if (obj != null) { + switch( typeof(obj) ) { + case 'function': + return '[Function' + (obj.name ? ': '+obj.name : '') + ']'; + break; + case 'object': + if ( obj instanceof Error) { return '[' + obj.toString() + ']' }; + if (depth >= maxdepth) return '[Object]' + if (seen) { + // add object to seen list + seen = seen.slice(0) + seen.push(obj); + } + if (obj.length != null) { //array + str += '['; + var arr = [] + for (var i in obj) { + if (seen && seen.indexOf(obj[i]) >= 0) arr.push('[Circular]'); + else arr.push(str_format.object_stringify(obj[i], depth+1, maxdepth, seen)); + } + str += arr.join(', ') + ']'; + } else if ('getMonth' in obj) { // date + return 'Date(' + obj + ')'; + } else { // object + str += '{'; + var arr = [] + for (var k in obj) { + if(obj.hasOwnProperty(k)) { + if (seen && seen.indexOf(obj[k]) >= 0) arr.push(k + ': [Circular]'); + else arr.push(k +': ' +str_format.object_stringify(obj[k], depth+1, maxdepth, seen)); + } + } + str += arr.join(', ') + '}'; + } + return str; + break; + case 'string': + return '"' + obj + '"'; + break + } + } + return '' + obj; + } + + str_format.format = function(parse_tree, argv) { + var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length; + for (i = 0; i < tree_length; i++) { + node_type = get_type(parse_tree[i]); + if (node_type === 'string') { + output.push(parse_tree[i]); + } + else if (node_type === 'array') { + match = parse_tree[i]; // convenience purposes only + if (match[2]) { // keyword argument + arg = argv[cursor]; + for (k = 0; k < match[2].length; k++) { + if (!arg.hasOwnProperty(match[2][k])) { + throw new Error(sprintf('[sprintf] property "%s" does not exist', match[2][k])); + } + arg = arg[match[2][k]]; + } + } + else if (match[1]) { // positional argument (explicit) + arg = argv[match[1]]; + } + else { // positional argument (implicit) + arg = argv[cursor++]; + } + + if (/[^sO]/.test(match[8]) && (get_type(arg) != 'number')) { + throw new Error(sprintf('[sprintf] expecting number but found %s "' + arg + '"', get_type(arg))); + } + switch (match[8]) { + case 'b': arg = arg.toString(2); break; + case 'c': arg = String.fromCharCode(arg); break; + case 'd': arg = parseInt(arg, 10); break; + case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break; + case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break; + case 'O': arg = str_format.object_stringify(arg, 0, parseInt(match[7]) || 5); break; + case 'o': arg = arg.toString(8); break; + case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break; + case 'u': arg = Math.abs(arg); break; + case 'x': arg = arg.toString(16); break; + case 'X': arg = arg.toString(16).toUpperCase(); break; + } + arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg); + pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' '; + pad_length = match[6] - String(arg).length; + pad = match[6] ? str_repeat(pad_character, pad_length) : ''; + output.push(match[5] ? arg + pad : pad + arg); + } + } + return output.join(''); + }; + + str_format.cache = {}; + + str_format.parse = function(fmt) { + var _fmt = fmt, match = [], parse_tree = [], arg_names = 0; + while (_fmt) { + if ((match = /^[^\x25]+/.exec(_fmt)) !== null) { + parse_tree.push(match[0]); + } + else if ((match = /^\x25{2}/.exec(_fmt)) !== null) { + parse_tree.push('%'); + } + else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosOuxX])/.exec(_fmt)) !== null) { + if (match[2]) { + arg_names |= 1; + var field_list = [], replacement_field = match[2], field_match = []; + if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); + while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') { + if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); + } + else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); + } + else { + throw new Error('[sprintf] ' + replacement_field); + } + } + } + else { + throw new Error('[sprintf] ' + replacement_field); + } + match[2] = field_list; + } + else { + arg_names |= 2; + } + if (arg_names === 3) { + throw new Error('[sprintf] mixing positional and named placeholders is not (yet) supported'); + } + parse_tree.push(match); + } + else { + throw new Error('[sprintf] ' + _fmt); + } + _fmt = _fmt.substring(match[0].length); + } + return parse_tree; + }; + + return str_format; +})(); + +var vsprintf = function(fmt, argv) { + var argvClone = argv.slice(); + argvClone.unshift(fmt); + return sprintf.apply(null, argvClone); +}; + +module.exports = sprintf; +sprintf.sprintf = sprintf; +sprintf.vsprintf = vsprintf; + +},{}],22:[function(require,module,exports){ +// Underscore.js 1.8.3 +// http://underscorejs.org +// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind, + nativeCreate = Object.create; + + // Naked function reference for surrogate-prototype-swapping. + var Ctor = function(){}; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.8.3'; + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + var optimizeCb = function(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + case 2: return function(value, other) { + return func.call(context, value, other); + }; + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + }; + + // A mostly-internal function to generate callbacks that can be applied + // to each element in a collection, returning the desired result — either + // identity, an arbitrary callback, a property matcher, or a property accessor. + var cb = function(value, context, argCount) { + if (value == null) return _.identity; + if (_.isFunction(value)) return optimizeCb(value, context, argCount); + if (_.isObject(value)) return _.matcher(value); + return _.property(value); + }; + _.iteratee = function(value, context) { + return cb(value, context, Infinity); + }; + + // An internal function for creating assigner functions. + var createAssigner = function(keysFunc, undefinedOnly) { + return function(obj) { + var length = arguments.length; + if (length < 2 || obj == null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index], + keys = keysFunc(source), + l = keys.length; + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; + } + } + return obj; + }; + }; + + // An internal function for creating a new object that inherits from another. + var baseCreate = function(prototype) { + if (!_.isObject(prototype)) return {}; + if (nativeCreate) return nativeCreate(prototype); + Ctor.prototype = prototype; + var result = new Ctor; + Ctor.prototype = null; + return result; + }; + + var property = function(key) { + return function(obj) { + return obj == null ? void 0 : obj[key]; + }; + }; + + // Helper for collection methods to determine whether a collection + // should be iterated as an array or as an object + // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + var getLength = property('length'); + var isArrayLike = function(collection) { + var length = getLength(collection); + return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; + }; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + _.each = _.forEach = function(obj, iteratee, context) { + iteratee = optimizeCb(iteratee, context); + var i, length; + if (isArrayLike(obj)) { + for (i = 0, length = obj.length; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var keys = _.keys(obj); + for (i = 0, length = keys.length; i < length; i++) { + iteratee(obj[keys[i]], keys[i], obj); + } + } + return obj; + }; + + // Return the results of applying the iteratee to each element. + _.map = _.collect = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length, + results = Array(length); + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + // Create a reducing function iterating left or right. + function createReduce(dir) { + // Optimized iterator function as using arguments.length + // in the main function will deoptimize the, see #1991. + function iterator(obj, iteratee, memo, keys, index, length) { + for (; index >= 0 && index < length; index += dir) { + var currentKey = keys ? keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + } + + return function(obj, iteratee, memo, context) { + iteratee = optimizeCb(iteratee, context, 4); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length, + index = dir > 0 ? 0 : length - 1; + // Determine the initial value if none is provided. + if (arguments.length < 3) { + memo = obj[keys ? keys[index] : index]; + index += dir; + } + return iterator(obj, iteratee, memo, keys, index, length); + }; + } + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + _.reduce = _.foldl = _.inject = createReduce(1); + + // The right-associative version of reduce, also known as `foldr`. + _.reduceRight = _.foldr = createReduce(-1); + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var key; + if (isArrayLike(obj)) { + key = _.findIndex(obj, predicate, context); + } else { + key = _.findKey(obj, predicate, context); + } + if (key !== void 0 && key !== -1) return obj[key]; + }; + + // Return all the elements that pass a truth test. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + predicate = cb(predicate, context); + _.each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, _.negate(cb(predicate)), context); + }; + + // Determine whether all of the elements match a truth test. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + }; + + // Determine if at least one element in the object matches a truth test. + // Aliased as `any`. + _.some = _.any = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + }; + + // Determine if the array or object contains a given item (using `===`). + // Aliased as `includes` and `include`. + _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) { + if (!isArrayLike(obj)) obj = _.values(obj); + if (typeof fromIndex != 'number' || guard) fromIndex = 0; + return _.indexOf(obj, item, fromIndex) >= 0; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + var func = isFunc ? method : value[method]; + return func == null ? func : func.apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matcher(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matcher(attrs)); + }; + + // Return the maximum element (or element-based computation). + _.max = function(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = isArrayLike(obj) ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value > result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = isArrayLike(obj) ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value < result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Shuffle a collection, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var set = isArrayLike(obj) ? obj : _.values(obj); + var length = set.length; + var shuffled = Array(length); + for (var index = 0, rand; index < length; index++) { + rand = _.random(0, index); + if (rand !== index) shuffled[index] = shuffled[rand]; + shuffled[rand] = set[index]; + } + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (!isArrayLike(obj)) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // Sort the object's values by a criterion produced by an iteratee. + _.sortBy = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iteratee(value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iteratee, context) { + var result = {}; + iteratee = cb(iteratee, context); + _.each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, value, key) { + if (_.has(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, value, key) { + if (_.has(result, key)) result[key]++; else result[key] = 1; + }); + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (isArrayLike(obj)) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return isArrayLike(obj) ? obj.length : _.keys(obj).length; + }; + + // Split a collection into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(obj, predicate, context) { + predicate = cb(predicate, context); + var pass = [], fail = []; + _.each(obj, function(value, key, obj) { + (predicate(value, key, obj) ? pass : fail).push(value); + }); + return [pass, fail]; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[0]; + return _.initial(array, array.length - n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. + _.initial = function(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[array.length - 1]; + return _.rest(array, Math.max(0, array.length - n)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, strict, startIndex) { + var output = [], idx = 0; + for (var i = startIndex || 0, length = getLength(input); i < length; i++) { + var value = input[i]; + if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { + //flatten current level of array or arguments object + if (!shallow) value = flatten(value, shallow, strict); + var j = 0, len = value.length; + output.length += len; + while (j < len) { + output[idx++] = value[j++]; + } + } else if (!strict) { + output[idx++] = value; + } + } + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, false); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iteratee, context) { + if (!_.isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = cb(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = getLength(array); i < length; i++) { + var value = array[i], + computed = iteratee ? iteratee(value, i, array) : value; + if (isSorted) { + if (!i || seen !== computed) result.push(value); + seen = computed; + } else if (iteratee) { + if (!_.contains(seen, computed)) { + seen.push(computed); + result.push(value); + } + } else if (!_.contains(result, value)) { + result.push(value); + } + } + return result; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(flatten(arguments, true, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = getLength(array); i < length; i++) { + var item = array[i]; + if (_.contains(result, item)) continue; + for (var j = 1; j < argsLength; j++) { + if (!_.contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = flatten(arguments, true, true, 1); + return _.filter(array, function(value){ + return !_.contains(rest, value); + }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + return _.unzip(arguments); + }; + + // Complement of _.zip. Unzip accepts an array of arrays and groups + // each array's elements on shared indices + _.unzip = function(array) { + var length = array && _.max(array, getLength).length || 0; + var result = Array(length); + + for (var index = 0; index < length; index++) { + result[index] = _.pluck(array, index); + } + return result; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + var result = {}; + for (var i = 0, length = getLength(list); i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // Generator function to create the findIndex and findLastIndex functions + function createPredicateIndexFinder(dir) { + return function(array, predicate, context) { + predicate = cb(predicate, context); + var length = getLength(array); + var index = dir > 0 ? 0 : length - 1; + for (; index >= 0 && index < length; index += dir) { + if (predicate(array[index], index, array)) return index; + } + return -1; + }; + } + + // Returns the first index on an array-like that passes a predicate test + _.findIndex = createPredicateIndexFinder(1); + _.findLastIndex = createPredicateIndexFinder(-1); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iteratee, context) { + iteratee = cb(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = getLength(array); + while (low < high) { + var mid = Math.floor((low + high) / 2); + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + }; + + // Generator function to create the indexOf and lastIndexOf functions + function createIndexFinder(dir, predicateFind, sortedIndex) { + return function(array, item, idx) { + var i = 0, length = getLength(array); + if (typeof idx == 'number') { + if (dir > 0) { + i = idx >= 0 ? idx : Math.max(idx + length, i); + } else { + length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; + } + } else if (sortedIndex && idx && length) { + idx = sortedIndex(array, item); + return array[idx] === item ? idx : -1; + } + if (item !== item) { + idx = predicateFind(slice.call(array, i, length), _.isNaN); + return idx >= 0 ? idx + i : -1; + } + for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { + if (array[idx] === item) return idx; + } + return -1; + }; + } + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex); + _.lastIndexOf = createIndexFinder(-1, _.findLastIndex); + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (stop == null) { + stop = start || 0; + start = 0; + } + step = step || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Determines whether to execute a function as a constructor + // or a normal function with the provided arguments + var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { + if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); + var self = baseCreate(sourceFunc.prototype); + var result = sourceFunc.apply(self, args); + if (_.isObject(result)) return result; + return self; + }; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); + var args = slice.call(arguments, 2); + var bound = function() { + return executeBound(func, bound, context, this, args.concat(slice.call(arguments))); + }; + return bound; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + var bound = function() { + var position = 0, length = boundArgs.length; + var args = Array(length); + for (var i = 0; i < length; i++) { + args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; + } + while (position < arguments.length) args.push(arguments[position++]); + return executeBound(func, bound, this, this, args); + }; + return bound; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var i, length = arguments.length, key; + if (length <= 1) throw new Error('bindAll must be passed function names'); + for (i = 1; i < length; i++) { + key = arguments[i]; + obj[key] = _.bind(obj[key], obj); + } + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = '' + (hasher ? hasher.apply(this, arguments) : key); + if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ + return func.apply(null, args); + }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = _.partial(_.delay, _, 1); + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + if (!options) options = {}; + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + if (!timeout) context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a negated version of the passed-in predicate. + _.negate = function(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + }; + + // Returns a function that will only be executed on and after the Nth call. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Returns a function that will only be executed up to (but not including) the Nth call. + _.before = function(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } + if (times <= 1) func = null; + return memo; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = _.partial(_.before, 2); + + // Object Functions + // ---------------- + + // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. + var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); + var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', + 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; + + function collectNonEnumProps(obj, keys) { + var nonEnumIdx = nonEnumerableProps.length; + var constructor = obj.constructor; + var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto; + + // Constructor is a special case. + var prop = 'constructor'; + if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop); + + while (nonEnumIdx--) { + prop = nonEnumerableProps[nonEnumIdx]; + if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { + keys.push(prop); + } + } + } + + // Retrieve the names of an object's own properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + }; + + // Retrieve all the property names of an object. + _.allKeys = function(obj) { + if (!_.isObject(obj)) return []; + var keys = []; + for (var key in obj) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Returns the results of applying the iteratee to each element of the object + // In contrast to _.map it returns an object + _.mapObject = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var keys = _.keys(obj), + length = keys.length, + results = {}, + currentKey; + for (var index = 0; index < length; index++) { + currentKey = keys[index]; + results[currentKey] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = createAssigner(_.allKeys); + + // Assigns a given object with all the own properties in the passed-in object(s) + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + _.extendOwn = _.assign = createAssigner(_.keys); + + // Returns the first key on an object that passes a predicate test + _.findKey = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = _.keys(obj), key; + for (var i = 0, length = keys.length; i < length; i++) { + key = keys[i]; + if (predicate(obj[key], key, obj)) return key; + } + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(object, oiteratee, context) { + var result = {}, obj = object, iteratee, keys; + if (obj == null) return result; + if (_.isFunction(oiteratee)) { + keys = _.allKeys(obj); + iteratee = optimizeCb(oiteratee, context); + } else { + keys = flatten(arguments, false, false, 1); + iteratee = function(value, key, obj) { return key in obj; }; + obj = Object(obj); + } + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + return result; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj, iteratee, context) { + if (_.isFunction(iteratee)) { + iteratee = _.negate(iteratee); + } else { + var keys = _.map(flatten(arguments, false, false, 1), String); + iteratee = function(value, key) { + return !_.contains(keys, key); + }; + } + return _.pick(obj, iteratee, context); + }; + + // Fill in a given object with default properties. + _.defaults = createAssigner(_.allKeys, true); + + // Creates an object that inherits from the given prototype object. + // If additional properties are provided then they will be added to the + // created object. + _.create = function(prototype, props) { + var result = baseCreate(prototype); + if (props) _.extendOwn(result, props); + return result; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Returns whether an object has a given set of `key:value` pairs. + _.isMatch = function(object, attrs) { + var keys = _.keys(attrs), length = keys.length; + if (object == null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + return true; + }; + + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + // Strings, numbers, regular expressions, dates, and booleans are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + } + + var areArrays = className === '[object Array]'; + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && + _.isFunction(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!eq(a[length], b[length], aStack, bStack)) return false; + } + } else { + // Deep compare objects. + var keys = _.keys(a), key; + length = keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (_.keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = keys[length]; + if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return true; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0; + return _.keys(obj).length === 0; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + var type = typeof obj; + return type === 'function' || type === 'object' && !!obj; + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError. + _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) === '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE < 9), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return _.has(obj, 'callee'); + }; + } + + // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8, + // IE 11 (#1621), and in Safari 8 (#1929). + if (typeof /./ != 'function' && typeof Int8Array != 'object') { + _.isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj !== +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return obj != null && hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iteratees. + _.identity = function(value) { + return value; + }; + + // Predicate-generating functions. Often useful outside of Underscore. + _.constant = function(value) { + return function() { + return value; + }; + }; + + _.noop = function(){}; + + _.property = property; + + // Generates a function for a given object that returns a given property. + _.propertyOf = function(obj) { + return obj == null ? function(){} : function(key) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + _.matcher = _.matches = function(attrs) { + attrs = _.extendOwn({}, attrs); + return function(obj) { + return _.isMatch(obj, attrs); + }; + }; + + // Run a function **n** times. + _.times = function(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = optimizeCb(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { + return new Date().getTime(); + }; + + // List of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + var unescapeMap = _.invert(escapeMap); + + // Functions for escaping and unescaping strings to/from HTML interpolation. + var createEscaper = function(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped + var source = '(?:' + _.keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + }; + _.escape = createEscaper(escapeMap); + _.unescape = createEscaper(unescapeMap); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property, fallback) { + var value = object == null ? void 0 : object[property]; + if (value === void 0) { + value = fallback; + } + return _.isFunction(value) ? value.call(object) : value; + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\u2028|\u2029/g; + + var escapeChar = function(match) { + return '\\' + escapes[match]; + }; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + // NB: `oldSettings` only exists for backwards compatibility. + _.template = function(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escaper, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offest. + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + try { + var render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function. Start chaining a wrapped Underscore object. + _.chain = function(obj) { + var instance = _(obj); + instance._chain = true; + return instance; + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(instance, obj) { + return instance._chain ? _(obj).chain() : obj; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + _.each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result(this, func.apply(_, args)); + }; + }); + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; + return result(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + _.each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result(this, method.apply(this._wrapped, arguments)); + }; + }); + + // Extracts the result from a wrapped and chained object. + _.prototype.value = function() { + return this._wrapped; + }; + + // Provide unwrapping proxy for some methods used in engine operations + // such as arithmetic and JSON stringification. + _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; + + _.prototype.toString = function() { + return '' + this._wrapped; + }; + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define === 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}.call(this)); + +},{}],23:[function(require,module,exports){ +// calc.js +// measure calculations + +var _ = require('underscore'); +var geocrunch = require('geocrunch'); + +var pad = function (num) { + return num < 10 ? '0' + num.toString() : num.toString(); +}; + +var ddToDms = function (coordinate, posSymbol, negSymbol) { + var dd = Math.abs(coordinate), + d = Math.floor(dd), + m = Math.floor((dd - d) * 60), + s = Math.round((dd - d - (m/60)) * 3600 * 100)/100, + directionSymbol = dd === coordinate ? posSymbol : negSymbol; + return pad(d) + '° ' + pad(m) + '\' ' + pad(s) + '" ' + directionSymbol; +}; + +var measure = function (latlngs) { + var last = _.last(latlngs); + var path = geocrunch.path(_.map(latlngs, function (latlng) { + return [latlng.lng, latlng.lat]; + })); + + var meters = path.distance({ + units: 'meters' + }); + var sqMeters = path.area({ + units: 'sqmeters' + }); + + return { + lastCoord: { + dd: { + x: last.lng, + y: last.lat + }, + dms: { + x: ddToDms(last.lng, 'E', 'W'), + y: ddToDms(last.lat, 'N', 'S') + } + }, + length: meters, + area: sqMeters + }; +}; + +module.exports = { + measure: measure // `measure(latLngArray)` - returns object with calced measurements for passed points +}; +},{"geocrunch":7,"underscore":22}],24:[function(require,module,exports){ +// dom.js +// utility functions for managing DOM elements + +var selectOne = function (selector, el) { + if (!el) { + el = document; + } + return el.querySelector(selector); +}; + +var selectAll = function (selector, el) { + if (!el) { + el = document; + } + return Array.prototype.slice.call(el.querySelectorAll(selector)); +}; + +var hide = function (el) { + if (el) { + el.setAttribute('style', 'display:none;'); + return el; + } +}; + +var show = function (el) { + if (el) { + el.removeAttribute('style'); + return el; + } +}; + +module.exports = { + $: selectOne, // `$('.myclass', baseElement)` - returns selected element or undefined + $$: selectAll, // `$$('.myclass', baseElement)` - returns array of selected elements + hide: hide, // `hide(someElement)` - hide passed dom element + show: show // `show(someElement)` - show passed dom element +}; +},{}],25:[function(require,module,exports){ +// ca.js +// Catalan i18n translations + +module.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(require,module,exports){ +// cn.js +// Chinese i18n translations + +module.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(require,module,exports){ +// da.js +// Danish i18n translations + +module.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(require,module,exports){ +// de.js +// German i18n translations + +module.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(require,module,exports){ +// de.js +// German i18n translations + +module.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(require,module,exports){ +// en.js +// English i18n translations + +module.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(require,module,exports){ +// en_UK.js +// British English i18n translations + +module.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(require,module,exports){ +// es.js +// Spanish i18n translations + +module.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(require,module,exports){ +// fa.js +// Persian (Farsi) i18n translations + +module.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(require,module,exports){ +// fil_PH.js +// Filipino i18n translations + +module.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(require,module,exports){ +// fr.js +// French i18n translations + +module.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(require,module,exports){ +// it.js +// Italian i18n translations + +module.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(require,module,exports){ +// nl.js +// Dutch i18n translations + +module.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(require,module,exports){ +// pl.js +// Polish i18n translations + +module.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(require,module,exports){ +// pt_BR.js +// portuguese brazillian i18n translations + +module.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(require,module,exports){ +// en.js +// portuguese i18n translations + +module.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(require,module,exports){ +// ru.js +// Russian i18n translations + +module.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(require,module,exports){ +// sv.js +// Swedish (svenska) i18n translations + +module.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': ' ' //space +}; + +},{}],43:[function(require,module,exports){ +// tr.js +// Turkish i18n translations + +module.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(require,module,exports){ +(function (global){ +// leaflet-measure.js + +var _ = require('underscore'); +var L = (typeof window !== "undefined" ? window['L'] : typeof global !== "undefined" ? global['L'] : null); +var humanize = require('humanize'); + +var units = require('./units'); +var calc = require('./calc'); +var dom = require('./dom'); +var $ = dom.$; + +var Symbology = require('./mapsymbology'); + + +var controlTemplate = _.template("-toggle js-toggle\" href=\"#\" title=\"<%= i18n.__('measureDistancesAndAreas') %>\"><%= i18n.__('measure') %>\n
    -interaction js-interaction\">\n
    \n

    <%= i18n.__('measureDistancesAndAreas') %>

    \n \n
    \n
    \n

    <%= i18n.__('measureDistancesAndAreas') %>

    \n

    <%= i18n.__('startCreating') %>

    \n
    \n \n
    \n
    "); +var resultsTemplate = _.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<% } %>"); +var pointPopupTemplate = _.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"); +var linePopupTemplate = _.template("

    <%= i18n.__('linearMeasurement') %>

    \n

    <%= model.lengthDisplay %>

    \n"); +var areaPopupTemplate = _.template("

    <%= i18n.__('areaMeasurement') %>

    \n

    <%= model.areaDisplay %>

    \n

    <%= model.lengthDisplay %> <%= i18n.__('perimeter') %>

    \n"); + +var i18n = new (require('i18n-2'))({ + devMode: false, + locales: { + 'ca': require('./i18n/ca'), + 'cn': require('./i18n/cn'), + 'da': require('./i18n/da'), + 'de': require('./i18n/de'), + 'de_CH': require('./i18n/de_CH'), + 'en': require('./i18n/en'), + 'en_UK': require('./i18n/en_UK'), + 'es': require('./i18n/es'), + 'fa': require('./i18n/fa'), + 'fil_PH': require('./i18n/fil_PH'), + 'fr': require('./i18n/fr'), + 'it': require('./i18n/it'), + 'nl': require('./i18n/nl'), + 'pl': require('./i18n/pl'), + 'pt_BR': require('./i18n/pt_BR'), + 'pt_PT': require('./i18n/pt_PT'), + 'ru': require('./i18n/ru'), + 'sv': require('./i18n/sv'), + 'tr': require('./i18n/tr') + } +}); + +L.Control.Measure = L.Control.extend({ + _className: 'leaflet-control-measure', + options: { + units: {}, + position: 'topright', + primaryLengthUnit: 'feet', + secondaryLengthUnit: 'miles', + primaryAreaUnit: 'acres', + activeColor: '#ABE67E', // base color for map features while actively measuring + completedColor: '#C8F2BE', // base color for permenant features generated from completed measure + captureZIndex: 10000, // z-index of the marker used to capture measure events + popupOptions: { // standard leaflet popup options http://leafletjs.com/reference.html#popup-options + className: 'leaflet-measure-resultpopup', + autoPanPadding: [10, 10] + } + }, + initialize: function (options) { + L.setOptions(this, options); + this.options.units = L.extend({}, units, this.options.units); + this._symbols = new Symbology(_.pick(this.options, 'activeColor', 'completedColor')); + i18n.setLocale(this.options.localization); + }, + onAdd: function (map) { + this._map = map; + this._latlngs = []; + this._initLayout(); + map.on('click', this._collapse, this); + this._layer = L.layerGroup().addTo(map); + return this._container; + }, + onRemove: function (map) { + map.off('click', this._collapse, this); + map.removeLayer(this._layer); + }, + _initLayout: function () { + var className = this._className, container = this._container = L.DomUtil.create('div', className); + var $toggle, $start, $cancel, $finish; + + container.innerHTML = controlTemplate({ + model: { + className: className + }, + i18n: i18n + }); + + // copied from leaflet + // https://bitbucket.org/ljagis/js-mapbootstrap/src/4ab1e9e896c08bdbc8164d4053b2f945143f4f3a/app/components/measure/leaflet-measure-control.js?at=master#cl-30 + container.setAttribute('aria-haspopup', true); + if (!L.Browser.touch) { + L.DomEvent.disableClickPropagation(container); + L.DomEvent.disableScrollPropagation(container); + } else { + L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation); + } + + $toggle = this.$toggle = $('.js-toggle', container); // collapsed content + this.$interaction = $('.js-interaction', container); // expanded content + $start = $('.js-start', container); // start button + $cancel = $('.js-cancel', container); // cancel button + $finish = $('.js-finish', container); // finish button + this.$startPrompt = $('.js-startprompt', container); // full area with button to start measurment + this.$measuringPrompt = $('.js-measuringprompt', container); // full area with all stuff for active measurement + this.$startHelp = $('.js-starthelp', container); // "Start creating a measurement by adding points" + this.$results = $('.js-results', container); // div with coordinate, linear, area results + this.$measureTasks = $('.js-measuretasks', container); // active measure buttons container + + this._collapse(); + this._updateMeasureNotStarted(); + + if (!L.Browser.android) { + L.DomEvent.on(container, 'mouseenter', this._expand, this); + L.DomEvent.on(container, 'mouseleave', this._collapse, this); + } + L.DomEvent.on($toggle, 'click', L.DomEvent.stop); + if (L.Browser.touch) { + L.DomEvent.on($toggle, 'click', this._expand, this); + } else { + L.DomEvent.on($toggle, 'focus', this._expand, this); + } + L.DomEvent.on($start, 'click', L.DomEvent.stop); + L.DomEvent.on($start, 'click', this._startMeasure, this); + L.DomEvent.on($cancel, 'click', L.DomEvent.stop); + L.DomEvent.on($cancel, 'click', this._finishMeasure, this); + L.DomEvent.on($finish, 'click', L.DomEvent.stop); + L.DomEvent.on($finish, 'click', this._handleMeasureDoubleClick, this); + }, + _expand: function () { + dom.hide(this.$toggle); + dom.show(this.$interaction); + }, + _collapse: function () { + if (!this._locked) { + dom.hide(this.$interaction); + dom.show(this.$toggle); + } + }, + // move between basic states: + // measure not started, started/in progress but no points added, in progress and with points + _updateMeasureNotStarted: function () { + dom.hide(this.$startHelp); + dom.hide(this.$results); + dom.hide(this.$measureTasks); + dom.hide(this.$measuringPrompt); + dom.show(this.$startPrompt); + }, + _updateMeasureStartedNoPoints: function () { + dom.hide(this.$results); + dom.show(this.$startHelp); + dom.show(this.$measureTasks); + dom.hide(this.$startPrompt); + dom.show(this.$measuringPrompt); + }, + _updateMeasureStartedWithPoints: function () { + dom.hide(this.$startHelp); + dom.show(this.$results); + dom.show(this.$measureTasks); + dom.hide(this.$startPrompt); + dom.show(this.$measuringPrompt); + }, + // get state vars and interface ready for measure + _startMeasure: function () { + this._locked = true; + this._measureVertexes = L.featureGroup().addTo(this._layer); + this._captureMarker = L.marker(this._map.getCenter(), { + clickable: true, + 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); + + L.DomEvent.on(this._container, 'mouseenter', this._handleMapMouseOut, this); + + this._updateMeasureStartedNoPoints(); + + this._map.fire('measurestart', null, false); + }, + // return to state with no measure in progress, undo `this._startMeasure` + _finishMeasure: function () { + var model = _.extend({}, this._resultsModel, { + points: this._latlngs + }); + + this._locked = false; + + L.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', model, false); + }, + // clear all running measure data + _clearMeasure: function () { + this._latlngs = []; + this._resultsModel = null; + this._measureVertexes.clearLayers(); + if (this._measureDrag) { + this._layer.removeLayer(this._measureDrag); + } + if (this._measureArea) { + this._layer.removeLayer(this._measureArea); + } + if (this._measureBoundary) { + this._layer.removeLayer(this._measureBoundary); + } + this._measureDrag = null; + this._measureArea = null; + this._measureBoundary = null; + }, + // centers the event capture marker + _centerCaptureMarker: function () { + this._captureMarker.setLatLng(this._map.getCenter()); + }, + // set icon on the capture marker + _setCaptureMarkerIcon: function () { + this._captureMarker.setIcon(L.divIcon({ + iconSize: this._map.getSize().multiplyBy(2) + })); + }, + // format measurements to nice display string based on units in options + // `{ lengthDisplay: '100 Feet (0.02 Miles)', areaDisplay: ... }` + _getMeasurementDisplayStrings: function (measurement) { + var unitDefinitions = this.options.units; + + return { + lengthDisplay: buildDisplay(measurement.length, this.options.primaryLengthUnit, this.options.secondaryLengthUnit, this.options.decPoint, this.options.thousandsSep), + areaDisplay: buildDisplay(measurement.area, this.options.primaryAreaUnit, this.options.secondaryAreaUnit, this.options.decPoint, this.options.thousandsSep) + }; + + function buildDisplay (val, primaryUnit, secondaryUnit, decPoint, thousandsSep) { + var display; + if (primaryUnit && unitDefinitions[primaryUnit]) { + display = formatMeasure(val, unitDefinitions[primaryUnit], decPoint, thousandsSep); + if (secondaryUnit && unitDefinitions[secondaryUnit]) { + display = display + ' (' + formatMeasure(val, unitDefinitions[secondaryUnit], decPoint, thousandsSep) + ')'; + } + } else { + display = formatMeasure(val, null, decPoint, thousandsSep); + } + return display; + } + + function formatMeasure (val, unit, decPoint, thousandsSep) { + return unit && unit.factor && unit.display ? + humanize.numberFormat(val * unit.factor, unit.decimals || 0, decPoint || i18n.__('decPoint'), thousandsSep || i18n.__('thousandsSep')) + ' ' + i18n.__([unit.display]) || unit.display : + humanize.numberFormat(val, 0, decPoint || i18n.__('decPoint'), thousandsSep || i18n.__('thousandsSep')); + } + }, + // update results area of dom with calced measure from `this._latlngs` + _updateResults: function () { + var calced = calc.measure(this._latlngs); + var resultsModel = this._resultsModel = _.extend({}, calced, this._getMeasurementDisplayStrings(calced), { + pointCount: this._latlngs.length + }); + this.$results.innerHTML = resultsTemplate({ + model: resultsModel, + humanize: humanize, + i18n: i18n + }); + }, + // mouse move handler while measure in progress + // adds floating measure marker under cursor + _handleMeasureMove: function (evt) { + if (!this._measureDrag) { + this._measureDrag = L.circleMarker(evt.latlng, this._symbols.getSymbol('measureDrag')).addTo(this._layer); + } else { + this._measureDrag.setLatLng(evt.latlng); + } + this._measureDrag.bringToFront(); + }, + // handler for both double click and clicking finish button + // do final calc and finish out current measure, clear dom and internal state, add permanent map features + _handleMeasureDoubleClick: function () { + var latlngs = this._latlngs, calced, resultFeature, popupContainer, popupContent, zoomLink, deleteLink; + + this._finishMeasure(); + + if (!latlngs.length) { + return; + } + + if (latlngs.length > 2) { + latlngs.push(_.first(latlngs)); // close path to get full perimeter measurement for areas + } + + calced = calc.measure(latlngs); + + if (latlngs.length === 1) { + resultFeature = L.circleMarker(latlngs[0], this._symbols.getSymbol('resultPoint')); + popupContent = pointPopupTemplate({ + model: calced, + humanize: humanize, + i18n: i18n + }); + } else if (latlngs.length === 2) { + resultFeature = L.polyline(latlngs, this._symbols.getSymbol('resultLine')); + popupContent = linePopupTemplate({ + model: _.extend({}, calced, this._getMeasurementDisplayStrings(calced)), + humanize: humanize, + i18n: i18n + }); + } else { + resultFeature = L.polygon(latlngs, this._symbols.getSymbol('resultArea')); + popupContent = areaPopupTemplate({ + model: _.extend({}, calced, this._getMeasurementDisplayStrings(calced)), + humanize: humanize, + i18n: i18n + }); + } + + popupContainer = L.DomUtil.create('div', ''); + popupContainer.innerHTML = popupContent; + + zoomLink = $('.js-zoomto', popupContainer); + if (zoomLink) { + L.DomEvent.on(zoomLink, 'click', L.DomEvent.stop); + L.DomEvent.on(zoomLink, 'click', function () { + if (resultFeature.getBounds) { + this._map.fitBounds(resultFeature.getBounds(), { + padding: [20, 20], + maxZoom: 17 + }); + } else if (resultFeature.getLatLng) { + this._map.panTo(resultFeature.getLatLng()); + } + }, this); + } + + deleteLink = $('.js-deletemarkup', popupContainer); + if (deleteLink) { + L.DomEvent.on(deleteLink, 'click', L.DomEvent.stop); + L.DomEvent.on(deleteLink, 'click', function () { + // TODO. maybe remove any event handlers on zoom and delete buttons? + this._layer.removeLayer(resultFeature); + }, this); + } + + resultFeature.addTo(this._layer); + resultFeature.bindPopup(popupContainer, this.options.popupOptions); + if (resultFeature.getBounds) { + resultFeature.openPopup(resultFeature.getBounds().getCenter()); + } else if (resultFeature.getLatLng) { + resultFeature.openPopup(resultFeature.getLatLng()); + } + }, + // handle map click during ongoing measurement + // add new clicked point, update measure layers and results ui + _handleMeasureClick: function (evt) { + var latlng = this._map.mouseEventToLatLng(evt.originalEvent), // get actual latlng instead of the marker's latlng from originalEvent + lastClick = _.last(this._latlngs), + vertexSymbol = this._symbols.getSymbol('measureVertex'); + + if (!lastClick || !latlng.equals(lastClick)) { // skip if same point as last click, happens on `dblclick` + this._latlngs.push(latlng); + this._addMeasureArea(this._latlngs); + this._addMeasureBoundary(this._latlngs); + + this._measureVertexes.eachLayer(function (layer) { + layer.setStyle(vertexSymbol); + // reset all vertexes to non-active class - only last vertex is active + // `layer.setStyle({ className: 'layer-measurevertex'})` doesn't work. https://github.com/leaflet/leaflet/issues/2662 + // set attribute on path directly + layer._path.setAttribute('class', vertexSymbol.className); + }); + + this._addNewVertex(latlng); + + if (this._measureBoundary) { + this._measureBoundary.bringToFront(); + } + this._measureVertexes.bringToFront(); + } + + this._updateResults(); + this._updateMeasureStartedWithPoints(); + }, + // handle map mouse out during ongoing measure + // remove floating cursor vertex from map + _handleMapMouseOut: function () { + if (this._measureDrag) { + this._layer.removeLayer(this._measureDrag); + this._measureDrag = null; + } + }, + // add various measure graphics to map - vertex, area, boundary + _addNewVertex: function (latlng) { + L.circleMarker(latlng, this._symbols.getSymbol('measureVertexActive')).addTo(this._measureVertexes); + }, + _addMeasureArea: function (latlngs) { + if (latlngs.length < 3) { + if (this._measureArea) { + this._layer.removeLayer(this._measureArea); + this._measureArea = null; + } + return; + } + if (!this._measureArea) { + this._measureArea = L.polygon(latlngs, this._symbols.getSymbol('measureArea')).addTo(this._layer); + } else { + this._measureArea.setLatLngs(latlngs); + } + }, + _addMeasureBoundary: function (latlngs) { + if (latlngs.length < 2) { + if (this._measureBoundary) { + this._layer.removeLayer(this._measureBoundary); + this._measureBoundary = null; + } + return; + } + if (!this._measureBoundary) { + this._measureBoundary = L.polyline(latlngs, this._symbols.getSymbol('measureBoundary')).addTo(this._layer); + } else { + this._measureBoundary.setLatLngs(latlngs); + } + } +}); + +L.Map.mergeOptions({ + measureControl: false +}); + +L.Map.addInitHook(function () { + if (this.options.measureControl) { + this.measureControl = (new L.Control.Measure()).addTo(this); + } +}); + +L.control.measure = function (options) { + return new L.Control.Measure(options); +}; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? 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(require,module,exports){ +// mapsymbology.js + +var _ = require('underscore'); + +var color = require('color'); + +var Symbology = function (options) { + this.setOptions(options); +}; + +Symbology.DEFAULTS = { + activeColor: '#ABE67E', // base color for map features while actively measuring + completedColor: '#C8F2BE' // base color for permenant features generated from completed measure +}; + +_.extend(Symbology.prototype, { + setOptions: function (options) { + this._options = _.extend({}, Symbology.DEFAULTS, this._options, options); + return this; + }, + getSymbol: function (name) { + var symbols = { + measureDrag: { + clickable: false, + radius: 4, + color: this._options.activeColor, + weight: 2, + opacity: 0.7, + fillColor: this._options.activeColor, + fillOpacity: 0.5, + className: 'layer-measuredrag' + }, + measureArea: { + clickable: false, + stroke: false, + fillColor: this._options.activeColor, + fillOpacity: 0.2, + className: 'layer-measurearea' + }, + measureBoundary: { + clickable: false, + color: this._options.activeColor, + weight: 2, + opacity: 0.9, + fill: false, + className: 'layer-measureboundary' + }, + measureVertex: { + clickable: false, + radius: 4, + color: this._options.activeColor, + weight: 2, + opacity: 1, + fillColor: this._options.activeColor, + fillOpacity: 0.7, + className: 'layer-measurevertex' + }, + measureVertexActive: { + clickable: false, + radius: 4, + color: this._options.activeColor, + weight: 2, + opacity: 1, + fillColor: color(this._options.activeColor).darken(0.15), + fillOpacity: 0.7, + className: 'layer-measurevertex active' + }, + resultArea: { + clickable: true, + color: this._options.completedColor, + weight: 2, + opacity: 0.9, + fillColor: this._options.completedColor, + fillOpacity: 0.2, + className: 'layer-measure-resultarea' + }, + resultLine: { + clickable: true, + color: this._options.completedColor, + weight: 3, + opacity: 0.9, + fill: false, + className: 'layer-measure-resultline' + }, + resultPoint: { + clickable: true, + radius: 4, + color: this._options.completedColor, + weight: 2, + opacity: 1, + fillColor: this._options.completedColor, + fillOpacity: 0.7, + className: 'layer-measure-resultpoint' + } + }; + return symbols[name]; + } +}); + +module.exports = Symbology; +},{"color":6,"underscore":22}],46:[function(require,module,exports){ +// units.js +// Unit configurations +// Factor is with respect to meters/sqmeters + +module.exports = { + acres: { + factor: 0.00024711, + display: 'acres', + decimals: 2 + }, + feet: { + factor: 3.2808, + display: 'feet', + decimals: 0 + }, + kilometers: { + factor: 0.001, + display: 'kilometers', + decimals: 2 + }, + hectares: { + factor: 0.0001, + 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: 0.000000386102, + display: 'sqmiles', + decimals: 2 + } +}; +},{}]},{},[44]); diff --git a/designsafe/static/vendor/leaflet-measure/dist/leaflet-measure.min.js b/designsafe/static/vendor/leaflet-measure/dist/leaflet-measure.min.js new file mode 100644 index 0000000000..1dfe9e231c --- /dev/null +++ b/designsafe/static/vendor/leaflet-measure/dist/leaflet-measure.min.js @@ -0,0 +1,4 @@ +!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/designsafe/static/vendor/leaflet-measure/example/basic.html b/designsafe/static/vendor/leaflet-measure/example/basic.html new file mode 100644 index 0000000000..574112931f --- /dev/null +++ b/designsafe/static/vendor/leaflet-measure/example/basic.html @@ -0,0 +1,90 @@ + + + + + leaflet-measure + + + + + + +

    leaflet-measure

    +

    github.com/ljagis/leaflet-measure

    +
    +

    measurefinish event data:

    +
    ...
    + + + + + \ No newline at end of file diff --git a/designsafe/static/vendor/leaflet-measure/example/leaflet-1.0-beta2.html b/designsafe/static/vendor/leaflet-measure/example/leaflet-1.0-beta2.html new file mode 100644 index 0000000000..05a10f9764 --- /dev/null +++ b/designsafe/static/vendor/leaflet-measure/example/leaflet-1.0-beta2.html @@ -0,0 +1,64 @@ + + + + + leaflet-measure with Leaflet 1.0.0 Beta 2 + + + + + + +

    leaflet-measure with Leaflet 1.0.0 Beta 2

    +

    github.com/ljagis/leaflet-measure

    +
    + + + + + \ No newline at end of file diff --git a/designsafe/static/vendor/leaflet-measure/example/leaflet-1.0.3.html b/designsafe/static/vendor/leaflet-measure/example/leaflet-1.0.3.html new file mode 100644 index 0000000000..c3c5c3641e --- /dev/null +++ b/designsafe/static/vendor/leaflet-measure/example/leaflet-1.0.3.html @@ -0,0 +1,63 @@ + + + + + leaflet-measure with Leaflet 1.0.0 Beta 2 + + + + + + +

    leaflet-measure with Leaflet 1.0.3

    +

    github.com/ljagis/leaflet-measure

    +
    + + + + + \ No newline at end of file diff --git a/designsafe/static/vendor/leaflet-measure/example/leaflet-measure-demo.jpg b/designsafe/static/vendor/leaflet-measure/example/leaflet-measure-demo.jpg new file mode 100644 index 0000000000..513dcd9867 Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/example/leaflet-measure-demo.jpg differ diff --git a/designsafe/static/vendor/leaflet-measure/example/leaflet-measure-demo_@2X.jpg b/designsafe/static/vendor/leaflet-measure/example/leaflet-measure-demo_@2X.jpg new file mode 100644 index 0000000000..3f66922603 Binary files /dev/null and b/designsafe/static/vendor/leaflet-measure/example/leaflet-measure-demo_@2X.jpg differ diff --git a/designsafe/static/vendor/tether/.bower.json b/designsafe/static/vendor/tether/.bower.json new file mode 100644 index 0000000000..5163b63ecf --- /dev/null +++ b/designsafe/static/vendor/tether/.bower.json @@ -0,0 +1,36 @@ +{ + "name": "tether", + "version": "1.4.0", + "homepage": "http://github.hubspot.com/tether", + "authors": [ + "Zack Bloom ", + "Adam Schwartz " + ], + "maintainers": [ + "Nicholas Hwang ", + "Trevor Burnham " + ], + "description": "A client-side library to make absolutely positioned elements attach to elements in the page efficiently.", + "keywords": [ + "javascript" + ], + "license": "MIT", + "main": "dist/js/tether.js", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "_release": "1.4.0", + "_resolution": { + "type": "version", + "tag": "v1.4.0", + "commit": "3d7119e590661f8c9e9e566c8a7640c189687215" + }, + "_source": "https://github.com/HubSpot/tether.git", + "_target": "^1.4.0", + "_originalSource": "tether", + "_direct": true +} \ No newline at end of file diff --git a/designsafe/static/vendor/tether/CHANGELOG.md b/designsafe/static/vendor/tether/CHANGELOG.md new file mode 100644 index 0000000000..d5a8047ff7 --- /dev/null +++ b/designsafe/static/vendor/tether/CHANGELOG.md @@ -0,0 +1,13 @@ +## v1.3.0 +- Tether instances now fire an 'update' event when attachments change due to constraints (#119) + +## v1.0.1 +- Update arrow mixin to change arrow pointer event + + +## v1.0.0 +- Coffeescript -> ES6 +- Proper UMD Wrapper +- Update build steps +- Add changelog +- Provide minified CSS diff --git a/designsafe/static/vendor/tether/CONTRIBUTING.md b/designsafe/static/vendor/tether/CONTRIBUTING.md new file mode 100644 index 0000000000..8e4b9c542f --- /dev/null +++ b/designsafe/static/vendor/tether/CONTRIBUTING.md @@ -0,0 +1,59 @@ +# Contributing Guide + +You will need: + +- Node.js/io.js & npm +- Bower +- Gulp + + +## Getting started + +1. Fork the project +2. Clone your forked project by running `git clone git@github.com:{ + YOUR_USERNAME }/tether.git` +3. Run `npm install` to install both node modules and bower components +4. Test that you can build the source by moving/renaming the existing `dist` + directory and running `npm run build` +5. Assuming everything went well, you should now have a `dist` directory that + matches the one you moved in step 4 + + +## Writing code! + +We use `gulp` to facilitate things like transpilation, minification, etc. so +can you focus on writing relevant code. If there is a fix or feature you would like +to contribute, we ask that you take the following steps: + +1. Most of the _editable_ code lives in the `src` directory while built code + will end up in the `dist` directory upon running `npm run build`. + +2. Depending on how big your changes are, bump the version numbers appropriately + in `bower.json` and `package.json`. We try to follow semver, so a good rule + of thumb for how to bump the version is: + - A fix to existing code, perform a patch bump e.g. x.x.0 -> x.x.1 + - New feature, perform a minor bump e.g. x.0.x -> x.1.x + - Breaking changes such a rewrite, perform a major bump e.g. + 1.x.x -> 2.x.x + + Versioning is hard, so just use good judgement and we'll be more than happy + to help out. + + __NOTE__: There is a `gulp` task that will automate some of the versioning. + You can run `gulp version:{type}` where type is `patch|minor|major` to + update both `bower.json` and `package.json` as well as add the appropriate + git tag. + +3. Provide a thoughtful commit message and push your changes to your fork using + `git push origin master` (assuming your forked project is using `origin` for + the remote name and you are on the `master` branch). + +4. Open a Pull Request on GitHub with a description of your changes. + + +## Testing + +Work in progress. We are hoping to add some tests, so if you would like to help +us get started, feel free to contact us through the Issues or open a Pull +Request. + diff --git a/designsafe/static/vendor/tether/LICENSE b/designsafe/static/vendor/tether/LICENSE new file mode 100644 index 0000000000..0e08c0a565 --- /dev/null +++ b/designsafe/static/vendor/tether/LICENSE @@ -0,0 +1,8 @@ +Copyright (c) 2014-2016 HubSpot, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/designsafe/static/vendor/tether/README.md b/designsafe/static/vendor/tether/README.md new file mode 100644 index 0000000000..e7db89361e --- /dev/null +++ b/designsafe/static/vendor/tether/README.md @@ -0,0 +1,106 @@ +## Tether + +[![GitHub +version](https://badge.fury.io/gh/HubSpot%2Ftether.svg)](http://badge.fury.io/gh/HubSpot%2Ftether) + +[Tether](http://github.hubspot.com/tether/) is a small, focused JavaScript library for defining and managing the position of user interface (UI) elements in relation to one another on a web page. It is a tool for web developers building features that require certain UI elements to be precisely positioned based on the location of another UI element. + +There are often situations in UI development where elements need to be attached to other elements, but placing them right next to each other in the [DOM tree](https://en.wikipedia.org/wiki/Document_Object_Model) can be problematic based on the context. For example, what happens if the element we’re attaching other elements to is fixed to the center of the screen? Or what if the element is inside a scrollable container? How can we prevent the attached element from being clipped as it disappears from view while a user is scrolling? Tether can solve all of these problems and more. + +Some common UI elements that have been built with Tether are [tooltips](http://github.hubspot.com/tooltip/docs/welcome), [select menus](http://github.hubspot.com/select/docs/welcome), [dropdown menus](http://github.hubspot.com/drop/docs/welcome), and [guided tours](http://github.hubspot.com/shepherd/docs/welcome). Tether is flexible and can be used to [solve](http://github.hubspot.com/tether/examples/out-of-bounds/) [all](http://github.hubspot.com/tether/examples/content-visible) [kinds](http://github.hubspot.com/tether/examples/element-scroll) [of](http://github.hubspot.com/tether/examples/enable-disable) interesting [problems](http://github.hubspot.com/tether/examples/viewport); it ensures UI elements stay where they need to be, based on the various user interactions (click, scroll, etc) and layout contexts (fixed positioning, inside scrollable containers, etc). + +Please have a look at the [documentation](http://github.hubspot.com/tether/) for a more detailed explanation of why you might need Tether for your next project. + +## What to Use Tether for and When to Use It + +Tether is a small, focused JavaScript library. For those who might be new to JavaScript, a library is simply a JavaScript file (or files) that contain useful JavaScript code to help achieve tasks easier and faster. Since Tether is a JavaScript user interface (**UI**) library, it contains code to help you to manage the way your website or web app appears. + +Tether’s goal to is to help you position your elements side-by-side when needed. + +Let’s say you’ve started working on your dream project—a fancy web app that’s sure to become the next big thing! An important feature of your new app is to allow users to comment on shared photos. However, due to limited vertical space and the overall layout of your new app, you’d like to display the comments **next** to the image, similar to how Instagram does it. + +Your HTML code might look something like this: + +```html +
    + Awesome Picture +
    + ... +
    +
    +``` + +Now, you could achieve this with some CSS using its `position` property, but going this route can be problematic since many of `position`’s values take elements **out** of the natural DOM flow. For example, if you have an element at the bottom of your HTML document, using `position: absolute` or `position: fixed` might could move it all the way to the top of your website in the browser. + +Not only that, but you also have to make manual adjustments to ensure **other** elements aren’t negatively affected by the positioned elements. Not to mention, you probably want your comment box to be **responsive**, and look good across different device sizes. Coding a solution for this manually is a challenge all on its own. + +**Enter Tether!** + +After installing Tether and including it in your project, you can begin using it! + +1. In your JavaScript file, create a new instance (or constructor function) of the `Tether` object: + + ```javascript + new Tether({}); + ``` + +2. Within the curly braces (`{}`) you can configure the library’s options. Tether’s extensive list of options can be found in the [Tether documentation](http://github.hubspot.com/tether/). + + ```javascript + new Tether({ + element: '.comments', + target: '.picture', + attachment: 'top right' + targetAttachment: 'top left' + }); + ``` + +Now you have a perfectly placed comment section to go with your awesome picture! It’ll even stay attached to the element when a user resizes their browser window. + +There are tons of other useful features of Tether as well, instead of “comment boxes” you could also build: + +* Tooltips for useful hints and tricks, +* Dropdown menus, +* Autocomplete popups for forms, +* and [more](http://github.hubspot.com/tether/examples/list_of_examples/)! + +## Install + +__npm__ +```sh +$ npm install tether +``` + +__bower__ +```sh +$ bower install tether +``` + +__download__ + +Or just download from the [releases](https://github.com/HubSpot/tether/releases). + +## Usage +You only need to include [tether.min.js](https://github.com/HubSpot/tether/blob/master/dist/js/tether.min.js) in your page: +``` + +``` +Or just use a CDN: +``` + +``` + +The css files in the [dist/css](https://github.com/HubSpot/tether/tree/master/dist/css) folder are not required to get tether running. + +For more details jump straight in to the detailed [Usage](http://github.hubspot.com/tether/#usage) page. + +[![Tether Docs](http://i.imgur.com/YCx8cLr.png)](http://github.hubspot.com/tether/#usage) + +[Demo & API Documentation](http://github.hubspot.com/tether/) + +## Contributing + +We encourage contributions of all kinds. If you would like to contribute in some way, please review our [guidelines for contributing](CONTRIBUTING.md). + +## License +Copyright © 2014-2016 HubSpot - [MIT License](LICENSE) diff --git a/designsafe/static/vendor/tether/archive.tar.gz.1024448019 b/designsafe/static/vendor/tether/archive.tar.gz.1024448019 new file mode 100644 index 0000000000..0dd9d357f7 Binary files /dev/null and b/designsafe/static/vendor/tether/archive.tar.gz.1024448019 differ diff --git a/designsafe/static/vendor/tether/bower.json b/designsafe/static/vendor/tether/bower.json new file mode 100644 index 0000000000..ea9b6b759e --- /dev/null +++ b/designsafe/static/vendor/tether/bower.json @@ -0,0 +1,26 @@ +{ + "name": "tether", + "version": "1.4.0", + "homepage": "http://github.hubspot.com/tether", + "authors": [ + "Zack Bloom ", + "Adam Schwartz " + ], + "maintainers": [ + "Nicholas Hwang ", + "Trevor Burnham " + ], + "description": "A client-side library to make absolutely positioned elements attach to elements in the page efficiently.", + "keywords": [ + "javascript" + ], + "license": "MIT", + "main": "dist/js/tether.js", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/designsafe/static/vendor/tether/component.json b/designsafe/static/vendor/tether/component.json new file mode 100644 index 0000000000..1e81c50db0 --- /dev/null +++ b/designsafe/static/vendor/tether/component.json @@ -0,0 +1,22 @@ +{ + "name": "tether", + "repo": "HubSpot/tether", + "version": "1.3.9", + "description": "A client-side library to make absolutely positioned elements attach to elements in the page efficiently.", + "authors": [ + "Zack Bloom ", + "Adam Schwartz " + ], + "maintainers": [ + "Nicholas Hwang " + ], + "license": "MIT", + "demo": "http://github.hubspot.com/tether/docs/welcome/", + "main": "dist/js/tether.js", + "styles": [ + "dist/css/tether.css" + ], + "scripts": [ + "dist/js/tether.js" + ] +} diff --git a/designsafe/static/vendor/tether/dist/css/tether-theme-arrows-dark.css b/designsafe/static/vendor/tether/dist/css/tether-theme-arrows-dark.css new file mode 100644 index 0000000000..c000dd7400 --- /dev/null +++ b/designsafe/static/vendor/tether/dist/css/tether-theme-arrows-dark.css @@ -0,0 +1,150 @@ +.tether-element, .tether-element:after, .tether-element:before, .tether-element *, .tether-element *:after, .tether-element *:before { + box-sizing: border-box; } + +.tether-element { + position: absolute; + display: none; } + .tether-element.tether-open { + display: block; } + +.tether-element.tether-theme-arrows-dark { + max-width: 100%; + max-height: 100%; } + .tether-element.tether-theme-arrows-dark .tether-content { + border-radius: 5px; + position: relative; + font-family: inherit; + background: #000; + color: #fff; + padding: 1em; + font-size: 1.1em; + line-height: 1.5em; } + .tether-element.tether-theme-arrows-dark .tether-content:before { + content: ""; + display: block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-width: 16px; + border-style: solid; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-center .tether-content { + margin-bottom: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-center .tether-content:before { + top: 100%; + left: 50%; + margin-left: -16px; + border-top-color: #000; + border-bottom: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-center .tether-content { + margin-top: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-center .tether-content:before { + bottom: 100%; + left: 50%; + margin-left: -16px; + border-bottom-color: #000; + border-top: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-right.tether-element-attached-middle .tether-content { + margin-right: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-right.tether-element-attached-middle .tether-content:before { + left: 100%; + top: 50%; + margin-top: -16px; + border-left-color: #000; + border-right: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-left.tether-element-attached-middle .tether-content { + margin-left: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-left.tether-element-attached-middle .tether-content:before { + right: 100%; + top: 50%; + margin-top: -16px; + border-right-color: #000; + border-left: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-left.tether-target-attached-center .tether-content { + left: -32px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-right.tether-target-attached-center .tether-content { + left: 32px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-middle .tether-content { + margin-top: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-middle .tether-content:before { + bottom: 100%; + left: 16px; + border-bottom-color: #000; + border-top: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-middle .tether-content { + margin-top: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-middle .tether-content:before { + bottom: 100%; + right: 16px; + border-bottom-color: #000; + border-top: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-middle .tether-content { + margin-bottom: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-middle .tether-content:before { + top: 100%; + left: 16px; + border-top-color: #000; + border-bottom: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-middle .tether-content { + margin-bottom: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-middle .tether-content:before { + top: 100%; + right: 16px; + border-top-color: #000; + border-bottom: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-bottom .tether-content { + margin-top: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-bottom .tether-content:before { + bottom: 100%; + left: 16px; + border-bottom-color: #000; + border-top: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-bottom .tether-content { + margin-top: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-bottom .tether-content:before { + bottom: 100%; + right: 16px; + border-bottom-color: #000; + border-top: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-top .tether-content { + margin-bottom: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-top .tether-content:before { + top: 100%; + left: 16px; + border-top-color: #000; + border-bottom: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-top .tether-content { + margin-bottom: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-top .tether-content:before { + top: 100%; + right: 16px; + border-top-color: #000; + border-bottom: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-left .tether-content { + margin-right: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-left .tether-content:before { + top: 16px; + left: 100%; + border-left-color: #000; + border-right: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-right .tether-content { + margin-left: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-right .tether-content:before { + top: 16px; + right: 100%; + border-right-color: #000; + border-left: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-left .tether-content { + margin-right: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-left .tether-content:before { + bottom: 16px; + left: 100%; + border-left-color: #000; + border-right: 0; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-right .tether-content { + margin-left: 16px; } + .tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-right .tether-content:before { + bottom: 16px; + right: 100%; + border-right-color: #000; + border-left: 0; } diff --git a/designsafe/static/vendor/tether/dist/css/tether-theme-arrows-dark.min.css b/designsafe/static/vendor/tether/dist/css/tether-theme-arrows-dark.min.css new file mode 100644 index 0000000000..73d263e98d --- /dev/null +++ b/designsafe/static/vendor/tether/dist/css/tether-theme-arrows-dark.min.css @@ -0,0 +1 @@ +.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-center .tether-content,.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-middle .tether-content,.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-top .tether-content,.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-middle .tether-content,.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-top .tether-content{margin-bottom:16px}.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-left .tether-content,.tether-element.tether-theme-arrows-dark.tether-element-attached-right.tether-element-attached-middle .tether-content,.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-left .tether-content{margin-right:16px}.tether-element,.tether-element *,.tether-element :after,.tether-element :before,.tether-element:after,.tether-element:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block}.tether-element.tether-theme-arrows-dark{max-width:100%;max-height:100%}.tether-element.tether-theme-arrows-dark .tether-content{border-radius:5px;position:relative;font-family:inherit;background:#000;color:#fff;padding:1em;font-size:1.1em;line-height:1.5em}.tether-element.tether-theme-arrows-dark .tether-content:before{content:"";display:block;position:absolute;width:0;height:0;border-color:transparent;border-width:16px;border-style:solid}.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-center .tether-content:before{top:100%;left:50%;margin-left:-16px;border-top-color:#000;border-bottom:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-center .tether-content{margin-top:16px}.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-center .tether-content:before{bottom:100%;left:50%;margin-left:-16px;border-bottom-color:#000;border-top:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-right .tether-content,.tether-element.tether-theme-arrows-dark.tether-element-attached-left.tether-element-attached-middle .tether-content,.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-right .tether-content{margin-left:16px}.tether-element.tether-theme-arrows-dark.tether-element-attached-right.tether-element-attached-middle .tether-content:before{left:100%;top:50%;margin-top:-16px;border-left-color:#000;border-right:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-left.tether-element-attached-middle .tether-content:before{right:100%;top:50%;margin-top:-16px;border-right-color:#000;border-left:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-bottom .tether-content,.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-middle .tether-content,.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-bottom .tether-content,.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-middle .tether-content{margin-top:16px}.tether-element.tether-theme-arrows-dark.tether-element-attached-left.tether-target-attached-center .tether-content{left:-32px}.tether-element.tether-theme-arrows-dark.tether-element-attached-right.tether-target-attached-center .tether-content{left:32px}.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-middle .tether-content:before{bottom:100%;left:16px;border-bottom-color:#000;border-top:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-middle .tether-content:before{bottom:100%;right:16px;border-bottom-color:#000;border-top:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-middle .tether-content:before{top:100%;left:16px;border-top-color:#000;border-bottom:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-middle .tether-content:before{top:100%;right:16px;border-top-color:#000;border-bottom:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-bottom .tether-content:before{bottom:100%;left:16px;border-bottom-color:#000;border-top:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-bottom .tether-content:before{bottom:100%;right:16px;border-bottom-color:#000;border-top:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-top .tether-content:before{top:100%;left:16px;border-top-color:#000;border-bottom:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-top .tether-content:before{top:100%;right:16px;border-top-color:#000;border-bottom:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-right.tether-target-attached-left .tether-content:before{top:16px;left:100%;border-left-color:#000;border-right:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-top.tether-element-attached-left.tether-target-attached-right .tether-content:before{top:16px;right:100%;border-right-color:#000;border-left:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-left .tether-content:before{bottom:16px;left:100%;border-left-color:#000;border-right:0}.tether-element.tether-theme-arrows-dark.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-right .tether-content:before{bottom:16px;right:100%;border-right-color:#000;border-left:0} \ No newline at end of file diff --git a/designsafe/static/vendor/tether/dist/css/tether-theme-arrows.css b/designsafe/static/vendor/tether/dist/css/tether-theme-arrows.css new file mode 100644 index 0000000000..e9e3bbfabf --- /dev/null +++ b/designsafe/static/vendor/tether/dist/css/tether-theme-arrows.css @@ -0,0 +1,154 @@ +.tether-element, .tether-element:after, .tether-element:before, .tether-element *, .tether-element *:after, .tether-element *:before { + box-sizing: border-box; } + +.tether-element { + position: absolute; + display: none; } + .tether-element.tether-open { + display: block; } + +.tether-element.tether-theme-arrows { + max-width: 100%; + max-height: 100%; } + .tether-element.tether-theme-arrows .tether-content { + border-radius: 5px; + position: relative; + font-family: inherit; + background: #fff; + color: inherit; + padding: 1em; + font-size: 1.1em; + line-height: 1.5em; + -webkit-transform: translateZ(0); + transform: translateZ(0); + -webkit-filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2)); + filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2)); } + .tether-element.tether-theme-arrows .tether-content:before { + content: ""; + display: block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-width: 16px; + border-style: solid; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-center .tether-content { + margin-bottom: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-center .tether-content:before { + top: 100%; + left: 50%; + margin-left: -16px; + border-top-color: #fff; + border-bottom: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-center .tether-content { + margin-top: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-center .tether-content:before { + bottom: 100%; + left: 50%; + margin-left: -16px; + border-bottom-color: #fff; + border-top: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-right.tether-element-attached-middle .tether-content { + margin-right: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-right.tether-element-attached-middle .tether-content:before { + left: 100%; + top: 50%; + margin-top: -16px; + border-left-color: #fff; + border-right: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-left.tether-element-attached-middle .tether-content { + margin-left: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-left.tether-element-attached-middle .tether-content:before { + right: 100%; + top: 50%; + margin-top: -16px; + border-right-color: #fff; + border-left: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-left.tether-target-attached-center .tether-content { + left: -32px; } + .tether-element.tether-theme-arrows.tether-element-attached-right.tether-target-attached-center .tether-content { + left: 32px; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-middle .tether-content { + margin-top: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-middle .tether-content:before { + bottom: 100%; + left: 16px; + border-bottom-color: #fff; + border-top: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-middle .tether-content { + margin-top: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-middle .tether-content:before { + bottom: 100%; + right: 16px; + border-bottom-color: #fff; + border-top: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-middle .tether-content { + margin-bottom: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-middle .tether-content:before { + top: 100%; + left: 16px; + border-top-color: #fff; + border-bottom: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-middle .tether-content { + margin-bottom: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-middle .tether-content:before { + top: 100%; + right: 16px; + border-top-color: #fff; + border-bottom: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-bottom .tether-content { + margin-top: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-bottom .tether-content:before { + bottom: 100%; + left: 16px; + border-bottom-color: #fff; + border-top: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-bottom .tether-content { + margin-top: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-bottom .tether-content:before { + bottom: 100%; + right: 16px; + border-bottom-color: #fff; + border-top: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-top .tether-content { + margin-bottom: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-top .tether-content:before { + top: 100%; + left: 16px; + border-top-color: #fff; + border-bottom: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-top .tether-content { + margin-bottom: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-top .tether-content:before { + top: 100%; + right: 16px; + border-top-color: #fff; + border-bottom: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-left .tether-content { + margin-right: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-left .tether-content:before { + top: 16px; + left: 100%; + border-left-color: #fff; + border-right: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-right .tether-content { + margin-left: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-right .tether-content:before { + top: 16px; + right: 100%; + border-right-color: #fff; + border-left: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-left .tether-content { + margin-right: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-left .tether-content:before { + bottom: 16px; + left: 100%; + border-left-color: #fff; + border-right: 0; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-right .tether-content { + margin-left: 16px; } + .tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-right .tether-content:before { + bottom: 16px; + right: 100%; + border-right-color: #fff; + border-left: 0; } diff --git a/designsafe/static/vendor/tether/dist/css/tether-theme-arrows.min.css b/designsafe/static/vendor/tether/dist/css/tether-theme-arrows.min.css new file mode 100644 index 0000000000..61994a9c4b --- /dev/null +++ b/designsafe/static/vendor/tether/dist/css/tether-theme-arrows.min.css @@ -0,0 +1 @@ +.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-center .tether-content,.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-middle .tether-content,.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-top .tether-content,.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-middle .tether-content,.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-top .tether-content{margin-bottom:16px}.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-left .tether-content,.tether-element.tether-theme-arrows.tether-element-attached-right.tether-element-attached-middle .tether-content,.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-left .tether-content{margin-right:16px}.tether-element,.tether-element *,.tether-element :after,.tether-element :before,.tether-element:after,.tether-element:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block}.tether-element.tether-theme-arrows{max-width:100%;max-height:100%}.tether-element.tether-theme-arrows .tether-content{border-radius:5px;position:relative;font-family:inherit;background:#fff;color:inherit;padding:1em;font-size:1.1em;line-height:1.5em;-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-filter:drop-shadow(0 1px 4px rgba(0, 0, 0, .2));filter:drop-shadow(0 1px 4px rgba(0, 0, 0, .2))}.tether-element.tether-theme-arrows .tether-content:before{content:"";display:block;position:absolute;width:0;height:0;border-color:transparent;border-width:16px;border-style:solid}.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-center .tether-content:before{top:100%;left:50%;margin-left:-16px;border-top-color:#fff;border-bottom:0}.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-center .tether-content{margin-top:16px}.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-center .tether-content:before{bottom:100%;left:50%;margin-left:-16px;border-bottom-color:#fff;border-top:0}.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-right .tether-content,.tether-element.tether-theme-arrows.tether-element-attached-left.tether-element-attached-middle .tether-content,.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-right .tether-content{margin-left:16px}.tether-element.tether-theme-arrows.tether-element-attached-right.tether-element-attached-middle .tether-content:before{left:100%;top:50%;margin-top:-16px;border-left-color:#fff;border-right:0}.tether-element.tether-theme-arrows.tether-element-attached-left.tether-element-attached-middle .tether-content:before{right:100%;top:50%;margin-top:-16px;border-right-color:#fff;border-left:0}.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-bottom .tether-content,.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-middle .tether-content,.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-bottom .tether-content,.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-middle .tether-content{margin-top:16px}.tether-element.tether-theme-arrows.tether-element-attached-left.tether-target-attached-center .tether-content{left:-32px}.tether-element.tether-theme-arrows.tether-element-attached-right.tether-target-attached-center .tether-content{left:32px}.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-middle .tether-content:before{bottom:100%;left:16px;border-bottom-color:#fff;border-top:0}.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-middle .tether-content:before{bottom:100%;right:16px;border-bottom-color:#fff;border-top:0}.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-middle .tether-content:before{top:100%;left:16px;border-top-color:#fff;border-bottom:0}.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-middle .tether-content:before{top:100%;right:16px;border-top-color:#fff;border-bottom:0}.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-bottom .tether-content:before{bottom:100%;left:16px;border-bottom-color:#fff;border-top:0}.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-bottom .tether-content:before{bottom:100%;right:16px;border-bottom-color:#fff;border-top:0}.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-top .tether-content:before{top:100%;left:16px;border-top-color:#fff;border-bottom:0}.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-top .tether-content:before{top:100%;right:16px;border-top-color:#fff;border-bottom:0}.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-right.tether-target-attached-left .tether-content:before{top:16px;left:100%;border-left-color:#fff;border-right:0}.tether-element.tether-theme-arrows.tether-element-attached-top.tether-element-attached-left.tether-target-attached-right .tether-content:before{top:16px;right:100%;border-right-color:#fff;border-left:0}.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-right.tether-target-attached-left .tether-content:before{bottom:16px;left:100%;border-left-color:#fff;border-right:0}.tether-element.tether-theme-arrows.tether-element-attached-bottom.tether-element-attached-left.tether-target-attached-right .tether-content:before{bottom:16px;right:100%;border-right-color:#fff;border-left:0} \ No newline at end of file diff --git a/designsafe/static/vendor/tether/dist/css/tether-theme-basic.css b/designsafe/static/vendor/tether/dist/css/tether-theme-basic.css new file mode 100644 index 0000000000..f2b3b5c6a2 --- /dev/null +++ b/designsafe/static/vendor/tether/dist/css/tether-theme-basic.css @@ -0,0 +1,21 @@ +.tether-element, .tether-element:after, .tether-element:before, .tether-element *, .tether-element *:after, .tether-element *:before { + box-sizing: border-box; } + +.tether-element { + position: absolute; + display: none; } + .tether-element.tether-open { + display: block; } + +.tether-element.tether-theme-basic { + max-width: 100%; + max-height: 100%; } + .tether-element.tether-theme-basic .tether-content { + border-radius: 5px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + font-family: inherit; + background: #fff; + color: inherit; + padding: 1em; + font-size: 1.1em; + line-height: 1.5em; } diff --git a/designsafe/static/vendor/tether/dist/css/tether-theme-basic.min.css b/designsafe/static/vendor/tether/dist/css/tether-theme-basic.min.css new file mode 100644 index 0000000000..f117c4799a --- /dev/null +++ b/designsafe/static/vendor/tether/dist/css/tether-theme-basic.min.css @@ -0,0 +1 @@ +.tether-element,.tether-element *,.tether-element :after,.tether-element :before,.tether-element:after,.tether-element:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block}.tether-element.tether-theme-basic{max-width:100%;max-height:100%}.tether-element.tether-theme-basic .tether-content{border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,.2);font-family:inherit;background:#fff;color:inherit;padding:1em;font-size:1.1em;line-height:1.5em} \ No newline at end of file diff --git a/designsafe/static/vendor/tether/dist/css/tether.css b/designsafe/static/vendor/tether/dist/css/tether.css new file mode 100644 index 0000000000..fc30f56745 --- /dev/null +++ b/designsafe/static/vendor/tether/dist/css/tether.css @@ -0,0 +1,8 @@ +.tether-element, .tether-element:after, .tether-element:before, .tether-element *, .tether-element *:after, .tether-element *:before { + box-sizing: border-box; } + +.tether-element { + position: absolute; + display: none; } + .tether-element.tether-open { + display: block; } diff --git a/designsafe/static/vendor/tether/dist/css/tether.min.css b/designsafe/static/vendor/tether/dist/css/tether.min.css new file mode 100644 index 0000000000..328251c90c --- /dev/null +++ b/designsafe/static/vendor/tether/dist/css/tether.min.css @@ -0,0 +1 @@ +.tether-element,.tether-element *,.tether-element :after,.tether-element :before,.tether-element:after,.tether-element:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block} \ No newline at end of file diff --git a/designsafe/static/vendor/tether/dist/js/tether.js b/designsafe/static/vendor/tether/dist/js/tether.js new file mode 100644 index 0000000000..ea141f01d0 --- /dev/null +++ b/designsafe/static/vendor/tether/dist/js/tether.js @@ -0,0 +1,1811 @@ +/*! tether 1.4.0 */ + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } else if (typeof exports === 'object') { + module.exports = factory(require, exports, module); + } else { + root.Tether = factory(); + } +}(this, function(require, exports, module) { + +'use strict'; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var TetherBase = undefined; +if (typeof TetherBase === 'undefined') { + TetherBase = { modules: [] }; +} + +var zeroElement = null; + +// Same as native getBoundingClientRect, except it takes into account parent offsets +// if the element lies within a nested document ( or + + + + +
    + +
    +
    +
    +

    Tether is a low-level UI library that can be used to position any element on a page next to any other element.

    +

    It can be used for dropdown menus, tooltips, popovers, tours, help information, scroll guides, autocompletes, etc. The possibilities are endless.

    +

    In this example we're showing an action menu tethered to a list item.

    +
    +
    +
    + +
    +
    +
    +

    Tether works by creating an absolutely positioned element and meticulously tracking the movements of a target which you specify.

    +

    The target and element can be tethered together in a variety of different ways.

    +

    Notice how the tethered element stays tethered to its target list item even as the left pane is scrolled up and down.

    +
    +
    +
    + +
    +
    +
    +

    Tether can keep your element positioned properly even in some tough situations.

    +

    Tether handles all of the common pain points:

    +
      +
    • Automatically detect collisions with the edge of the page or edge of the scrollParent
    • +
    • Automatically reposition on browser resize, scroll, and other events,
    • +
    • Constrain the position to any bounding box,
    • +
    +

    ...and a lot more.

    +
    +
    +
    + +
    +
    +
    +

    Interact with this demo.

    +

     

    +

    To learn more, check out our documentation.

    +
    +
    +
    + +
    + +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + diff --git a/designsafe/static/vendor/tether/docs/welcome/js/drop.js b/designsafe/static/vendor/tether/docs/welcome/js/drop.js new file mode 100644 index 0000000000..0058819408 --- /dev/null +++ b/designsafe/static/vendor/tether/docs/welcome/js/drop.js @@ -0,0 +1,239 @@ +(function() { + var Evented, MIRROR_ATTACH, addClass, allDrops, clickEvent, createContext, extend, hasClass, removeClass, sortAttach, touchDevice, _ref, + __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + _ref = Tether.Utils, extend = _ref.extend, addClass = _ref.addClass, removeClass = _ref.removeClass, hasClass = _ref.hasClass, Evented = _ref.Evented; + + touchDevice = 'ontouchstart' in document.documentElement; + + clickEvent = touchDevice ? 'touchstart' : 'click'; + + sortAttach = function(str) { + var first, second, _ref1, _ref2; + _ref1 = str.split(' '), first = _ref1[0], second = _ref1[1]; + if (first === 'left' || first === 'right') { + _ref2 = [second, first], first = _ref2[0], second = _ref2[1]; + } + return [first, second].join(' '); + }; + + MIRROR_ATTACH = { + left: 'right', + right: 'left', + top: 'bottom', + bottom: 'top', + middle: 'middle', + center: 'center' + }; + + allDrops = {}; + + createContext = function(options) { + var DropInstance, defaultOptions, drop, _name; + if (options == null) { + options = {}; + } + drop = function() { + return (function(func, args, ctor) { + ctor.prototype = func.prototype; + var child = new ctor, result = func.apply(child, args); + return Object(result) === result ? result : child; + })(DropInstance, arguments, function(){}); + }; + extend(drop, { + createContext: createContext, + drops: [], + defaults: {} + }); + defaultOptions = { + classPrefix: 'drop', + defaults: { + attach: 'bottom left', + openOn: 'click', + constrainToScrollParent: true, + constrainToWindow: true, + classes: '', + tetherOptions: {} + } + }; + extend(drop, defaultOptions, options); + extend(drop.defaults, defaultOptions.defaults, options.defaults); + if (allDrops[_name = drop.classPrefix] == null) { + allDrops[_name] = []; + } + drop.updateBodyClasses = function() { + var anyOpen, _drop, _i, _len, _ref1; + anyOpen = false; + _ref1 = allDrops[drop.classPrefix]; + for (_i = 0, _len = _ref1.length; _i < _len; _i++) { + _drop = _ref1[_i]; + if (!(_drop.isOpened())) { + continue; + } + anyOpen = true; + break; + } + if (anyOpen) { + return addClass(document.body, "" + drop.classPrefix + "-open"); + } else { + return removeClass(document.body, "" + drop.classPrefix + "-open"); + } + }; + DropInstance = (function(_super) { + __extends(DropInstance, _super); + + function DropInstance(options) { + this.options = options; + this.options = extend({}, drop.defaults, this.options); + this.target = this.options.target; + if (this.target == null) { + throw new Error('Drop Error: You must provide a target.'); + } + drop.drops.push(this); + allDrops[drop.classPrefix].push(this); + this.setupElements(); + this.setupEvents(); + this.setupTether(); + } + + DropInstance.prototype.setupElements = function() { + this.drop = document.createElement('div'); + addClass(this.drop, drop.classPrefix); + if (this.options.classes) { + addClass(this.drop, this.options.classes); + } + this.dropContent = document.createElement('div'); + addClass(this.dropContent, "" + drop.classPrefix + "-content"); + if (typeof this.options.content === 'object') { + this.dropContent.appendChild(this.options.content); + } else { + this.dropContent.innerHTML = this.options.content; + } + return this.drop.appendChild(this.dropContent); + }; + + DropInstance.prototype.setupTether = function() { + var constraints, dropAttach; + dropAttach = this.options.position.split(' '); + dropAttach[0] = MIRROR_ATTACH[dropAttach[0]]; + dropAttach = dropAttach.join(' '); + constraints = []; + if (this.options.constrainToScrollParent) { + constraints.push({ + to: 'scrollParent', + pin: 'top, bottom', + attachment: 'together none' + }); + } + if (this.options.constrainToWindow !== false) { + constraints.push({ + to: 'window', + pin: true, + attachment: 'together' + }); + } + constraints.push({ + to: 'scrollParent' + }); + options = { + element: this.drop, + target: this.target, + attachment: sortAttach(dropAttach), + targetAttachment: sortAttach(this.options.position), + classPrefix: drop.classPrefix, + offset: '0 0', + targetOffset: '0 0', + enabled: false, + constraints: constraints + }; + if (this.options.tether !== false) { + return this.tether = new Tether(extend({}, options, this.options.tether)); + } + }; + + DropInstance.prototype.setupEvents = function() { + var events, + _this = this; + if (!this.options.openOn) { + return; + } + events = this.options.openOn.split(' '); + if (__indexOf.call(events, 'click') >= 0) { + this.target.addEventListener(clickEvent, function() { + return _this.toggle(); + }); + document.addEventListener(clickEvent, function(event) { + if (!_this.isOpened()) { + return; + } + if (event.target === _this.drop || _this.drop.contains(event.target)) { + return; + } + if (event.target === _this.target || _this.target.contains(event.target)) { + return; + } + return _this.close(); + }); + } + if (__indexOf.call(events, 'hover') >= 0) { + this.target.addEventListener('mouseover', function() { + return _this.open(); + }); + return this.target.addEventListener('mouseout', function() { + return _this.close(); + }); + } + }; + + DropInstance.prototype.isOpened = function() { + return hasClass(this.drop, "" + drop.classPrefix + "-open"); + }; + + DropInstance.prototype.toggle = function() { + if (this.isOpened()) { + return this.close(); + } else { + return this.open(); + } + }; + + DropInstance.prototype.open = function() { + var _ref1; + if (!this.drop.parentNode) { + document.body.appendChild(this.drop); + } + addClass(this.target, "" + drop.classPrefix + "-open"); + addClass(this.drop, "" + drop.classPrefix + "-open"); + if ((_ref1 = this.tether) != null) { + _ref1.enable(); + } + this.trigger('open'); + return drop.updateBodyClasses(); + }; + + DropInstance.prototype.close = function() { + var _ref1; + removeClass(this.target, "" + drop.classPrefix + "-open"); + removeClass(this.drop, "" + drop.classPrefix + "-open"); + this.trigger('close'); + if ((_ref1 = this.tether) != null) { + _ref1.disable(); + } + return drop.updateBodyClasses(); + }; + + return DropInstance; + + })(Evented); + return drop; + }; + + window.Drop = createContext(); + + document.addEventListener('DOMContentLoaded', function() { + return Drop.updateBodyClasses(); + }); + +}).call(this); \ No newline at end of file diff --git a/designsafe/static/vendor/tether/docs/welcome/js/jquery.js b/designsafe/static/vendor/tether/docs/welcome/js/jquery.js new file mode 100644 index 0000000000..dfdf619cc7 --- /dev/null +++ b/designsafe/static/vendor/tether/docs/welcome/js/jquery.js @@ -0,0 +1,9597 @@ +/*! + * jQuery JavaScript Library v1.9.1 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright 2005, 2012 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2013-2-4 + */ +(function( window, undefined ) { + +// Can't do this because several apps including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +// Support: Firefox 18+ +//"use strict"; +var + // The deferred used on DOM ready + readyList, + + // A central reference to the root jQuery(document) + rootjQuery, + + // Support: IE<9 + // For `typeof node.method` instead of `node.method !== undefined` + core_strundefined = typeof undefined, + + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + location = window.location, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // [[Class]] -> type pairs + class2type = {}, + + // List of deleted data cache ids, so we can reuse them + core_deletedIds = [], + + core_version = "1.9.1", + + // Save a reference to some core methods + core_concat = core_deletedIds.concat, + core_push = core_deletedIds.push, + core_slice = core_deletedIds.slice, + core_indexOf = core_deletedIds.indexOf, + core_toString = class2type.toString, + core_hasOwn = class2type.hasOwnProperty, + core_trim = core_version.trim, + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Used for matching numbers + core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, + + // Used for splitting on whitespace + core_rnotwhite = /\S+/g, + + // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, + rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }, + + // The ready event handler + completed = function( event ) { + + // readyState === "complete" is good enough for us to call the dom ready in oldIE + if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { + detach(); + jQuery.ready(); + } + }, + // Clean-up method for dom ready events + detach = function() { + if ( document.addEventListener ) { + document.removeEventListener( "DOMContentLoaded", completed, false ); + window.removeEventListener( "load", completed, false ); + + } else { + document.detachEvent( "onreadystatechange", completed ); + window.detachEvent( "onload", completed ); + } + }; + +jQuery.fn = jQuery.prototype = { + // The current version of jQuery being used + jquery: core_version, + + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + + // scripts is true for back-compat + jQuery.merge( this, jQuery.parseHTML( + match[1], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return core_slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; + }, + + slice: function() { + return this.pushStack( core_slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: core_push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var src, copyIsArray, copy, name, options, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger("ready").off("ready"); + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + if ( obj == null ) { + return String( obj ); + } + return typeof obj === "object" || typeof obj === "function" ? + class2type[ core_toString.call(obj) ] || "object" : + typeof obj; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !core_hasOwn.call(obj, "constructor") && + !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || core_hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + // data: string of html + // context (optional): If specified, the fragment will be created in this context, defaults to document + // keepScripts (optional): If true, will include scripts passed in the html string + parseHTML: function( data, context, keepScripts ) { + if ( !data || typeof data !== "string" ) { + return null; + } + if ( typeof context === "boolean" ) { + keepScripts = context; + context = false; + } + context = context || document; + + var parsed = rsingleTag.exec( data ), + scripts = !keepScripts && []; + + // Single tag + if ( parsed ) { + return [ context.createElement( parsed[1] ) ]; + } + + parsed = jQuery.buildFragment( [ data ], context, scripts ); + if ( scripts ) { + jQuery( scripts ).remove(); + } + return jQuery.merge( [], parsed.childNodes ); + }, + + parseJSON: function( data ) { + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + if ( data === null ) { + return data; + } + + if ( typeof data === "string" ) { + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + if ( data ) { + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + } + } + } + + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + var xml, tmp; + if ( !data || typeof data !== "string" ) { + return null; + } + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && jQuery.trim( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + // args is for internal usage only + each: function( obj, callback, args ) { + var value, + i = 0, + length = obj.length, + isArray = isArraylike( obj ); + + if ( args ) { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } + } + + return obj; + }, + + // Use native String.trim function wherever possible + trim: core_trim && !core_trim.call("\uFEFF\xA0") ? + function( text ) { + return text == null ? + "" : + core_trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArraylike( Object(arr) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + core_push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + var len; + + if ( arr ) { + if ( core_indexOf ) { + return core_indexOf.call( arr, elem, i ); + } + + len = arr.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in arr && arr[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var l = second.length, + i = first.length, + j = 0; + + if ( typeof l === "number" ) { + for ( ; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var retVal, + ret = [], + i = 0, + length = elems.length; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, + i = 0, + length = elems.length, + isArray = isArraylike( elems ), + ret = []; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return core_concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var args, proxy, tmp; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = core_slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + // Multifunctional method to get and set values of a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + length = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < length; i++ ) { + fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; + }, + + now: function() { + return ( new Date() ).getTime(); + } +}); + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready ); + + // Standards-based browsers support DOMContentLoaded + } else if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed, false ); + + // If IE event model is used + } else { + // Ensure firing before onload, maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", completed ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", completed ); + + // If IE and not a frame + // continually check to see if the document is ready + var top = false; + + try { + top = window.frameElement == null && document.documentElement; + } catch(e) {} + + if ( top && top.doScroll ) { + (function doScrollCheck() { + if ( !jQuery.isReady ) { + + try { + // Use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + top.doScroll("left"); + } catch(e) { + return setTimeout( doScrollCheck, 50 ); + } + + // detach all dom ready events + detach(); + + // and execute any waiting functions + jQuery.ready(); + } + })(); + } + } + } + return readyList.promise( obj ); +}; + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +function isArraylike( obj ) { + var length = obj.length, + type = jQuery.type( obj ); + + if ( jQuery.isWindow( obj ) ) { + return false; + } + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === "array" || type !== "function" && + ( length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj ); +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // First callback to fire (used internally by add and fireWith) + firingStart, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( list && ( !fired || stack ) ) { + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function() { + deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = core_slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; + if( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +}); +jQuery.support = (function() { + + var support, all, a, + input, select, fragment, + opt, eventName, isSupported, i, + div = document.createElement("div"); + + // Setup + div.setAttribute( "className", "t" ); + div.innerHTML = "
    a"; + + // Support tests won't run in some limited or non-browser environments + all = div.getElementsByTagName("*"); + a = div.getElementsByTagName("a")[ 0 ]; + if ( !all || !a || !all.length ) { + return {}; + } + + // First batch of tests + select = document.createElement("select"); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName("input")[ 0 ]; + + a.style.cssText = "top:1px;float:left;opacity:.5"; + support = { + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: div.firstChild.nodeType === 3, + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: a.getAttribute("href") === "/a", + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.5/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) + checkOn: !!input.value, + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Tests for enctype support on a form (#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + + // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode + boxModel: document.compatMode === "CSS1Compat", + + // Will be defined later + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true, + boxSizingReliable: true, + pixelPosition: false + }; + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Support: IE<9 + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + // Check if we can trust getAttribute("value") + input = document.createElement("input"); + input.setAttribute( "value", "" ); + support.input = input.getAttribute( "value" ) === ""; + + // Check if an input maintains its value after becoming a radio + input.value = "t"; + input.setAttribute( "type", "radio" ); + support.radioValue = input.value === "t"; + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "checked", "t" ); + input.setAttribute( "name", "t" ); + + fragment = document.createDocumentFragment(); + fragment.appendChild( input ); + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<9 + // Opera does not clone events (and typeof div.attachEvent === undefined). + // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() + if ( div.attachEvent ) { + div.attachEvent( "onclick", function() { + support.noCloneEvent = false; + }); + + div.cloneNode( true ).click(); + } + + // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) + // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP), test/csp.php + for ( i in { submit: true, change: true, focusin: true }) { + div.setAttribute( eventName = "on" + i, "t" ); + + support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; + } + + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, marginDiv, tds, + divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + container = document.createElement("div"); + container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; + + body.appendChild( container ).appendChild( div ); + + // Support: IE8 + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + div.innerHTML = "
    t
    "; + tds = div.getElementsByTagName("td"); + tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Support: IE8 + // Check if empty table cells still have offsetWidth/Height + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check box-sizing and margin behavior + div.innerHTML = ""; + div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; + support.boxSizing = ( div.offsetWidth === 4 ); + support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 ); + + // Use window.getComputedStyle because jsdom on node.js will break without it. + if ( window.getComputedStyle ) { + support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; + support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. (#3333) + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + marginDiv = div.appendChild( document.createElement("div") ); + marginDiv.style.cssText = div.style.cssText = divReset; + marginDiv.style.marginRight = marginDiv.style.width = "0"; + div.style.width = "1px"; + + support.reliableMarginRight = + !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + } + + if ( typeof div.style.zoom !== core_strundefined ) { + // Support: IE<8 + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + div.innerHTML = ""; + div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Support: IE6 + // Check if elements with layout shrink-wrap their children + div.style.display = "block"; + div.innerHTML = "
    "; + div.firstChild.style.width = "5px"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + + if ( support.inlineBlockNeedsLayout ) { + // Prevent IE 6 from affecting layout for positioned elements #11048 + // Prevent IE from shrinking the body in IE 7 mode #12869 + // Support: IE<8 + body.style.zoom = 1; + } + } + + body.removeChild( container ); + + // Null elements to avoid leaks in IE + container = div = tds = marginDiv = null; + }); + + // Null elements to avoid leaks in IE + all = select = fragment = opt = a = input = null; + + return support; +})(); + +var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, + rmultiDash = /([A-Z])/g; + +function internalData( elem, name, data, pvt /* Internal Use Only */ ){ + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = core_deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; +} + +function internalRemoveData( elem, name, pvt ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var i, l, thisCache, + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } + } + } else { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = name.concat( jQuery.map( name, jQuery.camelCase ) ); + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; + } + } + + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); + + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + } else if ( jQuery.support.deleteExpando || cache != cache.window ) { + delete cache[ id ]; + + // When all else fails, null + } else { + cache[ id ] = null; + } +} + +jQuery.extend({ + cache: {}, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data ) { + return internalData( elem, name, data ); + }, + + removeData: function( elem, name ) { + return internalRemoveData( elem, name ); + }, + + // For internal use only. + _data: function( elem, name, data ) { + return internalData( elem, name, data, true ); + }, + + _removeData: function( elem, name ) { + return internalRemoveData( elem, name, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + // Do not set data on non-element because it will not be cleared (#8335). + if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { + return false; + } + + var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; + + // nodes accept data unless otherwise specified; rejection can be conditional + return !noData || noData !== true && elem.getAttribute("classid") === noData; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var attrs, name, + elem = this[0], + i = 0, + data = null; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attrs = elem.attributes; + for ( ; i < attrs.length; i++ ) { + name = attrs[i].name; + + if ( !name.indexOf( "data-" ) ) { + name = jQuery.camelCase( name.slice(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + return jQuery.access( this, function( value ) { + + if ( value === undefined ) { + // Try to fetch any internally stored data first + return elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; + } + + this.each(function() { + jQuery.data( this, key, value ); + }); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + hooks.cur = fn; + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery._removeData( elem, type + "queue" ); + jQuery._removeData( elem, key ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while( i-- ) { + tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var nodeHook, boolHook, + rclass = /[\t\r\n]/g, + rreturn = /\r/g, + rfocusable = /^(?:input|select|textarea|button|object)$/i, + rclickable = /^(?:a|area)$/i, + rboolean = /^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i, + ruseDefault = /^(?:checked|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + getSetInput = jQuery.support.input; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call( this, j, this.className ) ); + }); + } + + if ( proceed ) { + // The disjunction here is for better compressibility (see removeClass) + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + " " + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + elem.className = jQuery.trim( cur ); + + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = arguments.length === 0 || typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call( this, j, this.className ) ); + }); + } + if ( proceed ) { + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + "" + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + elem.className = value ? jQuery.trim( cur ) : ""; + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.match( core_rnotwhite ) || []; + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space separated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + // Toggle whole class name + } else if ( type === core_strundefined || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // If the element has a class name or if we're passed "false", + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var ret, hooks, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var val, + self = jQuery(this); + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, option, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one" || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? + max : + one ? index : 0; + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // oldIE doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + // Don't return options that are disabled or in a disabled optgroup + ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && + ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attr: function( elem, name, value ) { + var hooks, notxml, ret, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === core_strundefined ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + + } else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, value + "" ); + return value; + } + + } else if ( hooks && notxml && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + // In IE9+, Flash objects don't have .getAttribute (#12945) + // Support: IE9+ + if ( typeof elem.getAttribute !== core_strundefined ) { + ret = elem.getAttribute( name ); + } + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var name, propName, + i = 0, + attrNames = value && value.match( core_rnotwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( (name = attrNames[i++]) ) { + propName = jQuery.propFix[ name ] || name; + + // Boolean attributes get special treatment (#10870) + if ( rboolean.test( name ) ) { + // Set corresponding property to false for boolean attributes + // Also clear defaultChecked/defaultSelected (if appropriate) for IE<8 + if ( !getSetAttribute && ruseDefault.test( name ) ) { + elem[ jQuery.camelCase( "default-" + name ) ] = + elem[ propName ] = false; + } else { + elem[ propName ] = false; + } + + // See #9699 for explanation of this approach (setting first, then removal) + } else { + jQuery.attr( elem, name, "" ); + } + + elem.removeAttribute( getSetAttribute ? name : propName ); + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to default in case type is set after value during creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + var + // Use .prop to determine if this attribute is understood as boolean + prop = jQuery.prop( elem, name ), + + // Fetch it accordingly + attr = typeof prop === "boolean" && elem.getAttribute( name ), + detail = typeof prop === "boolean" ? + + getSetInput && getSetAttribute ? + attr != null : + // oldIE fabricates an empty string for missing boolean attributes + // and conflates checked/selected into attroperties + ruseDefault.test( name ) ? + elem[ jQuery.camelCase( "default-" + name ) ] : + !!attr : + + // fetch an attribute node for properties not recognized as boolean + elem.getAttributeNode( name ); + + return detail && detail.value !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + // IE<8 needs the *property* name + elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); + + // Use defaultChecked and defaultSelected for oldIE + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; + } + + return name; + } +}; + +// fix oldIE value attroperty +if ( !getSetInput || !getSetAttribute ) { + jQuery.attrHooks.value = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return jQuery.nodeName( elem, "input" ) ? + + // Ignore the value *property* by using defaultValue + elem.defaultValue : + + ret && ret.specified ? ret.value : undefined; + }, + set: function( elem, value, name ) { + if ( jQuery.nodeName( elem, "input" ) ) { + // Does not return so that setAttribute is also used + elem.defaultValue = value; + } else { + // Use nodeHook if defined (#1954); otherwise setAttribute is fine + return nodeHook && nodeHook.set( elem, value, name ); + } + } + }; +} + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return ret && ( name === "id" || name === "name" || name === "coords" ? ret.value !== "" : ret.specified ) ? + ret.value : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + elem.setAttributeNode( + (ret = elem.ownerDocument.createAttribute( name )) + ); + } + + ret.value = value += ""; + + // Break association with cloned elements by also using setAttribute (#9646) + return name === "value" || value === elem.getAttribute( name ) ? + value : + undefined; + } + }; + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + nodeHook.set( elem, value === "" ? false : value, name ); + } + }; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); +} + + +// Some attributes require a special call on IE +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret == null ? undefined : ret; + } + }); + }); + + // href/src property should get the full normalized URL (#10299/#12915) + jQuery.each([ "href", "src" ], function( i, name ) { + jQuery.propHooks[ name ] = { + get: function( elem ) { + return elem.getAttribute( name, 4 ); + } + }; + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Note: IE uppercases css property names, but if we were to .toLowerCase() + // .cssText, that would destroy case senstitivity in URL's, like in "background" + return elem.style.cssText || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = value + "" ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); +var rformElems = /^(?:input|select|textarea)$/i, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + var tmp, events, t, handleObjIn, + special, eventHandle, handleObj, + handlers, type, namespaces, origType, + elemData = jQuery._data( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + var j, handleObj, tmp, + origCount, t, events, + special, handlers, type, + namespaces, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery._removeData( elem, "events" ); + } + }, + + trigger: function( event, data, elem, onlyHandlers ) { + var handle, ontype, cur, + bubbleType, special, tmp, i, + eventPath = [ elem || document ], + type = core_hasOwn.call( event, "type" ) ? event.type : event, + namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + + cur = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + event.isTrigger = true; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { + + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + try { + elem[ type ](); + } catch ( e ) { + // IE<9 dies on focus/blur to hidden element (#1486,#12518) + // only reproducible on winXP IE8 native, not IE9 in IE8 mode + } + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, ret, handleObj, matched, j, + handlerQueue = [], + args = core_slice.call( arguments ), + handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var sel, handleObj, matches, i, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + + for ( ; cur != this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } + + return handlerQueue; + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: IE<9 + // Fix target property (#1925) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Support: Chrome 23+, Safari? + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Support: IE<9 + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) + event.metaKey = !!event.metaKey; + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var body, eventDoc, doc, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { + this.click(); + return false; + } + } + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== document.activeElement && this.focus ) { + try { + this.focus(); + return false; + } catch ( e ) { + // Support: IE<9 + // If we error on focus to hidden element (#1486, #12518), + // let .trigger() run the handlers + } + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === document.activeElement && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + + beforeunload: { + postDispatch: function( event ) { + + // Even when returnValue equals to undefined Firefox will still show alert + if ( event.result !== undefined ) { + event.originalEvent.returnValue = event.result; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + var name = "on" + type; + + if ( elem.detachEvent ) { + + // #8545, #7054, preventing memory leaks for custom events in IE6-8 + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === core_strundefined ) { + elem[ name ] = null; + } + + elem.detachEvent( name, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + if ( !e ) { + return; + } + + // If preventDefault exists, run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // Support: IE + // Otherwise set the returnValue property of the original event to false + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + if ( !e ) { + return; + } + // If stopPropagation exists, run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + + // Support: IE + // Set the cancelBubble property of the original event to true + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "submitBubbles" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "submitBubbles", true ); + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "changeBubbles", true ); + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var type, origFn; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +}); +/*! + * Sizzle CSS Selector Engine + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license + * http://sizzlejs.com/ + */ +(function( window, undefined ) { + +var i, + cachedruns, + Expr, + getText, + isXML, + compile, + hasDuplicate, + outermostContext, + + // Local document vars + setDocument, + document, + docElem, + documentIsXML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + sortOrder, + + // Instance-specific data + expando = "sizzle" + -(new Date()), + preferredDoc = window.document, + support = {}, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + + // General-purpose constants + strundefined = typeof undefined, + MAX_NEGATIVE = 1 << 31, + + // Array methods + arr = [], + pop = arr.pop, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf if we can't use a native one + indexOf = arr.indexOf || function( elem ) { + var i = 0, + len = this.length; + for ( ; i < len; i++ ) { + if ( this[i] === elem ) { + return i; + } + } + return -1; + }, + + + // Regular expressions + + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace( "w", "w#" ), + + // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors + operators = "([*^$|!~]?=)", + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + + "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", + + // Prefer arguments quoted, + // then not containing pseudos/brackets, + // then attribute selectors/non-parenthetical expressions, + // then anything else + // These preferences are here to reduce the number of selectors + // needing tokenize in the PSEUDO preFilter + pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ), + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + characterEncoding + ")" ), + "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), + "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), + "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rsibling = /[\x20\t\r\n\f]*[+~]/, + + rnative = /^[^{]+\{\s*\[native code/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rescape = /'|\\/g, + rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = /\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g, + funescape = function( _, escaped ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + return high !== high ? + escaped : + // BMP codepoint + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }; + +// Use a stripped-down slice if we can't use a native one +try { + slice.call( preferredDoc.documentElement.childNodes, 0 )[0].nodeType; +} catch ( e ) { + slice = function( i ) { + var elem, + results = []; + while ( (elem = this[i++]) ) { + results.push( elem ); + } + return results; + }; +} + +/** + * For feature detection + * @param {Function} fn The function to test for native support + */ +function isNative( fn ) { + return rnative.test( fn + "" ); +} + +/** + * Create key-value caches of limited size + * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var cache, + keys = []; + + return (cache = function( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key += " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key ] = value); + }); +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return fn( div ); + } catch (e) { + return false; + } finally { + // release memory in IE + div = null; + } +} + +function Sizzle( selector, context, results, seed ) { + var match, elem, m, nodeType, + // QSA vars + i, groups, old, nid, newContext, newSelector; + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + + context = context || document; + results = results || []; + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { + return []; + } + + if ( !documentIsXML && !seed ) { + + // Shortcuts + if ( (match = rquickExpr.exec( selector )) ) { + // Speed-up: Sizzle("#ID") + if ( (m = match[1]) ) { + if ( nodeType === 9 ) { + elem = context.getElementById( m ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && + contains( context, elem ) && elem.id === m ) { + results.push( elem ); + return results; + } + } + + // Speed-up: Sizzle("TAG") + } else if ( match[2] ) { + push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) ); + return results; + + // Speed-up: Sizzle(".CLASS") + } else if ( (m = match[3]) && support.getByClassName && context.getElementsByClassName ) { + push.apply( results, slice.call(context.getElementsByClassName( m ), 0) ); + return results; + } + } + + // QSA path + if ( support.qsa && !rbuggyQSA.test(selector) ) { + old = true; + nid = expando; + newContext = context; + newSelector = nodeType === 9 && selector; + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + groups = tokenize( selector ); + + if ( (old = context.getAttribute("id")) ) { + nid = old.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", nid ); + } + nid = "[id='" + nid + "'] "; + + i = groups.length; + while ( i-- ) { + groups[i] = nid + toSelector( groups[i] ); + } + newContext = rsibling.test( selector ) && context.parentNode || context; + newSelector = groups.join(","); + } + + if ( newSelector ) { + try { + push.apply( results, slice.call( newContext.querySelectorAll( + newSelector + ), 0 ) ); + return results; + } catch(qsaError) { + } finally { + if ( !old ) { + context.removeAttribute("id"); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Detect xml + * @param {Element|Object} elem An element or a document + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var doc = node ? node.ownerDocument || node : preferredDoc; + + // If no document and documentElement is available, return + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Set our document + document = doc; + docElem = doc.documentElement; + + // Support tests + documentIsXML = isXML( doc ); + + // Check if getElementsByTagName("*") returns only elements + support.tagNameNoComments = assert(function( div ) { + div.appendChild( doc.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Check if attributes should be retrieved by attribute nodes + support.attributes = assert(function( div ) { + div.innerHTML = ""; + var type = typeof div.lastChild.getAttribute("multiple"); + // IE8 returns a string for some attributes even when not present + return type !== "boolean" && type !== "string"; + }); + + // Check if getElementsByClassName can be trusted + support.getByClassName = assert(function( div ) { + // Opera can't find a second classname (in 9.6) + div.innerHTML = ""; + if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) { + return false; + } + + // Safari 3.2 caches class attributes and doesn't catch changes + div.lastChild.className = "e"; + return div.getElementsByClassName("e").length === 2; + }); + + // Check if getElementById returns elements by name + // Check if getElementsByName privileges form controls or returns elements by ID + support.getByName = assert(function( div ) { + // Inject content + div.id = expando + 0; + div.innerHTML = "
    "; + docElem.insertBefore( div, docElem.firstChild ); + + // Test + var pass = doc.getElementsByName && + // buggy browsers will return fewer than the correct 2 + doc.getElementsByName( expando ).length === 2 + + // buggy browsers will return more than the correct 0 + doc.getElementsByName( expando + 0 ).length; + support.getIdNotName = !doc.getElementById( expando ); + + // Cleanup + docElem.removeChild( div ); + + return pass; + }); + + // IE6/7 return modified attributes + Expr.attrHandle = assert(function( div ) { + div.innerHTML = ""; + return div.firstChild && typeof div.firstChild.getAttribute !== strundefined && + div.firstChild.getAttribute("href") === "#"; + }) ? + {} : + { + "href": function( elem ) { + return elem.getAttribute( "href", 2 ); + }, + "type": function( elem ) { + return elem.getAttribute("type"); + } + }; + + // ID find and filter + if ( support.getIdNotName ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && !documentIsXML ) { + var m = context.getElementById( id ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && !documentIsXML ) { + var m = context.getElementById( id ); + + return m ? + m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ? + [m] : + undefined : + []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.tagNameNoComments ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== strundefined ) { + return context.getElementsByTagName( tag ); + } + } : + function( tag, context ) { + var elem, + tmp = [], + i = 0, + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Name + Expr.find["NAME"] = support.getByName && function( tag, context ) { + if ( typeof context.getElementsByName !== strundefined ) { + return context.getElementsByName( name ); + } + }; + + // Class + Expr.find["CLASS"] = support.getByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== strundefined && !documentIsXML ) { + return context.getElementsByClassName( className ); + } + }; + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21), + // no need to also add to buggyMatches since matches checks buggyQSA + // A support test would require too much code (would include document ready) + rbuggyQSA = [ ":focus" ]; + + if ( (support.qsa = isNative(doc.querySelectorAll)) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explictly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = ""; + + // IE8 - Some boolean attributes are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + }); + + assert(function( div ) { + + // Opera 10-12/IE8 - ^= $= *= and empty values + // Should not select anything + div.innerHTML = ""; + if ( div.querySelectorAll("[i^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = isNative( (matches = docElem.matchesSelector || + docElem.mozMatchesSelector || + docElem.webkitMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = new RegExp( rbuggyMatches.join("|") ); + + // Element contains another + // Purposefully does not implement inclusive descendent + // As in, an element does not contain itself + contains = isNative(docElem.contains) || docElem.compareDocumentPosition ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + // Document order sorting + sortOrder = docElem.compareDocumentPosition ? + function( a, b ) { + var compare; + + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( (compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b )) ) { + if ( compare & 1 || a.parentNode && a.parentNode.nodeType === 11 ) { + if ( a === doc || contains( preferredDoc, a ) ) { + return -1; + } + if ( b === doc || contains( preferredDoc, b ) ) { + return 1; + } + return 0; + } + return compare & 4 ? -1 : 1; + } + + return a.compareDocumentPosition ? -1 : 1; + } : + function( a, b ) { + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Parentless nodes are either documents or disconnected + } else if ( !aup || !bup ) { + return a === doc ? -1 : + b === doc ? 1 : + aup ? -1 : + bup ? 1 : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + // Always assume the presence of duplicates if sort doesn't + // pass them to our comparison function (as in Google Chrome). + hasDuplicate = false; + [0, 0].sort( sortOrder ); + support.detectDuplicates = hasDuplicate; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + // rbuggyQSA always contains :focus, so no need for an existence check + if ( support.matchesSelector && !documentIsXML && (!rbuggyMatches || !rbuggyMatches.test(expr)) && !rbuggyQSA.test(expr) ) { + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch(e) {} + } + + return Sizzle( expr, document, null, [elem] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + var val; + + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + if ( !documentIsXML ) { + name = name.toLowerCase(); + } + if ( (val = Expr.attrHandle[ name ]) ) { + return val( elem ); + } + if ( documentIsXML || support.attributes ) { + return elem.getAttribute( name ); + } + return ( (val = elem.getAttributeNode( name )) || elem.getAttribute( name ) ) && elem[ name ] === true ? + name : + val && val.specified ? val.value : null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +// Document sorting and removing duplicates +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + i = 1, + j = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( ; (elem = results[i]); i++ ) { + if ( elem === results[ i - 1 ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + return results; +}; + +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && ( ~b.sourceIndex || MAX_NEGATIVE ) - ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +// Returns a function to use in pseudos for input types +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +// Returns a function to use in pseudos for buttons +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +// Returns a function to use in pseudos for positionals +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + for ( ; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (see #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[5] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[4] ) { + match[2] = match[4]; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeName ) { + if ( nodeName === "*" ) { + return function() { return true; }; + } + + nodeName = nodeName.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, outerCache, node, diff, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || (parent[ expando ] = {}); + cache = outerCache[ type ] || []; + nodeIndex = cache[0] === dirruns && cache[1]; + diff = cache[0] === dirruns && cache[2]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + // Use previously-cached element index if available + } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { + diff = cache[1]; + + // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) + } else { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { + // Cache the index of each encountered element + if ( useCache ) { + (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifider + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsXML ? + elem.getAttribute("xml:lang") || elem.getAttribute("lang") : + elem.lang) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), + // not comment, processing instructions, or others + // Thanks to Diego Perini for the nodeName shortcut + // Greater than "@" means alpha characters (specifically not starting with "#" or "?") + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( tokens = [] ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push( { + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +} + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var data, cache, outerCache, + dirkey = dirruns + " " + doneName; + + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { + if ( (data = cache[1]) === true || data === cachedruns ) { + return data === true; + } + } else { + cache = outerCache[ dir ] = [ dirkey ]; + cache[1] = matcher( elem, context, xml ) || cachedruns; + if ( cache[1] === true ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + // A counter to specify which element is currently being matched + var matcherCachedRuns = 0, + bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, expandContext ) { + var elem, j, matcher, + setMatched = [], + matchedCount = 0, + i = "0", + unmatched = seed && [], + outermost = expandContext != null, + contextBackup = outermostContext, + // We must always have either seed elements or context + elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); + + if ( outermost ) { + outermostContext = context !== document && context; + cachedruns = matcherCachedRuns; + } + + // Add elements passing elementMatchers directly to results + // Keep `i` a string if there are no elements so `matchedCount` will be "00" below + for ( ; (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + cachedruns = ++matcherCachedRuns; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // Apply set filters to unmatched elements + matchedCount += i; + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !group ) { + group = tokenize( selector ); + } + i = group.length; + while ( i-- ) { + cached = matcherFromTokens( group[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + } + return cached; +}; + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function select( selector, context, results, seed ) { + var i, tokens, token, type, find, + match = tokenize( selector ); + + if ( !seed ) { + // Try to minimize operations if there is only one group + if ( match.length === 1 ) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + context.nodeType === 9 && !documentIsXML && + Expr.relative[ tokens[1].type ] ) { + + context = Expr.find["ID"]( token.matches[0].replace( runescape, funescape ), context )[0]; + if ( !context ) { + return results; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && context.parentNode || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, slice.call( seed, 0 ) ); + return results; + } + + break; + } + } + } + } + } + + // Compile and execute a filtering function + // Provide `match` to avoid retokenization if we modified the selector above + compile( selector, match )( + seed, + context, + documentIsXML, + results, + rsibling.test( selector ) + ); + return results; +} + +// Deprecated +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Easy API for creating new setFilters +function setFilters() {} +Expr.filters = setFilters.prototype = Expr.pseudos; +Expr.setFilters = new setFilters(); + +// Initialize with the default document +setDocument(); + +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})( window ); +var runtil = /Until$/, + rparentsprev = /^(?:parents|prev(?:Until|All))/, + isSimple = /^.[^:#\[\.,]*$/, + rneedsContext = jQuery.expr.match.needsContext, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var i, ret, self, + len = this.length; + + if ( typeof selector !== "string" ) { + self = this; + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + ret = []; + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, this[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = ( this.selector ? this.selector + " " : "" ) + selector; + return ret; + }, + + has: function( target ) { + var i, + targets = jQuery( target, this ), + len = targets.length; + + return this.filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false) ); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true) ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + rneedsContext.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + ret = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + cur = this[i]; + + while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + } + cur = cur.parentNode; + } + } + + return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( jQuery.unique(all) ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +jQuery.fn.andSelf = jQuery.fn.addBack; + +function sibling( cur, dir ) { + do { + cur = cur[ dir ]; + } while ( cur && cur.nodeType !== 1 ); + + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( this.length > 1 && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rtbody = /\s*$/g, + + // We have to close these tags to support XHTML (#13200) + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
    ", "
    " ], + area: [ 1, "", "" ], + param: [ 1, "", "" ], + thead: [ 1, "", "
    " ], + tr: [ 2, "", "
    " ], + col: [ 2, "", "
    " ], + td: [ 3, "", "
    " ], + + // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, + // unless wrapped in a div with non-breaking characters in front of it. + _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
    ", "
    " ] + }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +jQuery.fn.extend({ + text: function( value ) { + return jQuery.access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); + }, null, value, arguments.length ); + }, + + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } + + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); + + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); + } + + wrap.map(function() { + var elem = this; + + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } + + return elem; + }).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); + }, + + append: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.insertBefore( elem, this.firstChild ); + } + }); + }, + + before: function() { + return this.domManip( arguments, false, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + }); + }, + + after: function() { + return this.domManip( arguments, false, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + }); + }, + + // keepData is for internal use only--do not document + remove: function( selector, keepData ) { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + if ( !selector || jQuery.filter( selector, [ elem ] ).length > 0 ) { + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem ) ); + } + + if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); + } + elem.parentNode.removeChild( elem ); + } + } + } + + return this; + }, + + empty: function() { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + + // If this is a select, ensure that it displays empty (#12336) + // Support: IE<9 + if ( elem.options && jQuery.nodeName( elem, "select" ) ) { + elem.options.length = 0; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function () { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + return jQuery.access( this, function( value ) { + var elem = this[0] || {}, + i = 0, + l = this.length; + + if ( value === undefined ) { + return elem.nodeType === 1 ? + elem.innerHTML.replace( rinlinejQuery, "" ) : + undefined; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && + ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && + !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1>" ); + + try { + for (; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + elem = this[i] || {}; + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch(e) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function( value ) { + var isFunc = jQuery.isFunction( value ); + + // Make sure that the elements are removed from the DOM before they are inserted + // this can help fix replacing a parent with child elements + if ( !isFunc && typeof value !== "string" ) { + value = jQuery( value ).not( this ).detach(); + } + + return this.domManip( [ value ], true, function( elem ) { + var next = this.nextSibling, + parent = this.parentNode; + + if ( parent ) { + jQuery( this ).remove(); + parent.insertBefore( elem, next ); + } + }); + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, table, callback ) { + + // Flatten any nested arrays + args = core_concat.apply( [], args ); + + var first, node, hasScripts, + scripts, doc, fragment, + i = 0, + l = this.length, + set = this, + iNoClone = l - 1, + value = args[0], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) { + return this.each(function( index ) { + var self = set.eq( index ); + if ( isFunction ) { + args[0] = value.call( this, index, table ? self.html() : undefined ); + } + self.domManip( args, table, callback ); + }); + } + + if ( l ) { + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first ) { + table = table && jQuery.nodeName( first, "tr" ); + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( + table && jQuery.nodeName( this[i], "table" ) ? + findOrAppend( this[i], "tbody" ) : + this[i], + node, + i + ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Hope ajax is available... + jQuery.ajax({ + url: node.src, + type: "GET", + dataType: "script", + async: false, + global: false, + "throws": true + }); + } else { + jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); + } + } + } + } + + // Fix #11809: Avoid leaking memory + fragment = first = null; + } + } + + return this; + } +}); + +function findOrAppend( elem, tag ) { + return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) ); +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + var attr = elem.getAttributeNode("type"); + elem.type = ( attr && attr.specified ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + if ( match ) { + elem.type = match[1]; + } else { + elem.removeAttribute("type"); + } + return elem; +} + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var elem, + i = 0; + for ( ; (elem = elems[i]) != null; i++ ) { + jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); + } +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function fixCloneNodeIssues( src, dest ) { + var nodeName, e, data; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + nodeName = dest.nodeName.toLowerCase(); + + // IE6-8 copies events bound via attachEvent when using cloneNode. + if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) { + data = jQuery._data( dest ); + + for ( e in data.events ) { + jQuery.removeEvent( dest, e, data.handle ); + } + + // Event data gets referenced instead of copied if the expando gets copied too + dest.removeAttribute( jQuery.expando ); + } + + // IE blanks contents when cloning scripts, and tries to evaluate newly-set text + if ( nodeName === "script" && dest.text !== src.text ) { + disableScript( dest ).text = src.text; + restoreScript( dest ); + + // IE6-10 improperly clones children of object elements using classid. + // IE10 throws NoModificationAllowedError if parent is null, #12132. + } else if ( nodeName === "object" ) { + if ( dest.parentNode ) { + dest.outerHTML = src.outerHTML; + } + + // This path appears unavoidable for IE9. When cloning an object + // element in IE9, the outerHTML strategy above is not sufficient. + // If the src has innerHTML and the destination does not, + // copy the src.innerHTML into the dest.innerHTML. #10324 + if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { + dest.innerHTML = src.innerHTML; + } + + } else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + + dest.defaultChecked = dest.checked = src.checked; + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.defaultSelected = dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + i = 0, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone(true); + jQuery( insert[i] )[ original ]( elems ); + + // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() + core_push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +}); + +function getAll( context, tag ) { + var elems, elem, + i = 0, + found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) : + undefined; + + if ( !found ) { + for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { + if ( !tag || jQuery.nodeName( elem, tag ) ) { + found.push( elem ); + } else { + jQuery.merge( found, getAll( elem, tag ) ); + } + } + } + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], found ) : + found; +} + +// Used in buildFragment, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( manipulation_rcheckableType.test( elem.type ) ) { + elem.defaultChecked = elem.checked; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var destElements, node, clone, i, srcElements, + inPage = jQuery.contains( elem.ownerDocument, elem ); + + if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { + clone = elem.cloneNode( true ); + + // IE<=8 does not properly clone detached, unknown element nodes + } else { + fragmentDiv.innerHTML = elem.outerHTML; + fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); + } + + if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + // Fix all IE cloning issues + for ( i = 0; (node = srcElements[i]) != null; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + fixCloneNodeIssues( node, destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0; (node = srcElements[i]) != null; i++ ) { + cloneCopyEvent( node, destElements[i] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + destElements = srcElements = node = null; + + // Return the cloned set + return clone; + }, + + buildFragment: function( elems, context, scripts, selection ) { + var j, elem, contains, + tmp, tag, tbody, wrap, + l = elems.length, + + // Ensure a safe fragment + safe = createSafeFragment( context ), + + nodes = [], + i = 0; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || safe.appendChild( context.createElement("div") ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + + tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[2]; + + // Descend through wrappers to the right content + j = wrap[0]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Manually add leading whitespace removed by IE + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); + } + + // Remove IE's autoinserted from table fragments + if ( !jQuery.support.tbody ) { + + // String was a , *may* have spurious + elem = tag === "table" && !rtbody.test( elem ) ? + tmp.firstChild : + + // String was a bare or + wrap[1] === "
    " && !rtbody.test( elem ) ? + tmp : + 0; + + j = elem && elem.childNodes.length; + while ( j-- ) { + if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { + elem.removeChild( tbody ); + } + } + } + + jQuery.merge( nodes, tmp.childNodes ); + + // Fix #12392 for WebKit and IE > 9 + tmp.textContent = ""; + + // Fix #12392 for oldIE + while ( tmp.firstChild ) { + tmp.removeChild( tmp.firstChild ); + } + + // Remember the top-level container for proper cleanup + tmp = safe.lastChild; + } + } + } + + // Fix #11356: Clear elements from fragment + if ( tmp ) { + safe.removeChild( tmp ); + } + + // Reset defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + if ( !jQuery.support.appendChecked ) { + jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); + } + + i = 0; + while ( (elem = nodes[ i++ ]) ) { + + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( safe.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( (elem = tmp[ j++ ]) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + tmp = null; + + return safe; + }, + + cleanData: function( elems, /* internal */ acceptData ) { + var elem, type, id, data, + i = 0, + internalKey = jQuery.expando, + cache = jQuery.cache, + deleteExpando = jQuery.support.deleteExpando, + special = jQuery.event.special; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( acceptData || jQuery.acceptData( elem ) ) { + + id = elem[ internalKey ]; + data = id && cache[ id ]; + + if ( data ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Remove cache only if it was not already removed by jQuery.event.remove + if ( cache[ id ] ) { + + delete cache[ id ]; + + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( deleteExpando ) { + delete elem[ internalKey ]; + + } else if ( typeof elem.removeAttribute !== core_strundefined ) { + elem.removeAttribute( internalKey ); + + } else { + elem[ internalKey ] = null; + } + + core_deletedIds.push( id ); + } + } + } + } + } +}); +var iframe, getStyles, curCSS, + ralpha = /alpha\([^)]*\)/i, + ropacity = /opacity\s*=\s*([^)]*)/, + rposition = /^(top|right|bottom|left)$/, + // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" + // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rmargin = /^margin/, + rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), + rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), + rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ), + elemdisplay = { BODY: "block" }, + + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: 0, + fontWeight: 400 + }, + + cssExpand = [ "Top", "Right", "Bottom", "Left" ], + cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; + +// return a css property mapped to a potentially vendor prefixed property +function vendorPropName( style, name ) { + + // shortcut for names that are not vendor prefixed + if ( name in style ) { + return name; + } + + // check for vendor prefixed names + var capName = name.charAt(0).toUpperCase() + name.slice(1), + origName = name, + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in style ) { + return name; + } + } + + return origName; +} + +function isHidden( elem, el ) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); +} + +function showHide( elements, show ) { + var display, elem, hidden, + values = [], + index = 0, + length = elements.length; + + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + values[ index ] = jQuery._data( elem, "olddisplay" ); + display = elem.style.display; + if ( show ) { + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !values[ index ] && display === "none" ) { + elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( elem.style.display === "" && isHidden( elem ) ) { + values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); + } + } else { + + if ( !values[ index ] ) { + hidden = isHidden( elem ); + + if ( display && display !== "none" || !hidden ) { + jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); + } + } + } + } + + // Set the display of most of the elements in a second loop + // to avoid the constant reflow + for ( index = 0; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + if ( !show || elem.style.display === "none" || elem.style.display === "" ) { + elem.style.display = show ? values[ index ] || "" : "none"; + } + } + + return elements; +} + +jQuery.fn.extend({ + css: function( name, value ) { + return jQuery.access( this, function( elem, name, value ) { + var len, styles, + map = {}, + i = 0; + + if ( jQuery.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + }, + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + var bool = typeof state === "boolean"; + + return this.each(function() { + if ( bool ? state : isHidden( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + }); + } +}); + +jQuery.extend({ + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Exclude the following css properties to add px + cssNumber: { + "columnCount": true, + "fillOpacity": true, + "fontWeight": true, + "lineHeight": true, + "opacity": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: { + // normalize float css property + "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" + }, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = jQuery.camelCase( name ), + style = elem.style; + + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + + // Make sure that NaN and null values aren't set. See: #7116 + if ( value == null || type === "number" && isNaN( value ) ) { + return; + } + + // If a number was passed in, add 'px' to the (except for certain CSS properties) + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { + value += "px"; + } + + // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, + // but it would mean to define eight (for every problematic property) identical functions + if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { + + // Wrapped to prevent IE from throwing errors when 'invalid' values are provided + // Fixes bug #5509 + try { + style[ name ] = value; + } catch(e) {} + } + + } else { + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var num, val, hooks, + origName = jQuery.camelCase( name ); + + // Make sure that we're working with the right name + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + //convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Return, converting to number if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; + } + return val; + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback, args ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.apply( elem, args || [] ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; + } +}); + +// NOTE: we've included the "window" in window.getComputedStyle +// because jsdom on node.js will break without it. +if ( window.getComputedStyle ) { + getStyles = function( elem ) { + return window.getComputedStyle( elem, null ); + }; + + curCSS = function( elem, name, _computed ) { + var width, minWidth, maxWidth, + computed = _computed || getStyles( elem ), + + // getPropertyValue is only needed for .css('filter') in IE9, see #12537 + ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined, + style = elem.style; + + if ( computed ) { + + if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right + // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels + // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values + if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret; + }; +} else if ( document.documentElement.currentStyle ) { + getStyles = function( elem ) { + return elem.currentStyle; + }; + + curCSS = function( elem, name, _computed ) { + var left, rs, rsLeft, + computed = _computed || getStyles( elem ), + ret = computed ? computed[ name ] : undefined, + style = elem.style; + + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret == null && style && style[ name ] ) { + ret = style[ name ]; + } + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + // but not position css attributes, as those are proportional to the parent element instead + // and we can't measure the parent instead because it might trigger a "stacking dolls" problem + if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { + + // Remember the original values + left = style.left; + rs = elem.runtimeStyle; + rsLeft = rs && rs.left; + + // Put in the new values to get a computed value out + if ( rsLeft ) { + rs.left = elem.currentStyle.left; + } + style.left = name === "fontSize" ? "1em" : ret; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + if ( rsLeft ) { + rs.left = rsLeft; + } + } + + return ret === "" ? "auto" : ret; + }; +} + +function setPositiveNumber( elem, value, subtract ) { + var matches = rnumsplit.exec( value ); + return matches ? + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : + value; +} + +function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { + var i = extra === ( isBorderBox ? "border" : "content" ) ? + // If we already have the right measurement, avoid augmentation + 4 : + // Otherwise initialize for horizontal or vertical properties + name === "width" ? 1 : 0, + + val = 0; + + for ( ; i < 4; i += 2 ) { + // both box models exclude margin, so add it if we want it + if ( extra === "margin" ) { + val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); + } + + if ( isBorderBox ) { + // border-box includes padding, so remove it if we want content + if ( extra === "content" ) { + val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // at this point, extra isn't border nor margin, so remove border + if ( extra !== "margin" ) { + val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } else { + // at this point, extra isn't content, so add padding + val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // at this point, extra isn't content nor padding, so add border + if ( extra !== "padding" ) { + val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + return val; +} + +function getWidthOrHeight( elem, name, extra ) { + + // Start with offset property, which is equivalent to the border-box value + var valueIsBorderBox = true, + val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + styles = getStyles( elem ), + isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // some non-html elements return undefined for offsetWidth, so check for null/undefined + // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 + // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 + if ( val <= 0 || val == null ) { + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name, styles ); + if ( val < 0 || val == null ) { + val = elem.style[ name ]; + } + + // Computed unit is not pixels. Stop here and return. + if ( rnumnonpx.test(val) ) { + return val; + } + + // we need the check for style in case a browser which returns unreliable values + // for getComputedStyle silently falls back to the reliable elem.style + valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); + + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + } + + // use the active box-sizing model to add/subtract irrelevant styles + return ( val + + augmentWidthOrHeight( + elem, + name, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles + ) + ) + "px"; +} + +// Try to determine the default display value of an element +function css_defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + // Use the already-created iframe if possible + iframe = ( iframe || + jQuery("